standout 7.2.0

Styled CLI template rendering with automatic terminal detection
Documentation
use clap::Command;
use console::Style;
use proptest::prelude::*;
use serde_json::{json, Value};
use serde_yaml;
use standout::cli::{App, Output};
use standout::{OutputMode, Theme};

// Strategy for generating arbitrary OutputMode values
// Per design guidelines: all 8 output modes must be covered
fn output_mode_strategy() -> impl Strategy<Value = OutputMode> {
    prop_oneof![
        Just(OutputMode::Auto),
        Just(OutputMode::Term),
        Just(OutputMode::Text),
        Just(OutputMode::TermDebug),
        Just(OutputMode::Json),
        Just(OutputMode::Yaml),
        Just(OutputMode::Xml),
        Just(OutputMode::Csv),
    ]
}

// Strategy for generating arbitrary Themes
// Tests: no theme, empty theme, and populated theme with styles
fn theme_strategy() -> impl Strategy<Value = Option<Theme>> {
    prop_oneof![
        Just(None),
        Just(Some(Theme::new())),
        Just(Some(
            Theme::new()
                .add("title", Style::new().bold())
                .add("highlight", Style::new().cyan())
                .add("error", Style::new().red().bold())
        )),
    ]
}

// Strategy for generating template variations
// Tests different rendering paths that work with any JSON input
fn template_strategy() -> impl Strategy<Value = &'static str> {
    prop_oneof![
        // Simple dump - basic MiniJinja path
        Just("{{ . }}"),
        // With style tags - exercises BBParser
        Just("[title]{{ . }}[/title]"),
        // Nested style tags
        Just("[highlight]Output: [title]{{ . }}[/title][/highlight]"),
    ]
}

// Strategy for generating arbitrary JSON data
fn json_data_strategy() -> impl Strategy<Value = Value> {
    let leaf = prop_oneof![
        Just(Value::Null),
        any::<bool>().prop_map(Value::Bool),
        any::<f64>().prop_map(|f| json!(f)),
        "[a-zA-Z0-9]*".prop_map(Value::String),
    ];
    leaf.prop_recursive(
        4,  // 4 levels deep
        64, // Max size 64 nodes
        10, // Items per collection
        |inner| {
            prop_oneof![
                prop::collection::vec(inner.clone(), 0..10).prop_map(Value::Array),
                prop::collection::hash_map("[a-zA-Z0-9]*", inner, 0..10)
                    .prop_map(|m| { Value::Object(m.into_iter().collect()) })
            ]
        },
    )
}

/// Helper to validate structured output
fn validate_structured_output(output: &str, mode: OutputMode) {
    match mode {
        OutputMode::Json => {
            let parsed: Result<Value, _> = serde_json::from_str(output);
            assert!(
                parsed.is_ok(),
                "JSON output should be parseable: {}",
                output
            );
        }
        OutputMode::Yaml => {
            let parsed: Result<Value, _> = serde_yaml::from_str(output);
            assert!(
                parsed.is_ok(),
                "YAML output should be parseable: {}",
                output
            );
        }
        OutputMode::Xml => {
            // XML output must be non-empty and contain a root element
            assert!(!output.is_empty(), "XML output should not be empty");
            assert!(
                output.contains('<') && output.contains('>'),
                "XML output should contain tags: {}",
                output
            );
        }
        OutputMode::Csv => {
            // CSV output must be non-empty
            assert!(!output.is_empty(), "CSV output should not be empty");
        }
        _ => {}
    }
}

proptest! {
    /// Property test for rendering invariants across all output modes
    #[test]
    fn test_rendering_invariants(
        mode in output_mode_strategy(),
        theme in theme_strategy(),
        template in template_strategy(),
        data in json_data_strategy()
    ) {
        let builder = App::builder()
            .command(
                "test",
                move |_m, _ctx| Ok(Output::Render(data.clone())),
                template,
            ).unwrap();

        let builder = if let Some(t) = theme {
            builder.theme(t)
        } else {
            builder
        };

        let app = builder.build().expect("Failed to build app");

        let cmd = Command::new("app").subcommand(Command::new("test"));
        let matches = cmd.try_get_matches_from(["app", "test"]).unwrap();

        let result = app.dispatch(matches, mode);

        // Invariants
        assert!(result.is_handled());
        if let Some(output) = result.output() {
            validate_structured_output(output, mode);
        }
    }
}