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)]
14pub enum GraphFormat {
15 Json,
17 Dot,
19 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#[derive(Debug, Serialize)]
48pub struct FederationGraph {
49 pub subgraphs: Vec<Subgraph>,
51
52 pub edges: Vec<Edge>,
54}
55
56#[derive(Debug, Serialize)]
58pub struct Subgraph {
59 pub name: String,
61
62 pub url: String,
64
65 pub entities: Vec<String>,
67}
68
69#[derive(Debug, Serialize)]
71pub struct Edge {
72 pub from: String,
74
75 pub to: String,
77
78 pub entity: String,
80}
81
82pub fn run(schema_path: &str, format: GraphFormat) -> Result<CommandResult> {
84 let schema_content = fs::read_to_string(schema_path)?;
86
87 let _schema: serde_json::Value = serde_json::from_str(&schema_content)?;
89
90 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 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
133fn to_dot(graph: &FederationGraph) -> String {
135 let mut dot = String::from("digraph federation {\n");
136
137 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 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
155fn to_mermaid(graph: &FederationGraph) -> String {
157 let mut mermaid = String::from("graph LR\n");
158
159 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 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}