schemaui 0.7.0

A Rust library for generating TUI and Web UIs from JSON Schemas for configuration management.
Documentation
use std::{fs, path::PathBuf};

use anyhow::Result;
use serde::Serialize;
use serde_json::{Value, json};

use crate::core::frontend::{Frontend, FrontendContext};
use crate::core::pipeline::SchemaPipeline;
use crate::io::{DocumentFormat, input::parse_document_str};
use crate::precompile::build_ui_artifact_bundle;
use crate::ui_ast::{UiAst, UiAstBundle, UiLayout};

fn schema_path() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("schemas")
        .join("test-comprehensive.schema.json")
}

fn schema_value() -> Value {
    let path = schema_path();
    let contents = fs::read_to_string(path).expect("schema file readable");
    parse_document_str(&contents, DocumentFormat::Json).expect("schema parses")
}

fn defaults_value() -> Value {
    json!({
        "simpleTypes": {
            "text": "hello from defaults",
            "number": 7,
            "toggle": true,
            "dropdown": "option2"
        }
    })
}

fn recursive_schema_value() -> Value {
    json!({
        "type": "object",
        "properties": {
            "tree": {"$ref": "#/definitions/treeNode"}
        },
        "definitions": {
            "treeNode": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "children": {
                        "type": "array",
                        "items": {"$ref": "#/definitions/treeNode"}
                    }
                }
            }
        }
    })
}

#[derive(Serialize)]
struct CapturedFrontendContext {
    ui_ast: UiAst,
    layout: UiLayout,
    schema: Value,
    initial_data: Value,
}

#[derive(Debug)]
struct CaptureFrontend;

impl Frontend for CaptureFrontend {
    fn run(self, ctx: FrontendContext) -> Result<Value> {
        Ok(serde_json::to_value(CapturedFrontendContext {
            ui_ast: ctx.ui_ast,
            layout: ctx.layout,
            schema: ctx.schema,
            initial_data: ctx.initial_data,
        })?)
    }
}

fn capture_pipeline_output(schema: Value, defaults: Value, bundle: Option<UiAstBundle>) -> Value {
    let pipeline = SchemaPipeline::new(schema)
        .with_defaults(Some(defaults))
        .with_prepared_ui_bundle(bundle);
    pipeline
        .run_with_frontend(CaptureFrontend)
        .expect("pipeline capture succeeds")
}

#[test]
fn schema_pipeline_with_prepared_ui_bundle_matches_runtime_context() {
    let schema = schema_value();
    let defaults = defaults_value();
    let bundle = build_ui_artifact_bundle(&schema, Some(&defaults))
        .expect("build UI artifact bundle")
        .ui;

    let runtime = capture_pipeline_output(schema.clone(), defaults.clone(), None);
    let prepared = capture_pipeline_output(schema, defaults, Some(bundle));

    assert_eq!(runtime, prepared);
}

#[test]
fn schema_pipeline_handles_recursive_schema_without_stack_overflow() {
    let runtime = capture_pipeline_output(
        recursive_schema_value(),
        Value::Object(serde_json::Map::new()),
        None,
    );

    let ui_roots = runtime["ui_ast"]["roots"]
        .as_array()
        .expect("ui_ast roots array");
    assert_eq!(ui_roots.len(), 1);
    assert_eq!(ui_roots[0]["pointer"], json!("/tree"));

    let tree_children = runtime["ui_ast"]["roots"][0]["kind"]["children"]
        .as_array()
        .expect("tree child nodes");
    let child_pointers: Vec<_> = tree_children
        .iter()
        .filter_map(|child| child["pointer"].as_str())
        .collect();
    assert!(child_pointers.contains(&"/tree/name"));
    assert!(child_pointers.contains(&"/tree/children"));

    let layout_roots = runtime["layout"]["roots"].as_array().expect("layout roots");
    assert_eq!(layout_roots.len(), 1);
    let field_pointers = layout_roots[0]["sections"][0]["field_pointers"]
        .as_array()
        .expect("layout field pointers");
    let field_pointers: Vec<_> = field_pointers
        .iter()
        .filter_map(|value| value.as_str())
        .collect();
    assert!(field_pointers.contains(&"/tree/name"));
    assert!(field_pointers.contains(&"/tree/children"));
}