1use std::collections::HashMap;
4use std::path::Path;
5
6use anyhow::Result;
7use serde_json::Value;
8
9use crate::resources::ResourceKind;
10
11#[derive(Default)]
13pub struct NameMap {
14 map: HashMap<(ResourceKind, String), String>,
15}
16
17impl NameMap {
18 pub fn new() -> Self {
19 Self {
20 map: HashMap::new(),
21 }
22 }
23
24 pub fn insert(&mut self, kind: ResourceKind, old: &str, new: &str) {
25 self.map.insert((kind, old.to_string()), new.to_string());
26 }
27
28 pub fn get(&self, kind: ResourceKind, old: &str) -> Option<&str> {
29 self.map.get(&(kind, old.to_string())).map(|s| s.as_str())
30 }
31
32 pub fn from_suffix(resources: &[(ResourceKind, String)], suffix: &str) -> Self {
34 let mut map = Self::new();
35 for (kind, name) in resources {
36 map.insert(*kind, name, &format!("{}{}", name, suffix));
37 }
38 map
39 }
40
41 pub fn from_answers_file(path: &Path) -> Result<Self> {
53 let content = std::fs::read_to_string(path)?;
54 let root: Value = serde_json::from_str(&content)?;
55
56 let obj = root
57 .as_object()
58 .ok_or_else(|| anyhow::anyhow!("Answers file must be a JSON object"))?;
59
60 let mut map = Self::new();
61
62 for kind in ResourceKind::all() {
63 if let Some(mappings) = obj.get(kind.api_path()) {
64 let mappings = mappings
65 .as_object()
66 .ok_or_else(|| anyhow::anyhow!("'{}' must be an object", kind.api_path()))?;
67
68 for (old_name, new_name) in mappings {
69 let new_name = new_name.as_str().ok_or_else(|| {
70 anyhow::anyhow!(
71 "Value for '{}' in '{}' must be a string",
72 old_name,
73 kind.api_path()
74 )
75 })?;
76 map.insert(*kind, old_name, new_name);
77 }
78 }
79 }
80
81 Ok(map)
82 }
83
84 fn find_by_name(&self, name: &str) -> Option<&str> {
88 for ((_, old_name), new_name) in &self.map {
89 if old_name == name {
90 return Some(new_name.as_str());
91 }
92 }
93 None
94 }
95}
96
97const REFERENCE_FIELDS: &[(ResourceKind, &[&str])] = &[
100 (
101 ResourceKind::Indexer,
102 &["dataSourceName", "targetIndexName", "skillsetName"],
103 ),
104 (
105 ResourceKind::KnowledgeSource,
106 &["indexName", "knowledgeBaseName"],
107 ),
108];
109
110const ARRAY_REFERENCE_FIELDS: &[(ResourceKind, &str, ResourceKind)] = &[(
113 ResourceKind::KnowledgeBase,
114 "knowledgeSources",
115 ResourceKind::KnowledgeSource,
116)];
117
118pub fn rewrite_references(
121 kind: ResourceKind,
122 definition: &mut Value,
123 name_map: &NameMap,
124) -> Vec<String> {
125 let mut warnings = Vec::new();
126
127 let obj = match definition.as_object_mut() {
128 Some(obj) => obj,
129 None => return warnings,
130 };
131
132 let string_fields = REFERENCE_FIELDS
134 .iter()
135 .find(|(k, _)| *k == kind)
136 .map(|(_, f)| *f)
137 .unwrap_or(&[]);
138
139 for field in string_fields {
140 if let Some(value) = obj.get(*field) {
141 if let Some(old_ref) = value.as_str() {
142 if let Some(new_ref) = name_map.find_by_name(old_ref) {
143 obj.insert(field.to_string(), Value::String(new_ref.to_string()));
144 } else if !old_ref.is_empty() {
145 warnings.push(format!(
146 "{} '{}' references '{}' via '{}' which is not in the copy set",
147 kind.display_name(),
148 obj.get("name")
149 .and_then(|n| n.as_str())
150 .unwrap_or("unknown"),
151 old_ref,
152 field,
153 ));
154 }
155 }
156 }
157 }
158
159 let resource_name = obj
161 .get("name")
162 .and_then(|n| n.as_str())
163 .unwrap_or("unknown")
164 .to_string();
165
166 for (ref_kind, field_name, target_kind) in ARRAY_REFERENCE_FIELDS {
167 if *ref_kind != kind {
168 continue;
169 }
170 if let Some(arr) = obj.get_mut(*field_name) {
171 if let Some(items) = arr.as_array_mut() {
172 for item in items {
173 if let Some(item_obj) = item.as_object_mut() {
174 if let Some(name_val) = item_obj.get("name") {
175 if let Some(old_name) = name_val.as_str() {
176 if let Some(new_name) = name_map.get(*target_kind, old_name) {
177 item_obj.insert(
178 "name".to_string(),
179 Value::String(new_name.to_string()),
180 );
181 } else if !old_name.is_empty() {
182 warnings.push(format!(
183 "{} '{}' references '{}' via '{}' which is not in the copy set",
184 kind.display_name(),
185 resource_name,
186 old_name,
187 field_name,
188 ));
189 }
190 }
191 }
192 }
193 }
194 }
195 }
196 }
197
198 warnings
199}
200
201pub fn compute_dependency_closure(
204 selected: &[(ResourceKind, String, Value)],
205 all_resources: &[(ResourceKind, String, Value)],
206) -> Vec<(ResourceKind, String, Value)> {
207 let mut result: Vec<(ResourceKind, String, Value)> = selected.to_vec();
208 let mut seen: HashMap<(ResourceKind, String), bool> = HashMap::new();
209
210 for (kind, name, _) in &result {
211 seen.insert((*kind, name.clone()), true);
212 }
213
214 let mut changed = true;
215 while changed {
216 changed = false;
217 let current: Vec<_> = result.clone();
218
219 for (kind, _, definition) in ¤t {
220 let ref_fields = REFERENCE_FIELDS
221 .iter()
222 .find(|(k, _)| k == kind)
223 .map(|(_, f)| *f)
224 .unwrap_or(&[]);
225
226 if let Some(obj) = definition.as_object() {
227 for field in ref_fields {
229 if let Some(ref_name) = obj.get(*field).and_then(|v| v.as_str()) {
230 for (ak, an, av) in all_resources {
231 if an == ref_name && !seen.contains_key(&(*ak, an.clone())) {
232 result.push((*ak, an.clone(), av.clone()));
233 seen.insert((*ak, an.clone()), true);
234 changed = true;
235 }
236 }
237 }
238 }
239
240 for (ref_kind, field_name, target_kind) in ARRAY_REFERENCE_FIELDS {
242 if ref_kind != kind {
243 continue;
244 }
245 if let Some(arr) = obj.get(*field_name).and_then(|v| v.as_array()) {
246 for item in arr {
247 if let Some(ref_name) = item
248 .as_object()
249 .and_then(|o| o.get("name"))
250 .and_then(|n| n.as_str())
251 {
252 for (ak, an, av) in all_resources {
253 if *ak == *target_kind
254 && an == ref_name
255 && !seen.contains_key(&(*ak, an.clone()))
256 {
257 result.push((*ak, an.clone(), av.clone()));
258 seen.insert((*ak, an.clone()), true);
259 changed = true;
260 }
261 }
262 }
263 }
264 }
265 }
266 }
267 }
268 }
269
270 result
271}
272
273pub fn expand_recursive(
281 selected: &[(ResourceKind, String, Value)],
282 all_local: &[(ResourceKind, String, Value)],
283) -> Vec<(ResourceKind, String, Value)> {
284 let mut result: Vec<(ResourceKind, String, Value)> = selected.to_vec();
285 let mut seen: std::collections::HashSet<(ResourceKind, String)> =
286 std::collections::HashSet::new();
287
288 for (kind, name, _) in &result {
289 seen.insert((*kind, name.clone()));
290 }
291
292 let mut changed = true;
293 while changed {
294 changed = false;
295 let current: Vec<_> = result.clone();
296
297 for (kind, _, definition) in ¤t {
298 if let Some(obj) = definition.as_object() {
299 let ref_fields = REFERENCE_FIELDS
301 .iter()
302 .find(|(k, _)| k == kind)
303 .map(|(_, f)| *f)
304 .unwrap_or(&[]);
305
306 for field in ref_fields {
307 if let Some(ref_name) = obj.get(*field).and_then(|v| v.as_str()) {
308 if ref_name.is_empty() {
309 continue;
310 }
311 for (ak, an, av) in all_local {
312 if an == ref_name && !seen.contains(&(*ak, an.clone())) {
313 result.push((*ak, an.clone(), av.clone()));
314 seen.insert((*ak, an.clone()));
315 changed = true;
316 }
317 }
318 }
319 }
320
321 for (ref_kind, field_name, target_kind) in ARRAY_REFERENCE_FIELDS {
323 if ref_kind == kind {
324 if let Some(arr) = obj.get(*field_name).and_then(|v| v.as_array()) {
326 for item in arr {
327 if let Some(ref_name) = item
328 .as_object()
329 .and_then(|o| o.get("name"))
330 .and_then(|n| n.as_str())
331 {
332 for (ak, an, av) in all_local {
333 if *ak == *target_kind
334 && an == ref_name
335 && !seen.contains(&(*ak, an.clone()))
336 {
337 result.push((*ak, an.clone(), av.clone()));
338 seen.insert((*ak, an.clone()));
339 changed = true;
340 }
341 }
342 }
343 }
344 }
345 }
346 if target_kind == kind {
347 for (ak, an, av) in all_local {
350 if *ak == *ref_kind && !seen.contains(&(*ak, an.clone())) {
351 if let Some(arr) = av
352 .as_object()
353 .and_then(|o| o.get(*field_name))
354 .and_then(|v| v.as_array())
355 {
356 let resource_name =
357 obj.get("name").and_then(|n| n.as_str()).unwrap_or("");
358 let contains = arr.iter().any(|item| {
359 item.as_object()
360 .and_then(|o| o.get("name"))
361 .and_then(|n| n.as_str())
362 == Some(resource_name)
363 });
364 if contains {
365 result.push((*ak, an.clone(), av.clone()));
366 seen.insert((*ak, an.clone()));
367 changed = true;
368 }
369 }
370 }
371 }
372 }
373 }
374 }
375 }
376 }
377
378 result
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384 use serde_json::json;
385
386 #[test]
387 fn test_name_map_insert_and_get() {
388 let mut map = NameMap::new();
389 map.insert(ResourceKind::Index, "old-idx", "new-idx");
390 assert_eq!(map.get(ResourceKind::Index, "old-idx"), Some("new-idx"));
391 assert_eq!(map.get(ResourceKind::Index, "missing"), None);
392 assert_eq!(map.get(ResourceKind::Indexer, "old-idx"), None);
393 }
394
395 #[test]
396 fn test_name_map_from_suffix() {
397 let resources = vec![
398 (ResourceKind::Index, "my-index".to_string()),
399 (ResourceKind::Indexer, "my-indexer".to_string()),
400 (ResourceKind::DataSource, "my-ds".to_string()),
401 ];
402 let map = NameMap::from_suffix(&resources, "-v2");
403
404 assert_eq!(
405 map.get(ResourceKind::Index, "my-index"),
406 Some("my-index-v2")
407 );
408 assert_eq!(
409 map.get(ResourceKind::Indexer, "my-indexer"),
410 Some("my-indexer-v2")
411 );
412 assert_eq!(map.get(ResourceKind::DataSource, "my-ds"), Some("my-ds-v2"));
413 }
414
415 #[test]
416 fn test_name_map_from_answers_file() {
417 let dir = tempfile::tempdir().unwrap();
418 let path = dir.path().join("answers.json");
419 std::fs::write(
420 &path,
421 r#"{
422 "indexes": { "old-idx": "new-idx" },
423 "indexers": { "old-ixer": "new-ixer" }
424 }"#,
425 )
426 .unwrap();
427
428 let map = NameMap::from_answers_file(&path).unwrap();
429 assert_eq!(map.get(ResourceKind::Index, "old-idx"), Some("new-idx"));
430 assert_eq!(map.get(ResourceKind::Indexer, "old-ixer"), Some("new-ixer"));
431 assert_eq!(map.get(ResourceKind::DataSource, "anything"), None);
432 }
433
434 #[test]
435 fn test_name_map_from_answers_file_missing_sections() {
436 let dir = tempfile::tempdir().unwrap();
437 let path = dir.path().join("answers.json");
438 std::fs::write(&path, r#"{ "indexes": { "a": "b" } }"#).unwrap();
439
440 let map = NameMap::from_answers_file(&path).unwrap();
441 assert_eq!(map.get(ResourceKind::Index, "a"), Some("b"));
442 assert_eq!(map.get(ResourceKind::Indexer, "anything"), None);
443 }
444
445 #[test]
446 fn test_rewrite_references_indexer() {
447 let mut name_map = NameMap::new();
448 name_map.insert(ResourceKind::DataSource, "old-ds", "new-ds");
449 name_map.insert(ResourceKind::Index, "old-idx", "new-idx");
450 name_map.insert(ResourceKind::Skillset, "old-sk", "new-sk");
451
452 let mut definition = json!({
453 "name": "my-indexer",
454 "dataSourceName": "old-ds",
455 "targetIndexName": "old-idx",
456 "skillsetName": "old-sk"
457 });
458
459 let warnings = rewrite_references(ResourceKind::Indexer, &mut definition, &name_map);
460
461 assert!(warnings.is_empty());
462 assert_eq!(definition["dataSourceName"], "new-ds");
463 assert_eq!(definition["targetIndexName"], "new-idx");
464 assert_eq!(definition["skillsetName"], "new-sk");
465 }
466
467 #[test]
468 fn test_rewrite_references_knowledge_source() {
469 let mut name_map = NameMap::new();
470 name_map.insert(ResourceKind::Index, "old-idx", "new-idx");
471 name_map.insert(ResourceKind::KnowledgeBase, "old-kb", "new-kb");
472
473 let mut definition = json!({
474 "name": "my-ks",
475 "indexName": "old-idx",
476 "knowledgeBaseName": "old-kb"
477 });
478
479 let warnings =
480 rewrite_references(ResourceKind::KnowledgeSource, &mut definition, &name_map);
481
482 assert!(warnings.is_empty());
483 assert_eq!(definition["indexName"], "new-idx");
484 assert_eq!(definition["knowledgeBaseName"], "new-kb");
485 }
486
487 #[test]
488 fn test_rewrite_references_warns_on_unmapped() {
489 let name_map = NameMap::new();
490
491 let mut definition = json!({
492 "name": "my-indexer",
493 "dataSourceName": "some-ds",
494 "targetIndexName": "some-idx",
495 "skillsetName": ""
496 });
497
498 let warnings = rewrite_references(ResourceKind::Indexer, &mut definition, &name_map);
499
500 assert_eq!(warnings.len(), 2);
503 assert!(warnings[0].contains("some-ds"));
504 assert!(warnings[1].contains("some-idx"));
505 }
506
507 #[test]
508 fn test_rewrite_references_non_referencing_kind() {
509 let name_map = NameMap::new();
510 let mut definition = json!({ "name": "my-index", "fields": [] });
511
512 let warnings = rewrite_references(ResourceKind::Index, &mut definition, &name_map);
513 assert!(warnings.is_empty());
514 }
515
516 #[test]
517 fn test_compute_dependency_closure_includes_deps() {
518 let selected = vec![(
519 ResourceKind::Indexer,
520 "my-indexer".to_string(),
521 json!({
522 "name": "my-indexer",
523 "dataSourceName": "my-ds",
524 "targetIndexName": "my-idx",
525 "skillsetName": ""
526 }),
527 )];
528
529 let all = vec![
530 (
531 ResourceKind::DataSource,
532 "my-ds".to_string(),
533 json!({ "name": "my-ds", "type": "azureblob" }),
534 ),
535 (
536 ResourceKind::Index,
537 "my-idx".to_string(),
538 json!({ "name": "my-idx", "fields": [] }),
539 ),
540 (
541 ResourceKind::Indexer,
542 "my-indexer".to_string(),
543 json!({
544 "name": "my-indexer",
545 "dataSourceName": "my-ds",
546 "targetIndexName": "my-idx",
547 "skillsetName": ""
548 }),
549 ),
550 (
551 ResourceKind::Index,
552 "unrelated-idx".to_string(),
553 json!({ "name": "unrelated-idx" }),
554 ),
555 ];
556
557 let result = compute_dependency_closure(&selected, &all);
558 assert_eq!(result.len(), 3); let names: Vec<_> = result.iter().map(|(_, n, _)| n.as_str()).collect();
560 assert!(names.contains(&"my-indexer"));
561 assert!(names.contains(&"my-ds"));
562 assert!(names.contains(&"my-idx"));
563 assert!(!names.contains(&"unrelated-idx"));
564 }
565
566 #[test]
567 fn test_compute_dependency_closure_deduplicates() {
568 let selected = vec![
569 (
570 ResourceKind::Indexer,
571 "ixer-1".to_string(),
572 json!({ "name": "ixer-1", "dataSourceName": "shared-ds", "targetIndexName": "idx-1" }),
573 ),
574 (
575 ResourceKind::Indexer,
576 "ixer-2".to_string(),
577 json!({ "name": "ixer-2", "dataSourceName": "shared-ds", "targetIndexName": "idx-2" }),
578 ),
579 ];
580
581 let all = vec![
582 (
583 ResourceKind::DataSource,
584 "shared-ds".to_string(),
585 json!({ "name": "shared-ds" }),
586 ),
587 (
588 ResourceKind::Index,
589 "idx-1".to_string(),
590 json!({ "name": "idx-1" }),
591 ),
592 (
593 ResourceKind::Index,
594 "idx-2".to_string(),
595 json!({ "name": "idx-2" }),
596 ),
597 (
598 ResourceKind::Indexer,
599 "ixer-1".to_string(),
600 json!({ "name": "ixer-1", "dataSourceName": "shared-ds", "targetIndexName": "idx-1" }),
601 ),
602 (
603 ResourceKind::Indexer,
604 "ixer-2".to_string(),
605 json!({ "name": "ixer-2", "dataSourceName": "shared-ds", "targetIndexName": "idx-2" }),
606 ),
607 ];
608
609 let result = compute_dependency_closure(&selected, &all);
610 assert_eq!(result.len(), 5);
612
613 let ds_count = result.iter().filter(|(_, n, _)| n == "shared-ds").count();
615 assert_eq!(ds_count, 1);
616 }
617
618 #[test]
619 fn test_from_suffix_empty() {
620 let resources: Vec<(ResourceKind, String)> = vec![];
621 let map = NameMap::from_suffix(&resources, "-test");
622 assert_eq!(map.get(ResourceKind::Index, "anything"), None);
623 }
624
625 #[test]
626 fn test_rewrite_references_kb_knowledge_sources_array() {
627 let mut name_map = NameMap::new();
628 name_map.insert(ResourceKind::KnowledgeSource, "ks-1", "ks-1-v2");
629 name_map.insert(ResourceKind::KnowledgeSource, "ks-2", "ks-2-v2");
630
631 let mut definition = json!({
632 "name": "my-kb",
633 "knowledgeSources": [
634 {"name": "ks-1"},
635 {"name": "ks-2"}
636 ]
637 });
638
639 let warnings = rewrite_references(ResourceKind::KnowledgeBase, &mut definition, &name_map);
640
641 assert!(warnings.is_empty());
642 let sources = definition["knowledgeSources"].as_array().unwrap();
643 assert_eq!(sources[0]["name"], "ks-1-v2");
644 assert_eq!(sources[1]["name"], "ks-2-v2");
645 }
646
647 #[test]
648 fn test_rewrite_references_kb_warns_on_unmapped_ks() {
649 let name_map = NameMap::new();
650
651 let mut definition = json!({
652 "name": "my-kb",
653 "knowledgeSources": [
654 {"name": "ks-unmapped"}
655 ]
656 });
657
658 let warnings = rewrite_references(ResourceKind::KnowledgeBase, &mut definition, &name_map);
659
660 assert_eq!(warnings.len(), 1);
661 assert!(warnings[0].contains("ks-unmapped"));
662 assert!(warnings[0].contains("knowledgeSources"));
663 }
664
665 #[test]
666 fn test_rewrite_references_kb_no_knowledge_sources_field() {
667 let name_map = NameMap::new();
668 let mut definition = json!({
669 "name": "my-kb",
670 "description": "No KS array"
671 });
672
673 let warnings = rewrite_references(ResourceKind::KnowledgeBase, &mut definition, &name_map);
674
675 assert!(warnings.is_empty());
676 }
677
678 #[test]
679 fn test_compute_dependency_closure_includes_kb_knowledge_sources() {
680 let selected = vec![(
681 ResourceKind::KnowledgeBase,
682 "my-kb".to_string(),
683 json!({
684 "name": "my-kb",
685 "knowledgeSources": [
686 {"name": "ks-1"},
687 {"name": "ks-2"}
688 ]
689 }),
690 )];
691
692 let all = vec![
693 (
694 ResourceKind::KnowledgeBase,
695 "my-kb".to_string(),
696 json!({
697 "name": "my-kb",
698 "knowledgeSources": [
699 {"name": "ks-1"},
700 {"name": "ks-2"}
701 ]
702 }),
703 ),
704 (
705 ResourceKind::KnowledgeSource,
706 "ks-1".to_string(),
707 json!({ "name": "ks-1", "indexName": "idx-1", "knowledgeBaseName": "my-kb" }),
708 ),
709 (
710 ResourceKind::KnowledgeSource,
711 "ks-2".to_string(),
712 json!({ "name": "ks-2", "indexName": "idx-1", "knowledgeBaseName": "my-kb" }),
713 ),
714 (
715 ResourceKind::Index,
716 "idx-1".to_string(),
717 json!({ "name": "idx-1", "fields": [] }),
718 ),
719 (
720 ResourceKind::KnowledgeSource,
721 "ks-other".to_string(),
722 json!({ "name": "ks-other", "indexName": "idx-2", "knowledgeBaseName": "other-kb" }),
723 ),
724 ];
725
726 let result = compute_dependency_closure(&selected, &all);
727 let names: Vec<_> = result.iter().map(|(_, n, _)| n.as_str()).collect();
728 assert!(names.contains(&"my-kb"));
730 assert!(names.contains(&"ks-1"));
731 assert!(names.contains(&"ks-2"));
732 assert!(names.contains(&"idx-1"));
733 assert!(!names.contains(&"ks-other"));
734 }
735
736 #[test]
739 fn test_expand_recursive_includes_kb_ks_children() {
740 let selected = vec![(
741 ResourceKind::KnowledgeBase,
742 "my-kb".to_string(),
743 json!({
744 "name": "my-kb",
745 "knowledgeSources": [
746 {"name": "ks-1"},
747 {"name": "ks-2"}
748 ]
749 }),
750 )];
751
752 let all = vec![
753 selected[0].clone(),
754 (
755 ResourceKind::KnowledgeSource,
756 "ks-1".to_string(),
757 json!({ "name": "ks-1", "indexName": "idx-1", "knowledgeBaseName": "my-kb" }),
758 ),
759 (
760 ResourceKind::KnowledgeSource,
761 "ks-2".to_string(),
762 json!({ "name": "ks-2", "indexName": "idx-1", "knowledgeBaseName": "my-kb" }),
763 ),
764 (
765 ResourceKind::Index,
766 "idx-1".to_string(),
767 json!({ "name": "idx-1", "fields": [] }),
768 ),
769 ];
770
771 let result = expand_recursive(&selected, &all);
772 let names: Vec<_> = result.iter().map(|(_, n, _)| n.as_str()).collect();
773 assert!(names.contains(&"my-kb"));
774 assert!(names.contains(&"ks-1"));
775 assert!(names.contains(&"ks-2"));
776 assert!(names.contains(&"idx-1"));
777 }
778
779 #[test]
780 fn test_expand_recursive_includes_indexer_dependencies() {
781 let selected = vec![(
782 ResourceKind::Indexer,
783 "my-ixer".to_string(),
784 json!({
785 "name": "my-ixer",
786 "dataSourceName": "my-ds",
787 "targetIndexName": "my-idx",
788 "skillsetName": "my-sk"
789 }),
790 )];
791
792 let all = vec![
793 selected[0].clone(),
794 (
795 ResourceKind::DataSource,
796 "my-ds".to_string(),
797 json!({ "name": "my-ds" }),
798 ),
799 (
800 ResourceKind::Index,
801 "my-idx".to_string(),
802 json!({ "name": "my-idx" }),
803 ),
804 (
805 ResourceKind::Skillset,
806 "my-sk".to_string(),
807 json!({ "name": "my-sk" }),
808 ),
809 ];
810
811 let result = expand_recursive(&selected, &all);
812 let names: Vec<_> = result.iter().map(|(_, n, _)| n.as_str()).collect();
813 assert!(names.contains(&"my-ixer"));
814 assert!(names.contains(&"my-ds"));
815 assert!(names.contains(&"my-idx"));
816 assert!(names.contains(&"my-sk"));
817 assert_eq!(result.len(), 4);
818 }
819
820 #[test]
821 fn test_expand_recursive_both_directions() {
822 let selected = vec![(
824 ResourceKind::KnowledgeSource,
825 "ks-1".to_string(),
826 json!({ "name": "ks-1", "indexName": "idx-1", "knowledgeBaseName": "my-kb" }),
827 )];
828
829 let all = vec![
830 selected[0].clone(),
831 (
832 ResourceKind::KnowledgeBase,
833 "my-kb".to_string(),
834 json!({
835 "name": "my-kb",
836 "knowledgeSources": [{"name": "ks-1"}, {"name": "ks-2"}]
837 }),
838 ),
839 (
840 ResourceKind::KnowledgeSource,
841 "ks-2".to_string(),
842 json!({ "name": "ks-2", "indexName": "idx-1", "knowledgeBaseName": "my-kb" }),
843 ),
844 (
845 ResourceKind::Index,
846 "idx-1".to_string(),
847 json!({ "name": "idx-1" }),
848 ),
849 ];
850
851 let result = expand_recursive(&selected, &all);
852 let names: Vec<_> = result.iter().map(|(_, n, _)| n.as_str()).collect();
853 assert!(names.contains(&"ks-1"));
856 assert!(names.contains(&"idx-1"));
857 assert!(names.contains(&"my-kb"));
858 assert!(names.contains(&"ks-2"));
859 }
860
861 #[test]
862 fn test_expand_recursive_deduplicates() {
863 let selected = vec![
864 (
865 ResourceKind::KnowledgeSource,
866 "ks-1".to_string(),
867 json!({ "name": "ks-1", "indexName": "idx-1", "knowledgeBaseName": "my-kb" }),
868 ),
869 (
870 ResourceKind::KnowledgeSource,
871 "ks-2".to_string(),
872 json!({ "name": "ks-2", "indexName": "idx-1", "knowledgeBaseName": "my-kb" }),
873 ),
874 ];
875
876 let all = vec![
877 selected[0].clone(),
878 selected[1].clone(),
879 (
880 ResourceKind::KnowledgeBase,
881 "my-kb".to_string(),
882 json!({
883 "name": "my-kb",
884 "knowledgeSources": [{"name": "ks-1"}, {"name": "ks-2"}]
885 }),
886 ),
887 (
888 ResourceKind::Index,
889 "idx-1".to_string(),
890 json!({ "name": "idx-1" }),
891 ),
892 ];
893
894 let result = expand_recursive(&selected, &all);
895 let idx_count = result.iter().filter(|(_, n, _)| n == "idx-1").count();
897 let kb_count = result.iter().filter(|(_, n, _)| n == "my-kb").count();
898 assert_eq!(idx_count, 1);
899 assert_eq!(kb_count, 1);
900 assert_eq!(result.len(), 4);
901 }
902
903 #[test]
904 fn test_expand_recursive_no_deps_no_children() {
905 let selected = vec![(
906 ResourceKind::Index,
907 "my-idx".to_string(),
908 json!({ "name": "my-idx", "fields": [] }),
909 )];
910
911 let all = vec![
912 selected[0].clone(),
913 (
914 ResourceKind::Index,
915 "other-idx".to_string(),
916 json!({ "name": "other-idx" }),
917 ),
918 ];
919
920 let result = expand_recursive(&selected, &all);
921 assert_eq!(result.len(), 1);
922 assert_eq!(result[0].1, "my-idx");
923 }
924
925 #[test]
926 fn test_expand_recursive_transitive_chain() {
927 let selected = vec![(
929 ResourceKind::KnowledgeBase,
930 "my-kb".to_string(),
931 json!({
932 "name": "my-kb",
933 "knowledgeSources": [{"name": "ks-1"}]
934 }),
935 )];
936
937 let all = vec![
938 selected[0].clone(),
939 (
940 ResourceKind::KnowledgeSource,
941 "ks-1".to_string(),
942 json!({ "name": "ks-1", "indexName": "idx-1", "knowledgeBaseName": "my-kb" }),
943 ),
944 (
945 ResourceKind::Index,
946 "idx-1".to_string(),
947 json!({ "name": "idx-1" }),
948 ),
949 ];
950
951 let result = expand_recursive(&selected, &all);
952 let names: Vec<_> = result.iter().map(|(_, n, _)| n.as_str()).collect();
953 assert!(names.contains(&"my-kb"));
954 assert!(names.contains(&"ks-1"));
955 assert!(names.contains(&"idx-1"));
956 assert_eq!(result.len(), 3);
957 }
958
959 #[test]
960 fn test_expand_recursive_empty_selected() {
961 let all = vec![(
962 ResourceKind::Index,
963 "idx-1".to_string(),
964 json!({ "name": "idx-1" }),
965 )];
966 let result = expand_recursive(&[], &all);
967 assert!(result.is_empty());
968 }
969
970 #[test]
971 fn test_expand_recursive_missing_dep_in_all_local() {
972 let selected = vec![(
974 ResourceKind::Indexer,
975 "my-ixer".to_string(),
976 json!({
977 "name": "my-ixer",
978 "dataSourceName": "missing-ds",
979 "targetIndexName": "also-missing",
980 "skillsetName": ""
981 }),
982 )];
983
984 let result = expand_recursive(&selected, &selected);
985 assert_eq!(result.len(), 1);
986 assert_eq!(result[0].1, "my-ixer");
987 }
988
989 #[test]
990 fn test_rewrite_references_indexer_partial_mapping() {
991 let mut name_map = NameMap::new();
993 name_map.insert(ResourceKind::DataSource, "old-ds", "new-ds");
994
995 let mut definition = json!({
996 "name": "my-indexer",
997 "dataSourceName": "old-ds",
998 "targetIndexName": "unmapped-idx",
999 "skillsetName": "unmapped-sk"
1000 });
1001
1002 let warnings = rewrite_references(ResourceKind::Indexer, &mut definition, &name_map);
1003
1004 assert_eq!(definition["dataSourceName"], "new-ds");
1005 assert_eq!(definition["targetIndexName"], "unmapped-idx"); assert_eq!(definition["skillsetName"], "unmapped-sk"); assert_eq!(warnings.len(), 2);
1008 assert!(warnings[0].contains("unmapped-idx"));
1009 assert!(warnings[1].contains("unmapped-sk"));
1010 }
1011
1012 #[test]
1013 fn test_rewrite_references_null_definition() {
1014 let name_map = NameMap::new();
1015 let mut definition = serde_json::Value::Null;
1016
1017 let warnings = rewrite_references(ResourceKind::Indexer, &mut definition, &name_map);
1018 assert!(warnings.is_empty());
1019 }
1020
1021 #[test]
1022 fn test_rewrite_references_kb_mixed_mapped_and_unmapped() {
1023 let mut name_map = NameMap::new();
1024 name_map.insert(ResourceKind::KnowledgeSource, "ks-1", "ks-1-v2");
1025 let mut definition = json!({
1028 "name": "my-kb",
1029 "knowledgeSources": [
1030 {"name": "ks-1"},
1031 {"name": "ks-2"}
1032 ]
1033 });
1034
1035 let warnings = rewrite_references(ResourceKind::KnowledgeBase, &mut definition, &name_map);
1036
1037 let sources = definition["knowledgeSources"].as_array().unwrap();
1038 assert_eq!(sources[0]["name"], "ks-1-v2"); assert_eq!(sources[1]["name"], "ks-2"); assert_eq!(warnings.len(), 1);
1041 assert!(warnings[0].contains("ks-2"));
1042 }
1043
1044 #[test]
1045 fn test_compute_dependency_closure_empty_selected() {
1046 let all = vec![(
1047 ResourceKind::Index,
1048 "idx-1".to_string(),
1049 json!({ "name": "idx-1" }),
1050 )];
1051 let result = compute_dependency_closure(&[], &all);
1052 assert!(result.is_empty());
1053 }
1054
1055 #[test]
1056 fn test_compute_dependency_closure_empty_all() {
1057 let selected = vec![(
1058 ResourceKind::Indexer,
1059 "my-ixer".to_string(),
1060 json!({
1061 "name": "my-ixer",
1062 "dataSourceName": "my-ds",
1063 "targetIndexName": "my-idx"
1064 }),
1065 )];
1066 let result = compute_dependency_closure(&selected, &[]);
1067 assert_eq!(result.len(), 1);
1069 assert_eq!(result[0].1, "my-ixer");
1070 }
1071
1072 #[test]
1073 fn test_rewrite_references_kb_empty_knowledge_sources_array() {
1074 let name_map = NameMap::new();
1075 let mut definition = json!({
1076 "name": "my-kb",
1077 "knowledgeSources": []
1078 });
1079
1080 let warnings = rewrite_references(ResourceKind::KnowledgeBase, &mut definition, &name_map);
1081
1082 assert!(warnings.is_empty());
1083 assert_eq!(definition["knowledgeSources"].as_array().unwrap().len(), 0);
1084 }
1085
1086 #[test]
1087 fn test_expand_recursive_ks_finds_parent_kb() {
1088 let selected = vec![(
1091 ResourceKind::KnowledgeSource,
1092 "ks-1".to_string(),
1093 json!({ "name": "ks-1", "indexName": "idx-1", "knowledgeBaseName": "my-kb" }),
1094 )];
1095
1096 let all = vec![
1097 selected[0].clone(),
1098 (
1099 ResourceKind::KnowledgeBase,
1100 "my-kb".to_string(),
1101 json!({
1102 "name": "my-kb",
1103 "knowledgeSources": [{"name": "ks-1"}, {"name": "ks-sibling"}]
1104 }),
1105 ),
1106 (
1107 ResourceKind::KnowledgeSource,
1108 "ks-sibling".to_string(),
1109 json!({ "name": "ks-sibling", "indexName": "idx-2", "knowledgeBaseName": "my-kb" }),
1110 ),
1111 (
1112 ResourceKind::Index,
1113 "idx-1".to_string(),
1114 json!({ "name": "idx-1" }),
1115 ),
1116 (
1117 ResourceKind::Index,
1118 "idx-2".to_string(),
1119 json!({ "name": "idx-2" }),
1120 ),
1121 ];
1122
1123 let result = expand_recursive(&selected, &all);
1124 let names: Vec<_> = result.iter().map(|(_, n, _)| n.as_str()).collect();
1125 assert!(names.contains(&"ks-1"));
1126 assert!(names.contains(&"my-kb")); assert!(names.contains(&"ks-sibling")); assert!(names.contains(&"idx-1")); assert!(names.contains(&"idx-2")); assert_eq!(result.len(), 5);
1131 }
1132
1133 #[test]
1134 fn test_rewrite_references_kb_knowledge_sources_not_an_array() {
1135 let name_map = NameMap::new();
1137 let mut definition = json!({
1138 "name": "my-kb",
1139 "knowledgeSources": {"name": "ks-1"}
1140 });
1141
1142 let warnings = rewrite_references(ResourceKind::KnowledgeBase, &mut definition, &name_map);
1143 assert!(warnings.is_empty());
1145 }
1146}