use std::path::{Path, PathBuf};
#[derive(Debug, thiserror::Error)]
pub enum LoadError {
#[error("I/O error reading {path}: {source}")]
IoError {
path: PathBuf,
source: std::io::Error,
},
#[error("parse error in {path}: {message}")]
ParseError {
path: PathBuf,
message: String,
},
#[error("schema error in {path}, field {field}: {message}")]
SchemaError {
path: PathBuf,
field: String,
message: String,
},
#[error(
"cross-reference error: {source_entity} in {source_file} references \
non-existent {target_entity} in {target_collection}"
)]
CrossReferenceError {
source_file: PathBuf,
source_entity: String,
target_collection: String,
target_entity: String,
},
#[error("constraint violation: {description}")]
ConstraintError {
description: String,
},
#[error(
"policy incompatible: {check} mismatch — policy has {policy_value}, \
system has {system_value}"
)]
PolicyIncompatible {
check: String,
policy_value: String,
system_value: String,
},
}
impl LoadError {
pub fn io(path: impl AsRef<Path>, source: std::io::Error) -> Self {
Self::IoError {
path: path.as_ref().to_path_buf(),
source,
}
}
pub fn parse(path: impl AsRef<Path>, message: impl Into<String>) -> Self {
Self::ParseError {
path: path.as_ref().to_path_buf(),
message: message.into(),
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use std::io;
#[test]
fn test_load_error_io_display() {
let io_err = io::Error::new(io::ErrorKind::NotFound, "no such file or directory");
let err = LoadError::io("system/hydros.json", io_err);
let display = err.to_string();
assert!(
display.contains("system/hydros.json"),
"display should contain path, got: {display}"
);
assert!(
display.contains("no such file or directory"),
"display should contain source message, got: {display}"
);
}
#[test]
fn test_load_error_parse_display() {
let err = LoadError::parse("stages.json", "unexpected end of input");
let display = err.to_string();
assert!(
display.contains("stages.json"),
"display should contain path, got: {display}"
);
assert!(
display.contains("unexpected end of input"),
"display should contain message, got: {display}"
);
}
#[test]
fn test_load_error_schema_display() {
let err = LoadError::SchemaError {
path: PathBuf::from("system/hydros.json"),
field: "bus_id".to_string(),
message: "required field is missing".to_string(),
};
let display = err.to_string();
assert!(
display.contains("system/hydros.json"),
"display should contain path, got: {display}"
);
assert!(
display.contains("bus_id"),
"display should contain field name, got: {display}"
);
assert!(
display.contains("required field is missing"),
"display should contain message, got: {display}"
);
}
#[test]
fn test_load_error_cross_reference_display() {
let err = LoadError::CrossReferenceError {
source_file: PathBuf::from("system/hydros.json"),
source_entity: "Hydro 'H1'".to_string(),
target_collection: "bus registry".to_string(),
target_entity: "BUS_99".to_string(),
};
let display = err.to_string();
assert!(
display.contains("Hydro 'H1'"),
"display should contain source_entity, got: {display}"
);
assert!(
display.contains("system/hydros.json"),
"display should contain source_file, got: {display}"
);
assert!(
display.contains("BUS_99"),
"display should contain target_entity, got: {display}"
);
assert!(
display.contains("bus registry"),
"display should contain target_collection, got: {display}"
);
}
#[test]
fn test_load_error_is_std_error() {
let err = LoadError::ConstraintError {
description: "hydro cascade contains a cycle".to_string(),
};
let dyn_err: &dyn std::error::Error = &err;
assert!(dyn_err.source().is_none());
assert!(err.to_string().contains("hydro cascade contains a cycle"));
}
#[test]
fn test_load_error_io_helper() {
let io_err = io::Error::new(io::ErrorKind::PermissionDenied, "permission denied");
let err = LoadError::io("config.json", io_err);
assert!(matches!(err, LoadError::IoError { .. }));
let display = err.to_string();
assert!(display.contains("config.json"));
assert!(display.contains("permission denied"));
}
}