1use thiserror::Error;
20
21#[derive(Debug, Error, Clone, PartialEq, Eq)]
28pub enum AvcError {
29 #[error("AVC serialization failed: {reason}")]
31 Serialization { reason: String },
32
33 #[error("AVC field `{field}` must not be empty")]
35 EmptyField { field: &'static str },
36
37 #[error("AVC schema version {got} is unsupported (supported: {supported})")]
39 UnsupportedSchema { got: u16, supported: u16 },
40
41 #[error(
43 "AVC protocol version {got} is unsupported (supported: {min_supported}..={max_supported})"
44 )]
45 UnsupportedProtocol {
46 got: u16,
47 min_supported: u16,
48 max_supported: u16,
49 },
50
51 #[error("AVC basis point field `{field}` value {value} exceeds 10_000")]
53 BasisPointOutOfRange { field: &'static str, value: u32 },
54
55 #[error("AVC timestamp invariant violated: {reason}")]
57 InvalidTimestamp { reason: String },
58
59 #[error("AVC delegation rejected: scope widened in `{dimension}`")]
61 DelegationWidens { dimension: &'static str },
62
63 #[error("AVC delegation rejected: {reason}")]
65 DelegationRejected { reason: String },
66
67 #[error("AVC registry error: {reason}")]
69 Registry { reason: String },
70
71 #[error("AVC invalid input: {reason}")]
73 InvalidInput { reason: String },
74}
75
76impl<T> From<ciborium::ser::Error<T>> for AvcError {
77 fn from(_: ciborium::ser::Error<T>) -> Self {
78 AvcError::Serialization {
79 reason: "CBOR serialization failed".into(),
80 }
81 }
82}
83
84impl<T> From<ciborium::de::Error<T>> for AvcError {
85 fn from(_: ciborium::de::Error<T>) -> Self {
86 AvcError::Serialization {
87 reason: "CBOR deserialization failed".into(),
88 }
89 }
90}
91
92impl From<exo_core::ExoError> for AvcError {
93 fn from(value: exo_core::ExoError) -> Self {
94 match value {
95 exo_core::ExoError::SerializationError { reason } => AvcError::Serialization { reason },
96 other => AvcError::InvalidInput {
97 reason: other.to_string(),
98 },
99 }
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn display_covers_every_variant() {
109 let cases: Vec<AvcError> = vec![
110 AvcError::Serialization {
111 reason: "cbor".into(),
112 },
113 AvcError::EmptyField { field: "purpose" },
114 AvcError::UnsupportedSchema {
115 got: 99,
116 supported: 1,
117 },
118 AvcError::UnsupportedProtocol {
119 got: 99,
120 min_supported: 1,
121 max_supported: 1,
122 },
123 AvcError::BasisPointOutOfRange {
124 field: "risk",
125 value: 99_999,
126 },
127 AvcError::InvalidTimestamp {
128 reason: "expired".into(),
129 },
130 AvcError::DelegationWidens {
131 dimension: "permissions",
132 },
133 AvcError::DelegationRejected {
134 reason: "depth".into(),
135 },
136 AvcError::Registry {
137 reason: "missing".into(),
138 },
139 AvcError::InvalidInput {
140 reason: "bad".into(),
141 },
142 ];
143 for err in cases {
144 let s = err.to_string();
145 assert!(!s.is_empty(), "error display empty for {err:?}");
146 }
147 }
148
149 #[test]
150 fn from_exo_error_serialization_preserves_reason() {
151 let inner = exo_core::ExoError::SerializationError {
152 reason: "boom".into(),
153 };
154 let mapped: AvcError = inner.into();
155 match mapped {
156 AvcError::Serialization { reason } => assert_eq!(reason, "boom"),
157 other => panic!("expected Serialization, got {other:?}"),
158 }
159 }
160
161 #[test]
162 fn from_exo_error_other_maps_to_invalid_input() {
163 let inner = exo_core::ExoError::InvalidMerkleProof;
164 let mapped: AvcError = inner.into();
165 match mapped {
166 AvcError::InvalidInput { reason } => assert!(reason.contains("invalid merkle proof")),
167 other => panic!("expected InvalidInput, got {other:?}"),
168 }
169 }
170
171 #[test]
172 fn ciborium_serialization_error_maps_to_serialization_variant() {
173 let inner: ciborium::ser::Error<std::io::Error> = ciborium::ser::Error::Value("bad".into());
174 let mapped: AvcError = inner.into();
175 assert!(matches!(mapped, AvcError::Serialization { .. }));
176 }
177
178 #[test]
179 fn ciborium_deserialization_error_maps_to_serialization_variant() {
180 let inner: ciborium::de::Error<std::io::Error> =
181 ciborium::de::Error::Semantic(None, "bad".into());
182 let mapped: AvcError = inner.into();
183 assert!(matches!(mapped, AvcError::Serialization { .. }));
184 }
185
186 #[test]
187 fn clone_eq_debug() {
188 let a = AvcError::EmptyField { field: "purpose" };
189 let b = a.clone();
190 assert_eq!(a, b);
191 assert!(format!("{a:?}").contains("EmptyField"));
192 }
193}