use thiserror::Error;
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum AvcError {
#[error("AVC serialization failed: {reason}")]
Serialization { reason: String },
#[error("AVC field `{field}` must not be empty")]
EmptyField { field: &'static str },
#[error("AVC schema version {got} is unsupported (supported: {supported})")]
UnsupportedSchema { got: u16, supported: u16 },
#[error(
"AVC protocol version {got} is unsupported (supported: {min_supported}..={max_supported})"
)]
UnsupportedProtocol {
got: u16,
min_supported: u16,
max_supported: u16,
},
#[error("AVC basis point field `{field}` value {value} exceeds 10_000")]
BasisPointOutOfRange { field: &'static str, value: u32 },
#[error("AVC timestamp invariant violated: {reason}")]
InvalidTimestamp { reason: String },
#[error("AVC delegation rejected: scope widened in `{dimension}`")]
DelegationWidens { dimension: &'static str },
#[error("AVC delegation rejected: {reason}")]
DelegationRejected { reason: String },
#[error("AVC registry error: {reason}")]
Registry { reason: String },
#[error("AVC invalid input: {reason}")]
InvalidInput { reason: String },
}
impl<T> From<ciborium::ser::Error<T>> for AvcError {
fn from(_: ciborium::ser::Error<T>) -> Self {
AvcError::Serialization {
reason: "CBOR serialization failed".into(),
}
}
}
impl<T> From<ciborium::de::Error<T>> for AvcError {
fn from(_: ciborium::de::Error<T>) -> Self {
AvcError::Serialization {
reason: "CBOR deserialization failed".into(),
}
}
}
impl From<exo_core::ExoError> for AvcError {
fn from(value: exo_core::ExoError) -> Self {
match value {
exo_core::ExoError::SerializationError { reason } => AvcError::Serialization { reason },
other => AvcError::InvalidInput {
reason: other.to_string(),
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn display_covers_every_variant() {
let cases: Vec<AvcError> = vec![
AvcError::Serialization {
reason: "cbor".into(),
},
AvcError::EmptyField { field: "purpose" },
AvcError::UnsupportedSchema {
got: 99,
supported: 1,
},
AvcError::UnsupportedProtocol {
got: 99,
min_supported: 1,
max_supported: 1,
},
AvcError::BasisPointOutOfRange {
field: "risk",
value: 99_999,
},
AvcError::InvalidTimestamp {
reason: "expired".into(),
},
AvcError::DelegationWidens {
dimension: "permissions",
},
AvcError::DelegationRejected {
reason: "depth".into(),
},
AvcError::Registry {
reason: "missing".into(),
},
AvcError::InvalidInput {
reason: "bad".into(),
},
];
for err in cases {
let s = err.to_string();
assert!(!s.is_empty(), "error display empty for {err:?}");
}
}
#[test]
fn from_exo_error_serialization_preserves_reason() {
let inner = exo_core::ExoError::SerializationError {
reason: "boom".into(),
};
let mapped: AvcError = inner.into();
match mapped {
AvcError::Serialization { reason } => assert_eq!(reason, "boom"),
other => panic!("expected Serialization, got {other:?}"),
}
}
#[test]
fn from_exo_error_other_maps_to_invalid_input() {
let inner = exo_core::ExoError::InvalidMerkleProof;
let mapped: AvcError = inner.into();
match mapped {
AvcError::InvalidInput { reason } => assert!(reason.contains("invalid merkle proof")),
other => panic!("expected InvalidInput, got {other:?}"),
}
}
#[test]
fn ciborium_serialization_error_maps_to_serialization_variant() {
let inner: ciborium::ser::Error<std::io::Error> = ciborium::ser::Error::Value("bad".into());
let mapped: AvcError = inner.into();
assert!(matches!(mapped, AvcError::Serialization { .. }));
}
#[test]
fn ciborium_deserialization_error_maps_to_serialization_variant() {
let inner: ciborium::de::Error<std::io::Error> =
ciborium::de::Error::Semantic(None, "bad".into());
let mapped: AvcError = inner.into();
assert!(matches!(mapped, AvcError::Serialization { .. }));
}
#[test]
fn clone_eq_debug() {
let a = AvcError::EmptyField { field: "purpose" };
let b = a.clone();
assert_eq!(a, b);
assert!(format!("{a:?}").contains("EmptyField"));
}
}