workshop-rs 0.3.7

Canonical multi-locale Overwatch Workshop semantic core: catalog, parser, WIR, validation, emitter.
Documentation
use workshop_rs::{Action, Condition, Event, Program, Rule, Value, Variable};

fn catalog() -> workshop_rs::catalog::Catalog {
    workshop_rs::catalog::Catalog::builtin().expect("builtin catalog")
}

#[test]
fn program_is_constructible_without_storage_ids() {
    let predicate = Value::call("isAlive", [Value::global_variable("Target")]);
    let condition = Condition::new(predicate.clone());
    let rule = Rule::new("Linear control flow", Event::Global)
        .condition(condition)
        .action(Action::If {
            condition: predicate.clone(),
        })
        .action(Action::call("Wait", [Value::number(1.0)]))
        .action(Action::ElseIf {
            condition: Value::Bool(false),
        })
        .action(Action::Else)
        .action(Action::While {
            condition: predicate,
        })
        .action(Action::disabled(Action::call("Abort", std::iter::empty())))
        .action(Action::End);

    let mut program = Program::new();
    program.global_variable(Variable::new("Target")).rule(rule);

    assert_eq!(program.global_variables[0].name, "Target");
    assert!(matches!(program.rules[0].actions[0], Action::If { .. }));
    assert!(matches!(program.rules[0].actions[2], Action::ElseIf { .. }));
    assert!(matches!(program.rules[0].actions[3], Action::Else));
    assert!(matches!(
        program.rules[0].actions[5],
        Action::Disabled { .. }
    ));
    assert!(matches!(program.rules[0].actions[6], Action::End));
}

#[test]
fn values_and_conditions_are_composable() {
    let value = Value::call(
        "add",
        [
            Value::global_variable("Score"),
            Value::Array(vec![Value::number(1.0), Value::number(2.0)]),
        ],
    );
    let condition = Condition::new(value.clone());
    let mut program = Program::default();
    program.rule(Rule::new("Composable", Event::Global).condition(condition));

    assert!(matches!(
        &program.rules[0].conditions[0].value,
        Value::Call { name, args } if name == "add" && args.len() == 2
    ));
}

#[test]
fn public_program_is_the_raw_parse_and_emit_boundary() {
    let catalog = catalog();
    let locale = workshop_rs::catalog::Locale::new("en-US");
    let source = "rule (\"public boundary\") {\n    event { Ongoing - Global; }\n    actions { Wait(1, Ignore Condition); }\n}\n";

    let parsed = workshop_rs::parser::parse(source, &catalog, &locale).expect("parses");
    assert_eq!(
        parsed
            .source(workshop_rs::source::FileId::from_index(0))
            .unwrap()
            .text(),
        source
    );
    let rule_span = parsed.rule_span(0).expect("public rule span");
    assert!(parsed.action_span(0, 0).is_some());
    let argument_span = parsed
        .action_argument_span(0, 0, 0)
        .expect("public action argument span");
    assert!(rule_span.start.line <= argument_span.start.line);
    let edit = parsed
        .edit_source(argument_span, "2")
        .expect("validated public source edit");
    let updated = parsed
        .source(workshop_rs::source::FileId::from_index(0))
        .unwrap()
        .apply(&[edit])
        .expect("apply public source edit");
    assert!(updated.text().contains("Wait(2, Ignore Condition)"));
    parsed.validate().expect("structurally validates");
    workshop_rs::validate::validate_canonical_ids(&parsed, &catalog).expect("catalog validates");
    assert!(parsed.semantic_issues(&catalog).is_empty());
    assert!(parsed.element_count(&catalog).expect("counts").total > 0);

    let emitted = workshop_rs::emitter::emit(&parsed, &catalog, &locale).expect("emits");
    let reparsed = workshop_rs::parser::parse(&emitted, &catalog, &locale).expect("reparses");
    assert!(workshop_rs::roundtrip::equivalent(&parsed, &reparsed));

    let edited =
        workshop_rs::parser::parse(updated.text(), &catalog, &locale).expect("reparses edit");
    assert!(!workshop_rs::roundtrip::equivalent(&parsed, &edited));
}

#[test]
fn independently_constructed_program_uses_the_same_operations() {
    let catalog = catalog();
    let locale = workshop_rs::catalog::Locale::new("en-US");
    let mut program = Program::new();
    program.global_variable(Variable::new("Score")).rule(
        Rule::new("constructed", Event::Global).action(Action::SetGlobalVariable {
            variable: "Score".to_string(),
            value: Value::number(1.0),
        }),
    );

    program.validate().expect("structurally validates");
    workshop_rs::validate::validate_canonical_ids(&program, &catalog).expect("catalog validates");
    let layout =
        workshop_rs::actions::action_width(&program, &catalog, &locale, &program.rules[0].actions)
            .expect("lays out");
    assert_eq!(layout.width, 1);
    let emitted = workshop_rs::emitter::emit(&program, &catalog, &locale).expect("emits");
    assert!(emitted.contains("Set Global Variable(Score, 1)"));
}

#[test]
fn disabled_public_semantics_fail_explicitly_at_the_storage_boundary() {
    let catalog = catalog();
    let locale = workshop_rs::catalog::Locale::new("en-US");

    let mut disabled_condition = Program::new();
    disabled_condition.rule(
        Rule::new("disabled condition", Event::Global)
            .condition(Condition::disabled(Value::Bool(true))),
    );
    assert!(matches!(
        disabled_condition.validate(),
        Err(workshop_rs::WorkshopError::Unsupported { .. })
    ));

    let mut disabled_action = Program::new();
    disabled_action.rule(
        Rule::new("disabled action", Event::Global)
            .action(Action::disabled(Action::call("Wait", [Value::number(1.0)]))),
    );
    assert!(matches!(
        workshop_rs::emitter::emit(&disabled_action, &catalog, &locale),
        Err(workshop_rs::WorkshopError::Unsupported { .. })
    ));
}