genson-core 0.7.6

Core library for JSON schema inference using genson-rs
Documentation
use serde_json::{json, Value};

use crate::genson_rs::node::{DataType, SchemaNode};

const DEFAULT_SCHEMA_URI: &str = "http://json-schema.org/schema#";
const NULL_SCHEMA_URI: &str = "NULL";

pub struct SchemaBuilder {
    schema_uri: Option<String>,
    root_node: SchemaNode,
}

impl SchemaBuilder {
    /// Create a new SchemaBuilder object. The schema_uri parameter is optional, a value
    /// of "AUTO" will automatically detect the schema URI based on the input schema, if no
    /// schema URI was detected, a default URI of "<http://json-schema.org/schema#>" will be used.
    /// A value of None will leave out the "$schema" keyword in the output schema.
    pub fn new(schema_uri: Option<&str>) -> Self {
        // TODO: the functionality to allow non-default node class with extended
        //  strategies is not supported yet
        let root_node: SchemaNode = SchemaNode::new();

        if let Some(uri) = schema_uri {
            if uri == "AUTO" {
                SchemaBuilder {
                    schema_uri: None,
                    root_node,
                }
            } else {
                SchemaBuilder {
                    schema_uri: Some(uri.to_string()),
                    root_node,
                }
            }
        } else {
            SchemaBuilder {
                schema_uri: Some(NULL_SCHEMA_URI.to_string()),
                root_node,
            }
        }
    }

    /// Merge in raw JSON schema object
    pub fn add_schema(&mut self, mut schema: Value) {
        self.add_schema_mut(&mut schema);
    }

    /// Like [`add_schema`](Self::add_schema) but leaves `schema` with the caller, so it can be
    /// dropped elsewhere (a large schema is costly to free). Removes `$schema` from it when
    /// that is adopted as this builder's schema URI.
    pub fn add_schema_mut(&mut self, schema: &mut Value) {
        if let Value::Object(schema_obj) = schema {
            if schema_obj.contains_key("$schema") && self.schema_uri.is_none() {
                self.schema_uri = Some(schema_obj["$schema"].as_str().unwrap().to_string());
                schema_obj.shift_remove("$schema");
            }
            self.root_node.add_schema(DataType::Schema(schema));
        } else {
            panic!("Invalid schema type - must be a valid JSON object")
        }
    }

    /// Merge `schemas` in order, same result as calling [`add_schema_mut`](Self::add_schema_mut)
    /// on each, with the merge parallelised across the root object's properties.
    pub fn add_schemas_mut(&mut self, schemas: &mut [Value]) {
        for schema in schemas.iter_mut() {
            let Value::Object(schema_obj) = schema else {
                panic!("Invalid schema type - must be a valid JSON object")
            };
            if schema_obj.contains_key("$schema") && self.schema_uri.is_none() {
                self.schema_uri = Some(schema_obj["$schema"].as_str().unwrap().to_string());
                schema_obj.shift_remove("$schema");
            }
        }
        let refs: Vec<&Value> = schemas.iter().collect();
        if !self.root_node.add_object_schemas_par(&refs) {
            for schema in refs {
                self.root_node.add_schema(DataType::Schema(schema));
            }
        }
    }

    /// Merge another builder into this one. When this builder is still empty and both
    /// share the same `$schema` handling, takes over its nodes instead of round-tripping
    /// through a schema value.
    pub fn absorb(&mut self, other: SchemaBuilder) {
        // `other` with the NULL uri emits no "$schema", so nothing else would be merged.
        if self.root_node.is_empty() && other.schema_uri.as_deref() == Some(NULL_SCHEMA_URI) {
            self.root_node = other.root_node;
        } else {
            self.add_schema(other.to_schema());
        }
    }

    /// Merge multiple raw JSON schema objects at once
    pub fn add_schemas(&mut self, schemas: &[Value]) {
        self.root_node.add_schemas(schemas);
    }

    /// Merge in another SchemaNode object
    pub fn add_schema_node(&mut self, node: SchemaNode) {
        self.root_node.add_schema(DataType::SchemaNode(&node));
    }

    /// Modify the schema to accomodate the input object
    pub fn add_object(&mut self, object: &simd_json::BorrowedValue) {
        self.root_node.add_object(DataType::Object(object));
    }

    /// Export the currently constructed schema as a JSON object
    pub fn to_schema(&self) -> Value {
        let mut base_schema = self.get_base_schema();

        let base_schema_map = base_schema.as_object_mut().unwrap();
        let Value::Object(node_schema_map) = self.root_node.to_schema() else {
            panic!("node schema must be an object")
        };

        for (key, value) in node_schema_map {
            base_schema_map.insert(key, value);
        }
        base_schema
    }

    fn get_base_schema(&self) -> Value {
        if let Some(uri) = &self.schema_uri {
            if uri == NULL_SCHEMA_URI {
                json!({})
            } else {
                json!({"$schema": uri})
            }
        } else {
            json!({"$schema": DEFAULT_SCHEMA_URI})
        }
    }

    /// Serialize the currently constructed schema to a JSON string
    pub fn to_json(&self) -> String {
        let schema = self.to_schema();
        schema.to_string()
    }
}