1mod error;
20
21use std::path::Path;
22
23use dendryform_core::Diagram;
24
25pub use error::ParseError;
26
27pub fn parse_yaml(input: &str) -> Result<Diagram, ParseError> {
29 let diagram: Diagram = serde_yml::from_str(input)?;
30 Ok(diagram)
31}
32
33pub fn parse_yaml_file(path: impl AsRef<Path>) -> Result<Diagram, ParseError> {
35 let content = std::fs::read_to_string(path)?;
36 parse_yaml(&content)
37}
38
39pub fn parse_json(input: &str) -> Result<Diagram, ParseError> {
41 let diagram: Diagram = serde_json::from_str(input)?;
42 Ok(diagram)
43}
44
45pub fn parse_json_file(path: impl AsRef<Path>) -> Result<Diagram, ParseError> {
47 let content = std::fs::read_to_string(path)?;
48 parse_json(&content)
49}
50
51pub fn version() -> &'static str {
53 env!("CARGO_PKG_VERSION")
54}
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59
60 #[test]
61 fn test_version_is_set() {
62 assert_eq!(version(), "0.1.0");
63 }
64
65 #[test]
66 fn test_parse_taproot_yaml() {
67 let yaml = include_str!("../../../examples/taproot/architecture.yaml");
68 let diagram = parse_yaml(yaml).expect("taproot.yml should parse successfully");
69 assert_eq!(diagram.header().title().text(), "system architecture");
70 assert_eq!(diagram.header().title().accent(), "taproot");
71 assert_eq!(
72 diagram.header().subtitle(),
73 "natural language analytics over BigQuery via MCP"
74 );
75 assert_eq!(diagram.header().theme(), "dark");
76 }
77
78 #[test]
79 fn test_parse_taproot_yaml_layers() {
80 let yaml = include_str!("../../../examples/taproot/architecture.yaml");
81 let diagram = parse_yaml(yaml).unwrap();
82 assert_eq!(diagram.layers().len(), 5);
84 }
85
86 #[test]
87 fn test_parse_taproot_yaml_edges() {
88 let yaml = include_str!("../../../examples/taproot/architecture.yaml");
89 let diagram = parse_yaml(yaml).unwrap();
90 assert_eq!(diagram.edges().len(), 18);
92 }
93
94 #[test]
95 fn test_parse_taproot_yaml_legend() {
96 let yaml = include_str!("../../../examples/taproot/architecture.yaml");
97 let diagram = parse_yaml(yaml).unwrap();
98 assert_eq!(diagram.legend().len(), 6);
99 }
100
101 #[test]
102 fn test_parse_ai_kasu_yaml() {
103 let yaml = include_str!("../../../examples/ai-kasu/architecture.yaml");
104 let diagram = parse_yaml(yaml).expect("ai-kasu architecture.yaml should parse");
105 assert_eq!(diagram.header().title().accent(), "ai-kasu");
106 assert_eq!(diagram.header().title().text(), "MCP server architecture");
107 assert_eq!(diagram.layers().len(), 7);
108 assert_eq!(diagram.legend().len(), 6);
109 assert!(diagram.edges().len() > 20);
110 }
111
112 #[test]
113 fn test_parse_oxur_lisp_yaml() {
114 let yaml = include_str!("../../../examples/oxur-lisp/architecture.yaml");
115 let diagram = parse_yaml(yaml).expect("oxur-lisp architecture.yaml should parse");
116 assert_eq!(diagram.header().title().accent(), "oxur");
117 assert_eq!(diagram.header().title().text(), "language architecture");
118 assert_eq!(diagram.layers().len(), 9);
119 assert_eq!(diagram.legend().len(), 6);
120 assert!(diagram.edges().len() > 15);
121 }
122
123 #[test]
124 fn test_parse_yaml_malformed() {
125 let yaml = "this: is: not: valid: yaml: [";
126 let err = parse_yaml(yaml).unwrap_err();
127 assert!(matches!(err, ParseError::Yaml(_)));
128 let msg = format!("{err}");
129 assert!(msg.contains("YAML parse error"));
130 }
131
132 #[test]
133 fn test_parse_yaml_missing_required_field() {
134 let yaml = r#"
135diagram:
136 title:
137 text: "test"
138 accent: "test"
139 subtitle: "test"
140layers: []
141edges: []
142"#;
143 let err = parse_yaml(yaml).unwrap_err();
145 assert!(matches!(err, ParseError::Yaml(_)));
146 }
147
148 #[test]
149 fn test_parse_yaml_duplicate_node_id() {
150 let yaml = r#"
151diagram:
152 title:
153 text: "test"
154 accent: "t"
155 subtitle: "sub"
156 theme: dark
157layers:
158 - tier:
159 id: main
160 nodes:
161 - id: app
162 kind: system
163 color: blue
164 icon: "◇"
165 title: "App"
166 description: "The app"
167 - id: app
168 kind: system
169 color: blue
170 icon: "◇"
171 title: "App duplicate"
172 description: "Same ID"
173"#;
174 let err = parse_yaml(yaml).unwrap_err();
175 let msg = format!("{err}");
176 assert!(msg.contains("duplicate node ID"));
177 }
178
179 #[test]
180 fn test_parse_yaml_dangling_edge() {
181 let yaml = r#"
182diagram:
183 title:
184 text: "test"
185 accent: "t"
186 subtitle: "sub"
187 theme: dark
188layers:
189 - tier:
190 id: main
191 nodes:
192 - id: app
193 kind: system
194 color: blue
195 icon: "◇"
196 title: "App"
197 description: "The app"
198edges:
199 - from: app
200 to: ghost
201 kind: uses
202"#;
203 let err = parse_yaml(yaml).unwrap_err();
204 let msg = format!("{err}");
205 assert!(msg.contains("ghost"));
206 }
207
208 #[test]
209 fn test_parse_json_round_trip() {
210 let yaml = include_str!("../../../examples/taproot/architecture.yaml");
211 let diagram = parse_yaml(yaml).unwrap();
212 let json = serde_json::to_string_pretty(&diagram).unwrap();
213 let from_json = parse_json(&json).unwrap();
214 assert_eq!(diagram, from_json);
215 }
216
217 #[test]
218 fn test_parse_json_malformed() {
219 let err = parse_json("{invalid json").unwrap_err();
220 assert!(matches!(err, ParseError::Json(_)));
221 let msg = format!("{err}");
222 assert!(msg.contains("JSON parse error"));
223 }
224
225 #[test]
226 fn test_parse_error_io() {
227 let err = parse_yaml_file("/nonexistent/path/to/file.yml").unwrap_err();
228 assert!(matches!(err, ParseError::Io(_)));
229 let msg = format!("{err}");
230 assert!(msg.contains("I/O error"));
231 }
232
233 #[test]
234 fn test_parse_error_display_and_source() {
235 let err = parse_yaml("not valid yaml [").unwrap_err();
236 let source = std::error::Error::source(&err);
238 assert!(source.is_some());
239 }
240
241 #[test]
242 fn test_parse_yaml_empty_tier_rejected() {
243 let yaml = r#"
244diagram:
245 title:
246 text: "test"
247 accent: "t"
248 subtitle: "sub"
249 theme: dark
250layers:
251 - tier:
252 id: empty
253 nodes: []
254"#;
255 let err = parse_yaml(yaml).unwrap_err();
256 let msg = format!("{err}");
257 assert!(msg.contains("empty"));
258 }
259
260 #[test]
261 fn test_parse_yaml_minimal_valid() {
262 let yaml = r#"
263diagram:
264 title:
265 text: "test"
266 accent: "t"
267 subtitle: "sub"
268 theme: dark
269layers:
270 - tier:
271 id: main
272 nodes:
273 - id: app
274 kind: system
275 color: blue
276 icon: "◇"
277 title: "App"
278 description: "The app"
279"#;
280 let diagram = parse_yaml(yaml).unwrap();
281 assert_eq!(diagram.header().title().text(), "test");
282 assert_eq!(diagram.layers().len(), 1);
283 assert!(diagram.edges().is_empty());
284 assert!(diagram.legend().is_empty());
285 }
286
287 #[test]
288 fn test_parse_json_file_nonexistent() {
289 let err = parse_json_file("/nonexistent/path/to/file.json").unwrap_err();
290 assert!(matches!(err, ParseError::Io(_)));
291 let msg = format!("{err}");
292 assert!(msg.contains("I/O error"));
293 }
294
295 #[test]
296 fn test_parse_json_file_round_trip() {
297 let yaml = include_str!("../../../examples/taproot/architecture.yaml");
299 let diagram = parse_yaml(yaml).unwrap();
300 let json = serde_json::to_string_pretty(&diagram).unwrap();
301
302 let tmp_dir = std::env::temp_dir();
303 let tmp_file = tmp_dir.join("dendryform_test_parse_json_file.json");
304 std::fs::write(&tmp_file, &json).unwrap();
305
306 let from_file = parse_json_file(&tmp_file).unwrap();
307 assert_eq!(diagram, from_file);
308
309 let _ = std::fs::remove_file(&tmp_file);
311 }
312
313 #[test]
314 fn test_parse_yaml_file_valid() {
315 let yaml = include_str!("../../../examples/taproot/architecture.yaml");
317 let tmp_dir = std::env::temp_dir();
318 let tmp_file = tmp_dir.join("dendryform_test_parse_yaml_file.yaml");
319 std::fs::write(&tmp_file, yaml).unwrap();
320
321 let diagram = parse_yaml_file(&tmp_file).unwrap();
322 assert_eq!(diagram.header().title().accent(), "taproot");
323 assert_eq!(diagram.layers().len(), 5);
324
325 let _ = std::fs::remove_file(&tmp_file);
327 }
328
329 #[test]
330 fn test_parse_json_malformed_missing_field() {
331 let json = r#"{"diagram": {"title": {"text": "t", "accent": "a"}, "subtitle": "s"}}"#;
332 let err = parse_json(json).unwrap_err();
333 assert!(matches!(err, ParseError::Json(_)));
334 }
335}