oxipdf-ir 0.1.0

Intermediate representation types for the oxipdf PDF engine
Documentation
//! IR versioning and compatibility rules.
//!
//! The `StyledTree` IR schema is owned by oxipdf and versioned independently (ยง10.1).
//! Consumers target a specific IR version and must handle migration across breaking changes.
//!
//! # Compatibility Rules
//!
//! - **Same major version**: forward-compatible. A tree built with IR v1.0 can be
//!   processed by an engine supporting IR v1.3. New optional fields added in minor
//!   versions use documented defaults when absent.
//! - **Different major version**: incompatible. The engine rejects the tree with
//!   an `InputValidationError` indicating the version mismatch.
//! - **Patch versions** (if used): no semantic difference; purely for documentation.

use std::fmt;

/// The current IR version supported by this build of oxipdf.
pub const CURRENT_IR_VERSION: IrVersion = IrVersion::new(1, 0);

/// A semantic version for the IR schema.
///
/// Only `major` and `minor` are used for compatibility decisions.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct IrVersion {
    major: u32,
    minor: u32,
}

impl IrVersion {
    /// Create a new IR version.
    #[must_use]
    pub const fn new(major: u32, minor: u32) -> Self {
        Self { major, minor }
    }

    /// Major version number. Breaking changes increment this.
    #[must_use]
    pub const fn major(self) -> u32 {
        self.major
    }

    /// Minor version number. Additive changes increment this.
    #[must_use]
    pub const fn minor(self) -> u32 {
        self.minor
    }

    /// Check whether a tree at `self` version is compatible with an engine
    /// supporting `engine_version`.
    ///
    /// Returns `true` if the tree can be processed by the engine:
    /// - Same major version required.
    /// - Tree minor version must not exceed engine minor version (the engine
    ///   cannot understand fields added in a newer minor version).
    #[must_use]
    pub const fn is_compatible_with(self, engine_version: IrVersion) -> bool {
        self.major == engine_version.major && self.minor <= engine_version.minor
    }
}

impl fmt::Display for IrVersion {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}.{}", self.major, self.minor)
    }
}

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

    #[test]
    fn same_version_compatible() {
        let v = IrVersion::new(1, 0);
        assert!(v.is_compatible_with(v));
    }

    #[test]
    fn older_minor_compatible_with_newer_engine() {
        let tree = IrVersion::new(1, 0);
        let engine = IrVersion::new(1, 3);
        assert!(tree.is_compatible_with(engine));
    }

    #[test]
    fn newer_minor_incompatible_with_older_engine() {
        let tree = IrVersion::new(1, 3);
        let engine = IrVersion::new(1, 0);
        assert!(!tree.is_compatible_with(engine));
    }

    #[test]
    fn different_major_incompatible() {
        let tree = IrVersion::new(1, 0);
        let engine = IrVersion::new(2, 0);
        assert!(!tree.is_compatible_with(engine));
    }

    #[test]
    fn display() {
        assert_eq!(IrVersion::new(1, 3).to_string(), "1.3");
    }
}