1use std::path::Path;
7
8use crate::errors::{CoreError, CoreResult};
9use ito_config::types::{IntegrationMode, WorktreeStrategy};
10
11pub fn read_json_config(path: &Path) -> CoreResult<serde_json::Value> {
17 let Ok(contents) = std::fs::read_to_string(path) else {
18 return Ok(serde_json::Value::Object(serde_json::Map::new()));
19 };
20 let v: serde_json::Value = serde_json::from_str(&contents).map_err(|e| {
21 CoreError::serde(format!("Invalid JSON in {}", path.display()), e.to_string())
22 })?;
23 match v {
24 serde_json::Value::Object(_) => Ok(v),
25 _ => Err(CoreError::serde(
26 format!("Expected JSON object in {}", path.display()),
27 "root value is not an object",
28 )),
29 }
30}
31
32pub fn write_json_config(path: &Path, value: &serde_json::Value) -> CoreResult<()> {
38 let mut bytes = serde_json::to_vec_pretty(value)
39 .map_err(|e| CoreError::serde("Failed to serialize JSON config", e.to_string()))?;
40 bytes.push(b'\n');
41 ito_common::io::write_atomic_std(path, bytes)
42 .map_err(|e| CoreError::io(format!("Failed to write config to {}", path.display()), e))?;
43 Ok(())
44}
45
46pub fn parse_json_value_arg(raw: &str, force_string: bool) -> serde_json::Value {
50 if force_string {
51 return serde_json::Value::String(raw.to_string());
52 }
53 match serde_json::from_str::<serde_json::Value>(raw) {
54 Ok(v) => v,
55 Err(_) => serde_json::Value::String(raw.to_string()),
56 }
57}
58
59pub fn json_split_path(key: &str) -> Vec<&str> {
61 let mut out: Vec<&str> = Vec::new();
62 for part in key.split('.') {
63 let part = part.trim();
64 if part.is_empty() {
65 continue;
66 }
67 out.push(part);
68 }
69 out
70}
71
72pub fn json_get_path<'a>(
74 root: &'a serde_json::Value,
75 parts: &[&str],
76) -> Option<&'a serde_json::Value> {
77 let mut cur = root;
78 for p in parts {
79 let serde_json::Value::Object(map) = cur else {
80 return None;
81 };
82 let next = map.get(*p)?;
83 cur = next;
84 }
85 Some(cur)
86}
87
88pub fn json_set_path(
94 root: &mut serde_json::Value,
95 parts: &[&str],
96 value: serde_json::Value,
97) -> CoreResult<()> {
98 if parts.is_empty() {
99 return Err(CoreError::validation("Invalid empty path"));
100 }
101
102 let mut cur = root;
103 for (i, key) in parts.iter().enumerate() {
104 let is_last = i + 1 == parts.len();
105
106 let is_object = matches!(cur, serde_json::Value::Object(_));
107 if !is_object {
108 *cur = serde_json::Value::Object(serde_json::Map::new());
109 }
110
111 let serde_json::Value::Object(map) = cur else {
112 return Err(CoreError::validation("Failed to set path"));
113 };
114
115 if is_last {
116 map.insert((*key).to_string(), value);
117 return Ok(());
118 }
119
120 let needs_object = match map.get(*key) {
121 Some(serde_json::Value::Object(_)) => false,
122 Some(_) => true,
123 None => true,
124 };
125 if needs_object {
126 map.insert(
127 (*key).to_string(),
128 serde_json::Value::Object(serde_json::Map::new()),
129 );
130 }
131
132 let Some(next) = map.get_mut(*key) else {
133 return Err(CoreError::validation("Failed to set path"));
134 };
135 cur = next;
136 }
137
138 Ok(())
139}
140
141pub fn validate_config_value(parts: &[&str], value: &serde_json::Value) -> CoreResult<()> {
150 let path = parts.join(".");
151 match path.as_str() {
152 "worktrees.strategy" => {
153 let Some(s) = value.as_str() else {
154 return Err(CoreError::validation(format!(
155 "Key '{}' requires a string value. Valid values: {}",
156 path,
157 WorktreeStrategy::ALL.join(", ")
158 )));
159 };
160 if WorktreeStrategy::parse_value(s).is_none() {
161 return Err(CoreError::validation(format!(
162 "Invalid value '{}' for key '{}'. Valid values: {}",
163 s,
164 path,
165 WorktreeStrategy::ALL.join(", ")
166 )));
167 }
168 }
169 "worktrees.apply.integration_mode" => {
170 let Some(s) = value.as_str() else {
171 return Err(CoreError::validation(format!(
172 "Key '{}' requires a string value. Valid values: {}",
173 path,
174 IntegrationMode::ALL.join(", ")
175 )));
176 };
177 if IntegrationMode::parse_value(s).is_none() {
178 return Err(CoreError::validation(format!(
179 "Invalid value '{}' for key '{}'. Valid values: {}",
180 s,
181 path,
182 IntegrationMode::ALL.join(", ")
183 )));
184 }
185 }
186 _ => {}
187 }
188 Ok(())
189}
190
191pub fn is_valid_worktree_strategy(s: &str) -> bool {
195 WorktreeStrategy::parse_value(s).is_some()
196}
197
198pub fn is_valid_integration_mode(s: &str) -> bool {
202 IntegrationMode::parse_value(s).is_some()
203}
204
205pub fn json_unset_path(root: &mut serde_json::Value, parts: &[&str]) -> CoreResult<bool> {
213 if parts.is_empty() {
214 return Err(CoreError::validation("Invalid empty path"));
215 }
216
217 let mut cur = root;
218 for (i, p) in parts.iter().enumerate() {
219 let is_last = i + 1 == parts.len();
220 let serde_json::Value::Object(map) = cur else {
221 return Ok(false);
222 };
223
224 if is_last {
225 return Ok(map.remove(*p).is_some());
226 }
227
228 let Some(next) = map.get_mut(*p) else {
229 return Ok(false);
230 };
231 cur = next;
232 }
233
234 Ok(false)
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240 use serde_json::json;
241
242 #[test]
243 fn validate_config_value_accepts_valid_strategy() {
244 let parts = ["worktrees", "strategy"];
245 let value = json!("checkout_subdir");
246 assert!(validate_config_value(&parts, &value).is_ok());
247
248 let value = json!("checkout_siblings");
249 assert!(validate_config_value(&parts, &value).is_ok());
250
251 let value = json!("bare_control_siblings");
252 assert!(validate_config_value(&parts, &value).is_ok());
253 }
254
255 #[test]
256 fn validate_config_value_rejects_invalid_strategy() {
257 let parts = ["worktrees", "strategy"];
258 let value = json!("custom_layout");
259 let err = validate_config_value(&parts, &value).unwrap_err();
260 let msg = err.to_string();
261 assert!(msg.contains("Invalid value"));
262 assert!(msg.contains("custom_layout"));
263 }
264
265 #[test]
266 fn validate_config_value_rejects_non_string_strategy() {
267 let parts = ["worktrees", "strategy"];
268 let value = json!(42);
269 let err = validate_config_value(&parts, &value).unwrap_err();
270 let msg = err.to_string();
271 assert!(msg.contains("requires a string value"));
272 }
273
274 #[test]
275 fn validate_config_value_accepts_valid_integration_mode() {
276 let parts = ["worktrees", "apply", "integration_mode"];
277 let value = json!("commit_pr");
278 assert!(validate_config_value(&parts, &value).is_ok());
279
280 let value = json!("merge_parent");
281 assert!(validate_config_value(&parts, &value).is_ok());
282 }
283
284 #[test]
285 fn validate_config_value_rejects_invalid_integration_mode() {
286 let parts = ["worktrees", "apply", "integration_mode"];
287 let value = json!("squash_merge");
288 let err = validate_config_value(&parts, &value).unwrap_err();
289 let msg = err.to_string();
290 assert!(msg.contains("Invalid value"));
291 assert!(msg.contains("squash_merge"));
292 }
293
294 #[test]
295 fn validate_config_value_accepts_unknown_keys() {
296 let parts = ["worktrees", "enabled"];
297 let value = json!(true);
298 assert!(validate_config_value(&parts, &value).is_ok());
299
300 let parts = ["some", "other", "key"];
301 let value = json!("anything");
302 assert!(validate_config_value(&parts, &value).is_ok());
303 }
304
305 #[test]
306 fn is_valid_worktree_strategy_checks_correctly() {
307 assert!(is_valid_worktree_strategy("checkout_subdir"));
308 assert!(is_valid_worktree_strategy("checkout_siblings"));
309 assert!(is_valid_worktree_strategy("bare_control_siblings"));
310 assert!(!is_valid_worktree_strategy("custom"));
311 assert!(!is_valid_worktree_strategy(""));
312 }
313
314 #[test]
315 fn is_valid_integration_mode_checks_correctly() {
316 assert!(is_valid_integration_mode("commit_pr"));
317 assert!(is_valid_integration_mode("merge_parent"));
318 assert!(!is_valid_integration_mode("squash"));
319 assert!(!is_valid_integration_mode(""));
320 }
321}