model: add exportServerSideProjectsToClient config property

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I638f8a1f2eb7d4f40de55aebd884ed9c6a6a6964
This commit is contained in:
raf 2026-02-07 13:25:55 +03:00
commit 92c3215e67
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF

View file

@ -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<String>,
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub author: Option<String>,
pub author: Option<String>,
#[serde(default)]
pub overrides: Vec<String>,
pub overrides: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub server_overrides: Option<Vec<String>>,
pub server_overrides: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_overrides: Option<Vec<String>>,
pub client_overrides: Option<Vec<String>>,
#[serde(default)]
pub paths: HashMap<String, String>,
pub paths: HashMap<String, String>,
#[serde(default)]
pub projects: Option<HashMap<String, ProjectConfig>>,
pub projects: Option<HashMap<String, ProjectConfig>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub export_profiles: Option<HashMap<String, crate::export::ProfileConfig>>,
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>,
}
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::<ConfigWrapper>(&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<P: AsRef<Path>>(&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()));
}
}