Skip to main content

boxology_cli_core/
classify.rs

1//! Pure classification of checked-in and regenerated schema document bytes.
2#![deny(missing_docs)]
3#![forbid(unsafe_code)]
4
5use boxology_classifier::ClassificationReport;
6use boxology_schema::{Diagnostics, SchemaDocument};
7use std::fmt;
8
9type Rule = (&'static str, &'static str, &'static str);
10const D1_SOURCE: &str =
11    "specs/s4-contract-change-classification.md D1; specs/s5-manifest-and-validation.md D5";
12const PAIRING_SOURCE: &str =
13    "specs/s4-contract-change-classification.md D2 D6; specs/s5-manifest-and-validation.md D5";
14const BASE_TEXT: &str = "the checked-in schema document must satisfy the strict format-1 reader";
15const SUBMITTED_TEXT: &str =
16    "the regenerated schema document must satisfy the strict format-1 reader";
17const PAIRING_TEXT: &str =
18    "the checked-in and regenerated schema documents must pair and satisfy classifier integrity";
19const BASE: Rule = ("BXW0077", BASE_TEXT, D1_SOURCE);
20const SUBMITTED: Rule = ("BXW0078", SUBMITTED_TEXT, D1_SOURCE);
21const PAIRING: Rule = ("BXW0079", PAIRING_TEXT, PAIRING_SOURCE);
22
23/// A stable classification failure with the underlying schema or classifier diagnostics.
24#[derive(Debug)]
25pub struct ClassifyError {
26    code: &'static str,
27    side: &'static str,
28    detail: &'static str,
29    diagnostics: Diagnostics,
30}
31
32impl ClassifyError {
33    /// Returns the stable `BXW####` code.
34    pub fn code(&self) -> &'static str {
35        self.code
36    }
37
38    /// Returns which classification stage failed: `base`, `submitted`, or `pairing`.
39    pub fn side(&self) -> &'static str {
40        self.side
41    }
42
43    /// Returns the stable static rule detail.
44    pub fn detail(&self) -> &'static str {
45        self.detail
46    }
47
48    /// Returns the schema or classifier diagnostics without changing their order or rendering.
49    pub fn diagnostics(&self) -> &Diagnostics {
50        &self.diagnostics
51    }
52
53    fn base(diagnostics: Diagnostics) -> Self {
54        Self {
55            code: BASE.0,
56            side: "base",
57            detail: BASE.1,
58            diagnostics,
59        }
60    }
61
62    fn submitted(diagnostics: Diagnostics) -> Self {
63        Self {
64            code: SUBMITTED.0,
65            side: "submitted",
66            detail: SUBMITTED.1,
67            diagnostics,
68        }
69    }
70
71    fn pairing(diagnostics: Diagnostics) -> Self {
72        Self {
73            code: PAIRING.0,
74            side: "pairing",
75            detail: PAIRING.1,
76            diagnostics,
77        }
78    }
79}
80
81impl fmt::Display for ClassifyError {
82    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
83        write!(
84            formatter,
85            "{} {}: {}: {}",
86            self.code, self.side, self.detail, self.diagnostics
87        )
88    }
89}
90
91impl std::error::Error for ClassifyError {}
92
93/// Classifies checked-in and regenerated schema bytes through the strict reader and classifier.
94///
95/// # Errors
96/// Returns `BXW0077` when the checked-in bytes fail the strict reader, `BXW0078` when the
97/// regenerated bytes fail the strict reader, or `BXW0079` when pairing or integrity fails.
98pub fn classify(
99    base: Option<&[u8]>,
100    submitted: &[u8],
101) -> Result<ClassificationReport, ClassifyError> {
102    let base = match base {
103        Some(bytes) => Some(SchemaDocument::parse(bytes).map_err(ClassifyError::base)?),
104        None => None,
105    };
106    let submitted = SchemaDocument::parse(submitted).map_err(ClassifyError::submitted)?;
107    boxology_classifier::classify(base.as_ref(), Some(&submitted)).map_err(ClassifyError::pairing)
108}