use boxology_classifier::classify;
use boxology_contract::{BoxId, CapabilityName, ExposureLevel, Idempotency};
use boxology_schema::{
BoundaryLeaf, InputSlot, OutputSlot, Provenance, SchemaCapability, SchemaDataField,
SchemaDataShape, SchemaDataType, SchemaDataVariant, SchemaDocument, SchemaField, SchemaPayload,
SchemaType, SchemaVariant, Shape, TypeExpression,
};
use serde_json::json;
use std::fs;
use std::path::Path;
use syn::visit::{self, Visit};
use syn::{ExprStruct, ImplItem, Item, Member, Meta, UseTree, Visibility};
const REVISION: &str = "sha256:29c955e4594137d11300bd0894da461c2a9a9ce9866c4fd9a3f4b5d89cb04176";
const OTHER_REVISION: &str =
"sha256:a45a70dacfc5e3ea7911944d3f4fd385da1de2cdabfac86d554d4a321e3244cc";
const RUST_SOURCES: &[&str] = &["lib.rs", "report.rs", "tests.rs"];
#[derive(Default)]
struct MacroDetector {
found: bool,
}
impl<'ast> Visit<'ast> for MacroDetector {
fn visit_macro(&mut self, item: &'ast syn::Macro) {
self.found = true;
visit::visit_macro(self, item);
}
}
fn allowed_derive(path: &syn::Path) -> bool {
path.get_ident().is_some_and(|ident| {
matches!(
ident.to_string().as_str(),
"Clone" | "Copy" | "Debug" | "Eq" | "Ord" | "PartialEq" | "PartialOrd"
)
})
}
fn allowed_production_attribute(attribute: &syn::Attribute) -> bool {
if attribute.path().is_ident("doc") {
return true;
}
if !attribute.path().is_ident("derive") {
return false;
}
let Ok(derives) = attribute.parse_args_with(
syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated,
) else {
return false;
};
!derives.is_empty() && derives.iter().all(allowed_derive)
}
#[derive(Default)]
struct ProductionLock {
bad: bool,
}
impl<'ast> Visit<'ast> for ProductionLock {
fn visit_attribute(&mut self, attribute: &'ast syn::Attribute) {
self.bad |= !allowed_production_attribute(attribute);
}
fn visit_item_mod(&mut self, _: &'ast syn::ItemMod) {
self.bad = true;
}
fn visit_macro(&mut self, _: &'ast syn::Macro) {
self.bad = true;
}
}
#[derive(Default)]
struct FindingConstructorLock {
count: usize,
missing_kind: bool,
}
impl<'ast> Visit<'ast> for FindingConstructorLock {
fn visit_expr_struct(&mut self, expression: &'ast ExprStruct) {
if expression
.path
.segments
.last()
.is_some_and(|segment| segment.ident == "Finding")
{
self.count += 1;
self.missing_kind |= !expression
.fields
.iter()
.any(|field| matches!(&field.member, Member::Named(name) if name == "kind"));
}
visit::visit_expr_struct(self, expression);
}
}
fn public_inventory(source: &str) -> Vec<String> {
let file = syn::parse_file(source).expect("public-surface source parses");
let mut inventory = Vec::new();
for item in file.items {
match item {
Item::Enum(item) if matches!(item.vis, Visibility::Public(_)) => {
inventory.push(format!("enum {}", item.ident));
}
Item::Fn(item) if matches!(item.vis, Visibility::Public(_)) => {
inventory.push(format!("fn {}", item.sig.ident));
}
Item::Struct(item) if matches!(item.vis, Visibility::Public(_)) => {
inventory.push(format!("struct {}", item.ident));
}
Item::Use(item) if matches!(item.vis, Visibility::Public(_)) => {
public_uses(&item.tree, "", &mut inventory);
}
Item::Impl(item) if item.trait_.is_none() => {
let Some(target) = (match item.self_ty.as_ref() {
syn::Type::Path(path) => path
.path
.segments
.last()
.map(|segment| segment.ident.to_string()),
_ => None,
}) else {
continue;
};
for item in item.items {
if let ImplItem::Fn(method) = item
&& matches!(method.vis, Visibility::Public(_))
{
inventory.push(format!("method {target}::{}", method.sig.ident));
}
}
}
_ => {}
}
}
inventory.sort();
inventory
}
fn public_uses(tree: &UseTree, prefix: &str, inventory: &mut Vec<String>) {
match tree {
UseTree::Path(path) => {
public_uses(&path.tree, &format!("{prefix}{}::", path.ident), inventory);
}
UseTree::Name(name) => inventory.push(format!("use {prefix}{}", name.ident)),
UseTree::Rename(rename) => {
inventory.push(format!("use {prefix}{} as {}", rename.ident, rename.rename))
}
UseTree::Group(group) => {
for tree in &group.items {
public_uses(tree, prefix, inventory);
}
}
UseTree::Glob(_) => inventory.push(format!("use {prefix}*")),
}
}
fn expected_public_inventory() -> Vec<String> {
[
"enum Class",
"fn classify",
"method Class::canonical_name",
"method ClassificationReport::findings",
"method ClassificationReport::verdict",
"method Finding::base_excerpt",
"method Finding::class",
"method Finding::code",
"method Finding::condition",
"method Finding::kind",
"method Finding::path",
"method Finding::submitted_excerpt",
"struct ClassificationReport",
"struct Finding",
"use report::render_json",
"use report::render_text",
]
.into_iter()
.map(String::from)
.collect()
}
fn assert_public_surface(source: &str) {
assert_eq!(public_inventory(source), expected_public_inventory());
}
fn assert_constructor_inventory(source: &str) {
let file = syn::parse_file(source).unwrap();
let mut lock = FindingConstructorLock::default();
lock.visit_file(&file);
assert_eq!(lock.count, 28, "exact Finding constructor inventory");
assert!(!lock.missing_kind, "every Finding constructor has kind");
let missing = source.replacen("kind: KIND_CONTRACT_INTRODUCED,", "", 1);
let file = syn::parse_file(&missing).unwrap();
let mut mutant = FindingConstructorLock::default();
mutant.visit_file(&file);
assert_eq!(mutant.count, lock.count);
assert!(mutant.missing_kind, "missing constructor kind must fail");
}
fn public_surface_mutants_fail_closed(source: &str) {
let expected = expected_public_inventory();
for mutant in [
source.replacen(
"pub fn classify(",
"pub struct ClassifierOptions;\npub fn classify(",
1,
),
source.replacen(
"pub fn classify(",
"pub fn classify_with_options(\n _base: Option<&SchemaDocument>,\n _submitted: Option<&SchemaDocument>,\n) -> Result<ClassificationReport, Diagnostics> {\n loop {}\n}\n\npub fn classify(",
1,
),
source.replacen(
"pub fn kind(",
"pub fn extra_kind(&self) -> &'static str { self.kind }\n\n pub fn kind(",
1,
),
] {
assert_ne!(public_inventory(&mutant), expected);
}
}
fn require_allowed_modules(source: &str) -> Result<(), &'static str> {
let file = syn::parse_file(source).map_err(|_| "invalid Rust source")?;
let mut macros = MacroDetector::default();
macros.visit_file(&file);
if macros.found {
return Err("production macros are forbidden");
}
if file.attrs.iter().any(|attribute| {
!attribute
.path()
.get_ident()
.is_some_and(|ident| matches!(ident.to_string().as_str(), "doc" | "deny" | "forbid"))
}) {
return Err("unexpected crate attribute");
}
let Some((item, production)) = file.items.split_last() else {
return Err("tests module must be terminal");
};
let Item::Mod(module) = item else {
return Err("tests module must be terminal");
};
let cfg_test = matches!(
&module.attrs[..],
[attribute]
if matches!(&attribute.meta, Meta::List(meta)
if meta.path.is_ident("cfg") && meta.tokens.to_string() == "test")
);
if module.ident != "tests"
|| !matches!(module.vis, Visibility::Inherited)
|| module.content.is_some()
|| !cfg_test
{
return Err("unexpected module declaration");
}
let mut lock = ProductionLock::default();
let mut report_modules = 0;
for item in production {
if let Item::Mod(module) = item {
if module.ident == "report"
&& module.attrs.is_empty()
&& matches!(module.vis, Visibility::Inherited)
&& module.content.is_none()
{
report_modules += 1;
} else {
return Err("unexpected module declaration");
}
} else {
lock.visit_item(item);
}
}
if report_modules != 1 {
return Err("exactly one report module is required");
}
if lock.bad {
return Err("unexpected production attribute");
}
Ok(())
}
fn rust_source_inventory(root: &Path) -> Result<Vec<String>, &'static str> {
if fs::symlink_metadata(root)
.map_err(|_| "cannot inspect source root")?
.file_type()
.is_symlink()
{
return Err("source root symlink is forbidden");
}
fn visit(root: &Path, directory: &Path, sources: &mut Vec<String>) -> Result<(), &'static str> {
for entry in fs::read_dir(directory).map_err(|_| "cannot read source directory")? {
let entry = entry.map_err(|_| "cannot read source entry")?;
let file_type = entry
.file_type()
.map_err(|_| "cannot read source entry type")?;
if file_type.is_symlink() {
return Err("source symlinks are forbidden");
}
let path = entry.path();
if file_type.is_dir() {
visit(root, &path, sources)?;
} else if file_type.is_file() && path.extension().is_some_and(|value| value == "rs") {
let relative = path
.strip_prefix(root)
.map_err(|_| "source escaped source root")?;
let parts: Result<Vec<_>, _> = relative
.iter()
.map(|part| part.to_str().ok_or("source path is not UTF-8"))
.collect();
sources.push(parts?.join("/"));
}
}
Ok(())
}
let mut sources = Vec::new();
visit(root, root, &mut sources)?;
sources.sort();
Ok(sources)
}
fn document(box_id: &str) -> SchemaDocument {
SchemaDocument {
box_id: BoxId::new(box_id).unwrap(),
capabilities: vec![SchemaCapability {
name: CapabilityName::new("greet").unwrap(),
docs: Vec::new(),
deprecation: None,
error: "GreetError".to_owned(),
input: InputSlot {
name: "name".to_owned(),
leaf: BoundaryLeaf::String,
},
output: OutputSlot {
leaf: BoundaryLeaf::String,
},
shape: Shape::Unary,
max_exposure: ExposureLevel::External,
idempotency: Idempotency::None,
}],
data_types: Vec::new(),
provenance: Provenance::new(json!(null)),
revision: REVISION.to_owned(),
types: vec![SchemaType {
name: "GreetError".to_owned(),
docs: Vec::new(),
deprecation: None,
variants: vec![SchemaVariant {
name: "EmptyName".to_owned(),
docs: Vec::new(),
deprecation: None,
payload: SchemaPayload::Unit,
}],
}],
}
}
#[rustfmt::skip]
fn structured_type(name: &str, shape: SchemaDataShape) -> SchemaDataType { SchemaDataType { name: name.to_owned(), docs: Vec::new(), deprecation: None, shape } }
#[rustfmt::skip]
fn structured_field(name: &str, ty: TypeExpression) -> SchemaDataField { SchemaDataField { name: name.to_owned(), docs: Vec::new(), deprecation: None, ty } }
#[rustfmt::skip]
fn structured_variant(name: &str) -> SchemaDataVariant { SchemaDataVariant { name: name.to_owned(), docs: Vec::new(), deprecation: None } }
#[test]
fn surface_and_live_evasions_are_locked() {
production_inventory_and_code_anchors_are_fail_closed();
symlinked_source_root_fails_inventory();
production_ast_escapes_fail_closed();
descendant_include_fails_the_production_inventory();
every_classifier_code_is_reachable();
}
fn production_inventory_and_code_anchors_are_fail_closed() {
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let root = manifest_dir.join("src");
assert_eq!(rust_source_inventory(&root).unwrap(), RUST_SOURCES);
let root_source = fs::read_to_string(root.join("lib.rs")).unwrap();
assert_eq!(require_allowed_modules(&root_source), Ok(()));
let source = include_str!("../src/lib.rs");
let report_source = include_str!("../src/report.rs");
let report_file = syn::parse_file(report_source).unwrap();
let mut report_macros = MacroDetector::default();
report_macros.visit_file(&report_file);
assert!(!report_macros.found);
let mut report_lock = ProductionLock::default();
for item in &report_file.items {
report_lock.visit_item(item);
}
assert!(!report_lock.bad);
assert!(!report_source.contains("\"BXC"));
assert!(report_source.contains("boxology.classification-report@2"));
assert!(!report_source.contains("boxology.classification-report@1"));
assert_eq!(
public_inventory(report_source),
vec!["fn render_json".to_owned(), "fn render_text".to_owned()]
);
for (anchor, count) in [
("fn push_text_excerpt(", 1),
("fn push_json_excerpt(", 1),
("fn push_escaped(", 1),
("fn must_escape(", 1),
("fn is_layout_or_spoofing_format(", 1),
("fn push_unicode_escape(", 1),
("finding.kind()", 2),
("finding.base_excerpt()", 2),
("finding.submitted_excerpt()", 2),
("push_escaped(&mut output, finding.path())", 2),
(" path=\\\"", 1),
("'\\u{08}' => output.push_str(\"\\\\b\"),", 1),
("'\\n' => output.push_str(\"\\\\n\"),", 1),
("character.is_control()", 1),
("'\\u{00AD}'", 1),
("'\\u{0600}'..='\\u{0605}'", 1),
("'\\u{061C}'", 1),
("'\\u{06DD}'", 1),
("'\\u{070F}'", 1),
("'\\u{0890}'..='\\u{0891}'", 1),
("'\\u{08E2}'", 1),
("'\\u{180E}'", 1),
("'\\u{200B}'..='\\u{200F}'", 1),
("'\\u{202A}'..='\\u{202E}'", 1),
("'\\u{2060}'..='\\u{2064}'", 1),
("'\\u{2066}'..='\\u{206F}'", 1),
("'\\u{FEFF}'", 1),
("'\\u{FFF9}'..='\\u{FFFB}'", 1),
("'\\u{110BD}'", 1),
("'\\u{110CD}'", 1),
("'\\u{13430}'..='\\u{1343F}'", 1),
("'\\u{1BCA0}'..='\\u{1BCA3}'", 1),
("'\\u{1D173}'..='\\u{1D17A}'", 1),
("'\\u{E0001}'", 1),
("'\\u{E0020}'..='\\u{E007F}'", 1),
("0x1_0000", 2),
("0xD800", 1),
("0xDC00", 1),
] {
assert_eq!(
report_source.matches(anchor).count(),
count,
"{anchor} count"
);
}
assert!(!report_source.contains("output.push_str(finding.path())"));
let dropped_kind = report_source.replacen("finding.kind()", "finding.code()", 1);
assert_eq!(dropped_kind.matches("finding.kind()").count(), 1);
let dropped_base = report_source.replacen("finding.base_excerpt()", "None", 1);
assert_eq!(dropped_base.matches("finding.base_excerpt()").count(), 1);
let dropped_submitted = report_source.replacen("finding.submitted_excerpt()", "None", 1);
assert_eq!(
dropped_submitted
.matches("finding.submitted_excerpt()")
.count(),
1
);
let verbatim_newline = report_source.replacen("'\\n' => output.push_str(\"\\\\n\"),", "", 1);
assert_eq!(
verbatim_newline
.matches("'\\n' => output.push_str(\"\\\\n\"),")
.count(),
0
);
let raw_human_path = report_source.replacen(
"push_escaped(&mut output, finding.path())",
"output.push_str(finding.path()); let _ = \"\"",
1,
);
assert_eq!(
raw_human_path
.matches("push_escaped(&mut output, finding.path())")
.count(),
1
);
assert!(raw_human_path.contains("output.push_str(finding.path())"));
let dropped_bidi =
report_source.replacen("'\\u{202A}'..='\\u{202E}'", "'\\u{202A}'..='\\u{202D}'", 1);
assert_eq!(dropped_bidi.matches("'\\u{202A}'..='\\u{202E}'").count(), 0);
let dropped_arabic_number =
report_source.replacen("'\\u{0600}'..='\\u{0605}'", "'\\u{0600}'..='\\u{0604}'", 1);
assert_eq!(
dropped_arabic_number
.matches("'\\u{0600}'..='\\u{0605}'")
.count(),
0
);
let dropped_supplementary = report_source.replacen(
"'\\u{1BCA0}'..='\\u{1BCA3}'",
"'\\u{1BCA0}'..='\\u{1BCA2}'",
1,
);
assert_eq!(
dropped_supplementary
.matches("'\\u{1BCA0}'..='\\u{1BCA3}'")
.count(),
0
);
let dropped_tag = report_source.replacen(
"'\\u{E0020}'..='\\u{E007F}'",
"'\\u{E0020}'..='\\u{E007E}'",
1,
);
assert_eq!(
dropped_tag.matches("'\\u{E0020}'..='\\u{E007F}'").count(),
0
);
let dropped_surrogate = report_source.replacen("0xD800", "0xD801", 1);
assert_eq!(dropped_surrogate.matches("0xD800").count(), 0);
let dropped_control = report_source.replacen("character.is_control()", "false", 1);
assert_eq!(dropped_control.matches("character.is_control()").count(), 0);
assert_public_surface(source);
assert_constructor_inventory(source);
public_surface_mutants_fail_closed(source);
let anchors = [
("BXC0024", "Diagnostic::classification_requires_document()"),
("BXC0025", "Diagnostic::box_id_mismatch()"),
("BXC0026", "\"BXC0026\""),
("BXC0027", "\"BXC0027\""),
("BXC0028", "\"BXC0028\""),
("BXC0031", "\"BXC0031\""),
("BXC0032", "\"BXC0032\""),
("BXC0033", "\"BXC0033\""),
("BXC0034", "\"BXC0034\""),
("BXC0035", "\"BXC0035\""),
("BXC0036", "\"BXC0036\""),
("BXC0039", "\"BXC0039\""),
("BXC0040", "\"BXC0040\""),
("BXC0041", "\"BXC0041\""),
("BXC0042", "\"BXC0042\""),
("BXC0043", "\"BXC0043\""),
("BXC0044", "\"BXC0044\""),
("BXC0045", "\"BXC0045\""),
("BXC0046", "\"BXC0046\""),
("BXC0047", "\"BXC0047\""),
("BXC0048", "\"BXC0048\""),
("BXC0049", "\"BXC0049\""),
("BXC0050", "\"BXC0050\""),
("BXC0051", "\"BXC0051\""),
("BXC0052", "\"BXC0052\""),
("BXC0063", "\"BXC0063\""),
("BXC0064", "\"BXC0064\""),
("BXC0065", "\"BXC0065\""),
("BXC0066", "\"BXC0066\""),
("BXC0067", "\"BXC0067\""),
("BXC0068", "\"BXC0068\""),
("BXC0069", "\"BXC0069\""),
("BXC0036 condition", "\"unknown-variant tolerance\""),
(
"BXC0037",
"Diagnostic::integrity_findings_under_equal_revisions()",
),
(
"BXC0038",
"Diagnostic::integrity_silence_under_differing_revisions()",
),
("classify", "pub fn classify("),
];
for (code, anchor) in anchors {
assert_eq!(source.matches(anchor).count(), 1, "{code} anchor count");
}
let needle = format!("{}BXC", '"');
let mut emitted: Vec<&str> = Vec::new();
for (at, _) in source.match_indices(needle.as_str()) {
let code = source.get(at + 1..at + 8).unwrap_or_default();
if !emitted.contains(&code) {
emitted.push(code);
}
}
emitted.sort_unstable();
let mut reserved = boxology_schema::CLASSIFIER_RESERVED_CODES.to_vec();
reserved.sort_unstable();
assert_eq!(emitted, reserved);
}
fn symlinked_source_root_fails_inventory() {
let link = std::env::temp_dir().join(format!("classifier-source-link-{}", std::process::id()));
let target = link.with_extension("target");
fs::create_dir(&target).unwrap();
std::os::unix::fs::symlink(&target, &link).unwrap();
let result = rust_source_inventory(&link);
fs::remove_file(link).unwrap();
fs::remove_dir(target).unwrap();
assert_eq!(result, Err("source root symlink is forbidden"));
}
fn production_ast_escapes_fail_closed() {
let source = include_str!("../src/lib.rs");
for mutant in [
source.replacen("pub fn classify(", "#[cfg(test)]\npub fn classify(", 1),
source.replacen("pub fn classify(", "#[classifier]\npub fn classify(", 1),
source.replacen(
"pub fn classify(",
"#[cfg_attr(test, classifier)]\npub fn classify(",
1,
),
] {
assert_eq!(mutant.matches("pub fn classify(").count(), 1);
assert_eq!(
require_allowed_modules(&mutant),
Err("unexpected production attribute")
);
}
let divergent = format!("{source}\npub fn classify() {{}}\n");
assert_eq!(divergent.matches("pub fn classify(").count(), 2);
assert_eq!(
require_allowed_modules(&divergent),
Err("tests module must be terminal")
);
}
fn descendant_include_fails_the_production_inventory() {
let source = include_str!("../src/lib.rs");
let marker = "\n#[cfg(test)]\nmod tests;";
for attack in [
"include!(\"hidden/probe.rs\");",
"std::include!(\"../review_external_include.rs\");",
"macro_rules! hidden { () => { include!(\"../hidden.rs\"); } }\nhidden!();",
] {
let mutant = source.replacen(marker, &format!("\n{attack}{marker}"), 1);
assert_eq!(
require_allowed_modules(&mutant),
Err("production macros are forbidden")
);
}
}
fn every_classifier_code_is_reachable() {
let missing = classify(None, None).unwrap_err().into_vec();
let mismatch = classify(Some(&document("hello")), Some(&document("other")))
.unwrap_err()
.into_vec();
let introduced = classify(None, Some(&document("hello"))).unwrap();
let removed = classify(Some(&document("hello")), None).unwrap();
let mut input_name_changed = document("hello");
input_name_changed.capabilities[0].input.name = "label".to_owned();
input_name_changed.revision = OTHER_REVISION.to_owned();
let input_name_changed = classify(Some(&document("hello")), Some(&input_name_changed)).unwrap();
let mut type_added = document("hello");
type_added.capabilities.push(SchemaCapability {
name: CapabilityName::new("wave").unwrap(),
docs: Vec::new(),
deprecation: None,
error: "WaveError".to_owned(),
input: InputSlot {
name: "name".to_owned(),
leaf: BoundaryLeaf::String,
},
output: OutputSlot {
leaf: BoundaryLeaf::String,
},
shape: Shape::Unary,
max_exposure: ExposureLevel::External,
idempotency: Idempotency::None,
});
type_added.types.push(SchemaType {
name: "WaveError".to_owned(),
docs: Vec::new(),
deprecation: None,
variants: vec![SchemaVariant {
name: "EmptyName".to_owned(),
docs: Vec::new(),
deprecation: None,
payload: SchemaPayload::Unit,
}],
});
type_added.revision = OTHER_REVISION.to_owned();
let additive = classify(Some(&document("hello")), Some(&type_added)).unwrap();
let mut capability_removed = document("hello");
capability_removed.capabilities.pop();
capability_removed.revision = OTHER_REVISION.to_owned();
let capability_removed = classify(Some(&document("hello")), Some(&capability_removed)).unwrap();
let mut input_leaf_changed = document("hello");
input_leaf_changed.capabilities[0].input.leaf = BoundaryLeaf::Bool;
input_leaf_changed.revision = OTHER_REVISION.to_owned();
let input_leaf_changed = classify(Some(&document("hello")), Some(&input_leaf_changed)).unwrap();
let mut output_leaf_changed = document("hello");
output_leaf_changed.capabilities[0].output.leaf = BoundaryLeaf::Bool;
output_leaf_changed.revision = OTHER_REVISION.to_owned();
let output_leaf_changed =
classify(Some(&document("hello")), Some(&output_leaf_changed)).unwrap();
let mut capability_metadata = document("hello");
let capability = &mut capability_metadata.capabilities[0];
capability.docs.push("docs".to_owned());
capability.deprecation = Some("retired".to_owned());
capability.error = "OtherError".to_owned();
capability.max_exposure = ExposureLevel::Internal;
capability.idempotency = Idempotency::Inherent;
capability_metadata.revision = OTHER_REVISION.to_owned();
let capability_metadata =
classify(Some(&document("hello")), Some(&capability_metadata)).unwrap();
let mut high = document("hello");
high.capabilities[0].idempotency = Idempotency::Inherent;
let mut low = high.clone();
low.capabilities[0].max_exposure = ExposureLevel::CodeOnly;
high.revision = OTHER_REVISION.to_owned();
let raised = classify(Some(&low), Some(&high)).unwrap();
low.revision = OTHER_REVISION.to_owned();
high.revision = REVISION.to_owned();
high.capabilities[0].idempotency = Idempotency::None;
let weakened = classify(Some(&low), Some(&high)).unwrap();
let mut field_base = document("hello");
field_base.types[0].variants[0].payload = SchemaPayload::Named(vec![
SchemaField {
docs: Vec::new(),
deprecation: None,
name: "old".to_owned(),
ty: BoundaryLeaf::String,
},
SchemaField {
docs: Vec::new(),
deprecation: None,
name: "changed".to_owned(),
ty: BoundaryLeaf::String,
},
]);
let mut field_submitted = field_base.clone();
field_submitted.types[0].variants[0].payload = SchemaPayload::Named(vec![
SchemaField {
docs: Vec::new(),
deprecation: None,
name: "changed".to_owned(),
ty: BoundaryLeaf::Bool,
},
SchemaField {
docs: Vec::new(),
deprecation: None,
name: "new".to_owned(),
ty: BoundaryLeaf::String,
},
]);
field_submitted.revision = OTHER_REVISION.to_owned();
let fields = classify(Some(&field_base), Some(&field_submitted)).unwrap();
let mut payload = document("hello");
payload.types[0].variants[0].payload = SchemaPayload::Value {
docs: Vec::new(),
deprecation: None,
ty: BoundaryLeaf::String,
};
payload.revision = OTHER_REVISION.to_owned();
let payload = classify(Some(&document("hello")), Some(&payload)).unwrap();
let mut reordered_base = document("hello");
let mut extra = reordered_base.capabilities[0].clone();
extra.name = CapabilityName::new("wave").unwrap();
reordered_base.capabilities.push(extra);
let mut reordered = reordered_base.clone();
reordered.capabilities.swap(0, 1);
reordered.revision = OTHER_REVISION.to_owned();
let fail_closed = classify(Some(&reordered_base), Some(&reordered)).unwrap();
let mut type_removed_base = document("hello");
type_removed_base.capabilities.push(SchemaCapability {
name: CapabilityName::new("wave").unwrap(),
docs: Vec::new(),
deprecation: None,
error: "WaveError".to_owned(),
input: InputSlot {
name: "name".to_owned(),
leaf: BoundaryLeaf::String,
},
output: OutputSlot {
leaf: BoundaryLeaf::String,
},
shape: Shape::Unary,
max_exposure: ExposureLevel::External,
idempotency: Idempotency::None,
});
type_removed_base.types.push(SchemaType {
name: "WaveError".to_owned(),
docs: Vec::new(),
deprecation: None,
variants: vec![SchemaVariant {
name: "EmptyName".to_owned(),
docs: Vec::new(),
deprecation: None,
payload: SchemaPayload::Unit,
}],
});
let mut type_removed_submitted = document("hello");
type_removed_submitted.revision = OTHER_REVISION.to_owned();
let type_removed = classify(Some(&type_removed_base), Some(&type_removed_submitted)).unwrap();
let mut type_docs = document("hello");
type_docs.types[0].docs.push("docs".to_owned());
type_docs.revision = OTHER_REVISION.to_owned();
let docs = classify(Some(&document("hello")), Some(&type_docs)).unwrap();
let mut type_deprecation = document("hello");
type_deprecation.types[0].deprecation = Some("retired".to_owned());
type_deprecation.revision = OTHER_REVISION.to_owned();
let deprecation = classify(Some(&document("hello")), Some(&type_deprecation)).unwrap();
let mut variant_removed_base = document("hello");
variant_removed_base.types[0].variants.push(SchemaVariant {
name: "Other".to_owned(),
docs: Vec::new(),
deprecation: None,
payload: SchemaPayload::Unit,
});
let mut variant_removed_submitted = document("hello");
variant_removed_submitted.revision = OTHER_REVISION.to_owned();
let variant_removed = classify(
Some(&variant_removed_base),
Some(&variant_removed_submitted),
)
.unwrap();
let mut variant_addition = document("hello");
variant_addition.types[0].variants.push(SchemaVariant {
name: "Other".to_owned(),
docs: Vec::new(),
deprecation: None,
payload: SchemaPayload::Unit,
});
variant_addition.revision = OTHER_REVISION.to_owned();
let conditional = classify(Some(&document("hello")), Some(&variant_addition)).unwrap();
let mut structured_base = document("hello");
structured_base.capabilities[0].input.leaf = TypeExpression::Local("Payload".to_owned());
structured_base.capabilities[0].output.leaf = TypeExpression::Local("Payload".to_owned());
structured_base.data_types = vec![
structured_type(
"Mood",
SchemaDataShape::Enum(vec![structured_variant("Old")]),
),
structured_type("Switch", SchemaDataShape::Struct(Vec::new())),
structured_type(
"Payload",
SchemaDataShape::Struct(vec![
structured_field("old", TypeExpression::String),
structured_field("changed", TypeExpression::String),
structured_field("mood", TypeExpression::Local("Mood".to_owned())),
structured_field("switch", TypeExpression::Local("Switch".to_owned())),
]),
),
];
let mut structured_submitted = structured_base.clone();
structured_submitted.data_types[0].shape =
SchemaDataShape::Enum(vec![structured_variant("New")]);
structured_submitted.data_types[1].shape =
SchemaDataShape::Enum(vec![structured_variant("On")]);
if let SchemaDataShape::Struct(fields) = &mut structured_submitted.data_types[2].shape {
fields.remove(0);
fields[0].ty = TypeExpression::Bool;
fields.push(structured_field("new", TypeExpression::Bool));
}
structured_submitted.revision = OTHER_REVISION.to_owned();
let structured = classify(Some(&structured_base), Some(&structured_submitted)).unwrap();
let mut equal_revision_diff = document("hello");
equal_revision_diff.types[0].variants.push(SchemaVariant {
name: "Other".to_owned(),
docs: Vec::new(),
deprecation: None,
payload: SchemaPayload::Unit,
});
let integrity_equal = classify(Some(&document("hello")), Some(&equal_revision_diff))
.unwrap_err()
.into_vec();
let mut revision_only = document("hello");
revision_only.revision = OTHER_REVISION.to_owned();
let integrity_silence = classify(Some(&document("hello")), Some(&revision_only))
.unwrap_err()
.into_vec();
let mut reached = vec![
missing[0].code(),
mismatch[0].code(),
integrity_equal[0].code(),
integrity_silence[0].code(),
];
for report in [
&introduced,
&removed,
&additive,
&capability_removed,
&input_name_changed,
&input_leaf_changed,
&output_leaf_changed,
&capability_metadata,
&raised,
&weakened,
&fields,
&payload,
&fail_closed,
&type_removed,
&docs,
&deprecation,
&variant_removed,
&conditional,
&structured,
] {
reached.extend(report.findings().iter().map(|finding| finding.code()));
}
reached.sort_unstable();
reached.dedup();
let mut expected = boxology_schema::CLASSIFIER_RESERVED_CODES.to_vec();
expected.extend(["BXC0024", "BXC0025", "BXC0037", "BXC0038"]);
expected.sort_unstable();
assert_eq!(reached, expected);
}