standout-render 14.0.0

Styled terminal rendering with templates, themes, and adaptive color support
use proptest::prelude::*;
use standout_render::{
    render_with_output, ColorPolicy, FormattedText, RenderData, Representation, Theme,
};

const KEY_EXPRESSIONS: [&str; 6] = [
    "raw_key",
    "typed_key",
    "raw_key | style_as('heading')",
    "key_macro()",
    "captured_key",
    "key_macro() | style_as('heading')",
];

const SETUP: &str = "{% macro key_macro() %}[heading]{{ raw_key }}[/heading]{% endmacro %}{% set captured_key %}[heading]{{ raw_key }}[/heading]{% endset %}";

fn render(source: &str, key: &str, value: &str) -> String {
    let data = RenderData::from_iter([
        ("raw_key", RenderData::from(key)),
        (
            "typed_key",
            FormattedText::text(key).styled("heading").unwrap().into(),
        ),
        (
            "value",
            FormattedText::text(value).styled("heading").unwrap().into(),
        ),
    ]);
    render_with_output(
        &format!("{SETUP}{source}"),
        &data,
        &Theme::new().add("heading", console::Style::new().bold()),
        Representation::Human,
        ColorPolicy::Always,
    )
    .unwrap()
}

#[test]
fn formatted_map_keys_match_plain_lookup_without_flattening_values() {
    for key in KEY_EXPRESSIONS {
        assert_eq!(
            render(
                &format!("{{% set item = {{{key}: value}} %}}{{{{ item[raw_key] }}}}"),
                "name",
                "[payload]"
            ),
            render("{{ value }}", "name", "[payload]"),
            "{key}"
        );
    }
}

#[test]
fn formatted_subscripts_find_plain_map_keys_without_flattening_values() {
    for key in KEY_EXPRESSIONS {
        assert_eq!(
            render(
                &format!("{{% set item = {{raw_key: value}} %}}{{{{ item[{key}] }}}}"),
                "name",
                "[payload]"
            ),
            render("{{ value }}", "name", "[payload]"),
            "{key}"
        );
    }
}

#[test]
fn attribute_filters_use_plain_keys_and_keep_formatted_results() {
    for key in KEY_EXPRESSIONS {
        for lookup in [
            format!("item | attr({key})"),
            format!("item | attr(*[{key}])"),
            format!("[item] | map(attribute={key}) | first"),
            format!("[item] | map(**{{'attribute': {key}}}) | first"),
            format!("[item] | sort(attribute={key}) | first | attr(raw_key)"),
            format!("[item] | sort(**{{'attribute': {key}}}) | first | attr(raw_key)"),
            format!("[item] | unique(attribute={key}) | first | attr(raw_key)"),
            format!("[item] | unique(**{{'attribute': {key}}}) | first | attr(raw_key)"),
            format!("([item] | groupby({key}) | first).list[0][raw_key]"),
            format!("([item] | groupby(attribute={key}) | first).list[0][raw_key]"),
            format!("([item] | groupby(*[{key}]) | first).list[0][raw_key]"),
            format!("([item] | groupby(**{{'attribute': {key}}}) | first).list[0][raw_key]"),
        ] {
            assert_eq!(
                render(
                    &format!("{{% set item = {{raw_key: value}} %}}{{{{ {lookup} }}}}"),
                    "name",
                    "[payload]"
                ),
                render("{{ value }}", "name", "[payload]"),
                "{lookup}"
            );
        }
    }
}

#[test]
fn comparisons_project_nested_leaves_without_changing_displayed_values() {
    assert_eq!(
        render("{{ raw_key in [typed_key] }}|{{ typed_key in [typed_key] }}|{{ {'x': [typed_key]} == {'x': [raw_key]} }}|{{ typed_key }}", "name", "payload"),
        format!("true|true|true|{}", render("{{ typed_key }}", "name", "payload"))
    );
}

#[test]
fn map_filter_names_project_without_projecting_filter_values() {
    for key in KEY_EXPRESSIONS {
        assert_eq!(
            render(
                &format!("{{{{ ['ok'] | map(*[{key}]) | first }}}}"),
                "upper",
                "payload"
            ),
            "OK"
        );
    }
    assert_eq!(
        render(
            "{{ [value] | map('style_as', 'heading') | first }}",
            "name",
            "[payload]"
        ),
        render("{{ value | style_as('heading') }}", "name", "[payload]")
    );
}

#[test]
fn comparison_projection_keeps_lazy_membership_short_circuiting() {
    use minijinja::Value;
    use std::sync::{
        atomic::{AtomicUsize, Ordering},
        Arc,
    };
    let visited = Arc::new(AtomicUsize::new(0));
    let values = Value::make_object_iterable(visited.clone(), |visited| {
        Box::new((0..1_000_000_000).map(move |_| {
            visited.fetch_add(1, Ordering::Relaxed);
            Value::from(FormattedText::text("name").styled("heading").unwrap())
        }))
    });
    let environment = standout_render::template::new_environment();
    assert_eq!(
        environment
            .render_str(
                "{{ 'name' in (values | __standout_plain_for_comparison) }}",
                minijinja::context!(values)
            )
            .unwrap(),
        "true"
    );
    assert_eq!(visited.load(Ordering::Relaxed), 1);
}

#[test]
fn comparison_projection_preserves_same_object_equality_without_iteration() {
    use minijinja::Value;
    use std::sync::{
        atomic::{AtomicUsize, Ordering},
        Arc,
    };
    let visited = Arc::new(AtomicUsize::new(0));
    let values = Value::make_object_iterable(visited.clone(), |visited| {
        Box::new((0..8).map(move |_| {
            visited.fetch_add(1, Ordering::Relaxed);
            Value::from(FormattedText::text("name").styled("heading").unwrap())
        }))
    });
    let environment = standout_render::template::new_environment();
    for source in [
        "{{ values == values }}",
        "{{ (values | __standout_plain_for_comparison) == (values | __standout_plain_for_comparison) }}",
    ] {
        assert_eq!(environment.render_str(source, minijinja::context!(values => values.clone())).unwrap(), "true");
        assert_eq!(visited.load(Ordering::Relaxed), 0, "{source}");
    }
}

#[test]
fn absent_keys_differ_from_present_undefined_values() {
    for key in ["raw_key", "typed_key"] {
        for (name, expected) in [("a", "true"), ("b", "false")] {
            assert_eq!(
                render(&format!("{{{{ {key} in {{'a': missing}} }}}}"), name, ""),
                expected
            );
            assert_eq!(
                render(&format!("{{{{ {key} in {{'a': 1}} }}}}"), name, ""),
                expected
            );
        }
    }
    assert_eq!(
        render(
            "{{ {'a': missing} == {'b': missing} }}|{{ {'a': missing} == {'a': missing} }}",
            "",
            ""
        ),
        "false|true"
    );
}

const COMPARISON_TESTS: [(&str, bool); 16] = [
    ("eq", true),
    ("equalto", true),
    ("==", true),
    ("ne", false),
    ("!=", false),
    ("lt", false),
    ("lessthan", false),
    ("<", false),
    ("le", true),
    ("<=", true),
    ("gt", false),
    ("greaterthan", false),
    (">", false),
    ("ge", true),
    (">=", true),
    ("in", true),
];

#[test]
fn comparison_test_aliases_agree_with_operators_and_preserve_selected_values() {
    let selected = render("{{ typed_key }}", "name", "payload");
    for (name, expected) in COMPARISON_TESTS {
        let rhs = if name == "in" { "[raw_key]" } else { "raw_key" };
        if name.chars().all(char::is_alphabetic) {
            assert_eq!(
                render(
                    &format!("{{{{ typed_key is {name}({rhs}) }}}}"),
                    "name",
                    "payload"
                ),
                expected.to_string(),
                "{name}"
            );
        }
        for (filter, keep) in [("select", expected), ("reject", !expected)] {
            assert_eq!(
                render(
                    &format!("{{{{ [typed_key] | {filter}('{name}', {rhs}) | first }}}}"),
                    "name",
                    "payload"
                ),
                if keep { selected.as_str() } else { "" },
                "{filter} {name}"
            );
        }
        assert_eq!(render(&format!("{{{{ [{{'key': typed_key}}] | selectattr('key', '{name}', {rhs}) | map(attribute='key') | first }}}}"), "name", "payload"), if expected { selected.as_str() } else { "" }, "selectattr {name}");
    }
    assert_eq!(
        render(
            "{{ typed_key is sameas(raw_key) }}|{{ typed_key is string }}",
            "name",
            "payload"
        ),
        "false|false"
    );
}

fn ordinary_data() -> impl Strategy<Value = serde_json::Value> {
    let scalar = prop_oneof![
        Just(serde_json::Value::Null),
        any::<bool>().prop_map(serde_json::Value::Bool),
        (-8_i64..=8).prop_map(serde_json::Value::from),
        "[a-c]{0,4}".prop_map(serde_json::Value::String),
    ];
    scalar.prop_recursive(3, 16, 3, |child| {
        prop_oneof![
            prop::collection::vec(child.clone(), 0..4).prop_map(serde_json::Value::Array),
            prop::collection::btree_map("[a-c]{1,2}", child, 0..4)
                .prop_map(|map| serde_json::Value::Object(map.into_iter().collect())),
        ]
    })
}

fn keys() -> impl Strategy<Value = String> {
    prop::collection::vec(
        prop::sample::select(vec!["a", "9", "é", "", "[", "]", "\\", "'", " "]),
        0..10,
    )
    .prop_map(|parts| format!("k{}", parts.concat()))
}

proptest! {
    #![proptest_config(ProptestConfig { cases: 48, failure_persistence: None, ..ProptestConfig::default() })]

    #[test]
    fn ordinary_comparisons_agree_with_minijinja(left in ordinary_data(), right in ordinary_data()) {
        let original = minijinja::Environment::new();
        let adapted = standout_render::template::new_environment();
        for (operator, right) in [("==", &right), ("!=", &right), ("==", &left), ("!=", &left), ("in", &right), ("not in", &right)] {
            if matches!(operator, "in" | "not in") && !matches!(right, serde_json::Value::Array(_) | serde_json::Value::Object(_) | serde_json::Value::String(_)) {
                continue;
            }
            let context = minijinja::context!(left => &left, right => right);
            let expected = original.compile_expression(&format!("left {operator} right")).unwrap().eval(context.clone()).map(|value| value.is_true()).map_err(|error| error.kind());
            let actual = adapted.compile_expression(&format!("(left | __standout_plain_for_comparison) {operator} (right | __standout_plain_for_comparison)")).unwrap().eval(context).map(|value| value.is_true()).map_err(|error| error.kind());
            prop_assert_eq!(actual, expected, "left={:?}, right={:?}, operator={}", left, right, operator);
        }
        for (name, _) in COMPARISON_TESTS {
            let context = minijinja::context!(left => &left, right => &right);
            let source = format!("[left] | select('{name}', right) | list | length");
            let expected = original.compile_expression(&source).unwrap().eval(context.clone()).map(|value| value.to_string()).map_err(|error| error.kind());
            let actual = adapted.compile_expression(&source).unwrap().eval(context).map(|value| value.to_string()).map_err(|error| error.kind());
            prop_assert_eq!(actual, expected, "left={:?}, right={:?}, test={}", left, right, name);
        }
    }

    #[test]
    fn textual_key_positions_agree_and_preserve_typed_values(
        key in keys(),
        payload in prop::collection::vec(prop::sample::select(vec!["a", "[heading]", "[/heading]", "\\", "é", "", "\x1b[2J", "\0"]), 0..8).prop_map(|parts| parts.concat()),
        insertion in 0..KEY_EXPRESSIONS.len(),
        lookup in 0..KEY_EXPRESSIONS.len(),
    ) {
        let insert = KEY_EXPRESSIONS[insertion];
        let lookup = KEY_EXPRESSIONS[lookup];
        let setup = format!("{{% set item = {{{insert}: value}} %}}");
        let expected = render("{{ value }}", &key, &payload);
        for operation in [format!("item[{lookup}]"), format!("item | attr({lookup})"), format!("[item] | map(attribute={lookup}) | first")] {
            let source = format!("{setup}{{{{ {operation} }}}}");
            prop_assert_eq!(render(&source, &key, &payload), expected.as_str(), "{}", source);
        }
        let membership = format!("{setup}{{{{ {lookup} in item }}}}|{{{{ {lookup} == raw_key }}}}");
        prop_assert_eq!(render(&membership, &key, &payload), "true|true");
    }
}