Skip to main content

agm_core/renderer/
json.rs

1//! JSON renderer: pretty-prints the AgmFile AST using serde_json.
2
3use crate::model::file::AgmFile;
4
5/// Pretty-prints the `AgmFile` AST as JSON using serde_json.
6///
7/// This is a direct serialization of the Rust model types. It preserves
8/// serde field names (e.g., `"node"` for id, `"type"` for node_type).
9/// Optional fields set to `None` are omitted. `span` is skipped.
10#[must_use]
11pub fn render_json(file: &AgmFile) -> String {
12    serde_json::to_string_pretty(file).expect("AgmFile is always serializable")
13}
14
15// ---------------------------------------------------------------------------
16// Tests
17// ---------------------------------------------------------------------------
18
19#[cfg(test)]
20mod tests {
21    use super::*;
22    use crate::model::fields::{FieldValue, NodeType, Span};
23    use crate::model::file::{AgmFile, Header};
24    use crate::model::node::Node;
25    use std::collections::BTreeMap;
26
27    fn minimal_file() -> AgmFile {
28        AgmFile {
29            header: Header {
30                agm: "1".to_owned(),
31                package: "test.minimal".to_owned(),
32                version: "0.1.0".to_owned(),
33                title: None,
34                owner: None,
35                imports: None,
36                default_load: None,
37                description: None,
38                tags: None,
39                status: None,
40                load_profiles: None,
41                target_runtime: None,
42            },
43            nodes: vec![Node {
44                id: "test.node".to_owned(),
45                node_type: NodeType::Facts,
46                summary: "a minimal test node".to_owned(),
47                priority: None,
48                stability: None,
49                confidence: None,
50                status: None,
51                depends: None,
52                related_to: None,
53                replaces: None,
54                conflicts: None,
55                see_also: None,
56                items: None,
57                steps: None,
58                fields: None,
59                input: None,
60                output: None,
61                detail: None,
62                rationale: None,
63                tradeoffs: None,
64                resolution: None,
65                examples: None,
66                notes: None,
67                code: None,
68                code_blocks: None,
69                verify: None,
70                agent_context: None,
71                target: None,
72                execution_status: None,
73                executed_by: None,
74                executed_at: None,
75                execution_log: None,
76                retry_count: None,
77                parallel_groups: None,
78                memory: None,
79                scope: None,
80                applies_when: None,
81                valid_from: None,
82                valid_until: None,
83                tags: None,
84                aliases: None,
85                keywords: None,
86                extra_fields: BTreeMap::new(),
87                span: Span::new(1, 3),
88            }],
89        }
90    }
91
92    #[test]
93    fn test_render_json_minimal_valid_json() {
94        let file = minimal_file();
95        let output = render_json(&file);
96        let parsed: serde_json::Value =
97            serde_json::from_str(&output).expect("should be valid JSON");
98        assert!(parsed.is_object());
99    }
100
101    #[test]
102    fn test_render_json_omits_none_fields() {
103        let file = minimal_file();
104        let output = render_json(&file);
105        assert!(!output.contains("priority"));
106        assert!(!output.contains("depends"));
107        assert!(!output.contains("steps"));
108        assert!(!output.contains("code"));
109        assert!(!output.contains("execution_status"));
110        assert!(!output.contains("memory"));
111        assert!(!output.contains("title"));
112        assert!(!output.contains("owner"));
113    }
114
115    #[test]
116    fn test_render_json_span_not_serialized() {
117        let file = minimal_file();
118        let output = render_json(&file);
119        assert!(!output.contains("start_line"));
120        assert!(!output.contains("end_line"));
121        assert!(!output.contains("span"));
122    }
123
124    #[test]
125    fn test_render_json_uses_spec_field_names() {
126        let file = minimal_file();
127        let output = render_json(&file);
128        // serde renames: id -> "node", node_type -> "type"
129        assert!(output.contains("\"node\""));
130        assert!(output.contains("\"type\""));
131        assert!(!output.contains("\"id\""));
132        assert!(!output.contains("\"node_type\""));
133    }
134
135    #[test]
136    fn test_render_json_extra_fields_inlined() {
137        let mut file = minimal_file();
138        file.nodes[0].extra_fields.insert(
139            "custom_key".to_owned(),
140            FieldValue::Scalar("custom_val".to_owned()),
141        );
142        let output = render_json(&file);
143        assert!(output.contains("custom_key"));
144        assert!(output.contains("custom_val"));
145    }
146
147    #[test]
148    fn test_render_json_agm_version_as_stored_string() {
149        // The raw serde renderer emits agm as stored (string "1"),
150        // while the canonical renderer converts it to integer.
151        let file = minimal_file();
152        let output = render_json(&file);
153        // The agm field is present.
154        assert!(output.contains("\"agm\""));
155    }
156}