fraiseql_cli/commands/federation/
graph.rs1use std::{fmt::Display, fs, str::FromStr};
6
7use anyhow::Result;
8use serde::Serialize;
9
10use crate::output::CommandResult;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14#[non_exhaustive]
15pub enum GraphFormat {
16 Json,
18 Dot,
20 Mermaid,
22}
23
24impl FromStr for GraphFormat {
25 type Err = String;
26
27 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
28 match s.to_lowercase().as_str() {
29 "json" => Ok(GraphFormat::Json),
30 "dot" => Ok(GraphFormat::Dot),
31 "mermaid" => Ok(GraphFormat::Mermaid),
32 other => Err(format!("Unknown format: {other}. Use json, dot, or mermaid")),
33 }
34 }
35}
36
37impl Display for GraphFormat {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 match self {
40 GraphFormat::Json => write!(f, "json"),
41 GraphFormat::Dot => write!(f, "dot"),
42 GraphFormat::Mermaid => write!(f, "mermaid"),
43 }
44 }
45}
46
47#[derive(Debug, Serialize)]
49pub struct FederationGraph {
50 pub subgraphs: Vec<Subgraph>,
52
53 pub edges: Vec<Edge>,
55}
56
57#[derive(Debug, Serialize)]
59pub struct Subgraph {
60 pub name: String,
62
63 pub url: String,
65
66 pub entities: Vec<String>,
68}
69
70#[derive(Debug, Serialize)]
72pub struct Edge {
73 pub from: String,
75
76 pub to: String,
78
79 pub entity: String,
81}
82
83pub fn run(schema_path: &str, format: GraphFormat) -> Result<CommandResult> {
90 let schema_content = fs::read_to_string(schema_path)?;
92
93 let _schema: serde_json::Value = serde_json::from_str(&schema_content)?;
95
96 let graph = FederationGraph {
98 subgraphs: vec![
99 Subgraph {
100 name: "users".to_string(),
101 url: "http://users.service/graphql".to_string(),
102 entities: vec!["User".to_string()],
103 },
104 Subgraph {
105 name: "posts".to_string(),
106 url: "http://posts.service/graphql".to_string(),
107 entities: vec!["Post".to_string()],
108 },
109 Subgraph {
110 name: "comments".to_string(),
111 url: "http://comments.service/graphql".to_string(),
112 entities: vec!["Comment".to_string()],
113 },
114 ],
115 edges: vec![
116 Edge {
117 from: "users".to_string(),
118 to: "posts".to_string(),
119 entity: "User".to_string(),
120 },
121 Edge {
122 from: "posts".to_string(),
123 to: "comments".to_string(),
124 entity: "Post".to_string(),
125 },
126 ],
127 };
128
129 let output = match format {
131 GraphFormat::Json => serde_json::to_value(&graph)?,
132 GraphFormat::Dot => serde_json::Value::String(to_dot(&graph)),
133 GraphFormat::Mermaid => serde_json::Value::String(to_mermaid(&graph)),
134 };
135
136 Ok(CommandResult::success("federation/graph", output))
137}
138
139fn to_dot(graph: &FederationGraph) -> String {
141 let mut dot = String::from("digraph federation {\n");
142
143 for subgraph in &graph.subgraphs {
145 let entities = subgraph.entities.join(", ");
146 dot.push_str(&format!(
147 " {} [label=\"{}\\n[{}]\"];\n",
148 subgraph.name, subgraph.name, entities
149 ));
150 }
151
152 for edge in &graph.edges {
154 dot.push_str(&format!(" {} -> {} [label=\"{}\"];\n", edge.from, edge.to, edge.entity));
155 }
156
157 dot.push_str("}\n");
158 dot
159}
160
161fn to_mermaid(graph: &FederationGraph) -> String {
163 let mut mermaid = String::from("graph LR\n");
164
165 for subgraph in &graph.subgraphs {
167 let entities = subgraph.entities.join("<br/>");
168 mermaid.push_str(&format!(
169 " {}[\"{}\\n[{}\\n]\"]\n",
170 subgraph.name, subgraph.name, entities
171 ));
172 }
173
174 for edge in &graph.edges {
176 mermaid.push_str(&format!(" {} -->|{}| {}\n", edge.from, edge.entity, edge.to));
177 }
178
179 mermaid
180}
181
182#[allow(clippy::unwrap_used)] #[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn test_graph_format_from_str() {
189 assert_eq!("json".parse::<GraphFormat>().unwrap(), GraphFormat::Json);
190 assert_eq!("dot".parse::<GraphFormat>().unwrap(), GraphFormat::Dot);
191 assert_eq!("mermaid".parse::<GraphFormat>().unwrap(), GraphFormat::Mermaid);
192 }
193
194 #[test]
195 fn test_graph_format_case_insensitive() {
196 assert_eq!("JSON".parse::<GraphFormat>().unwrap(), GraphFormat::Json);
197 assert_eq!("DOT".parse::<GraphFormat>().unwrap(), GraphFormat::Dot);
198 }
199
200 #[test]
201 fn test_graph_format_invalid() {
202 assert!(
203 "invalid".parse::<GraphFormat>().is_err(),
204 "expected Err for unknown federation graph format"
205 );
206 }
207
208 #[test]
209 fn test_to_dot_format() {
210 let graph = FederationGraph {
211 subgraphs: vec![Subgraph {
212 name: "a".to_string(),
213 url: "http://a".to_string(),
214 entities: vec!["A".to_string()],
215 }],
216 edges: vec![],
217 };
218
219 let dot = to_dot(&graph);
220 assert!(dot.contains("digraph"));
221 assert!(dot.contains('a'));
222 }
223
224 #[test]
225 fn test_to_mermaid_format() {
226 let graph = FederationGraph {
227 subgraphs: vec![Subgraph {
228 name: "a".to_string(),
229 url: "http://a".to_string(),
230 entities: vec!["A".to_string()],
231 }],
232 edges: vec![],
233 };
234
235 let mermaid = to_mermaid(&graph);
236 assert!(mermaid.contains("graph"));
237 assert!(mermaid.contains('a'));
238 }
239}