#![expect(
clippy::disallowed_types,
reason = "dev/verification tooling over JSON artifacts (the catalogue, results, wire \
exchanges) — not the application (#1694)"
)]
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)>,
}
#[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_statement(
path: &Path,
validator: &jsonschema::Validator,
) -> Result<crate::party::Statement, 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,
}
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")?,
})
});
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,
} = 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) {
match load_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)
}