#![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 thiserror::Error;
#[derive(Debug, Error)]
pub enum LoadError {
#[error("{path}: {source}")]
Io {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("{path}: YAML: {message}")]
Yaml {
path: PathBuf,
message: String,
},
#[error("{path}: schema: {message}")]
Schema {
path: PathBuf,
message: String,
},
#[error("{path}: model: {message}")]
Model {
path: PathBuf,
message: String,
},
}
impl LoadError {
#[must_use]
pub fn detail(&self) -> String {
match self {
Self::Io { source, .. } => source.to_string(),
Self::Yaml { message, .. } => format!("YAML: {message}"),
Self::Schema { message, .. } => format!("schema: {message}"),
Self::Model { message, .. } => format!("model: {message}"),
}
}
#[must_use]
pub fn path(&self) -> &Path {
match self {
Self::Io { path, .. }
| Self::Yaml { path, .. }
| Self::Schema { path, .. }
| Self::Model { path, .. } => path,
}
}
}
fn saphyr_options() -> serde_saphyr::Options {
let mut options = serde_saphyr::Options::default();
options.duplicate_keys = serde_saphyr::DuplicateKeyPolicy::Error;
options.strict_booleans = true;
options
}
pub fn yaml_str_to_value(text: &str, name: &Path) -> Result<serde_json::Value, LoadError> {
serde_saphyr::from_str_with_options(text, saphyr_options()).map_err(|e| LoadError::Yaml {
path: name.to_owned(),
message: e.to_string(),
})
}
pub fn yaml_file_to_value(path: &Path) -> Result<serde_json::Value, LoadError> {
let text = std::fs::read_to_string(path).map_err(|source| LoadError::Io {
path: path.to_owned(),
source,
})?;
yaml_str_to_value(&text, path)
}
pub fn validate_against(
validator: &jsonschema::Validator,
value: &serde_json::Value,
path: &Path,
) -> Result<(), LoadError> {
let violations: Vec<String> = validator
.iter_errors(value)
.map(|e| format!("{}: {e}", e.instance_path()))
.collect();
if violations.is_empty() {
Ok(())
} else {
Err(LoadError::Schema {
path: path.to_owned(),
message: violations.join("; "),
})
}
}
pub fn load_artifact<T: serde::de::DeserializeOwned>(
path: &Path,
validator: &jsonschema::Validator,
) -> Result<T, LoadError> {
let value = yaml_file_to_value(path)?;
validate_against(validator, &value, path)?;
serde_json::from_value(value).map_err(|e| LoadError::Model {
path: path.to_owned(),
message: e.to_string(),
})
}
pub fn compile_schema(
schema: &serde_json::Value,
name: &str,
) -> Result<jsonschema::Validator, LoadError> {
jsonschema::validator_for(schema).map_err(|e| LoadError::Schema {
path: PathBuf::from(name),
message: format!("schema does not compile: {e}"),
})
}