//! Normalize a loose JSON spec into a validated [`TopologyGraph`]. //! //! Mirrors agentorg's `org_adapter` normalization: callers may omit each //! edge's `kind`, and it is filled with the sensible default for the declared //! topology. (YAML/CSV inputs can be added later behind feature flags.) use serde::Deserialize; use crate::graph::{Edge, EdgeKind, Node, TopologyGraph}; use crate::kind::TopologyKind; use crate::TopologyError; #[derive(Deserialize)] struct EdgeSpec { from: String, to: String, #[serde(default)] kind: Option, } #[derive(Deserialize)] struct GraphSpec { kind: TopologyKind, nodes: Vec, #[serde(default)] edges: Vec, } /// The natural edge semantic for a topology, used when a spec omits it. fn default_edge_kind(t: TopologyKind) -> EdgeKind { use TopologyKind::*; match t { Hierarchical => EdgeKind::DelegatesTo, Flat | Mesh | Holacratic | Debate => EdgeKind::PeersWith, Pipeline | Ring => EdgeKind::PipesTo, Swarm | HubSpoke | StarMoe => EdgeKind::RoutesTo, Market => EdgeKind::BidsTo, Blackboard => EdgeKind::ReadsWrites, } } /// Parse and normalize a JSON topology spec into a validated graph. pub fn from_json(s: &str) -> Result { let spec: GraphSpec = serde_json::from_str(s).map_err(|e| TopologyError::Spec(e.to_string()))?; let default = default_edge_kind(spec.kind); let edges = spec .edges .into_iter() .map(|e| Edge { from: e.from, to: e.to, kind: e.kind.unwrap_or(default), }) .collect(); TopologyGraph::new(spec.kind, spec.nodes, edges) } /// Serialize a graph back to pretty JSON. pub fn to_json(g: &TopologyGraph) -> Result { serde_json::to_string_pretty(g).map_err(|e| TopologyError::Spec(e.to_string())) } #[cfg(test)] mod tests { use super::*; #[test] fn fills_default_edge_kind() { let g = from_json( r#"{ "kind": "pipeline", "nodes": [{"id":"a","role":"x"},{"id":"b","role":"y"}], "edges": [{"from":"a","to":"b"}] }"#, ) .unwrap(); assert_eq!(g.edges[0].kind, EdgeKind::PipesTo); } #[test] fn explicit_edge_kind_wins() { let g = from_json( r#"{ "kind": "hierarchical", "nodes": [{"id":"a","role":"x"},{"id":"b","role":"y"}], "edges": [{"from":"a","to":"b","kind":"reports_to"}] }"#, ) .unwrap(); assert_eq!(g.edges[0].kind, EdgeKind::ReportsTo); } #[test] fn round_trips_through_json() { let g = TopologyGraph::new( TopologyKind::HubSpoke, vec![Node::new("hub", "router"), Node::new("s1", "worker")], vec![Edge { from: "hub".into(), to: "s1".into(), kind: EdgeKind::RoutesTo, }], ) .unwrap(); let json = to_json(&g).unwrap(); let back = from_json(&json).unwrap(); assert_eq!(g, back); } #[test] fn bad_spec_is_an_error() { assert!(matches!(from_json("not json"), Err(TopologyError::Spec(_)))); } }