schemaui 0.12.2

A Rust library for generating TUI and Web UIs from JSON Schemas for configuration management.
Documentation
use serde_json::{Map, Value};

use super::{
    ArrayValidation, ObjectValidation, Result, Schema, SchemaObject, SchemaResolver, UiNode,
    UiNodeKind,
};

pub(super) fn visit_schema_entry(
    resolver: &SchemaResolver<'_>,
    schema: &Schema,
    pointer: String,
    required: bool,
    active_refs: &mut Vec<String>,
) -> Result<UiNode> {
    let recursive_pointer = pointer.clone();
    with_resolved_schema(
        resolver,
        schema,
        active_refs,
        move |resolved| {
            Ok(recursive_boundary_node(
                &resolved,
                recursive_pointer,
                required,
            ))
        },
        move |resolved, active_refs| {
            visit_schema(resolver, &resolved, pointer, required, active_refs)
        },
    )
}

pub(super) fn visit_schema(
    resolver: &SchemaResolver<'_>,
    schema: &SchemaObject,
    pointer: String,
    required: bool,
    active_refs: &mut Vec<String>,
) -> Result<UiNode> {
    if let Some(subschemas) = schema.subschemas.as_ref()
        && let Some(all_of) = subschemas.all_of.as_ref()
        && !all_of.is_empty()
    {
        let merged = super::schema_helpers::merge_all_of(resolver, all_of)?;
        return visit_schema(resolver, &merged, pointer, required, active_refs);
    }

    if let Some(subschemas) = schema.subschemas.as_ref() {
        if let Some(one_of) = subschemas.one_of.as_ref() {
            return super::composite::build_composite_node(
                resolver,
                one_of,
                super::CompositeMode::OneOf,
                schema,
                pointer,
                required,
                active_refs,
            );
        }
        if let Some(any_of) = subschemas.any_of.as_ref() {
            return super::composite::build_composite_node(
                resolver,
                any_of,
                super::CompositeMode::AnyOf,
                schema,
                pointer,
                required,
                active_refs,
            );
        }
    }

    if let Some(template) =
        super::key_value::build_key_value_template(resolver, schema, active_refs)?
    {
        return Ok(UiNode {
            pointer,
            title: super::schema_helpers::schema_title(schema),
            description: super::schema_helpers::schema_description(schema),
            required,
            default_value: super::defaults::schema_default_or_const(schema),
            kind: UiNodeKind::KeyValue {
                template: Box::new(template),
            },
        });
    }

    if super::schema_helpers::is_array_schema(schema) {
        let array = schema.array.as_ref();
        let item_node = match array {
            Some(array) if array.items.is_some() => {
                visit_array_item_kind(resolver, array, active_refs)?
            }
            _ => array_boundary_item_kind(),
        };
        let default_value = super::defaults::schema_default_or_const(schema)
            .or_else(|| Some(Value::Array(Vec::new())));
        return Ok(UiNode {
            pointer,
            title: super::schema_helpers::schema_title(schema),
            description: super::schema_helpers::schema_description(schema),
            required,
            default_value,
            kind: UiNodeKind::Array {
                item: Box::new(item_node),
                min_items: array.and_then(|inner| inner.min_items).map(u64::from),
                max_items: array.and_then(|inner| inner.max_items).map(u64::from),
            },
        });
    }

    if super::schema_helpers::is_object_schema(schema) {
        let fallback_object = ObjectValidation::default();
        let object = schema.object.as_ref().map_or(&fallback_object, Box::as_ref);
        let required_fields = super::schema_helpers::required_list(object);
        let mut children = Vec::new();
        for (name, child_schema) in &object.properties {
            let child_pointer = super::naming::append_pointer(&pointer, name);
            let child = visit_schema_entry(
                resolver,
                child_schema,
                child_pointer,
                required_fields.contains(name),
                active_refs,
            )?;
            children.push(child);
        }
        let default_value =
            super::defaults::schema_default_or_const(schema).or(Some(Value::Object(Map::new())));
        return Ok(UiNode {
            pointer,
            title: super::schema_helpers::schema_title(schema),
            description: super::schema_helpers::schema_description(schema),
            required,
            default_value,
            kind: UiNodeKind::Object {
                children,
                required: required_fields,
            },
        });
    }

    let (scalar, enum_options, enum_values, nullable) = super::defaults::detect_scalar(schema)?;
    let default_value = super::defaults::schema_default_or_const(schema)
        .or_else(|| super::defaults::infer_default_scalar(scalar, enum_values.as_ref()));
    Ok(UiNode {
        pointer,
        title: super::schema_helpers::schema_title(schema),
        description: super::schema_helpers::schema_description(schema),
        required,
        default_value,
        kind: UiNodeKind::Field {
            scalar,
            enum_options,
            enum_values,
            nullable,
        },
    })
}

pub(super) fn visit_kind(
    resolver: &SchemaResolver<'_>,
    schema: &SchemaObject,
    active_refs: &mut Vec<String>,
) -> Result<UiNodeKind> {
    if let Some(subschemas) = schema.subschemas.as_ref()
        && let Some(all_of) = subschemas.all_of.as_ref()
        && !all_of.is_empty()
    {
        let merged = super::schema_helpers::merge_all_of(resolver, all_of)?;
        return visit_kind(resolver, &merged, active_refs);
    }

    if let Some(subschemas) = schema.subschemas.as_ref() {
        if let Some(one_of) = subschemas.one_of.as_ref() {
            return super::composite::build_composite_kind(
                resolver,
                one_of,
                super::CompositeMode::OneOf,
                active_refs,
            );
        }
        if let Some(any_of) = subschemas.any_of.as_ref() {
            return super::composite::build_composite_kind(
                resolver,
                any_of,
                super::CompositeMode::AnyOf,
                active_refs,
            );
        }
    }

    if let Some(template) =
        super::key_value::build_key_value_template(resolver, schema, active_refs)?
    {
        return Ok(UiNodeKind::KeyValue {
            template: Box::new(template),
        });
    }

    if super::schema_helpers::is_array_schema(schema) {
        let array = schema.array.as_ref();
        let item_node = match array {
            Some(array) if array.items.is_some() => {
                visit_array_item_kind(resolver, array, active_refs)?
            }
            _ => array_boundary_item_kind(),
        };
        return Ok(UiNodeKind::Array {
            item: Box::new(item_node),
            min_items: array.and_then(|inner| inner.min_items).map(u64::from),
            max_items: array.and_then(|inner| inner.max_items).map(u64::from),
        });
    }

    if super::schema_helpers::is_object_schema(schema) {
        let fallback_object = ObjectValidation::default();
        let object = schema.object.as_ref().map_or(&fallback_object, Box::as_ref);
        let required_fields = super::schema_helpers::required_list(object);
        let mut children = Vec::new();
        for (name, child_schema) in &object.properties {
            let pointer = super::naming::append_pointer("", name);
            let node = visit_schema_entry(
                resolver,
                child_schema,
                pointer,
                required_fields.contains(name),
                active_refs,
            )?;
            children.push(node);
        }
        return Ok(UiNodeKind::Object {
            children,
            required: required_fields,
        });
    }

    let (scalar, enum_options, enum_values, nullable) = super::defaults::detect_scalar(schema)?;
    Ok(UiNodeKind::Field {
        scalar,
        enum_options,
        enum_values,
        nullable,
    })
}

pub(super) fn visit_array_item_kind(
    resolver: &SchemaResolver<'_>,
    array: &ArrayValidation,
    active_refs: &mut Vec<String>,
) -> Result<UiNodeKind> {
    let item_schema = super::schema_helpers::array_item_schema(array)?;
    with_resolved_schema(
        resolver,
        item_schema,
        active_refs,
        |resolved| normalize_embedded_kind(resolver, &resolved, recursive_boundary_kind(&resolved)),
        |resolved, active_refs| {
            if super::schema_helpers::is_object_schema(&resolved)
                && !super::schema_helpers::has_composite_subschemas(&resolved)
            {
                super::composite::build_single_variant_composite_kind(
                    resolver,
                    &resolved,
                    active_refs,
                )
            } else {
                let kind = visit_kind(resolver, &resolved, active_refs)?;
                normalize_embedded_kind(resolver, &resolved, kind)
            }
        },
    )
}

pub(super) fn normalize_embedded_kind(
    resolver: &SchemaResolver<'_>,
    schema: &SchemaObject,
    kind: UiNodeKind,
) -> Result<UiNodeKind> {
    match kind {
        kind @ UiNodeKind::Array { .. } | kind @ UiNodeKind::Object { .. } => {
            super::composite::build_single_variant_overlay_kind(resolver, schema, kind)
        }
        other => Ok(other),
    }
}

pub(super) fn recursive_boundary_kind(schema: &SchemaObject) -> UiNodeKind {
    if super::schema_helpers::is_array_schema(schema) {
        let array = schema.array.as_ref();
        return UiNodeKind::Array {
            item: Box::new(array_boundary_item_kind()),
            min_items: array.and_then(|inner| inner.min_items).map(u64::from),
            max_items: array.and_then(|inner| inner.max_items).map(u64::from),
        };
    }

    if let Ok((scalar, enum_options, enum_values, nullable)) =
        super::defaults::detect_scalar(schema)
    {
        return UiNodeKind::Field {
            scalar,
            enum_options,
            enum_values,
            nullable,
        };
    }

    UiNodeKind::Object {
        children: Vec::new(),
        required: Vec::new(),
    }
}

pub(super) fn with_resolved_schema<T, F, R>(
    resolver: &SchemaResolver<'_>,
    schema: &Schema,
    active_refs: &mut Vec<String>,
    on_recursive: R,
    on_resolved: F,
) -> Result<T>
where
    F: FnOnce(SchemaObject, &mut Vec<String>) -> Result<T>,
    R: FnOnce(SchemaObject) -> Result<T>,
{
    let resolved = resolver.resolve_schema(schema)?;
    if let Some(reference) = super::schema_reference(schema) {
        if active_refs.iter().any(|active| active == reference) {
            return on_recursive(resolved);
        }
        active_refs.push(reference.to_string());
        let result = on_resolved(resolved, active_refs);
        active_refs.pop();
        result
    } else {
        on_resolved(resolved, active_refs)
    }
}

fn array_boundary_item_kind() -> UiNodeKind {
    UiNodeKind::Object {
        children: Vec::new(),
        required: Vec::new(),
    }
}

fn recursive_boundary_node(schema: &SchemaObject, pointer: String, required: bool) -> UiNode {
    let kind = recursive_boundary_kind(schema);
    let default_value = match &kind {
        UiNodeKind::Field {
            scalar,
            enum_values,
            ..
        } => super::defaults::schema_default_or_const(schema)
            .or_else(|| super::defaults::infer_default_scalar(*scalar, enum_values.as_ref())),
        UiNodeKind::Array { .. } => super::defaults::schema_default_or_const(schema)
            .or_else(|| Some(Value::Array(Vec::new()))),
        UiNodeKind::KeyValue { .. } | UiNodeKind::Object { .. } => {
            super::defaults::schema_default_or_const(schema)
                .or_else(|| Some(Value::Object(Map::new())))
        }
        UiNodeKind::Composite {
            variants,
            allow_multiple,
            ..
        } => super::defaults::schema_default_or_const(schema)
            .or_else(|| super::defaults::infer_default_for_composite(variants, *allow_multiple)),
    };

    UiNode {
        pointer,
        title: super::schema_helpers::schema_title(schema),
        description: super::schema_helpers::schema_description(schema),
        required,
        default_value,
        kind,
    }
}