#![expect(
clippy::disallowed_types,
reason = "dev/verification tooling over JSON artifacts (the catalogue, results, wire \
exchanges), whose shapes belong to the artifacts and the SUT"
)]
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use crate::load::{LoadError, compile_schema, load_artifact};
use crate::model::binding::OperationBinding;
use crate::model::capability::CapabilityMatrix;
use crate::model::case::CaseCore;
use crate::model::corpus::CorpusManifest;
use crate::model::register::AmbiguityRegister;
use crate::model::vocab_files::{OutcomesVocab, SelectorsVocab};
use crate::model::wire_surface::WireSurface;
use crate::schema;
#[derive(Debug, Default)]
pub struct ArtifactSet {
pub cases: Vec<(PathBuf, CaseCore)>,
pub performance: Vec<(PathBuf, crate::perf::PerformanceCase)>,
pub bindings: Vec<(PathBuf, OperationBinding)>,
pub outcomes: Option<(PathBuf, OutcomesVocab)>,
pub selectors: Option<(PathBuf, SelectorsVocab)>,
pub matrix: Option<(PathBuf, CapabilityMatrix)>,
pub journeys: Option<(PathBuf, crate::perf::JourneyCatalogue)>,
pub corpus: Option<(PathBuf, CorpusManifest)>,
pub register: Option<(PathBuf, AmbiguityRegister)>,
pub wire_surface: Option<(PathBuf, WireSurface)>,
pub corpus_dir: Option<PathBuf>,
pub parties: Vec<(PathBuf, crate::party::Statement)>,
pub party_ixits: Vec<(PathBuf, crate::ixit::Ixit)>,
}
#[derive(Debug, Default)]
pub struct Loaded {
pub set: ArtifactSet,
pub errors: Vec<LoadError>,
}
fn yaml_files_under(dir: &Path) -> Vec<PathBuf> {
let mut files = Vec::new();
let mut stack = vec![dir.to_owned()];
while let Some(current) = stack.pop() {
let Ok(entries) = std::fs::read_dir(¤t) else {
continue;
};
for entry in entries.flatten() {
let Ok(kind) = entry.file_type() else {
continue;
};
let path = entry.path();
let is_dir = if kind.is_symlink() {
path.is_dir()
} else {
kind.is_dir()
};
if is_dir {
stack.push(path);
} else if path.extension().is_some_and(|e| e == "yaml" || e == "yml") {
files.push(path);
}
}
}
files.sort();
files
}
fn statement_files_under(dir: &Path) -> Vec<PathBuf> {
let Ok(entries) = std::fs::read_dir(dir) else {
return Vec::new();
};
let mut files: Vec<PathBuf> = entries
.flatten()
.map(|e| e.path().join("statement.json"))
.filter(|p| p.is_file())
.collect();
files.sort();
files
}
fn load_json_document<T: serde::de::DeserializeOwned>(
path: &Path,
validator: &jsonschema::Validator,
) -> Result<T, LoadError> {
let text = std::fs::read_to_string(path).map_err(|source| LoadError::Io {
path: path.to_owned(),
source,
})?;
let value: serde_json::Value = serde_json::from_str(&text).map_err(|e| LoadError::Model {
path: path.to_owned(),
message: format!("JSON: {e}"),
})?;
crate::load::validate_against(validator, &value, path)?;
serde_json::from_value(value).map_err(|e| LoadError::Model {
path: path.to_owned(),
message: e.to_string(),
})
}
fn load_performance_case(path: &Path) -> Result<crate::perf::PerformanceCase, LoadError> {
let value = crate::load::yaml_file_to_value(path)?;
let case: crate::perf::PerformanceCase =
serde_json::from_value(value).map_err(|e| LoadError::Model {
path: path.to_owned(),
message: e.to_string(),
})?;
case.check_invariants()
.map_err(|message| LoadError::Model {
path: path.to_owned(),
message,
})?;
Ok(case)
}
struct Schemas {
case: &'static jsonschema::Validator,
binding: &'static jsonschema::Validator,
outcomes: &'static jsonschema::Validator,
selectors: &'static jsonschema::Validator,
matrix: &'static jsonschema::Validator,
corpus: &'static jsonschema::Validator,
register: &'static jsonschema::Validator,
journeys: &'static jsonschema::Validator,
wire_surface: &'static jsonschema::Validator,
statement: &'static jsonschema::Validator,
ixit: &'static jsonschema::Validator,
}
static SCHEMAS: LazyLock<Result<Schemas, (PathBuf, String)>> = LazyLock::new(|| {
fn one(
schema: &serde_json::Value,
name: &'static str,
) -> Result<&'static jsonschema::Validator, (PathBuf, String)> {
match compile_schema(schema, name) {
Ok(v) => Ok(Box::leak(Box::new(v))),
Err(LoadError::Schema { path, message }) => Err((path, message)),
Err(other) => Err((PathBuf::from(name), other.to_string())),
}
}
Ok(Schemas {
case: one(&schema::case_core_schema(), "case-core.schema.json")?,
binding: one(
&schema::operation_binding_schema(),
"operation-binding.schema.json",
)?,
outcomes: one(&schema::outcomes_schema(), "outcomes.schema.json")?,
selectors: one(&schema::selectors_schema(), "selectors.schema.json")?,
matrix: one(
&schema::capability_matrix_schema(),
"capability-matrix.schema.json",
)?,
corpus: one(
&schema::corpus_manifest_schema(),
"corpus-manifest.schema.json",
)?,
register: one(
&schema::ambiguity_register_schema(),
"ambiguity-register.schema.json",
)?,
journeys: one(
&schema::journey_catalogue_schema(),
"journey-catalogue.schema.json",
)?,
wire_surface: one(&schema::wire_surface_schema(), "wire-surface.schema.json")?,
statement: one(&schema::statement_schema(), "statement.schema.json")?,
ixit: one(&schema::ixit_schema(), "ixit.schema.json")?,
})
});
fn compiled_schemas() -> Result<&'static Schemas, LoadError> {
SCHEMAS
.as_ref()
.map_err(|(path, message)| LoadError::Schema {
path: path.clone(),
message: message.clone(),
})
}
#[expect(
clippy::too_many_lines,
reason = "one singleton-loading block per artifact family"
)]
pub fn load_root(root: &Path) -> Result<Loaded, LoadError> {
let schemas = compiled_schemas()?;
let Schemas {
case: case_schema,
binding: binding_schema,
outcomes: outcomes_schema,
selectors: selectors_schema,
matrix: matrix_schema,
corpus: corpus_schema,
register: register_schema,
journeys: journeys_schema,
wire_surface: wire_surface_schema,
statement: statement_schema,
ixit: ixit_schema,
} = schemas;
let mut loaded = Loaded::default();
if let Some(party_dir) = root.parent().map(|p| p.join("party")) {
for path in statement_files_under(&party_dir) {
let ixit_path = path.with_file_name("ixit.json");
if ixit_path.is_file() {
match load_json_document::<crate::ixit::Ixit>(&ixit_path, ixit_schema) {
Ok(ixit) => loaded.set.party_ixits.push((ixit_path, ixit)),
Err(e) => loaded.errors.push(e),
}
}
match load_json_document::<crate::party::Statement>(&path, statement_schema) {
Ok(statement) => loaded.set.parties.push((path, statement)),
Err(e) => loaded.errors.push(e),
}
}
}
let performance_dir = root.join("schedule/performance");
for path in yaml_files_under(&root.join("schedule")) {
if path.starts_with(&performance_dir) {
match load_performance_case(&path) {
Ok(case) => loaded.set.performance.push((path, case)),
Err(e) => loaded.errors.push(e),
}
continue;
}
match load_artifact::<CaseCore>(&path, case_schema) {
Ok(case) => loaded.set.cases.push((path, case)),
Err(e) => loaded.errors.push(e),
}
}
for path in yaml_files_under(&root.join("bindings")) {
match load_artifact::<OperationBinding>(&path, binding_schema) {
Ok(binding) => loaded.set.bindings.push((path, binding)),
Err(e) => loaded.errors.push(e),
}
}
let mut singleton = |rel: &str, out: &mut dyn FnMut(PathBuf, &Path) -> Option<LoadError>| {
let path = root.join(rel);
if path.exists()
&& let Some(e) = out(path.clone(), &path)
{
loaded.errors.push(e);
}
};
singleton(
"vocab/outcomes.yaml",
&mut |path, p| match load_artifact::<OutcomesVocab>(p, outcomes_schema) {
Ok(v) => {
loaded.set.outcomes = Some((path, v));
None
}
Err(e) => Some(e),
},
);
singleton(
"vocab/selectors.yaml",
&mut |path, p| match load_artifact::<SelectorsVocab>(p, selectors_schema) {
Ok(v) => {
loaded.set.selectors = Some((path, v));
None
}
Err(e) => Some(e),
},
);
singleton(
"vocab/capability_matrix.yaml",
&mut |path, p| match load_artifact::<CapabilityMatrix>(p, matrix_schema) {
Ok(v) => {
loaded.set.matrix = Some((path, v));
None
}
Err(e) => Some(e),
},
);
singleton(
"vocab/journey_catalogue.yaml",
&mut |path, p| match load_artifact::<crate::perf::JourneyCatalogue>(p, journeys_schema) {
Ok(v) => {
loaded.set.journeys = Some((path, v));
None
}
Err(e) => Some(e),
},
);
singleton(
"corpus/MANIFEST.yaml",
&mut |path, p| match load_artifact::<CorpusManifest>(p, corpus_schema) {
Ok(v) => {
loaded.set.corpus_dir = path.parent().map(Path::to_owned);
loaded.set.corpus = Some((path, v));
None
}
Err(e) => Some(e),
},
);
singleton(
"registers/ambiguities.yaml",
&mut |path, p| match load_artifact::<AmbiguityRegister>(p, register_schema) {
Ok(v) => {
loaded.set.register = Some((path, v));
None
}
Err(e) => Some(e),
},
);
singleton(
"vocab/wire_surface.yaml",
&mut |path, p| match load_artifact::<WireSurface>(p, wire_surface_schema) {
Ok(v) => {
loaded.set.wire_surface = Some((path, v));
None
}
Err(e) => Some(e),
},
);
Ok(loaded)
}
#[cfg(test)]
#[expect(
clippy::panic_in_result_fn,
reason = "Result-returning tests in the Book ch11 shape, each asserting; \
clippy offers no allow-in-tests knob for this lint"
)]
mod tests {
use super::*;
fn put(root: &Path, rel: &str, body: &str) -> std::io::Result<()> {
let path = root.join(rel);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(path, body)
}
#[test]
fn a_defective_file_is_one_error_against_itself_and_the_load_continues()
-> Result<(), Box<dyn std::error::Error>> {
let tmp = assert_fs::TempDir::new()?;
let root = tmp.path().join("artifacts");
std::fs::create_dir_all(&root)?;
put(
&root,
"schedule/ehr/I_EHR_SERVICE.create_ehr-main.yaml",
"id: I_EHR_SERVICE.create_ehr-main\n\
kind: functional\n\
component: EHR\n\
sm_operation: I_EHR_SERVICE.create_ehr\n\
test_purpose: t\n\
description: d\n\
spec_refs: [\"ITS-REST master02 §EHR\"]\n\
capabilities: [EhrOperations]\n\
flow:\n - { step: 1, call: create_ehr, expect: created }\n",
)?;
put(
&root,
"schedule/performance/PERF-broken.yaml",
"id: PERF-broken\nkind: functional\ncomponent: PERFORMANCE\n\
description: d\ntest_purpose: t\nspec_refs: []\nclass: POC\n\
corpus: cnf.scale.10k\n\
workload: { arrival_rate: 2/s, warmup: PT5M, duration: PT1H,\n\
\x20 journeys: { chart_review: 100% } }\nthresholds: []\n",
)?;
put(&root, "vocab/selectors.yaml", "body_selectors: 7\n")?;
put(tmp.path(), "party/acme/statement.json", "not json\n")?;
let loaded = load_root(&root)?;
let failing: Vec<String> = loaded
.errors
.iter()
.map(|e| e.path().display().to_string())
.collect();
for expected in [
"schedule/performance/PERF-broken.yaml",
"vocab/selectors.yaml",
"party/acme/statement.json",
] {
assert!(
failing.iter().any(|p| p.ends_with(expected)),
"{expected} missing from {failing:?}"
);
}
assert_eq!(loaded.set.cases.len(), 1, "{:?}", loaded.errors);
assert!(loaded.set.performance.is_empty());
assert!(loaded.set.selectors.is_none());
assert!(loaded.set.parties.is_empty());
Ok(())
}
#[test]
fn an_empty_root_loads_clean_and_empty() -> Result<(), Box<dyn std::error::Error>> {
let tmp = assert_fs::TempDir::new()?;
let loaded = load_root(tmp.path())?;
assert!(loaded.errors.is_empty(), "{:?}", loaded.errors);
assert!(loaded.set.cases.is_empty());
assert!(loaded.set.bindings.is_empty());
assert!(loaded.set.matrix.is_none());
assert!(loaded.set.corpus_dir.is_none());
Ok(())
}
}