oxipdf-ir 0.1.0

Intermediate representation types for the oxipdf PDF engine
Documentation
//! Input validation errors for the oxipdf IR (§7).
//!
//! These errors are produced during `StyledTree` construction and validation.
//! Invalid IR is always a hard error — no silent fixup or fallback.

use crate::node::NodeId;
use crate::version::IrVersion;

/// Errors produced when validating a `StyledTree`.
///
/// Each variant includes machine-readable context sufficient for the consumer
/// to identify and fix the problem without guesswork.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum InputValidationError {
    /// The tree contains no nodes.
    #[error("StyledTree must contain at least one node")]
    EmptyTree,

    /// The tree's IR version is incompatible with this engine build.
    #[error(
        "IR version {tree_version} is incompatible with engine version {engine_version} \
         (same major version required, tree minor <= engine minor)"
    )]
    IncompatibleVersion {
        tree_version: IrVersion,
        engine_version: IrVersion,
    },

    /// A node's children list references a `NodeId` that does not exist.
    #[error(
        "{parent} references child {child} which does not exist \
         (tree has {node_count} nodes)"
    )]
    InvalidChildReference {
        parent: NodeId,
        child: NodeId,
        node_count: u32,
    },

    /// A node appears in the children list of more than one parent,
    /// violating the tree (single-parent) invariant.
    #[error("{child} is a child of both {parent_a} and {parent_b}")]
    MultipleParents {
        child: NodeId,
        parent_a: NodeId,
        parent_b: NodeId,
    },

    /// A node lists itself as its own child.
    #[error("{node} references itself as a child")]
    SelfReference { node: NodeId },

    /// A non-root node is not reachable from the root via any children chain.
    #[error("{node} is not reachable from the root")]
    OrphanNode { node: NodeId },

    /// A resource limit defined in [`ResourceLimits`](crate::config::ResourceLimits)
    /// has been exceeded.
    #[error("resource limit exceeded: {limit_name} (current: {current}, max: {max})")]
    ResourceLimitExceeded {
        /// Human-readable name of the limit (e.g. "node_count", "tree_depth").
        limit_name: &'static str,
        /// The actual value encountered.
        current: u64,
        /// The configured maximum.
        max: u64,
        /// Path from root to the node where the limit was detected.
        /// Empty if the limit is tree-global (e.g. total node count).
        node_path: Vec<NodeId>,
    },

    /// The page template configuration is invalid (e.g., margins exceed
    /// page dimensions, negative margin values).
    #[error("invalid page template: {detail}")]
    InvalidPageTemplate { detail: String },

    /// A feature is present in the IR but is not supported by this engine
    /// version. This is a hard error — unsupported features never produce
    /// silent fallbacks.
    #[error("unsupported feature \"{feature_name}\" at {node_id}: {detail}")]
    UnsupportedFeature {
        /// The node where the unsupported feature was encountered.
        node_id: NodeId,
        /// Machine-readable feature name (e.g. "math_layout", "cmyk_color").
        feature_name: String,
        /// Human-readable explanation of what is unsupported and why.
        detail: String,
    },
}

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

    #[test]
    fn error_display_messages() {
        let err = InputValidationError::EmptyTree;
        assert_eq!(err.to_string(), "StyledTree must contain at least one node");

        let err = InputValidationError::IncompatibleVersion {
            tree_version: IrVersion::new(2, 0),
            engine_version: IrVersion::new(1, 0),
        };
        assert!(err.to_string().contains("2.0"));
        assert!(err.to_string().contains("1.0"));

        let err = InputValidationError::ResourceLimitExceeded {
            limit_name: "node_count",
            current: 1_500_000,
            max: 1_000_000,
            node_path: vec![],
        };
        assert!(err.to_string().contains("node_count"));
        assert!(err.to_string().contains("1500000"));
        assert!(err.to_string().contains("1000000"));
    }

    #[test]
    fn unsupported_feature_display() {
        let err = InputValidationError::UnsupportedFeature {
            node_id: NodeId::from_raw(7),
            feature_name: "cmyk_color".into(),
            detail: "CMYK color space is not supported".into(),
        };
        let msg = err.to_string();
        assert!(msg.contains("cmyk_color"));
        assert!(msg.contains("node#7"));
        assert!(msg.contains("CMYK"));
    }

    #[test]
    fn resource_limit_with_node_path() {
        let err = InputValidationError::ResourceLimitExceeded {
            limit_name: "tree_depth",
            current: 300,
            max: 256,
            node_path: vec![
                NodeId::from_raw(0),
                NodeId::from_raw(3),
                NodeId::from_raw(12),
            ],
        };
        assert!(err.to_string().contains("tree_depth"));
        assert_eq!(
            match &err {
                InputValidationError::ResourceLimitExceeded { node_path, .. } => node_path.len(),
                _ => 0,
            },
            3
        );
    }

    #[test]
    fn error_is_std_error() {
        let err: Box<dyn std::error::Error> = Box::new(InputValidationError::EmptyTree);
        // Verify it implements std::error::Error
        assert!(!err.to_string().is_empty());
    }
}