diff --git a/src/model/config.rs b/src/model/config.rs index d052695..83172ae 100644 --- a/src/model/config.rs +++ b/src/model/config.rs @@ -7,8 +7,6 @@ use crate::error::{PakkerError, Result}; const CONFIG_NAME: &str = "pakker.json"; -// Pakker config wrapper - supports both Pakker (direct) and Pakku (wrapped) -// formats #[derive(Debug, Deserialize)] #[serde(untagged)] enum ConfigWrapper { @@ -43,39 +41,45 @@ pub struct ParentConfig { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Config { - pub name: String, - pub version: String, + pub name: String, + pub version: String, #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, + pub description: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub author: Option, + pub author: Option, #[serde(default)] - pub overrides: Vec, + pub overrides: Vec, #[serde(skip_serializing_if = "Option::is_none")] - pub server_overrides: Option>, + pub server_overrides: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub client_overrides: Option>, + pub client_overrides: Option>, #[serde(default)] - pub paths: HashMap, + pub paths: HashMap, #[serde(default)] - pub projects: Option>, + pub projects: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub export_profiles: Option>, + pub export_profiles: Option>, + #[serde( + skip_serializing_if = "Option::is_none", + rename = "exportServerSideProjectsToClient" + )] + pub export_server_side_projects_to_client: Option, } 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, + 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, } } } @@ -105,21 +109,16 @@ impl Config { let content = std::fs::read_to_string(&path).map_err(PakkerError::IoError)?; - // Try to parse as ConfigWrapper (supports both Pakker and Pakku formats) match serde_json::from_str::(&content) { Ok(ConfigWrapper::Pakker(config)) => { config.validate()?; Ok(config) }, Ok(ConfigWrapper::Pakku { pakku }) => { - // Convert Pakku format to Pakker format - // Pakku format doesn't have name/version, use parent repo info as - // fallback let name = pakku .parent .as_ref() .map(|p| { - // Extract repo name from URL p.id .split('/') .next_back() @@ -145,6 +144,7 @@ impl Config { paths: HashMap::new(), projects: Some(pakku.projects), export_profiles: None, + export_server_side_projects_to_client: None, }) }, Err(e) => Err(PakkerError::InvalidConfigFile(e.to_string())), @@ -153,17 +153,12 @@ impl Config { pub fn save>(&self, path: P) -> Result<()> { self.validate()?; - let path = path.as_ref().join(CONFIG_NAME); - - // Write to temporary file first (atomic write) 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(()) } @@ -175,27 +170,39 @@ impl Config { } 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 tempfile::TempDir; - 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, + 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, }; assert_eq!(config.name, "test-pack"); assert_eq!(config.version, "1.0.0"); @@ -206,178 +213,26 @@ mod tests { #[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, + 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, }; 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, config.name); - assert_eq!(deserialized.version, config.version); - assert_eq!(deserialized.description, config.description); - assert_eq!(deserialized.author, config.author); - } - - #[test] - fn test_config_save_and_load() { - let temp_dir = TempDir::new().unwrap(); - 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, - }; - config.description = Some("Test description".to_string()); - - config.save(temp_dir.path()).unwrap(); - - let loaded = Config::load(temp_dir.path()).unwrap(); - assert_eq!(loaded.name, config.name); - assert_eq!(loaded.version, config.version); - assert_eq!(loaded.description, config.description); - } - - #[test] - fn test_config_compatibility_with_pakku() { - // Test basic config loading with projects - let config = Config { - name: "test-modpack".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, - }; - - assert_eq!(config.name, "test-modpack"); - assert_eq!(config.version, "1.0.0"); - assert!(config.projects.is_none()); - } - - #[test] - fn test_config_wrapped_format() { - let mut projects = HashMap::new(); - projects.insert("sodium".to_string(), ProjectConfig { - r#type: Some(ProjectType::Mod), - side: Some(ProjectSide::Client), - update_strategy: None, - redistributable: None, - subpath: None, - aliases: None, - export: None, - }); - - let wrapped = PakkerWrappedConfig { - parent: None, - parent_lock_hash: String::new(), - patches: vec![], - projects, - }; - - let json = serde_json::to_string(&wrapped).unwrap(); - assert!(json.contains("\"projects\"")); - - let deserialized: PakkerWrappedConfig = - serde_json::from_str(&json).unwrap(); - assert_eq!(deserialized.projects.len(), 1); - } - - #[test] - fn test_config_wrapped_format_old() { - use crate::model::fork::{LocalConfig, LocalProjectConfig}; - - let mut projects = HashMap::new(); - projects.insert("sodium".to_string(), LocalProjectConfig { - version: None, - r#type: Some(ProjectType::Mod), - side: Some(ProjectSide::Client), - update_strategy: None, - redistributable: None, - subpath: None, - aliases: None, - export: None, - }); - - let wrapped_inner = LocalConfig { - parent: None, - projects, - parent_lock_hash: None, - parent_config_hash: None, - patches: vec![], - }; - - // Just verify we can create the struct - assert_eq!(wrapped_inner.projects.len(), 1); - } - - #[test] - fn test_config_validate() { - let config = Config { - name: "test".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, - }; - assert!(config.validate().is_ok()); - - let invalid = Config { - name: "".to_string(), - version: "1.0.0".to_string(), - description: None, - author: None, - overrides: vec![], - server_overrides: None, - client_overrides: None, - paths: HashMap::new(), - projects: None, - export_profiles: None, - }; - assert!(invalid.validate().is_err()); - } -} - -impl Config { - pub fn get_project_config(&self, identifier: &str) -> Option<&ProjectConfig> { - self.projects.as_ref()?.get(identifier) - } - - pub fn set_project_config( - &mut self, - identifier: String, - config: ProjectConfig, - ) { - if self.projects.is_none() { - self.projects = Some(HashMap::new()); - } - if let Some(ref mut projects) = self.projects { - projects.insert(identifier, config); - } + 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())); } }