use std::collections::{BTreeMap, BTreeSet};
use serde_json::Value;
use thiserror::Error;
use crate::registry;
use crate::resources::traits::ResourceRef;
#[derive(Debug, Error)]
pub enum GraphError {
#[error("dependency cycle involving: {}", .0.join(" -> "))]
Cycle(Vec<String>),
}
pub fn push_order(items: &[(ResourceRef, Value)]) -> Result<Vec<ResourceRef>, GraphError> {
let in_set: BTreeSet<&ResourceRef> = items.iter().map(|(r, _)| r).collect();
let mut deps: BTreeMap<&ResourceRef, BTreeSet<&ResourceRef>> = BTreeMap::new();
for (r, body) in items {
let entry = deps.entry(r).or_default();
for (kind, name) in registry::extract_references(r.kind, body) {
let dep = ResourceRef { kind, name };
if let Some(&found) = in_set.get(&dep) {
if found != r {
entry.insert(found);
}
}
}
}
let mut order = Vec::with_capacity(items.len());
let mut remaining: BTreeMap<&ResourceRef, BTreeSet<&ResourceRef>> = deps;
while !remaining.is_empty() {
let ready: Vec<&ResourceRef> = remaining
.iter()
.filter(|(_, d)| d.is_empty())
.map(|(r, _)| *r)
.collect();
if ready.is_empty() {
let cycle: Vec<String> = remaining.keys().map(|r| r.to_string()).collect();
return Err(GraphError::Cycle(cycle));
}
for r in &ready {
remaining.remove(*r);
}
for d in remaining.values_mut() {
for r in &ready {
d.remove(*r);
}
}
order.extend(ready.into_iter().cloned());
}
Ok(order)
}
pub fn delete_order(items: &[(ResourceRef, Value)]) -> Result<Vec<ResourceRef>, GraphError> {
let mut order = push_order(items)?;
order.reverse();
Ok(order)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::resources::traits::ResourceKind;
use serde_json::json;
fn r(kind: ResourceKind, name: &str) -> ResourceRef {
ResourceRef::new(kind, name)
}
fn full_chain() -> Vec<(ResourceRef, Value)> {
vec![
(
r(ResourceKind::KnowledgeBase, "kb"),
json!({"name": "kb", "knowledgeSources": [{"name": "ks"}]}),
),
(
r(ResourceKind::Indexer, "idxr"),
json!({
"name": "idxr",
"dataSourceName": "ds",
"targetIndexName": "idx",
"skillsetName": "skills"
}),
),
(
r(ResourceKind::KnowledgeSource, "ks"),
json!({
"name": "ks",
"kind": "searchIndex",
"searchIndexParameters": {"searchIndexName": "idx"}
}),
),
(r(ResourceKind::Index, "idx"), json!({"name": "idx"})),
(
r(ResourceKind::Skillset, "skills"),
json!({"name": "skills"}),
),
(r(ResourceKind::DataSource, "ds"), json!({"name": "ds"})),
(
r(ResourceKind::Agent, "agent"),
json!({
"name": "agent",
"model": "gpt-5-mini",
"tools": [{"type": "mcp", "x-rigg-ref": "knowledge-bases/kb"}]
}),
),
(
r(ResourceKind::Deployment, "gpt-5-mini"),
json!({"name": "gpt-5-mini", "properties": {"raiPolicyName": "rai"}}),
),
(r(ResourceKind::Guardrail, "rai"), json!({"name": "rai"})),
]
}
fn pos(order: &[ResourceRef], kind: ResourceKind, name: &str) -> usize {
order
.iter()
.position(|x| x.kind == kind && x.name == name)
.unwrap_or_else(|| panic!("{name} not in order"))
}
#[test]
fn push_order_respects_all_edges() {
let items = full_chain();
let order = push_order(&items).unwrap();
assert_eq!(order.len(), items.len());
let p = |k, n| pos(&order, k, n);
assert!(p(ResourceKind::DataSource, "ds") < p(ResourceKind::Indexer, "idxr"));
assert!(p(ResourceKind::Index, "idx") < p(ResourceKind::Indexer, "idxr"));
assert!(p(ResourceKind::Skillset, "skills") < p(ResourceKind::Indexer, "idxr"));
assert!(p(ResourceKind::Index, "idx") < p(ResourceKind::KnowledgeSource, "ks"));
assert!(p(ResourceKind::KnowledgeSource, "ks") < p(ResourceKind::KnowledgeBase, "kb"));
assert!(p(ResourceKind::KnowledgeBase, "kb") < p(ResourceKind::Agent, "agent"));
assert!(p(ResourceKind::Guardrail, "rai") < p(ResourceKind::Deployment, "gpt-5-mini"));
assert!(p(ResourceKind::Deployment, "gpt-5-mini") < p(ResourceKind::Agent, "agent"));
}
#[test]
fn delete_order_is_reversed() {
let items = full_chain();
let push = push_order(&items).unwrap();
let mut del = delete_order(&items).unwrap();
del.reverse();
assert_eq!(push, del);
}
#[test]
fn external_references_ignored() {
let items = vec![(
r(ResourceKind::Indexer, "idxr"),
json!({"name": "idxr", "dataSourceName": "not-in-set", "targetIndexName": "idx"}),
)];
let order = push_order(&items).unwrap();
assert_eq!(order.len(), 1);
}
#[test]
fn deterministic_order_for_independent_nodes() {
let items = vec![
(r(ResourceKind::Index, "zebra"), json!({"name": "zebra"})),
(r(ResourceKind::Index, "apple"), json!({"name": "apple"})),
(
r(ResourceKind::DataSource, "mango"),
json!({"name": "mango"}),
),
];
let order = push_order(&items).unwrap();
assert_eq!(order[0].name, "mango");
assert_eq!(order[1].name, "apple");
assert_eq!(order[2].name, "zebra");
}
#[test]
fn cycle_detected_with_names() {
let items = vec![
(
r(ResourceKind::Agent, "a"),
json!({"name": "a", "x-rigg-ref": "agents/b"}),
),
(
r(ResourceKind::Agent, "b"),
json!({"name": "b", "x-rigg-ref": "agents/a"}),
),
];
let err = push_order(&items).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("agents/a") && msg.contains("agents/b"));
}
}