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::{FieldState, FormState, RootSectionState, SectionState},
};

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

fn mk_section(id: &str, fields: &[&str]) -> SectionState {
    SectionState {
        id: id.to_string(),
        title: id.to_string(),
        description: None,
        path: vec![id.to_string()],
        depth: 0,
        fields: fields.iter().map(|name| mk_field(name)).collect(),
        scroll_offset: 0,
    }
}

#[test]
fn field_navigation_wraps_across_roots() {
    let root_a = RootSectionState {
        id: "a".into(),
        title: "A".into(),
        description: None,
        sections: vec![mk_section("a/one", &["a1", "a2"])],
    };
    let root_b = RootSectionState {
        id: "b".into(),
        title: "B".into(),
        description: None,
        sections: vec![mk_section("b/one", &["b1"]), mk_section("b/two", &["b2"])],
    };
    let mut state = FormState::from_roots_for_test(vec![root_a, root_b]);
    state.set_field_index(1);
    state.focus_next_field();
    assert_eq!(state.root_index(), 1);
    assert_eq!(state.section_index(), 0);
    assert_eq!(state.field_index(), 0);
    state.focus_next_field();
    assert_eq!(state.section_index(), 1);
    assert_eq!(state.field_index(), 0);
    state.focus_next_field();
    assert_eq!(state.root_index(), 0);
    assert_eq!(state.section_index(), 0);
    assert_eq!(state.field_index(), 0);
}

#[test]
fn field_navigation_wraps_backwards_across_roots() {
    let root_a = RootSectionState {
        id: "a".into(),
        title: "A".into(),
        description: None,
        sections: vec![mk_section("a/one", &["a1"])],
    };
    let root_b = RootSectionState {
        id: "b".into(),
        title: "B".into(),
        description: None,
        sections: vec![mk_section("b/one", &["b1", "b2"])],
    };
    let mut state = FormState::from_roots_for_test(vec![root_a, root_b]);
    state.focus_prev_field();
    assert_eq!(state.root_index(), 1);
    assert_eq!(state.section_index(), 0);
    assert_eq!(state.field_index(), 1);
}

#[test]
fn section_navigation_cycles_ordered_tree() {
    let sections_a = vec![mk_section("auth", &["user"]), mk_section("db", &["url"])];
    let sections_b = vec![mk_section("cache", &["ttl"])];
    let mut state = FormState::from_roots_for_test(vec![
        RootSectionState {
            id: "runtime".into(),
            title: "Runtime".into(),
            description: None,
            sections: sections_a,
        },
        RootSectionState {
            id: "ops".into(),
            title: "Ops".into(),
            description: None,
            sections: sections_b,
        },
    ]);
    state.focus_next_section(1);
    assert_eq!(state.root_index(), 0);
    assert_eq!(state.section_index(), 1);
    state.focus_next_section(1);
    assert_eq!(state.root_index(), 1);
    assert_eq!(state.section_index(), 0);
    state.focus_next_section(1);
    assert_eq!(state.root_index(), 0);
    assert_eq!(state.section_index(), 0);
}

#[test]
fn skips_empty_sections_when_focusing() {
    let empty = mk_section("app", &[]);
    let server = mk_section("server", &["host"]);
    let storage = mk_section("storage", &["path"]);
    let mut state = FormState::from_sections("app", "App", None, vec![empty, server, storage]);
    assert_eq!(
        state.section_index(),
        1,
        "should jump to first populated section"
    );
    state.focus_next_section(1);
    assert_eq!(
        state.section_index(),
        2,
        "Ctrl+Tab should skip empty sections"
    );
    state.focus_next_section(1);
    assert_eq!(
        state.section_index(),
        1,
        "wrap keeps focusable sections only"
    );
}