workshop-rs 0.1.16

Canonical multi-locale Overwatch Workshop semantic core: catalog, parser, WIR, validation, emitter.
Documentation
use workshop_rs::catalog::{Catalog, Locale};
use workshop_rs::emitter;
use workshop_rs::wir::{self, Action, Event, Value, ValueNode};

fn program_with_structured_actions() -> (wir::Program, Vec<wir::ActionId>) {
    let mut program = wir::Program::default();
    let global = program.global_variables.push(wir::WorkshopVariable {
        name: "index".into(),
        index: 0,
        span: None,
        name_span: None,
    });
    let player = program.player_variables.push(wir::WorkshopVariable {
        name: "index".into(),
        index: 0,
        span: None,
        name_span: None,
    });
    let condition = program.values.push(ValueNode::new(Value::Bool(true), None));
    let number = program.values.push(ValueNode::new(
        Value::Number {
            value: 0.0,
            text: "0".into(),
        },
        None,
    ));
    fn leaf(program: &mut wir::Program, number: wir::ValueId) -> wir::ActionId {
        program.actions.push(Action::Call {
            name: "wait".into(),
            args: vec![number],
            span: None,
        })
    }
    let if_body = leaf(&mut program, number);
    let else_body = leaf(&mut program, number);
    let if_action = program.actions.push(Action::If {
        branches: vec![wir::IfBranch {
            condition,
            body: vec![if_body],
        }],
        else_body: Some(vec![else_body]),
        span: None,
    });
    let while_body = leaf(&mut program, number);
    let while_action = program.actions.push(Action::While {
        condition,
        body: vec![while_body],
        span: None,
    });
    let for_global_body = leaf(&mut program, number);
    let for_global = program.actions.push(Action::ForGlobalVariable {
        variable: global,
        start: number,
        stop: number,
        step: number,
        body: vec![for_global_body],
        span: None,
        target_span: None,
    });
    let nested_if_body = if_action;
    let for_player = program.actions.push(Action::ForPlayerVariable {
        player: program
            .values
            .push(ValueNode::new(Value::EventPlayer, None)),
        variable: player,
        start: number,
        stop: number,
        step: number,
        body: vec![nested_if_body],
        span: None,
    });
    let trailing_leaf = leaf(&mut program, number);
    let actions = vec![
        if_action,
        while_action,
        for_global,
        for_player,
        trailing_leaf,
    ];
    program.rules.push(wir::Rule {
        name: "layout".into(),
        span: None,
        name_span: None,
        disabled: false,
        event: Event::Global,
        conditions: vec![],
        actions: actions.clone(),
    });
    (program, actions)
}

#[test]
fn structured_action_widths_count_native_expansion() {
    let (program, actions) = program_with_structured_actions();
    let catalog = Catalog::builtin().unwrap();
    let locale = Locale::new("en-US");

    assert_eq!(
        emitter::action_width(&program, &catalog, &locale, &actions[..1])
            .unwrap()
            .width,
        5
    );
    assert_eq!(
        emitter::action_width(&program, &catalog, &locale, &actions[1..2])
            .unwrap()
            .width,
        3
    );
    assert_eq!(
        emitter::action_width(&program, &catalog, &locale, &actions[2..3])
            .unwrap()
            .width,
        3
    );
    assert_eq!(
        emitter::action_width(&program, &catalog, &locale, &actions[3..4])
            .unwrap()
            .width,
        7
    );
}

#[test]
fn layout_matches_canonical_emission_for_a_nested_sequence() {
    let (program, actions) = program_with_structured_actions();
    let catalog = Catalog::builtin().unwrap();
    let locale = Locale::new("en-US");
    let emitted = emitter::emit(&program, &catalog, &locale).unwrap();
    let action_text = emitted
        .split_once("actions {\n")
        .unwrap()
        .1
        .split_once("\n    }")
        .unwrap()
        .0;
    let emitted_width = action_text
        .lines()
        .filter(|line| !line.trim().is_empty())
        .count();
    let layout = emitter::action_width(&program, &catalog, &locale, &actions).unwrap();
    assert_eq!(layout.width, emitted_width);
    assert_eq!(layout.width, 19);
}

#[test]
fn invalid_layout_requests_fail_explicitly() {
    let mut program = wir::Program::default();
    let dangling = wir::ActionId::from_index(0);
    program.rules.push(wir::Rule {
        name: "invalid".into(),
        span: None,
        name_span: None,
        disabled: false,
        event: Event::Global,
        conditions: vec![],
        actions: vec![dangling],
    });
    let error = emitter::action_width(
        &program,
        &Catalog::builtin().unwrap(),
        &Locale::new("en-US"),
        &[dangling],
    )
    .unwrap_err();
    assert!(matches!(
        error,
        emitter::ActionLayoutError::InvalidWIR(wir::error::IrError::DanglingReference {
            what: "action",
            id: 0
        })
    ));
}