boxology_cli_core/
check.rs1#![deny(missing_docs)]
3#![forbid(unsafe_code)]
4
5use boxology_contract::BoxId;
6use boxology_schema::{Diagnostics, SchemaDocument};
7use boxology_workspace::{
8 ClassificationFinding, ClassificationFindings, ContractClassificationCompletion,
9};
10use std::fmt;
11
12type Rule = (&'static str, &'static str, &'static str);
13const CHECK_D1_SOURCE: &str =
14 "specs/s4-contract-change-classification.md D1; specs/s5-manifest-and-validation.md D6";
15const CHECK_PAIRING_SOURCE: &str =
16 "specs/s4-contract-change-classification.md D2 D6; specs/s5-manifest-and-validation.md D6";
17const CHECK_BASE_TEXT: &str =
18 "the base-revision schema document must satisfy the strict format-1 reader";
19const CHECK_SUBMITTED_TEXT: &str =
20 "the checked-in schema document must satisfy the strict format-1 reader";
21const CHECK_PAIRING_TEXT: &str =
22 "the base-revision and checked-in schema documents must pair and satisfy classifier integrity";
23const CHECK_BASE: Rule = ("BXW0080", CHECK_BASE_TEXT, CHECK_D1_SOURCE);
24const CHECK_SUBMITTED: Rule = ("BXW0081", CHECK_SUBMITTED_TEXT, CHECK_D1_SOURCE);
25const CHECK_PAIRING: Rule = ("BXW0082", CHECK_PAIRING_TEXT, CHECK_PAIRING_SOURCE);
26
27#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29pub struct DuplicatePackages;
30
31#[derive(Clone, Debug, Eq, PartialEq)]
33pub struct PackageSchemas {
34 package: BoxId,
35 base: Option<Vec<u8>>,
36 submitted: Vec<u8>,
37}
38
39impl PackageSchemas {
40 pub fn new(package: BoxId, base: Option<Vec<u8>>, submitted: Vec<u8>) -> Self {
42 Self {
43 package,
44 base,
45 submitted,
46 }
47 }
48
49 pub fn package(&self) -> &BoxId {
51 &self.package
52 }
53
54 pub fn base(&self) -> Option<&[u8]> {
56 self.base.as_deref()
57 }
58
59 pub fn submitted(&self) -> &[u8] {
61 &self.submitted
62 }
63}
64
65#[derive(Debug)]
67pub struct CheckClassificationError {
68 package: BoxId,
69 code: &'static str,
70 side: &'static str,
71 detail: &'static str,
72 diagnostics: Diagnostics,
73}
74
75impl CheckClassificationError {
76 pub fn package(&self) -> &BoxId {
78 &self.package
79 }
80
81 pub fn code(&self) -> &'static str {
83 self.code
84 }
85
86 pub fn side(&self) -> &'static str {
88 self.side
89 }
90
91 pub fn detail(&self) -> &'static str {
93 self.detail
94 }
95
96 pub fn diagnostics(&self) -> &Diagnostics {
98 &self.diagnostics
99 }
100
101 fn fail(package: BoxId, rule: Rule, side: &'static str, diagnostics: Diagnostics) -> Self {
102 Self {
103 package,
104 code: rule.0,
105 side,
106 detail: rule.1,
107 diagnostics,
108 }
109 }
110}
111
112impl fmt::Display for CheckClassificationError {
113 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
114 write!(
115 formatter,
116 "{} {} {}: {}: {}",
117 self.code, self.package, self.side, self.detail, self.diagnostics
118 )
119 }
120}
121
122#[derive(Debug)]
124pub enum ClassifyStepError {
125 Duplicate(DuplicatePackages),
127 Classification(CheckClassificationError),
129}
130
131pub fn classify_step(
140 packages: &[PackageSchemas],
141) -> Result<ContractClassificationCompletion, ClassifyStepError> {
142 if has_duplicate_package(packages) {
143 return Err(ClassifyStepError::Duplicate(DuplicatePackages));
144 }
145 let mut ordered: Vec<&PackageSchemas> = packages.iter().collect();
146 ordered.sort_by_key(|package| package.package.as_str());
147 let mut findings = Vec::new();
148 for package in ordered {
149 findings.extend(classify_package(package).map_err(ClassifyStepError::Classification)?);
150 }
151 Ok(match ClassificationFindings::new(findings) {
152 None => ContractClassificationCompletion::Passed,
153 Some(findings) => ContractClassificationCompletion::Failed(findings),
154 })
155}
156
157fn has_duplicate_package(packages: &[PackageSchemas]) -> bool {
158 let mut seen: Vec<&str> = packages
159 .iter()
160 .map(|package| package.package.as_str())
161 .collect();
162 seen.sort_unstable();
163 seen.windows(2).any(|pair| pair[0] == pair[1])
164}
165
166fn classify_package(
167 package: &PackageSchemas,
168) -> Result<Vec<ClassificationFinding>, CheckClassificationError> {
169 let package_id = package.package.clone();
170 let base = match package.base.as_deref() {
171 Some(bytes) => Some(SchemaDocument::parse(bytes).map_err(|diagnostics| {
172 CheckClassificationError::fail(package_id.clone(), CHECK_BASE, "base", diagnostics)
173 })?),
174 None => None,
175 };
176 let submitted = SchemaDocument::parse(&package.submitted).map_err(|diagnostics| {
177 CheckClassificationError::fail(
178 package_id.clone(),
179 CHECK_SUBMITTED,
180 "submitted",
181 diagnostics,
182 )
183 })?;
184 let report =
185 boxology_classifier::classify(base.as_ref(), Some(&submitted)).map_err(|diagnostics| {
186 CheckClassificationError::fail(
187 package_id.clone(),
188 CHECK_PAIRING,
189 "pairing",
190 diagnostics,
191 )
192 })?;
193 Ok(report
194 .findings()
195 .iter()
196 .map(|finding| {
197 ClassificationFinding::new(
198 package_id.clone(),
199 finding.path().to_owned(),
200 finding.code().to_owned(),
201 finding.class().canonical_name().to_owned(),
202 finding.condition().map(str::to_owned),
203 )
204 })
205 .collect())
206}