schema2000/renderer/
json_schema_renderer.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::model::{ArrayNode, NodeType, ObjectNode, ObjectProperty};
4use crate::SchemaHypothesis;
5use serde_json::json;
6use serde_json::value::Value;
7use serde_json::Map;
8
9#[must_use]
10#[allow(clippy::missing_panics_doc)]
11pub fn render_schema(schema: &SchemaHypothesis) -> String {
12    serde_json::to_string_pretty(&render_json_schema(schema)).unwrap()
13}
14
15fn render_json_schema(schema: &SchemaHypothesis) -> Value {
16    render_node(&schema.root)
17}
18
19fn render_node(node_type: &NodeType) -> Value {
20    match node_type {
21        NodeType::String(_) => json!({"type": "string"}),
22        NodeType::Integer(_) => json!({"type": "integer"}),
23        NodeType::Number(_) => json!({"type": "number"}),
24        NodeType::Boolean => json!({"type": "boolean"}),
25        NodeType::Null => json!({"type": "null"}),
26        NodeType::Array(node_types) => Value::Object(generate_array_map(node_types)),
27        NodeType::Object(ObjectNode { properties }) => {
28            Value::Object(generate_object_map(properties))
29        }
30        NodeType::Any(node_types) => Value::Object(generate_any_map(&node_types.nodes)),
31    }
32}
33
34fn generate_any_map(node_types: &BTreeSet<NodeType>) -> Map<String, Value> {
35    let mut map = Map::new();
36    map.insert(
37        "anyOf".to_string(),
38        node_types.iter().map(render_node).collect(),
39    );
40
41    map
42}
43
44fn generate_array_map(node_type: &ArrayNode) -> Map<String, Value> {
45    let mut map = Map::new();
46    map.insert("type".to_string(), Value::String("array".to_string()));
47    node_type
48        .items
49        .as_ref()
50        .map(|node_type| map.insert("items".to_string(), render_node(node_type)));
51    map
52}
53
54fn generate_object_map(properties: &BTreeMap<String, ObjectProperty>) -> Map<String, Value> {
55    let required_props: Vec<Value> = properties
56        .iter()
57        .filter_map(|(key, value)| {
58            if value.required {
59                Option::Some(Value::String(key.to_string()))
60            } else {
61                Option::None
62            }
63        })
64        .collect();
65
66    let object_properties: Map<String, Value> = properties
67        .iter()
68        .map(|(key, value)| (key.to_string(), render_node(&value.node_type)))
69        .collect();
70
71    let mut map = Map::new();
72
73    map.insert("type".to_string(), Value::String("object".to_string()));
74    map.insert("required".to_string(), Value::Array(required_props));
75    map.insert("properties".to_string(), Value::Object(object_properties));
76
77    map
78}
79
80#[cfg(test)]
81mod test {
82    use maplit::{btreemap, btreeset};
83    use serde_json::json;
84
85    use crate::model::{
86        AnyNode, ArrayNode, IntegerNode, NodeType, ObjectNode, ObjectProperty, SchemaHypothesis,
87        StringNode,
88    };
89    use crate::renderer::json_schema_renderer::{render_json_schema, render_node};
90
91    #[test]
92    fn test_object() {
93        let hypothesis = SchemaHypothesis::new(ObjectNode::new(btreemap! {
94            "name".to_string() => ObjectProperty::new(StringNode::new()),
95        }));
96
97        let actual = render_json_schema(&hypothesis);
98
99        assert_eq!(
100            actual,
101            json!(
102                {
103                    "type": "object",
104                    "required": ["name"],
105                    "properties": {
106                        "name": {
107                            "type": "string"
108                        }
109                    }
110                }
111            )
112        );
113    }
114
115    #[test]
116    fn test_array() {
117        let hypothesis = SchemaHypothesis::new(ArrayNode::new_many(btreeset![
118            StringNode::new().into(),
119            IntegerNode::new().into()
120        ]));
121
122        let actual = render_json_schema(&hypothesis);
123
124        assert_eq!(
125            actual,
126            json!(
127                {
128                    "type": "array",
129                    "items": {
130                        "anyOf": [
131                            {
132                                "type": "integer"
133                            },
134                            {
135                                "type": "string"
136                            }
137                        ]
138                    }
139                }
140            )
141        );
142    }
143
144    #[test]
145    fn test_array_single_type() {
146        let hypothesis =
147            SchemaHypothesis::new(ArrayNode::new_many(btreeset!(StringNode::new().into())));
148
149        let actual = render_json_schema(&hypothesis);
150
151        assert_eq!(
152            actual,
153            json!(
154                {
155                    "type": "array",
156                    "items": {
157                        "type": "string"
158                    }
159                }
160            )
161        );
162    }
163
164    #[test]
165    fn test_empty_array() {
166        let hypothesis = SchemaHypothesis::new(ArrayNode::new_untyped());
167
168        let actual = render_json_schema(&hypothesis);
169
170        assert_eq!(actual, json!({ "type": "array" }));
171    }
172
173    #[test]
174    fn test_any() {
175        let node_type = AnyNode::new(btreeset![StringNode::new().into(), NodeType::Boolean]).into();
176
177        let actual = render_node(&node_type);
178
179        assert_eq!(
180            actual,
181            json!({
182                "anyOf": [
183                    {"type": "boolean"},
184                    {"type": "string"},
185                ]
186            })
187        );
188    }
189
190    #[test]
191    fn test_any_one() {
192        let node_type = AnyNode::new(btreeset![StringNode::new().into()]).into();
193
194        let actual = render_node(&node_type);
195
196        assert_eq!(
197            actual,
198            json!({
199                "anyOf": [
200                    {"type": "string"}
201                ]
202            })
203        );
204    }
205
206    #[test]
207    fn test_any_empty() {
208        let node_type = AnyNode::new(btreeset![]).into();
209
210        let actual = render_node(&node_type);
211
212        assert_eq!(
213            actual,
214            json!({
215                "anyOf": []
216            })
217        );
218    }
219
220    #[test]
221    fn test_any_complex_types() {
222        let node_type = AnyNode::new(btreeset![ObjectNode::new(btreemap! {
223            "id".to_string() => ObjectProperty::new(IntegerNode::new())
224        })
225        .into()])
226        .into();
227
228        let actual = render_node(&node_type);
229
230        assert_eq!(
231            actual,
232            json!({
233                "anyOf": [
234                    {
235                        "type": "object",
236                        "properties": {
237                            "id": {
238                                "type": "integer"
239                            }
240                        },
241                        "required": ["id"]
242                    }
243                ]
244            })
245        );
246    }
247}