diff --git a/Cargo.lock b/Cargo.lock index 8029be1..b07f8af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2074,6 +2074,7 @@ dependencies = [ "reqwest", "semver", "serde", + "serde_json", "sha1", "sha2", "tempfile", @@ -3194,9 +3195,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.125" +version = "1.0.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" dependencies = [ "itoa", "memchr", diff --git a/mcmpmgr/Cargo.toml b/mcmpmgr/Cargo.toml index 8597e13..0761b3f 100644 --- a/mcmpmgr/Cargo.toml +++ b/mcmpmgr/Cargo.toml @@ -13,6 +13,7 @@ pathdiff = "0.2.1" reqwest = { version = "0.12.5", features = ["json"] } semver = { version = "1.0.23", features = ["serde"] } serde = { version = "1.0.207", features = ["derive"] } +serde_json = "1.0.128" sha1 = "0.10.6" sha2 = "0.10.8" tempfile = "3.12.0" diff --git a/mcmpmgr/src/file_merge.rs b/mcmpmgr/src/file_merge.rs new file mode 100644 index 0000000..b462962 --- /dev/null +++ b/mcmpmgr/src/file_merge.rs @@ -0,0 +1,150 @@ +use std::{any::Any, default, str::FromStr}; + +#[derive(Debug, Clone, Copy)] +pub enum FileType { + Json, + Yaml, + Toml, +} + +impl FromStr for FileType { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + Ok(if s.contains("json") { + FileType::Json + } else if s.contains("toml") { + FileType::Toml + } else if s.contains("yaml") || s.contains("yml") { + FileType::Yaml + } else { + anyhow::bail!("Unmergable file type: {s}") + }) + } +} + +fn merge_json( + src: &serde_json::Value, + dst: &mut serde_json::Value, + overwrite_existing: bool, +) -> anyhow::Result<()> { + if src.is_object() && dst.is_object() { + let src = src.as_object().unwrap(); + let dst = dst.as_object_mut().unwrap(); + + for (k, v) in src.iter() { + if v.is_object() { + let dst_v = dst.entry(k).or_insert(serde_json::json!({})); + merge_json(v, dst_v, overwrite_existing)?; + } else { + if overwrite_existing || !dst.contains_key(k) { + dst.insert(k.to_string(), v.clone()); + } + } + } + } else { + // TODO: Keep track of path for better errors + anyhow::bail!("Cannot merge non-objects: {src:#?} and {dst:#?}") + } + Ok(()) +} + +#[test] +fn test_merge_json() { + let src = serde_json::json!({ + "a": 3, + "b": { + "x": { + + }, + "y": { + "test": "thing" + } + }, + "c": {} + }); + let dst = serde_json::json!({ + "b": { + "y": { + "test": "something" + } + }, + "c": { + "foo": "bar" + } + }); + + let mut merged_overwrite = dst.clone(); + let mut merged_retained = dst.clone(); + merge_json(&src, &mut merged_overwrite, true).unwrap(); + merge_json(&src, &mut merged_retained, false).unwrap(); + + assert!( + merged_overwrite["b"]["y"]["test"] == "thing", + "//b/y/test wasn't overwritten with \"thing\". src={}, dst={}", + src, + merged_overwrite + ); + assert!( + merged_overwrite["a"] == 3, + "//a was not set to 3. src={}, dst={}", + src, + merged_overwrite + ); + assert!( + merged_overwrite["b"]["x"].is_object(), + "//b/x is not an object. src={}, dst={}", + src, + merged_overwrite + ); + assert!( + merged_overwrite["c"]["foo"] == "bar", + "//c/foo != bar. src={}, dst={}", + src, + merged_overwrite + ); + + assert!( + merged_retained["b"]["y"]["test"] == "something", + "//b/y/test was overwritten. src={}, dst={}", + src, + merged_retained + ); + assert!( + merged_retained["a"] == 3, + "//a was not set to 3. src={}, dst={}", + src, + merged_retained + ); + assert!( + merged_retained["b"]["x"].is_object(), + "//b/x is not an object. src={}, dst={}", + src, + merged_retained + ); + assert!( + merged_retained["c"]["foo"] == "bar", + "//c/foo != bar. src={}, dst={}", + src, + merged_retained + ); +} + +/// Merge `src` into `dst` if it is a supported file type +fn merge_files( + src: &str, + dst: &str, + overwrite_existing: bool, + file_type: FileType, +) -> anyhow::Result { + Ok(match file_type { + FileType::Json => { + let src_val = serde_json::from_str(src)?; + let mut dst_val = serde_json::from_str(dst)?; + merge_json(&src_val, &mut dst_val, overwrite_existing)?; + dst_val.to_string() + } + FileType::Yaml => todo!(), + FileType::Toml => todo!(), + }) +} diff --git a/mcmpmgr/src/file_meta.rs b/mcmpmgr/src/file_meta.rs index 27ac9e4..091d789 100644 --- a/mcmpmgr/src/file_meta.rs +++ b/mcmpmgr/src/file_meta.rs @@ -1,6 +1,6 @@ use crate::providers::DownloadSide; use serde::{Deserialize, Serialize}; -use std::{fmt::Display, path::{Path, PathBuf}, str::FromStr}; +use std::{fmt::Display, path::Path, str::FromStr}; #[derive(Debug, Clone, Serialize, Deserialize, Hash)] pub struct FileMeta { @@ -9,7 +9,7 @@ pub struct FileMeta { /// Which side the files should be applied on pub side: DownloadSide, /// When to apply the files to the instance - pub apply_policy: FileApplyPolicy + pub apply_policy: FileApplyPolicy, } #[derive(Debug, Clone, Serialize, Deserialize, Hash, PartialEq, Eq)] @@ -17,7 +17,11 @@ pub enum FileApplyPolicy { /// Always ensure the file or folder exactly matches that defined in the pack Always, /// Only apply the file or folder if it doesn't already exist in the pack - Once + Once, + /// Merge into folders and files, retaining existing values in files when a file already exists + MergeRetain, + /// Merge into folders and files, overwriting existing values in files when a file already exists + MergeOverwrite, } impl FromStr for FileApplyPolicy { @@ -27,6 +31,8 @@ impl FromStr for FileApplyPolicy { match s.to_ascii_lowercase().as_str() { "always" => Ok(Self::Always), "once" => Ok(Self::Once), + "mergeretain" => Ok(Self::MergeRetain), + "mergeoverwrite" => Ok(Self::MergeOverwrite), _ => anyhow::bail!("Invalid apply policy {}. Expected one of: always, once", s), } } @@ -37,6 +43,8 @@ impl Display for FileApplyPolicy { match self { Self::Always => write!(f, "Always"), Self::Once => write!(f, "Once"), + Self::MergeRetain => write!(f, "MergeRetain"), + Self::MergeOverwrite => write!(f, "MergeOverwrite"), } } } diff --git a/mcmpmgr/src/lib.rs b/mcmpmgr/src/lib.rs index e364a04..6c734e0 100644 --- a/mcmpmgr/src/lib.rs +++ b/mcmpmgr/src/lib.rs @@ -1,5 +1,6 @@ -pub mod mod_meta; +pub mod file_merge; pub mod file_meta; +pub mod mod_meta; pub mod modpack; pub mod profiles; pub mod providers; diff --git a/mcmpmgr/src/main.rs b/mcmpmgr/src/main.rs index df3d02e..b8c409e 100644 --- a/mcmpmgr/src/main.rs +++ b/mcmpmgr/src/main.rs @@ -1,3 +1,4 @@ +mod file_merge; mod file_meta; mod mod_meta; mod modpack;