Skip to main content

fraiseql_cli/commands/federation/
graph.rs

1//! Federation graph export command
2//!
3//! Usage: fraiseql federation graph <schema.compiled.json> [--format=json|dot|mermaid]
4
5use std::{fmt::Display, fs, str::FromStr};
6
7use anyhow::Result;
8use serde::Serialize;
9
10use crate::output::CommandResult;
11
12/// Export format for federation graph
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum GraphFormat {
15    /// JSON format (machine-readable)
16    Json,
17    /// DOT format (Graphviz)
18    Dot,
19    /// Mermaid format (documentation)
20    Mermaid,
21}
22
23impl FromStr for GraphFormat {
24    type Err = String;
25
26    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
27        match s.to_lowercase().as_str() {
28            "json" => Ok(GraphFormat::Json),
29            "dot" => Ok(GraphFormat::Dot),
30            "mermaid" => Ok(GraphFormat::Mermaid),
31            other => Err(format!("Unknown format: {other}. Use json, dot, or mermaid")),
32        }
33    }
34}
35
36impl Display for GraphFormat {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        match self {
39            GraphFormat::Json => write!(f, "json"),
40            GraphFormat::Dot => write!(f, "dot"),
41            GraphFormat::Mermaid => write!(f, "mermaid"),
42        }
43    }
44}
45
46/// Federation graph data
47#[derive(Debug, Serialize)]
48pub struct FederationGraph {
49    /// Subgraphs in the federation
50    pub subgraphs: Vec<Subgraph>,
51
52    /// Edges representing entity relationships
53    pub edges: Vec<Edge>,
54}
55
56/// Subgraph in federation
57#[derive(Debug, Serialize)]
58pub struct Subgraph {
59    /// Subgraph name
60    pub name: String,
61
62    /// Subgraph endpoint URL
63    pub url: String,
64
65    /// Entities provided by this subgraph
66    pub entities: Vec<String>,
67}
68
69/// Edge representing entity relationship between subgraphs
70#[derive(Debug, Serialize)]
71pub struct Edge {
72    /// Source subgraph
73    pub from: String,
74
75    /// Target subgraph
76    pub to: String,
77
78    /// Entity linking the subgraphs
79    pub entity: String,
80}
81
82/// Run federation graph command
83pub fn run(schema_path: &str, format: GraphFormat) -> Result<CommandResult> {
84    // Load schema file
85    let schema_content = fs::read_to_string(schema_path)?;
86
87    // Parse as JSON to verify structure (validation only)
88    let _schema: serde_json::Value = serde_json::from_str(&schema_content)?;
89
90    // Build federation graph (simulated for now)
91    let graph = FederationGraph {
92        subgraphs: vec![
93            Subgraph {
94                name:     "users".to_string(),
95                url:      "http://users.service/graphql".to_string(),
96                entities: vec!["User".to_string()],
97            },
98            Subgraph {
99                name:     "posts".to_string(),
100                url:      "http://posts.service/graphql".to_string(),
101                entities: vec!["Post".to_string()],
102            },
103            Subgraph {
104                name:     "comments".to_string(),
105                url:      "http://comments.service/graphql".to_string(),
106                entities: vec!["Comment".to_string()],
107            },
108        ],
109        edges:     vec![
110            Edge {
111                from:   "users".to_string(),
112                to:     "posts".to_string(),
113                entity: "User".to_string(),
114            },
115            Edge {
116                from:   "posts".to_string(),
117                to:     "comments".to_string(),
118                entity: "Post".to_string(),
119            },
120        ],
121    };
122
123    // Export in requested format
124    let output = match format {
125        GraphFormat::Json => serde_json::to_value(&graph)?,
126        GraphFormat::Dot => serde_json::Value::String(to_dot(&graph)),
127        GraphFormat::Mermaid => serde_json::Value::String(to_mermaid(&graph)),
128    };
129
130    Ok(CommandResult::success("federation/graph", output))
131}
132
133/// Convert federation graph to DOT format (Graphviz)
134fn to_dot(graph: &FederationGraph) -> String {
135    let mut dot = String::from("digraph federation {\n");
136
137    // Add subgraph nodes
138    for subgraph in &graph.subgraphs {
139        let entities = subgraph.entities.join(", ");
140        dot.push_str(&format!(
141            "    {} [label=\"{}\\n[{}]\"];\n",
142            subgraph.name, subgraph.name, entities
143        ));
144    }
145
146    // Add edges
147    for edge in &graph.edges {
148        dot.push_str(&format!("    {} -> {} [label=\"{}\"];\n", edge.from, edge.to, edge.entity));
149    }
150
151    dot.push_str("}\n");
152    dot
153}
154
155/// Convert federation graph to Mermaid format
156fn to_mermaid(graph: &FederationGraph) -> String {
157    let mut mermaid = String::from("graph LR\n");
158
159    // Add nodes
160    for subgraph in &graph.subgraphs {
161        let entities = subgraph.entities.join("<br/>");
162        mermaid.push_str(&format!(
163            "    {}[\"{}\\n[{}\\n]\"]\n",
164            subgraph.name, subgraph.name, entities
165        ));
166    }
167
168    // Add edges
169    for edge in &graph.edges {
170        mermaid.push_str(&format!("    {} -->|{}| {}\n", edge.from, edge.entity, edge.to));
171    }
172
173    mermaid
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn test_graph_format_from_str() {
182        assert_eq!("json".parse::<GraphFormat>().unwrap(), GraphFormat::Json);
183        assert_eq!("dot".parse::<GraphFormat>().unwrap(), GraphFormat::Dot);
184        assert_eq!("mermaid".parse::<GraphFormat>().unwrap(), GraphFormat::Mermaid);
185    }
186
187    #[test]
188    fn test_graph_format_case_insensitive() {
189        assert_eq!("JSON".parse::<GraphFormat>().unwrap(), GraphFormat::Json);
190        assert_eq!("DOT".parse::<GraphFormat>().unwrap(), GraphFormat::Dot);
191    }
192
193    #[test]
194    fn test_graph_format_invalid() {
195        assert!("invalid".parse::<GraphFormat>().is_err());
196    }
197
198    #[test]
199    fn test_to_dot_format() {
200        let graph = FederationGraph {
201            subgraphs: vec![Subgraph {
202                name:     "a".to_string(),
203                url:      "http://a".to_string(),
204                entities: vec!["A".to_string()],
205            }],
206            edges:     vec![],
207        };
208
209        let dot = to_dot(&graph);
210        assert!(dot.contains("digraph"));
211        assert!(dot.contains('a'));
212    }
213
214    #[test]
215    fn test_to_mermaid_format() {
216        let graph = FederationGraph {
217            subgraphs: vec![Subgraph {
218                name:     "a".to_string(),
219                url:      "http://a".to_string(),
220                entities: vec!["A".to_string()],
221            }],
222            edges:     vec![],
223        };
224
225        let mermaid = to_mermaid(&graph);
226        assert!(mermaid.contains("graph"));
227        assert!(mermaid.contains('a'));
228    }
229}