pakker/src/model/config.rs
NotAShelf a89184a358
model: add file_count_preference for multi-file selection support
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ia27c829dbcc21a7fcfc8e6f67f9e33276a6a6964
2026-03-03 23:35:13 +03:00

245 lines
8.3 KiB
Rust

use std::{collections::HashMap, path::Path};
use serde::{Deserialize, Serialize};
use super::enums::{ProjectSide, ProjectType, UpdateStrategy};
use crate::error::{PakkerError, Result};
const CONFIG_NAME: &str = "pakker.json";
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum ConfigWrapper {
Pakker(Config),
Pakku { pakku: PakkerWrappedConfig },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PakkerWrappedConfig {
pub parent: Option<ParentConfig>,
#[serde(default)]
pub parent_lock_hash: String,
#[serde(default)]
pub patches: Vec<serde_json::Value>,
#[serde(default)]
pub projects: HashMap<String, ProjectConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ParentConfig {
pub id: String,
pub r#ref: String,
pub ref_type: String,
pub remote_name: String,
#[serde(rename = "type")]
pub type_: String,
pub version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Config {
pub name: String,
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub author: Option<String>,
#[serde(default)]
pub overrides: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub server_overrides: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_overrides: Option<Vec<String>>,
#[serde(default)]
pub paths: HashMap<String, String>,
#[serde(default)]
pub projects: Option<HashMap<String, ProjectConfig>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub export_profiles: Option<HashMap<String, crate::export::ProfileConfig>>,
#[serde(
skip_serializing_if = "Option::is_none",
rename = "exportServerSideProjectsToClient"
)]
pub export_server_side_projects_to_client: Option<bool>,
/// Number of files to select per project (defaults to 1)
#[serde(skip_serializing_if = "Option::is_none")]
pub file_count_preference: Option<usize>,
}
impl Default for Config {
fn default() -> Self {
Self {
name: String::new(),
version: String::new(),
description: None,
author: None,
overrides: vec!["overrides".to_string()],
server_overrides: None,
client_overrides: None,
paths: HashMap::new(),
projects: Some(HashMap::new()),
export_profiles: None,
export_server_side_projects_to_client: None,
file_count_preference: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ProjectConfig {
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub r#type: Option<ProjectType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub side: Option<ProjectSide>,
#[serde(skip_serializing_if = "Option::is_none")]
pub update_strategy: Option<UpdateStrategy>,
#[serde(skip_serializing_if = "Option::is_none")]
pub redistributable: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subpath: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub aliases: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub export: Option<bool>,
}
impl Config {
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref().join(CONFIG_NAME);
let content =
std::fs::read_to_string(&path).map_err(PakkerError::IoError)?;
match serde_json::from_str::<ConfigWrapper>(&content) {
Ok(ConfigWrapper::Pakker(config)) => {
config.validate()?;
Ok(config)
},
Ok(ConfigWrapper::Pakku { pakku }) => {
let name = pakku
.parent
.as_ref()
.map(|p| {
p.id
.split('/')
.next_back()
.unwrap_or(&p.id)
.trim_end_matches(".git")
.to_string()
})
.unwrap_or_else(|| "unknown".to_string());
let version = pakku
.parent
.as_ref()
.map_or_else(|| "unknown".to_string(), |p| p.version.clone());
Ok(Self {
name,
version,
description: None,
author: None,
overrides: vec!["overrides".to_string()],
server_overrides: None,
client_overrides: None,
paths: HashMap::new(),
projects: Some(pakku.projects),
export_profiles: None,
export_server_side_projects_to_client: None,
file_count_preference: None,
})
},
Err(e) => Err(PakkerError::InvalidConfigFile(e.to_string())),
}
}
pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
self.validate()?;
let path = path.as_ref().join(CONFIG_NAME);
let temp_path = path.with_extension("tmp");
let content = serde_json::to_string_pretty(self)
.map_err(PakkerError::SerializationError)?;
std::fs::write(&temp_path, content)?;
std::fs::rename(temp_path, path)?;
Ok(())
}
pub fn validate(&self) -> Result<()> {
if self.name.is_empty() {
return Err(PakkerError::InvalidConfigFile(
"Config name cannot be empty".to_string(),
));
}
Ok(())
}
pub fn get_project_config(&self, project_id: &str) -> Option<&ProjectConfig> {
self.projects.as_ref()?.get(project_id)
}
pub fn set_project_config(
&mut self,
project_id: String,
project_config: ProjectConfig,
) {
let projects = self.projects.get_or_insert_with(HashMap::new);
projects.insert(project_id, project_config);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_new() {
let config = Config {
name: "test-pack".to_string(),
version: "1.0.0".to_string(),
description: None,
author: None,
overrides: vec!["overrides".to_string()],
server_overrides: None,
client_overrides: None,
paths: HashMap::new(),
projects: None,
export_profiles: None,
export_server_side_projects_to_client: None,
file_count_preference: None,
};
assert_eq!(config.name, "test-pack");
assert_eq!(config.version, "1.0.0");
assert_eq!(config.overrides, vec!["overrides"]);
assert!(config.projects.is_none());
}
#[test]
fn test_config_serialization() {
let mut config = Config {
name: "test-pack".to_string(),
version: "1.0.0".to_string(),
description: None,
author: None,
overrides: vec!["overrides".to_string()],
server_overrides: None,
client_overrides: None,
paths: HashMap::new(),
projects: None,
export_profiles: None,
export_server_side_projects_to_client: None,
file_count_preference: None,
};
config.description = Some("A test modpack".to_string());
config.author = Some("Test Author".to_string());
let json = serde_json::to_string(&config).unwrap();
let deserialized: Config = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.name, "test-pack");
assert_eq!(deserialized.version, "1.0.0");
assert_eq!(deserialized.description, Some("A test modpack".to_string()));
assert_eq!(deserialized.author, Some("Test Author".to_string()));
}
}