schemaui 0.11.0

A Rust library for generating TUI and Web UIs from JSON Schemas for configuration management.
Documentation
use crate::{
    tui::model::{FieldKind, FieldSchema},
    tui::state::field::components::{
        BoolComponent, ComponentPalette, EnumComponent, FieldComponent, TextComponent,
    },
};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use serde_json::json;
use std::sync::Arc;

fn integer_schema() -> FieldSchema {
    FieldSchema {
        name: "count".to_string(),
        path: vec!["count".to_string()],
        pointer: "/count".to_string(),
        title: "count".to_string(),
        description: None,
        kind: FieldKind::Integer,
        required: false,
        default: None,
        metadata: Default::default(),
    }
}

fn string_schema(default: &str) -> FieldSchema {
    FieldSchema {
        name: "name".to_string(),
        path: vec!["name".to_string()],
        pointer: "/name".to_string(),
        title: "name".to_string(),
        description: None,
        kind: FieldKind::String,
        required: false,
        default: Some(json!(default)),
        metadata: Default::default(),
    }
}

fn number_schema() -> FieldSchema {
    FieldSchema {
        name: "ratio".to_string(),
        path: vec!["ratio".to_string()],
        pointer: "/ratio".to_string(),
        title: "ratio".to_string(),
        description: None,
        kind: FieldKind::Number,
        required: false,
        default: None,
        metadata: Default::default(),
    }
}

fn bool_schema() -> FieldSchema {
    FieldSchema {
        name: "enabled".to_string(),
        path: vec!["enabled".to_string()],
        pointer: "/enabled".to_string(),
        title: "enabled".to_string(),
        description: None,
        kind: FieldKind::Boolean,
        required: false,
        default: Some(json!(false)),
        metadata: Default::default(),
    }
}

fn enum_schema() -> FieldSchema {
    FieldSchema {
        name: "mode".to_string(),
        path: vec!["mode".to_string()],
        pointer: "/mode".to_string(),
        title: "mode".to_string(),
        description: None,
        kind: FieldKind::Enum {
            labels: vec!["alpha".to_string(), "beta".to_string(), "gamma".to_string()],
            values: vec![json!("alpha"), json!("beta"), json!("gamma")],
        },
        required: false,
        default: Some(json!("alpha")),
        metadata: Default::default(),
    }
}

#[test]
fn text_component_supports_numeric_stepper() {
    let schema = integer_schema();
    let mut component = TextComponent::new(&schema, Arc::new(ComponentPalette::default()));
    let key = KeyEvent::new(KeyCode::Right, KeyModifiers::NONE);
    assert!(component.handle_key(&schema, &key));
    assert_eq!(component.display_value(&schema), "1");
    let key = KeyEvent::new(KeyCode::Left, KeyModifiers::NONE);
    assert!(component.handle_key(&schema, &key));
    assert_eq!(component.display_value(&schema), "0");
}

#[test]
fn text_component_rejects_control_characters() {
    let schema = integer_schema();
    let mut component = TextComponent::new(&schema, Arc::new(ComponentPalette::default()));
    let ctrl_a = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL);
    assert!(!component.handle_key(&schema, &ctrl_a));
    assert_eq!(component.display_value(&schema), "");
}

#[test]
fn number_input_rejects_non_numeric_chars_but_accepts_decimal_and_exponent_editing() {
    let schema = number_schema();
    let mut component = TextComponent::new(&schema, Arc::new(ComponentPalette::default()));

    assert!(!component.handle_key(
        &schema,
        &KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE)
    ));
    assert_eq!(component.display_value(&schema), "");

    for key in [
        KeyEvent::new(KeyCode::Char('-'), KeyModifiers::NONE),
        KeyEvent::new(KeyCode::Char('1'), KeyModifiers::NONE),
        KeyEvent::new(KeyCode::Char('.'), KeyModifiers::NONE),
        KeyEvent::new(KeyCode::Char('2'), KeyModifiers::NONE),
        KeyEvent::new(KeyCode::Char('e'), KeyModifiers::NONE),
        KeyEvent::new(KeyCode::Char('-'), KeyModifiers::NONE),
        KeyEvent::new(KeyCode::Char('3'), KeyModifiers::NONE),
    ] {
        assert!(
            component.handle_key(&schema, &key),
            "expected {key:?} to be accepted"
        );
    }
    assert_eq!(component.display_value(&schema), "-1.2e-3");

    assert!(!component.handle_key(
        &schema,
        &KeyEvent::new(KeyCode::Char('s'), KeyModifiers::NONE)
    ));
    assert_eq!(component.display_value(&schema), "-1.2e-3");
}

#[test]
fn integer_input_rejects_decimal_and_exponent_chars() {
    let schema = integer_schema();
    let mut component = TextComponent::new(&schema, Arc::new(ComponentPalette::default()));

    assert!(component.handle_key(
        &schema,
        &KeyEvent::new(KeyCode::Char('1'), KeyModifiers::NONE)
    ));
    assert!(!component.handle_key(
        &schema,
        &KeyEvent::new(KeyCode::Char('.'), KeyModifiers::NONE)
    ));
    assert!(!component.handle_key(
        &schema,
        &KeyEvent::new(KeyCode::Char('e'), KeyModifiers::NONE)
    ));
    assert!(!component.handle_key(
        &schema,
        &KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE)
    ));
    assert_eq!(component.display_value(&schema), "1");
}

#[test]
fn text_component_supports_cursor_editing_delete_undo_redo_and_ctrl_w() {
    let schema = string_schema("alpha beta");
    let mut component = TextComponent::new(&schema, Arc::new(ComponentPalette::default()));

    assert!(component.handle_key(&schema, &KeyEvent::new(KeyCode::Left, KeyModifiers::NONE)));
    assert!(component.handle_key(&schema, &KeyEvent::new(KeyCode::Left, KeyModifiers::NONE)));
    assert!(component.handle_key(
        &schema,
        &KeyEvent::new(KeyCode::Char('X'), KeyModifiers::SHIFT)
    ));
    assert_eq!(component.display_value(&schema), "alpha beXta");

    assert!(component.handle_key(&schema, &KeyEvent::new(KeyCode::Delete, KeyModifiers::NONE)));
    assert_eq!(component.display_value(&schema), "alpha beXa");

    assert!(component.handle_key(
        &schema,
        &KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE)
    ));
    assert_eq!(component.display_value(&schema), "alpha bea");

    assert!(component.handle_key(
        &schema,
        &KeyEvent::new(KeyCode::Char('z'), KeyModifiers::CONTROL)
    ));
    assert_eq!(component.display_value(&schema), "alpha beXa");
    assert!(component.handle_key(
        &schema,
        &KeyEvent::new(KeyCode::Char('y'), KeyModifiers::CONTROL)
    ));
    assert_eq!(component.display_value(&schema), "alpha bea");

    let mut component = TextComponent::new(&schema, Arc::new(ComponentPalette::default()));
    assert!(component.handle_key(
        &schema,
        &KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL)
    ));
    assert_eq!(component.display_value(&schema), "alpha ");
    assert!(component.handle_key(
        &schema,
        &KeyEvent::new(KeyCode::Char('z'), KeyModifiers::CONTROL)
    ));
    assert_eq!(component.display_value(&schema), "alpha beta");
}

#[test]
fn enum_and_bool_components_keep_left_right_toggle_behavior() {
    let palette = Arc::new(ComponentPalette::default());

    let bool_schema = bool_schema();
    let mut bool_component = BoolComponent::new(&bool_schema, Arc::clone(&palette));
    assert!(!bool_component.bool_value().expect("bool default"));
    assert!(bool_component.handle_key(
        &bool_schema,
        &KeyEvent::new(KeyCode::Right, KeyModifiers::NONE)
    ));
    assert!(bool_component.bool_value().expect("bool toggled"));
    assert!(bool_component.handle_key(
        &bool_schema,
        &KeyEvent::new(KeyCode::Left, KeyModifiers::NONE)
    ));
    assert!(!bool_component.bool_value().expect("bool toggled back"));

    let enum_schema = enum_schema();
    let FieldKind::Enum { labels, values } = &enum_schema.kind else {
        panic!("expected enum field kind");
    };
    let mut enum_component = EnumComponent::new(labels, values, &enum_schema, Arc::clone(&palette));
    assert_eq!(enum_component.display_value(&enum_schema), "alpha");
    assert!(enum_component.handle_key(
        &enum_schema,
        &KeyEvent::new(KeyCode::Right, KeyModifiers::NONE)
    ));
    assert_eq!(enum_component.display_value(&enum_schema), "beta");
    assert!(enum_component.handle_key(
        &enum_schema,
        &KeyEvent::new(KeyCode::Left, KeyModifiers::NONE)
    ));
    assert_eq!(enum_component.display_value(&enum_schema), "alpha");
}