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, #[serde(default)] pub parent_lock_hash: String, #[serde(default)] pub patches: Vec, #[serde(default)] pub projects: HashMap, } #[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, #[serde(skip_serializing_if = "Option::is_none")] pub author: Option, #[serde(default)] pub overrides: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub server_overrides: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub client_overrides: Option>, #[serde(default)] pub paths: HashMap, #[serde(default)] pub projects: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub export_profiles: Option>, #[serde( skip_serializing_if = "Option::is_none", rename = "exportServerSideProjectsToClient" )] pub export_server_side_projects_to_client: Option, /// Number of files to select per project (defaults to 1) #[serde(skip_serializing_if = "Option::is_none")] pub file_count_preference: 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, 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, #[serde(skip_serializing_if = "Option::is_none")] pub side: Option, #[serde(skip_serializing_if = "Option::is_none")] pub update_strategy: Option, #[serde(skip_serializing_if = "Option::is_none")] pub redistributable: Option, #[serde(skip_serializing_if = "Option::is_none")] pub subpath: Option, #[serde(skip_serializing_if = "Option::is_none")] pub aliases: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub export: Option, } impl Config { pub fn load>(path: P) -> Result { 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::(&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>(&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())); } }