agpm_cli/templating/utils.rs
1//! Utility functions for the templating system.
2
3use serde_json::Value;
4
5/// Perform a deep merge of two JSON values.
6///
7/// Recursively merges `overrides` into `base`. For objects, fields from `overrides`
8/// are added or replace fields in `base`. For arrays and primitives, `overrides`
9/// completely replaces `base`.
10///
11/// # Arguments
12///
13/// * `base` - The base JSON value
14/// * `overrides` - The override values to merge into base
15///
16/// # Returns
17///
18/// Returns the merged JSON value.
19///
20/// # Examples
21///
22/// ```rust,no_run
23/// use serde_json::json;
24/// use agpm_cli::templating::deep_merge_json;
25///
26/// let base = json!({ "project": { "name": "agpm", "language": "rust" } });
27/// let overrides = json!({ "project": { "language": "python", "framework": "fastapi" } });
28///
29/// let result = deep_merge_json(base, &overrides);
30/// // result: { "project": { "name": "agpm", "language": "python", "framework": "fastapi" } }
31/// ```
32pub fn deep_merge_json(mut base: Value, overrides: &Value) -> Value {
33 match (base.as_object_mut(), overrides.as_object()) {
34 (Some(base_obj), Some(override_obj)) => {
35 // Both are objects - recursively merge
36 for (key, override_value) in override_obj {
37 match base_obj.get_mut(key) {
38 Some(base_value) if base_value.is_object() && override_value.is_object() => {
39 // Recursively merge nested objects
40 let merged = deep_merge_json(base_value.clone(), override_value);
41 base_obj.insert(key.clone(), merged);
42 }
43 _ => {
44 // For non-objects or missing keys, override completely
45 base_obj.insert(key.clone(), override_value.clone());
46 }
47 }
48 }
49 base
50 }
51 (_, _) => {
52 // If override is not an object, or base is not an object, override replaces base
53 overrides.clone()
54 }
55 }
56}
57
58/// Convert Unix-style path (from lockfile) to platform-native format for display in templates.
59///
60/// Lockfiles always use Unix-style forward slashes for cross-platform compatibility,
61/// but when rendering templates, we want to show paths in the platform's native format
62/// so users see `.claude\agents\helper.md` on Windows and `.claude/agents/helper.md` on Unix.
63///
64/// # Arguments
65///
66/// * `unix_path` - Path string with forward slashes (from lockfile)
67///
68/// # Returns
69///
70/// Platform-native path string (backslashes on Windows, forward slashes on Unix)
71///
72/// # Examples
73///
74/// ```
75/// # use agpm_cli::templating::to_native_path_display;
76/// #[cfg(windows)]
77/// assert_eq!(to_native_path_display(".claude/agents/test.md"), ".claude\\agents\\test.md");
78///
79/// #[cfg(not(windows))]
80/// assert_eq!(to_native_path_display(".claude/agents/test.md"), ".claude/agents/test.md");
81/// ```
82pub fn to_native_path_display(unix_path: &str) -> String {
83 #[cfg(windows)]
84 {
85 unix_path.replace('/', "\\")
86 }
87 #[cfg(not(windows))]
88 {
89 unix_path.to_string()
90 }
91}