formualizer-sheetport 0.5.1

SheetPort runtime: bind manifests to workbooks, validate IO, and run sessions deterministically
Documentation
use crate::error::SheetPortError;
use crate::location::{AreaLocation, FieldLocation, ScalarLocation, TableLocation};
use formualizer_common::RangeAddress;
use formualizer_parse::parser::ReferenceType;
use sheetport_spec::{
    FieldSelector, LayoutDescriptor, Selector, SelectorA1, SelectorLayout, SelectorName,
    SelectorStructRef, TableSelector,
};

#[derive(Debug, Clone)]
pub enum ResolvedSelector {
    Range(RangeAddress),
    Name(String),
    StructRef(String),
    Table(TableSelector),
    Layout(LayoutDescriptor),
}

pub fn resolve_scalar_location(
    port_id: &str,
    selector: &Selector,
) -> Result<ScalarLocation, SheetPortError> {
    match resolve_selector(port_id, selector)? {
        ResolvedSelector::Range(range) => {
            if range.height() != 1 || range.width() != 1 {
                return Err(SheetPortError::InvariantViolation {
                    port: port_id.to_string(),
                    message: format!(
                        "scalar ports must point to a single cell, got {}x{}",
                        range.height(),
                        range.width()
                    ),
                });
            }
            Ok(ScalarLocation::Cell(range))
        }
        ResolvedSelector::Name(name) => Ok(ScalarLocation::Name(name)),
        ResolvedSelector::StructRef(sr) => Ok(ScalarLocation::StructRef(sr)),
        ResolvedSelector::Table(_) | ResolvedSelector::Layout(_) => {
            Err(SheetPortError::UnsupportedSelector {
                port: port_id.to_string(),
                reason: "scalar ports cannot target table or layout selectors".to_string(),
            })
        }
    }
}

pub fn resolve_area_location(
    port_id: &str,
    selector: &Selector,
) -> Result<AreaLocation, SheetPortError> {
    match resolve_selector(port_id, selector)? {
        ResolvedSelector::Range(range) => Ok(AreaLocation::Range(range)),
        ResolvedSelector::Name(name) => Ok(AreaLocation::Name(name)),
        ResolvedSelector::StructRef(sr) => Ok(AreaLocation::StructRef(sr)),
        ResolvedSelector::Layout(layout) => Ok(AreaLocation::Layout(layout)),
        ResolvedSelector::Table(_) => Err(SheetPortError::UnsupportedSelector {
            port: port_id.to_string(),
            reason: "area selectors cannot directly reference workbook tables".to_string(),
        }),
    }
}

pub fn resolve_table_location(
    port_id: &str,
    selector: &Selector,
) -> Result<TableLocation, SheetPortError> {
    match resolve_selector(port_id, selector)? {
        ResolvedSelector::Table(table) => Ok(TableLocation::Table(table)),
        ResolvedSelector::Layout(layout) => Ok(TableLocation::Layout(layout)),
        other => Err(SheetPortError::UnsupportedSelector {
            port: port_id.to_string(),
            reason: format!("table ports require table or layout selectors, got {other:?}"),
        }),
    }
}

pub fn resolve_field_location(
    port_id: &str,
    field: &str,
    selector: &FieldSelector,
) -> Result<FieldLocation, SheetPortError> {
    match selector {
        FieldSelector::A1(SelectorA1 { a1 }) => {
            let range = parse_a1_range(port_id, a1)?;
            if range.height() != 1 || range.width() != 1 {
                return Err(SheetPortError::InvariantViolation {
                    port: port_id.to_string(),
                    message: format!(
                        "record field `{field}` must resolve to a single cell, got {}x{}",
                        range.height(),
                        range.width()
                    ),
                });
            }
            Ok(FieldLocation::Cell(range))
        }
        FieldSelector::Name(SelectorName { name }) => Ok(FieldLocation::Name(name.to_string())),
        FieldSelector::StructRef(SelectorStructRef { struct_ref }) => {
            Ok(FieldLocation::StructRef(struct_ref.to_string()))
        }
    }
}

fn resolve_selector(
    port_id: &str,
    selector: &Selector,
) -> Result<ResolvedSelector, SheetPortError> {
    match selector {
        Selector::A1(SelectorA1 { a1 }) => parse_a1_range(port_id, a1).map(ResolvedSelector::Range),
        Selector::Name(SelectorName { name }) => Ok(ResolvedSelector::Name(name.to_string())),
        Selector::StructRef(SelectorStructRef { struct_ref }) => {
            Ok(ResolvedSelector::StructRef(struct_ref.to_string()))
        }
        Selector::Table(selector) => Ok(ResolvedSelector::Table(selector.table.clone())),
        Selector::Layout(SelectorLayout { layout }) => Ok(ResolvedSelector::Layout(layout.clone())),
    }
}

fn parse_a1_range(port_id: &str, raw: &str) -> Result<RangeAddress, SheetPortError> {
    match ReferenceType::from_string(raw) {
        Ok(ReferenceType::Cell {
            sheet, row, col, ..
        }) => {
            let sheet = sheet.unwrap_or_default();
            if sheet.is_empty() {
                return Err(SheetPortError::InvariantViolation {
                    port: port_id.to_string(),
                    message: format!("reference `{raw}` must include a sheet name"),
                });
            }
            RangeAddress::new(sheet, row, col, row, col).map_err(|msg| {
                SheetPortError::InvariantViolation {
                    port: port_id.to_string(),
                    message: msg.to_string(),
                }
            })
        }
        Ok(ReferenceType::Range {
            sheet,
            start_row,
            start_col,
            end_row,
            end_col,
            ..
        }) => {
            let sheet = sheet.unwrap_or_default();
            if sheet.is_empty() {
                return Err(SheetPortError::InvariantViolation {
                    port: port_id.to_string(),
                    message: format!("reference `{raw}` must include a sheet name"),
                });
            }
            let sr = require_coord(port_id, raw, "start_row", start_row)?;
            let sc = require_coord(port_id, raw, "start_col", start_col)?;
            let er = require_coord(port_id, raw, "end_row", end_row)?;
            let ec = require_coord(port_id, raw, "end_col", end_col)?;
            RangeAddress::new(sheet, sr, sc, er, ec).map_err(|msg| {
                SheetPortError::InvariantViolation {
                    port: port_id.to_string(),
                    message: msg.to_string(),
                }
            })
        }
        Ok(other) => Err(SheetPortError::UnsupportedSelector {
            port: port_id.to_string(),
            reason: format!("A1 selector `{raw}` resolved to unsupported reference `{other:?}`"),
        }),
        Err(source) => Err(SheetPortError::InvalidReference {
            port: port_id.to_string(),
            reference: raw.to_string(),
            details: source.to_string(),
        }),
    }
}

fn require_coord(
    port_id: &str,
    raw: &str,
    label: &str,
    value: Option<u32>,
) -> Result<u32, SheetPortError> {
    value.ok_or_else(|| SheetPortError::InvariantViolation {
        port: port_id.to_string(),
        message: format!("reference `{raw}` is missing `{label}`"),
    })
}