1use std::collections::{BTreeMap, BTreeSet};
9
10use serde_json::Value;
11use thiserror::Error;
12
13use crate::registry;
14use crate::resources::traits::ResourceRef;
15
16#[derive(Debug, Error)]
17pub enum GraphError {
18 #[error("dependency cycle involving: {}", .0.join(" -> "))]
19 Cycle(Vec<String>),
20}
21
22pub fn push_order(items: &[(ResourceRef, Value)]) -> Result<Vec<ResourceRef>, GraphError> {
25 let in_set: BTreeSet<&ResourceRef> = items.iter().map(|(r, _)| r).collect();
26
27 let mut deps: BTreeMap<&ResourceRef, BTreeSet<&ResourceRef>> = BTreeMap::new();
29 for (r, body) in items {
30 let entry = deps.entry(r).or_default();
31 for (kind, name) in registry::extract_references(r.kind, body) {
32 let dep = ResourceRef { kind, name };
33 if let Some(&found) = in_set.get(&dep) {
34 if found != r {
35 entry.insert(found);
36 }
37 }
38 }
39 }
40
41 let mut order = Vec::with_capacity(items.len());
43 let mut remaining: BTreeMap<&ResourceRef, BTreeSet<&ResourceRef>> = deps;
44 while !remaining.is_empty() {
45 let ready: Vec<&ResourceRef> = remaining
46 .iter()
47 .filter(|(_, d)| d.is_empty())
48 .map(|(r, _)| *r)
49 .collect();
50 if ready.is_empty() {
51 let cycle: Vec<String> = remaining.keys().map(|r| r.to_string()).collect();
53 return Err(GraphError::Cycle(cycle));
54 }
55 for r in &ready {
56 remaining.remove(*r);
57 }
58 for (_, d) in remaining.iter_mut() {
59 for r in &ready {
60 d.remove(*r);
61 }
62 }
63 order.extend(ready.into_iter().cloned());
64 }
65 Ok(order)
66}
67
68pub fn delete_order(items: &[(ResourceRef, Value)]) -> Result<Vec<ResourceRef>, GraphError> {
70 let mut order = push_order(items)?;
71 order.reverse();
72 Ok(order)
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78 use crate::resources::traits::ResourceKind;
79 use serde_json::json;
80
81 fn r(kind: ResourceKind, name: &str) -> ResourceRef {
82 ResourceRef::new(kind, name)
83 }
84
85 fn full_chain() -> Vec<(ResourceRef, Value)> {
86 vec![
87 (
88 r(ResourceKind::KnowledgeBase, "kb"),
89 json!({"name": "kb", "knowledgeSources": [{"name": "ks"}]}),
90 ),
91 (
92 r(ResourceKind::Indexer, "idxr"),
93 json!({
94 "name": "idxr",
95 "dataSourceName": "ds",
96 "targetIndexName": "idx",
97 "skillsetName": "skills"
98 }),
99 ),
100 (
101 r(ResourceKind::KnowledgeSource, "ks"),
102 json!({
103 "name": "ks",
104 "kind": "searchIndex",
105 "searchIndexParameters": {"searchIndexName": "idx"}
106 }),
107 ),
108 (r(ResourceKind::Index, "idx"), json!({"name": "idx"})),
109 (
110 r(ResourceKind::Skillset, "skills"),
111 json!({"name": "skills"}),
112 ),
113 (r(ResourceKind::DataSource, "ds"), json!({"name": "ds"})),
114 (
115 r(ResourceKind::Agent, "agent"),
116 json!({
117 "name": "agent",
118 "model": "gpt-5-mini",
119 "tools": [{"type": "mcp", "x-rigg-ref": "knowledge-bases/kb"}]
120 }),
121 ),
122 (
123 r(ResourceKind::Deployment, "gpt-5-mini"),
124 json!({"name": "gpt-5-mini", "properties": {"raiPolicyName": "rai"}}),
125 ),
126 (r(ResourceKind::Guardrail, "rai"), json!({"name": "rai"})),
127 ]
128 }
129
130 fn pos(order: &[ResourceRef], kind: ResourceKind, name: &str) -> usize {
131 order
132 .iter()
133 .position(|x| x.kind == kind && x.name == name)
134 .unwrap_or_else(|| panic!("{name} not in order"))
135 }
136
137 #[test]
138 fn push_order_respects_all_edges() {
139 let items = full_chain();
140 let order = push_order(&items).unwrap();
141 assert_eq!(order.len(), items.len());
142 let p = |k, n| pos(&order, k, n);
143 assert!(p(ResourceKind::DataSource, "ds") < p(ResourceKind::Indexer, "idxr"));
144 assert!(p(ResourceKind::Index, "idx") < p(ResourceKind::Indexer, "idxr"));
145 assert!(p(ResourceKind::Skillset, "skills") < p(ResourceKind::Indexer, "idxr"));
146 assert!(p(ResourceKind::Index, "idx") < p(ResourceKind::KnowledgeSource, "ks"));
147 assert!(p(ResourceKind::KnowledgeSource, "ks") < p(ResourceKind::KnowledgeBase, "kb"));
148 assert!(p(ResourceKind::KnowledgeBase, "kb") < p(ResourceKind::Agent, "agent"));
149 assert!(p(ResourceKind::Guardrail, "rai") < p(ResourceKind::Deployment, "gpt-5-mini"));
150 assert!(p(ResourceKind::Deployment, "gpt-5-mini") < p(ResourceKind::Agent, "agent"));
151 }
152
153 #[test]
154 fn delete_order_is_reversed() {
155 let items = full_chain();
156 let push = push_order(&items).unwrap();
157 let mut del = delete_order(&items).unwrap();
158 del.reverse();
159 assert_eq!(push, del);
160 }
161
162 #[test]
163 fn external_references_ignored() {
164 let items = vec![(
165 r(ResourceKind::Indexer, "idxr"),
166 json!({"name": "idxr", "dataSourceName": "not-in-set", "targetIndexName": "idx"}),
167 )];
168 let order = push_order(&items).unwrap();
169 assert_eq!(order.len(), 1);
170 }
171
172 #[test]
173 fn deterministic_order_for_independent_nodes() {
174 let items = vec![
175 (r(ResourceKind::Index, "zebra"), json!({"name": "zebra"})),
176 (r(ResourceKind::Index, "apple"), json!({"name": "apple"})),
177 (
178 r(ResourceKind::DataSource, "mango"),
179 json!({"name": "mango"}),
180 ),
181 ];
182 let order = push_order(&items).unwrap();
183 assert_eq!(order[0].name, "mango");
185 assert_eq!(order[1].name, "apple");
186 assert_eq!(order[2].name, "zebra");
187 }
188
189 #[test]
190 fn cycle_detected_with_names() {
191 let items = vec![
194 (
195 r(ResourceKind::Agent, "a"),
196 json!({"name": "a", "x-rigg-ref": "agents/b"}),
197 ),
198 (
199 r(ResourceKind::Agent, "b"),
200 json!({"name": "b", "x-rigg-ref": "agents/a"}),
201 ),
202 ];
203 let err = push_order(&items).unwrap_err();
204 let msg = err.to_string();
205 assert!(msg.contains("agents/a") && msg.contains("agents/b"));
206 }
207}