schema2000 0.1.0

Schema2000 is a crate derive a JSON schema from given JSON objects
Documentation
use std::collections::{BTreeMap, BTreeSet};

use crate::model::{ArrayNode, NodeType, ObjectNode, ObjectProperty};
use crate::SchemaHypothesis;
use serde_json::json;
use serde_json::value::Value;
use serde_json::Map;

#[must_use]
#[allow(clippy::missing_panics_doc)]
pub fn render_schema(schema: &SchemaHypothesis) -> String {
    serde_json::to_string_pretty(&render_json_schema(schema)).unwrap()
}

fn render_json_schema(schema: &SchemaHypothesis) -> Value {
    render_node(&schema.root)
}

fn render_node(node_type: &NodeType) -> Value {
    match node_type {
        NodeType::String(_) => json!({"type": "string"}),
        NodeType::Integer(_) => json!({"type": "integer"}),
        NodeType::Number(_) => json!({"type": "number"}),
        NodeType::Boolean => json!({"type": "boolean"}),
        NodeType::Null => json!({"type": "null"}),
        NodeType::Array(node_types) => Value::Object(generate_array_map(node_types)),
        NodeType::Object(ObjectNode { properties }) => {
            Value::Object(generate_object_map(properties))
        }
        NodeType::Any(node_types) => Value::Object(generate_any_map(&node_types.nodes)),
    }
}

fn generate_any_map(node_types: &BTreeSet<NodeType>) -> Map<String, Value> {
    let mut map = Map::new();
    map.insert(
        "anyOf".to_string(),
        node_types.iter().map(render_node).collect(),
    );

    map
}

fn generate_array_map(node_type: &ArrayNode) -> Map<String, Value> {
    let mut map = Map::new();
    map.insert("type".to_string(), Value::String("array".to_string()));
    node_type
        .items
        .as_ref()
        .map(|node_type| map.insert("items".to_string(), render_node(node_type)));
    map
}

fn generate_object_map(properties: &BTreeMap<String, ObjectProperty>) -> Map<String, Value> {
    let required_props: Vec<Value> = properties
        .iter()
        .filter_map(|(key, value)| {
            if value.required {
                Option::Some(Value::String(key.to_string()))
            } else {
                Option::None
            }
        })
        .collect();

    let object_properties: Map<String, Value> = properties
        .iter()
        .map(|(key, value)| (key.to_string(), render_node(&value.node_type)))
        .collect();

    let mut map = Map::new();

    map.insert("type".to_string(), Value::String("object".to_string()));
    map.insert("required".to_string(), Value::Array(required_props));
    map.insert("properties".to_string(), Value::Object(object_properties));

    map
}

#[cfg(test)]
mod test {
    use maplit::{btreemap, btreeset};
    use serde_json::json;

    use crate::model::{
        AnyNode, ArrayNode, IntegerNode, NodeType, ObjectNode, ObjectProperty, SchemaHypothesis,
        StringNode,
    };
    use crate::renderer::json_schema_renderer::{render_json_schema, render_node};

    #[test]
    fn test_object() {
        let hypothesis = SchemaHypothesis::new(ObjectNode::new(btreemap! {
            "name".to_string() => ObjectProperty::new(StringNode::new()),
        }));

        let actual = render_json_schema(&hypothesis);

        assert_eq!(
            actual,
            json!(
                {
                    "type": "object",
                    "required": ["name"],
                    "properties": {
                        "name": {
                            "type": "string"
                        }
                    }
                }
            )
        );
    }

    #[test]
    fn test_array() {
        let hypothesis = SchemaHypothesis::new(ArrayNode::new_many(btreeset![
            StringNode::new().into(),
            IntegerNode::new().into()
        ]));

        let actual = render_json_schema(&hypothesis);

        assert_eq!(
            actual,
            json!(
                {
                    "type": "array",
                    "items": {
                        "anyOf": [
                            {
                                "type": "integer"
                            },
                            {
                                "type": "string"
                            }
                        ]
                    }
                }
            )
        );
    }

    #[test]
    fn test_array_single_type() {
        let hypothesis =
            SchemaHypothesis::new(ArrayNode::new_many(btreeset!(StringNode::new().into())));

        let actual = render_json_schema(&hypothesis);

        assert_eq!(
            actual,
            json!(
                {
                    "type": "array",
                    "items": {
                        "type": "string"
                    }
                }
            )
        );
    }

    #[test]
    fn test_empty_array() {
        let hypothesis = SchemaHypothesis::new(ArrayNode::new_untyped());

        let actual = render_json_schema(&hypothesis);

        assert_eq!(actual, json!({ "type": "array" }));
    }

    #[test]
    fn test_any() {
        let node_type = AnyNode::new(btreeset![StringNode::new().into(), NodeType::Boolean]).into();

        let actual = render_node(&node_type);

        assert_eq!(
            actual,
            json!({
                "anyOf": [
                    {"type": "boolean"},
                    {"type": "string"},
                ]
            })
        );
    }

    #[test]
    fn test_any_one() {
        let node_type = AnyNode::new(btreeset![StringNode::new().into()]).into();

        let actual = render_node(&node_type);

        assert_eq!(
            actual,
            json!({
                "anyOf": [
                    {"type": "string"}
                ]
            })
        );
    }

    #[test]
    fn test_any_empty() {
        let node_type = AnyNode::new(btreeset![]).into();

        let actual = render_node(&node_type);

        assert_eq!(
            actual,
            json!({
                "anyOf": []
            })
        );
    }

    #[test]
    fn test_any_complex_types() {
        let node_type = AnyNode::new(btreeset![ObjectNode::new(btreemap! {
            "id".to_string() => ObjectProperty::new(IntegerNode::new())
        })
        .into()])
        .into();

        let actual = render_node(&node_type);

        assert_eq!(
            actual,
            json!({
                "anyOf": [
                    {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "integer"
                            }
                        },
                        "required": ["id"]
                    }
                ]
            })
        );
    }
}