formualizer-sheetport 0.5.2

SheetPort runtime: bind manifests to workbooks, validate IO, and run sessions deterministically
Documentation
use formualizer_common::LiteralValue;
use formualizer_sheetport::{EvalOptions, InputUpdate, PortValue, SheetPort, SheetPortSession};
use formualizer_workbook::Workbook;
use sheetport_spec::Manifest;

fn make_test_workbook() -> Workbook {
    let mut wb = Workbook::new();
    wb.add_sheet("Inputs").unwrap();
    wb.add_sheet("Inventory").unwrap();
    wb.add_sheet("Outputs").unwrap();
    wb
}

#[test]
fn sheetport_initializes_with_matching_workbook() {
    let yaml = include_str!("../../sheetport-spec/tests/fixtures/supply_planning.yaml");
    let manifest: Manifest = Manifest::from_yaml_str(yaml).expect("fixture parses");
    let mut workbook = make_test_workbook();

    SheetPort::new(&mut workbook, manifest).expect("sheetport binds to workbook");
}

#[test]
fn sheetport_fails_for_missing_sheet() {
    let yaml = include_str!("../../sheetport-spec/tests/fixtures/supply_planning.yaml");
    let manifest: Manifest = Manifest::from_yaml_str(yaml).expect("fixture parses");
    let mut workbook = Workbook::new();
    workbook.add_sheet("Inputs").unwrap();
    workbook.add_sheet("Outputs").unwrap();

    let err = match SheetPort::new(&mut workbook, manifest) {
        Ok(_) => panic!("expected missing sheet error"),
        Err(err) => err,
    };
    match err {
        formualizer_sheetport::SheetPortError::MissingSheet { port, sheet } => {
            assert_eq!(port, "sku_inventory");
            assert_eq!(sheet, "Inventory");
        }
        other => panic!("unexpected error: {other:?}"),
    }
}

#[test]
fn sheetport_session_supports_owned_workbook() {
    let manifest_yaml = r#"
spec: fio
spec_version: "0.3.0"
manifest:
  id: session-test
  name: Session Test
  workbook:
    uri: memory://session.xlsx
    locale: en-US
    date_system: 1900
ports:
  - id: input_a
    dir: in
    shape: scalar
    location:
      a1: Sheet!A1
    schema:
      type: number
  - id: output_b
    dir: out
    shape: scalar
    location:
      a1: Sheet!B1
    schema:
      type: number
"#;
    let manifest: Manifest =
        Manifest::from_yaml_str(manifest_yaml).expect("session manifest parses");

    let mut workbook = Workbook::new();
    workbook.add_sheet("Sheet").unwrap();
    workbook
        .set_value("Sheet", 1, 1, LiteralValue::Number(5.0))
        .expect("set A1");
    workbook
        .set_value("Sheet", 1, 2, LiteralValue::Number(10.0))
        .expect("set B1");

    let mut session = SheetPortSession::new(workbook, manifest).expect("session created");

    let inputs = session.read_inputs().expect("read inputs");
    match inputs.get("input_a") {
        Some(PortValue::Scalar(LiteralValue::Number(n))) => assert_eq!(*n, 5.0),
        other => panic!("unexpected input value: {other:?}"),
    }

    let mut update = InputUpdate::new();
    update.insert("input_a", PortValue::Scalar(LiteralValue::Number(7.5)));
    session.write_inputs(update).expect("write inputs");
    assert_eq!(
        session
            .workbook()
            .get_value("Sheet", 1, 1)
            .unwrap_or(LiteralValue::Empty),
        LiteralValue::Number(7.5)
    );

    let outputs = session
        .evaluate_once(EvalOptions::default())
        .expect("evaluate once");
    match outputs.get("output_b") {
        Some(PortValue::Scalar(LiteralValue::Number(n))) => assert_eq!(*n, 10.0),
        other => panic!("unexpected output value: {other:?}"),
    }
}

#[test]
fn sheetport_rejects_unsupported_profile() {
    let manifest_yaml = r#"
 spec: fio
 spec_version: "0.3.0"
 capabilities: { profile: full-v0 }
 manifest: { id: profile-test, name: Profile Test }
 ports:
   - id: input_a
     dir: in
     shape: scalar
     location:
       a1: Sheet!A1
     schema:
       type: number
 "#;
    let manifest: Manifest = Manifest::from_yaml_str(manifest_yaml).expect("manifest parses");

    let mut workbook = Workbook::new();
    workbook.add_sheet("Sheet").unwrap();

    let err = match SheetPort::new(&mut workbook, manifest) {
        Ok(_) => panic!("expected unsupported profile error"),
        Err(err) => err,
    };
    match err {
        formualizer_sheetport::SheetPortError::InvalidManifest { issues } => {
            assert!(
                issues
                    .iter()
                    .any(|issue| issue.path == "capabilities.profile"),
                "expected profile issue, got {issues:#?}"
            );
        }
        other => panic!("unexpected error: {other:?}"),
    }
}

#[test]
fn sheetport_rejects_struct_ref_under_core_profile() {
    let manifest_yaml = r#"
spec: fio
spec_version: "0.3.0"
manifest: { id: core-structref, name: Core StructRef }
ports:
  - id: input_a
    dir: in
    shape: scalar
    location: { struct_ref: "Tbl[Col]" }
    schema: { type: number }
"#;
    let manifest: Manifest = Manifest::from_yaml_str(manifest_yaml).expect("manifest parses");

    let mut workbook = Workbook::new();
    workbook.add_sheet("Sheet").unwrap();

    let err = match SheetPort::new(&mut workbook, manifest) {
        Ok(_) => panic!("expected invalid manifest"),
        Err(err) => err,
    };
    match err {
        formualizer_sheetport::SheetPortError::InvalidManifest { issues } => {
            assert!(
                issues.iter().any(|issue| issue.path == "ports[0].location"),
                "expected location issue, got {issues:#?}"
            );
        }
        other => panic!("unexpected error: {other:?}"),
    }
}

#[test]
fn sheetport_rejects_table_selector_under_core_profile() {
    let manifest_yaml = r#"
spec: fio
spec_version: "0.3.0"
manifest: { id: core-table, name: Core Table }
ports:
  - id: input_t
    dir: in
    shape: table
    location:
      table:
        name: Tbl
    schema:
      kind: table
      columns:
        - { name: a, type: number }
"#;
    let manifest: Manifest = Manifest::from_yaml_str(manifest_yaml).expect("manifest parses");

    let mut workbook = Workbook::new();
    workbook.add_sheet("Sheet").unwrap();

    let err = match SheetPort::new(&mut workbook, manifest) {
        Ok(_) => panic!("expected invalid manifest"),
        Err(err) => err,
    };
    match err {
        formualizer_sheetport::SheetPortError::InvalidManifest { issues } => {
            assert!(
                issues.iter().any(|issue| issue.path == "ports[0].location"),
                "expected location issue, got {issues:#?}"
            );
        }
        other => panic!("unexpected error: {other:?}"),
    }
}

#[test]
fn sheetport_rejects_scalar_layout_selector() {
    let manifest_yaml = r#"
spec: fio
spec_version: "0.3.0"
manifest: { id: core-scalar-layout, name: Core Scalar Layout }
ports:
  - id: input_a
    dir: in
    shape: scalar
    location:
      layout:
        sheet: Sheet
        header_row: 1
        anchor_col: A
        terminate: first_blank_row
    schema: { type: number }
"#;
    let manifest: Manifest = Manifest::from_yaml_str(manifest_yaml).expect("manifest parses");

    let mut workbook = Workbook::new();
    workbook.add_sheet("Sheet").unwrap();

    let err = match SheetPort::new(&mut workbook, manifest) {
        Ok(_) => panic!("expected invalid manifest"),
        Err(err) => err,
    };
    match err {
        formualizer_sheetport::SheetPortError::InvalidManifest { issues } => {
            assert!(
                issues.iter().any(|issue| issue.path == "ports[0].location"),
                "expected location issue, got {issues:#?}"
            );
        }
        other => panic!("unexpected error: {other:?}"),
    }
}

#[test]
fn sheetport_rejects_record_field_struct_ref_under_core_profile() {
    let manifest_yaml = r#"
spec: fio
spec_version: "0.3.0"
manifest: { id: core-record-field, name: Core Record Field }
ports:
  - id: rec
    dir: in
    shape: record
    location: { a1: Sheet!A1:B1 }
    schema:
      kind: record
      fields:
        a:
          type: number
          location: { struct_ref: "Tbl[Col]" }
"#;
    let manifest: Manifest = Manifest::from_yaml_str(manifest_yaml).expect("manifest parses");

    let mut workbook = Workbook::new();
    workbook.add_sheet("Sheet").unwrap();

    let err = match SheetPort::new(&mut workbook, manifest) {
        Ok(_) => panic!("expected invalid manifest"),
        Err(err) => err,
    };
    match err {
        formualizer_sheetport::SheetPortError::InvalidManifest { issues } => {
            assert!(
                issues
                    .iter()
                    .any(|issue| issue.path == "ports[0].schema.fields.a.location"),
                "expected field location issue, got {issues:#?}"
            );
        }
        other => panic!("unexpected error: {other:?}"),
    }
}