step-io 0.1.0-alpha.1

STEP (ISO 10303) file I/O for Rust.
Documentation
use std::collections::BTreeMap;

use crate::parser::lexer::{LexError, Span};

/// A parsed attribute value from a Part 21 entity instance.
///
/// Covers every construct allowed in a parameter position by ISO 10303-21.
#[derive(Debug, Clone, PartialEq)]
pub enum Attribute {
    Integer(i64),
    Real(f64),
    /// Raw string with outer quotes stripped. Escape sequences (`''`, `\X\HH`,
    /// `\X2\...\X0\`) are **not** decoded — decoding is deferred to the IR stage.
    String(String),
    /// Enumeration value without the surrounding dots (e.g. `.T.` → `"T"`).
    Enum(String),
    /// Hex-encoded binary without the surrounding quotes.
    Binary(String),
    /// Entity reference `#N`.
    EntityRef(u64),
    /// Unset / omitted attribute (`$`).
    Unset,
    /// Derived attribute (`*`).
    Derived,
    /// Parenthesised list of parameters. May be nested.
    List(Vec<Attribute>),
    /// Typed parameter such as `LENGTH_MEASURE(1.E-07)`.
    Typed {
        type_name: String,
        value: Box<Attribute>,
    },
}

/// A single sub-entity inside a complex entity instance.
#[derive(Debug, Clone, PartialEq)]
pub struct RawEntityPart {
    /// Entity type name (always stored **upper-cased**).
    pub name: String,
    pub attributes: Vec<Attribute>,
}

/// A parsed Part 21 entity instance (one line in the DATA section).
#[derive(Debug, Clone, PartialEq)]
pub enum RawEntity {
    Simple {
        id: u64,
        /// Entity type name (always stored **upper-cased**).
        name: String,
        attributes: Vec<Attribute>,
        /// Position of the `#N` token that opened this instance.
        span: Span,
    },
    Complex {
        id: u64,
        parts: Vec<RawEntityPart>,
        /// Position of the `#N` token that opened this instance.
        span: Span,
    },
}

impl RawEntity {
    /// The numeric identifier (`#N`).
    #[must_use]
    pub fn id(&self) -> u64 {
        match self {
            Self::Simple { id, .. } | Self::Complex { id, .. } => *id,
        }
    }

    /// Source position of the `#N` token.
    #[must_use]
    pub fn span(&self) -> Span {
        match self {
            Self::Simple { span, .. } | Self::Complex { span, .. } => *span,
        }
    }
}

/// The complete result of parsing a Part 21 file.
#[derive(Debug)]
pub struct EntityGraph {
    /// Identified application protocol. The raw `FILE_SCHEMA` string list
    /// is preserved inside [`StepSchema`] itself — see its docs.
    pub schema: super::schema::StepSchema,
    /// Raw HEADER entities (`FILE_DESCRIPTION`, `FILE_NAME`, `FILE_SCHEMA`).
    pub header: Vec<RawEntity>,
    /// DATA section entities keyed by their `#N` identifier.
    /// `BTreeMap` gives deterministic iteration order (useful for the writer stage).
    pub entities: BTreeMap<u64, RawEntity>,
}

impl EntityGraph {
    /// Look up an entity by its `#N` identifier.
    #[must_use]
    pub fn get(&self, id: u64) -> Option<&RawEntity> {
        self.entities.get(&id)
    }
}

/// Parse error kinds specific to the Part 21 parser.
///
/// Lexer errors are wrapped in [`ParseError::Lex`]; the remaining variants
/// describe structural problems detected during parsing.
#[derive(Debug, Clone, PartialEq)]
pub enum ParseError {
    /// A lexical error bubbled up from the tokenizer.
    Lex(LexError),
    /// The token stream ended before the parser expected.
    UnexpectedEof { expected: &'static str },
    /// A token was found where a different kind was expected.
    UnexpectedToken {
        expected: &'static str,
        found: super::lexer::TokenKind,
        span: Span,
    },
    /// Two entities share the same `#N` identifier.
    DuplicateEntityId { id: u64, span: Span },
    /// A required HEADER entity is missing.
    MissingHeaderEntity { name: &'static str },
    /// `FILE_SCHEMA` does not contain a list of strings.
    MalformedFileSchema { span: Span },
    /// An attribute appeared in an invalid position (e.g. `$` inside a list).
    InvalidAttributePosition { span: Span, detail: &'static str },
}

impl From<LexError> for ParseError {
    fn from(err: LexError) -> Self {
        Self::Lex(err)
    }
}

impl std::fmt::Display for ParseError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Lex(inner) => inner.fmt(f),
            Self::UnexpectedEof { expected } => {
                write!(
                    f,
                    "parse error: unexpected end of file, expected {expected}"
                )
            }
            Self::UnexpectedToken {
                expected,
                found,
                span,
            } => write!(
                f,
                "parse error at line {}, column {}: expected {expected}, found {found:?}",
                span.line, span.column,
            ),
            Self::DuplicateEntityId { id, span } => write!(
                f,
                "parse error at line {}, column {}: duplicate entity #{id}",
                span.line, span.column,
            ),
            Self::MissingHeaderEntity { name } => {
                write!(f, "parse error: missing required HEADER entity {name}")
            }
            Self::MalformedFileSchema { span } => write!(
                f,
                "parse error at line {}, column {}: malformed FILE_SCHEMA",
                span.line, span.column,
            ),
            Self::InvalidAttributePosition { span, detail } => write!(
                f,
                "parse error at line {}, column {}: {detail}",
                span.line, span.column,
            ),
        }
    }
}

impl std::error::Error for ParseError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::Lex(inner) => Some(inner),
            _ => None,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn raw_entity_id_simple() {
        let e = RawEntity::Simple {
            id: 42,
            name: "TEST".into(),
            attributes: vec![],
            span: Span {
                start: 0,
                end: 3,
                line: 1,
                column: 1,
            },
        };
        assert_eq!(e.id(), 42);
    }

    #[test]
    fn raw_entity_id_complex() {
        let e = RawEntity::Complex {
            id: 99,
            parts: vec![],
            span: Span {
                start: 0,
                end: 3,
                line: 1,
                column: 1,
            },
        };
        assert_eq!(e.id(), 99);
    }

    #[test]
    fn raw_entity_span_accessor() {
        let span = Span {
            start: 10,
            end: 20,
            line: 3,
            column: 5,
        };
        let e = RawEntity::Simple {
            id: 1,
            name: "A".into(),
            attributes: vec![],
            span,
        };
        assert_eq!(e.span(), span);
    }

    #[test]
    fn parse_error_from_lex_error() {
        let lex_err = LexError {
            kind: crate::parser::lexer::LexErrorKind::UnexpectedCharacter,
            span: Span {
                start: 0,
                end: 1,
                line: 1,
                column: 1,
            },
            snippet: "@".into(),
        };
        let parse_err = ParseError::from(lex_err.clone());
        assert_eq!(parse_err, ParseError::Lex(lex_err));
    }

    #[test]
    fn parse_error_display_delegates_for_lex() {
        let lex_err = LexError {
            kind: crate::parser::lexer::LexErrorKind::UnexpectedCharacter,
            span: Span {
                start: 0,
                end: 1,
                line: 1,
                column: 1,
            },
            snippet: "@".into(),
        };
        let parse_err = ParseError::Lex(lex_err.clone());
        // Lex variant delegates to LexError's Display — no "parse error:" prefix.
        assert_eq!(parse_err.to_string(), lex_err.to_string());
    }

    #[test]
    fn parse_error_implements_std_error() {
        fn assert_error<E: std::error::Error>(_: &E) {}
        let err = ParseError::UnexpectedEof {
            expected: "semicolon",
        };
        assert_error(&err);
    }

    #[test]
    fn entity_graph_get_returns_entity() {
        let span = Span {
            start: 0,
            end: 1,
            line: 1,
            column: 1,
        };
        let entity = RawEntity::Simple {
            id: 1,
            name: "X".into(),
            attributes: vec![],
            span,
        };
        let mut entities = BTreeMap::new();
        entities.insert(1, entity.clone());
        let graph = EntityGraph {
            schema: super::super::schema::StepSchema::default(),
            header: vec![],
            entities,
        };
        assert_eq!(graph.get(1), Some(&entity));
        assert_eq!(graph.get(999), None);
    }
}