boxology-cli-core 0.1.1

Reusable command-line boundary
Documentation
//! Pure classification of checked-in and regenerated schema document bytes.
#![deny(missing_docs)]
#![forbid(unsafe_code)]

use boxology_classifier::ClassificationReport;
use boxology_schema::{Diagnostics, SchemaDocument};
use std::fmt;

type Rule = (&'static str, &'static str, &'static str);
const D1_SOURCE: &str =
    "specs/s4-contract-change-classification.md D1; specs/s5-manifest-and-validation.md D5";
const PAIRING_SOURCE: &str =
    "specs/s4-contract-change-classification.md D2 D6; specs/s5-manifest-and-validation.md D5";
const BASE_TEXT: &str = "the checked-in schema document must satisfy the strict format-1 reader";
const SUBMITTED_TEXT: &str =
    "the regenerated schema document must satisfy the strict format-1 reader";
const PAIRING_TEXT: &str =
    "the checked-in and regenerated schema documents must pair and satisfy classifier integrity";
const BASE: Rule = ("BXW0077", BASE_TEXT, D1_SOURCE);
const SUBMITTED: Rule = ("BXW0078", SUBMITTED_TEXT, D1_SOURCE);
const PAIRING: Rule = ("BXW0079", PAIRING_TEXT, PAIRING_SOURCE);

/// A stable classification failure with the underlying schema or classifier diagnostics.
#[derive(Debug)]
pub struct ClassifyError {
    code: &'static str,
    side: &'static str,
    detail: &'static str,
    diagnostics: Diagnostics,
}

impl ClassifyError {
    /// Returns the stable `BXW####` code.
    pub fn code(&self) -> &'static str {
        self.code
    }

    /// Returns which classification stage failed: `base`, `submitted`, or `pairing`.
    pub fn side(&self) -> &'static str {
        self.side
    }

    /// Returns the stable static rule detail.
    pub fn detail(&self) -> &'static str {
        self.detail
    }

    /// Returns the schema or classifier diagnostics without changing their order or rendering.
    pub fn diagnostics(&self) -> &Diagnostics {
        &self.diagnostics
    }

    fn base(diagnostics: Diagnostics) -> Self {
        Self {
            code: BASE.0,
            side: "base",
            detail: BASE.1,
            diagnostics,
        }
    }

    fn submitted(diagnostics: Diagnostics) -> Self {
        Self {
            code: SUBMITTED.0,
            side: "submitted",
            detail: SUBMITTED.1,
            diagnostics,
        }
    }

    fn pairing(diagnostics: Diagnostics) -> Self {
        Self {
            code: PAIRING.0,
            side: "pairing",
            detail: PAIRING.1,
            diagnostics,
        }
    }
}

impl fmt::Display for ClassifyError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            formatter,
            "{} {}: {}: {}",
            self.code, self.side, self.detail, self.diagnostics
        )
    }
}

impl std::error::Error for ClassifyError {}

/// Classifies checked-in and regenerated schema bytes through the strict reader and classifier.
///
/// # Errors
/// Returns `BXW0077` when the checked-in bytes fail the strict reader, `BXW0078` when the
/// regenerated bytes fail the strict reader, or `BXW0079` when pairing or integrity fails.
pub fn classify(
    base: Option<&[u8]>,
    submitted: &[u8],
) -> Result<ClassificationReport, ClassifyError> {
    let base = match base {
        Some(bytes) => Some(SchemaDocument::parse(bytes).map_err(ClassifyError::base)?),
        None => None,
    };
    let submitted = SchemaDocument::parse(submitted).map_err(ClassifyError::submitted)?;
    boxology_classifier::classify(base.as_ref(), Some(&submitted)).map_err(ClassifyError::pairing)
}