1use serde::de::DeserializeOwned;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::path::{Path, PathBuf};
10
11use crate::Error;
12
13pub type ReferenceMap = HashMap<String, String>;
16
17fn enrich_depends_on(
22 value: &mut serde_json::Value,
23 instance_path: &str,
24 references: &ReferenceMap,
25) {
26 enrich_depends_on_recursive(value, instance_path, "", references);
27}
28
29fn enrich_depends_on_recursive(
31 value: &mut serde_json::Value,
32 instance_path: &str,
33 field_path: &str,
34 references: &ReferenceMap,
35) {
36 match value {
37 serde_json::Value::Object(obj) => {
38 if let Some(serde_json::Value::Array(deps)) = obj.get_mut("dependsOn") {
40 let depends_on_path = if field_path.is_empty() {
41 "dependsOn".to_string()
42 } else {
43 format!("{}.dependsOn", field_path)
44 };
45
46 for (i, dep) in deps.iter_mut().enumerate() {
48 if let serde_json::Value::Object(dep_obj) = dep {
49 if dep_obj.contains_key("_name") {
51 continue;
52 }
53
54 let meta_key = format!("{}/{}[{}]", instance_path, depends_on_path, i);
56 if let Some(reference) = references.get(&meta_key) {
57 dep_obj.insert(
59 "_name".to_string(),
60 serde_json::Value::String(reference.clone()),
61 );
62 }
63 }
64 }
65 }
66
67 for (key, child) in obj.iter_mut() {
69 if key == "dependsOn" {
70 continue; }
72 let child_path = if field_path.is_empty() {
73 key.clone()
74 } else {
75 format!("{}.{}", field_path, key)
76 };
77 enrich_depends_on_recursive(child, instance_path, &child_path, references);
78 }
79 }
80 serde_json::Value::Array(arr) => {
81 for (i, child) in arr.iter_mut().enumerate() {
82 let child_path = format!("{}[{}]", field_path, i);
83 enrich_depends_on_recursive(child, instance_path, &child_path, references);
84 }
85 }
86 _ => {}
87 }
88}
89
90#[derive(Debug, Clone)]
95pub struct ModuleEvaluation {
96 pub root: PathBuf,
98
99 pub instances: HashMap<PathBuf, Instance>,
101}
102
103impl ModuleEvaluation {
104 pub fn from_raw(
112 root: PathBuf,
113 raw_instances: HashMap<String, serde_json::Value>,
114 project_paths: Vec<String>,
115 references: Option<ReferenceMap>,
116 ) -> Self {
117 let project_set: std::collections::HashSet<&str> =
119 project_paths.iter().map(String::as_str).collect();
120
121 let instances = raw_instances
122 .into_iter()
123 .map(|(path, mut value)| {
124 let path_buf = PathBuf::from(&path);
125 let kind = if project_set.contains(path.as_str()) {
127 InstanceKind::Project
128 } else {
129 InstanceKind::Base
130 };
131
132 if let Some(ref refs) = references {
134 enrich_depends_on(&mut value, &path, refs);
135 }
136
137 let instance = Instance {
138 path: path_buf.clone(),
139 kind,
140 value,
141 };
142 (path_buf, instance)
143 })
144 .collect();
145
146 Self { root, instances }
147 }
148
149 pub fn bases(&self) -> impl Iterator<Item = &Instance> {
151 self.instances
152 .values()
153 .filter(|i| matches!(i.kind, InstanceKind::Base))
154 }
155
156 pub fn projects(&self) -> impl Iterator<Item = &Instance> {
158 self.instances
159 .values()
160 .filter(|i| matches!(i.kind, InstanceKind::Project))
161 }
162
163 pub fn root_instance(&self) -> Option<&Instance> {
165 self.instances.get(Path::new("."))
166 }
167
168 pub fn get(&self, path: &Path) -> Option<&Instance> {
170 self.instances.get(path)
171 }
172
173 pub fn base_count(&self) -> usize {
175 self.bases().count()
176 }
177
178 pub fn project_count(&self) -> usize {
180 self.projects().count()
181 }
182
183 pub fn ancestors(&self, path: &Path) -> Vec<PathBuf> {
188 if path == Path::new(".") {
190 return Vec::new();
191 }
192
193 let mut ancestors = Vec::new();
194 let mut current = path.to_path_buf();
195
196 while let Some(parent) = current.parent() {
197 if parent.as_os_str().is_empty() {
198 ancestors.push(PathBuf::from("."));
200 break;
201 }
202 ancestors.push(parent.to_path_buf());
203 current = parent.to_path_buf();
204 }
205
206 ancestors
207 }
208
209 pub fn is_inherited(&self, child_path: &Path, field: &str) -> bool {
214 let Some(child) = self.instances.get(child_path) else {
215 return false;
216 };
217
218 let Some(child_value) = child.value.get(field) else {
219 return false;
220 };
221
222 for ancestor_path in self.ancestors(child_path) {
224 if let Some(ancestor) = self.instances.get(&ancestor_path)
225 && let Some(ancestor_value) = ancestor.value.get(field)
226 && child_value == ancestor_value
227 {
228 return true;
229 }
230 }
231
232 false
233 }
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct Instance {
239 pub path: PathBuf,
241
242 pub kind: InstanceKind,
244
245 pub value: serde_json::Value,
247}
248
249impl Instance {
250 pub fn deserialize<T: DeserializeOwned>(&self) -> crate::Result<T> {
262 serde_json::from_value(self.value.clone()).map_err(|e| {
263 Error::configuration(format!(
264 "Failed to deserialize {} as {}: {}",
265 self.path.display(),
266 std::any::type_name::<T>(),
267 e
268 ))
269 })
270 }
271
272 pub fn project_name(&self) -> Option<&str> {
274 if matches!(self.kind, InstanceKind::Project) {
275 self.value.get("name").and_then(|v| v.as_str())
276 } else {
277 None
278 }
279 }
280
281 pub fn get_field(&self, field: &str) -> Option<&serde_json::Value> {
283 self.value.get(field)
284 }
285
286 pub fn has_field(&self, field: &str) -> bool {
288 self.value.get(field).is_some()
289 }
290}
291
292#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
294pub enum InstanceKind {
295 Base,
297 Project,
299}
300
301impl std::fmt::Display for InstanceKind {
302 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
303 match self {
304 Self::Base => write!(f, "Base"),
305 Self::Project => write!(f, "Project"),
306 }
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313 use serde_json::json;
314
315 fn create_test_module() -> ModuleEvaluation {
316 let mut raw = HashMap::new();
317
318 raw.insert(
320 ".".to_string(),
321 json!({
322 "env": { "SHARED": "value" },
323 "owners": { "rules": { "default": { "pattern": "**", "owners": ["@owner"] } } }
324 }),
325 );
326
327 raw.insert(
329 "projects/api".to_string(),
330 json!({
331 "name": "api",
332 "env": { "SHARED": "value" },
333 "owners": { "rules": { "default": { "pattern": "**", "owners": ["@owner"] } } }
334 }),
335 );
336
337 raw.insert(
339 "projects/web".to_string(),
340 json!({
341 "name": "web",
342 "env": { "SHARED": "value" },
343 "owners": { "rules": { "local": { "pattern": "**", "owners": ["@web-team"] } } }
344 }),
345 );
346
347 let project_paths = vec!["projects/api".to_string(), "projects/web".to_string()];
349
350 ModuleEvaluation::from_raw(PathBuf::from("/test/repo"), raw, project_paths, None)
351 }
352
353 #[test]
354 fn test_instance_kind_detection() {
355 let module = create_test_module();
356
357 assert_eq!(module.base_count(), 1);
358 assert_eq!(module.project_count(), 2);
359
360 let root = module.root_instance().unwrap();
361 assert!(matches!(root.kind, InstanceKind::Base));
362
363 let api = module.get(Path::new("projects/api")).unwrap();
364 assert!(matches!(api.kind, InstanceKind::Project));
365 assert_eq!(api.project_name(), Some("api"));
366 }
367
368 #[test]
369 fn test_ancestors() {
370 let module = create_test_module();
371
372 let ancestors = module.ancestors(Path::new("projects/api"));
373 assert_eq!(ancestors.len(), 2);
374 assert_eq!(ancestors[0], PathBuf::from("projects"));
375 assert_eq!(ancestors[1], PathBuf::from("."));
376
377 let root_ancestors = module.ancestors(Path::new("."));
378 assert!(root_ancestors.is_empty());
379 }
380
381 #[test]
382 fn test_is_inherited() {
383 let module = create_test_module();
384
385 assert!(module.is_inherited(Path::new("projects/api"), "owners"));
387
388 assert!(!module.is_inherited(Path::new("projects/web"), "owners"));
390
391 assert!(module.is_inherited(Path::new("projects/api"), "env"));
393 }
394
395 #[test]
396 fn test_instance_kind_display() {
397 assert_eq!(InstanceKind::Base.to_string(), "Base");
398 assert_eq!(InstanceKind::Project.to_string(), "Project");
399 }
400
401 #[test]
402 fn test_instance_deserialize() {
403 #[derive(Debug, Deserialize, PartialEq)]
404 struct TestConfig {
405 name: String,
406 env: std::collections::HashMap<String, String>,
407 }
408
409 let instance = Instance {
410 path: PathBuf::from("test/path"),
411 kind: InstanceKind::Project,
412 value: json!({
413 "name": "my-project",
414 "env": { "FOO": "bar" }
415 }),
416 };
417
418 let config: TestConfig = instance.deserialize().unwrap();
419 assert_eq!(config.name, "my-project");
420 assert_eq!(config.env.get("FOO"), Some(&"bar".to_string()));
421 }
422
423 #[test]
424 fn test_instance_deserialize_error() {
425 #[derive(Debug, Deserialize)]
426 #[allow(dead_code)]
427 struct RequiredFields {
428 required_field: String,
429 }
430
431 let instance = Instance {
432 path: PathBuf::from("test/path"),
433 kind: InstanceKind::Base,
434 value: json!({}), };
436
437 let result: crate::Result<RequiredFields> = instance.deserialize();
438 assert!(result.is_err());
439
440 let err = result.unwrap_err();
441 let msg = err.to_string();
442 assert!(
443 msg.contains("test/path"),
444 "Error should mention path: {}",
445 msg
446 );
447 assert!(
448 msg.contains("RequiredFields"),
449 "Error should mention target type: {}",
450 msg
451 );
452 }
453
454 #[test]
459 fn test_module_evaluation_empty() {
460 let module =
461 ModuleEvaluation::from_raw(PathBuf::from("/test"), HashMap::new(), vec![], None);
462
463 assert_eq!(module.base_count(), 0);
464 assert_eq!(module.project_count(), 0);
465 assert!(module.root_instance().is_none());
466 }
467
468 #[test]
469 fn test_module_evaluation_root_only() {
470 let mut raw = HashMap::new();
471 raw.insert(".".to_string(), json!({"key": "value"}));
472
473 let module = ModuleEvaluation::from_raw(PathBuf::from("/test"), raw, vec![], None);
474
475 assert_eq!(module.base_count(), 1);
476 assert_eq!(module.project_count(), 0);
477 assert!(module.root_instance().is_some());
478 }
479
480 #[test]
481 fn test_module_evaluation_get_nonexistent() {
482 let module =
483 ModuleEvaluation::from_raw(PathBuf::from("/test"), HashMap::new(), vec![], None);
484
485 assert!(module.get(Path::new("nonexistent")).is_none());
486 }
487
488 #[test]
489 fn test_module_evaluation_multiple_projects() {
490 let mut raw = HashMap::new();
491 raw.insert("proj1".to_string(), json!({"name": "proj1"}));
492 raw.insert("proj2".to_string(), json!({"name": "proj2"}));
493 raw.insert("proj3".to_string(), json!({"name": "proj3"}));
494
495 let project_paths = vec![
496 "proj1".to_string(),
497 "proj2".to_string(),
498 "proj3".to_string(),
499 ];
500
501 let module = ModuleEvaluation::from_raw(PathBuf::from("/test"), raw, project_paths, None);
502
503 assert_eq!(module.project_count(), 3);
504 assert_eq!(module.base_count(), 0);
505 }
506
507 #[test]
508 fn test_module_evaluation_ancestors_deep_path() {
509 let module =
510 ModuleEvaluation::from_raw(PathBuf::from("/test"), HashMap::new(), vec![], None);
511
512 let ancestors = module.ancestors(Path::new("a/b/c/d"));
513 assert_eq!(ancestors.len(), 4);
514 assert_eq!(ancestors[0], PathBuf::from("a/b/c"));
515 assert_eq!(ancestors[1], PathBuf::from("a/b"));
516 assert_eq!(ancestors[2], PathBuf::from("a"));
517 assert_eq!(ancestors[3], PathBuf::from("."));
518 }
519
520 #[test]
521 fn test_module_evaluation_is_inherited_no_child() {
522 let module =
523 ModuleEvaluation::from_raw(PathBuf::from("/test"), HashMap::new(), vec![], None);
524
525 assert!(!module.is_inherited(Path::new("nonexistent"), "field"));
527 }
528
529 #[test]
530 fn test_module_evaluation_is_inherited_no_field() {
531 let mut raw = HashMap::new();
532 raw.insert("child".to_string(), json!({"other": "value"}));
533
534 let module = ModuleEvaluation::from_raw(PathBuf::from("/test"), raw, vec![], None);
535
536 assert!(!module.is_inherited(Path::new("child"), "missing_field"));
538 }
539
540 #[test]
545 fn test_instance_get_field() {
546 let instance = Instance {
547 path: PathBuf::from("test"),
548 kind: InstanceKind::Project,
549 value: json!({
550 "name": "my-project",
551 "version": "1.0.0"
552 }),
553 };
554
555 assert_eq!(instance.get_field("name"), Some(&json!("my-project")));
556 assert_eq!(instance.get_field("version"), Some(&json!("1.0.0")));
557 assert!(instance.get_field("nonexistent").is_none());
558 }
559
560 #[test]
561 fn test_instance_has_field() {
562 let instance = Instance {
563 path: PathBuf::from("test"),
564 kind: InstanceKind::Project,
565 value: json!({"name": "test", "env": {}}),
566 };
567
568 assert!(instance.has_field("name"));
569 assert!(instance.has_field("env"));
570 assert!(!instance.has_field("missing"));
571 }
572
573 #[test]
574 fn test_instance_project_name_base() {
575 let instance = Instance {
576 path: PathBuf::from("test"),
577 kind: InstanceKind::Base,
578 value: json!({"name": "should-be-ignored"}),
579 };
580
581 assert!(instance.project_name().is_none());
583 }
584
585 #[test]
586 fn test_instance_project_name_missing() {
587 let instance = Instance {
588 path: PathBuf::from("test"),
589 kind: InstanceKind::Project,
590 value: json!({}),
591 };
592
593 assert!(instance.project_name().is_none());
594 }
595
596 #[test]
597 fn test_instance_clone() {
598 let instance = Instance {
599 path: PathBuf::from("original"),
600 kind: InstanceKind::Project,
601 value: json!({"name": "test"}),
602 };
603
604 let cloned = instance.clone();
605 assert_eq!(cloned.path, instance.path);
606 assert_eq!(cloned.kind, instance.kind);
607 assert_eq!(cloned.value, instance.value);
608 }
609
610 #[test]
611 fn test_instance_serialize() {
612 let instance = Instance {
613 path: PathBuf::from("test/path"),
614 kind: InstanceKind::Project,
615 value: json!({"name": "my-project"}),
616 };
617
618 let json = serde_json::to_string(&instance).unwrap();
619 assert!(json.contains("test/path"));
620 assert!(json.contains("Project"));
621 assert!(json.contains("my-project"));
622 }
623
624 #[test]
629 fn test_instance_kind_equality() {
630 assert_eq!(InstanceKind::Base, InstanceKind::Base);
631 assert_eq!(InstanceKind::Project, InstanceKind::Project);
632 assert_ne!(InstanceKind::Base, InstanceKind::Project);
633 }
634
635 #[test]
636 fn test_instance_kind_copy() {
637 let kind = InstanceKind::Project;
638 let copied = kind;
639 assert_eq!(kind, copied);
640 }
641
642 #[test]
643 fn test_instance_kind_serialize() {
644 let base_json = serde_json::to_string(&InstanceKind::Base).unwrap();
645 let project_json = serde_json::to_string(&InstanceKind::Project).unwrap();
646
647 assert!(base_json.contains("Base"));
648 assert!(project_json.contains("Project"));
649 }
650
651 #[test]
652 fn test_instance_kind_deserialize() {
653 let base: InstanceKind = serde_json::from_str("\"Base\"").unwrap();
654 let project: InstanceKind = serde_json::from_str("\"Project\"").unwrap();
655
656 assert_eq!(base, InstanceKind::Base);
657 assert_eq!(project, InstanceKind::Project);
658 }
659}