1use crate::Language;
7
8pub type Result<T> = std::result::Result<T, Error>;
10
11#[derive(Debug, thiserror::Error)]
13pub enum Error {
14 #[error("{}", .0.message(Language::En))]
15 Crypto(#[from] CryptoError),
16
17 #[error("{}", .0.message(Language::En))]
18 Did(#[from] DidError),
19
20 #[error("{}", .0.message(Language::En))]
21 Credential(#[from] CredentialError),
22
23 #[error("{}", .0.message(Language::En))]
24 Serialization(#[from] SerializationError),
25
26 #[error("{}", .0.message(Language::En))]
27 Store(#[from] StoreError),
28
29 #[error("{}", .0.message(Language::En))]
30 Protocol(#[from] ProtocolError),
31
32 #[error("{context}: {source}")]
33 WithContext { source: Box<Error>, context: String },
34}
35
36impl Error {
37 pub fn with_context(self, context: impl Into<String>) -> Self {
50 Error::WithContext {
51 source: Box::new(self),
52 context: context.into(),
53 }
54 }
55}
56
57pub trait BilingualError {
59 fn message(&self, lang: Language) -> &str;
61}
62
63pub trait ErrorContext<T> {
67 fn context(self, ctx: impl Into<String>) -> Result<T>;
69}
70
71impl<T> ErrorContext<T> for Result<T> {
72 fn context(self, ctx: impl Into<String>) -> Result<T> {
73 self.map_err(|e| e.with_context(ctx))
74 }
75}
76
77#[derive(Debug, thiserror::Error)]
78pub enum CryptoError {
79 #[error("Key generation failed / Échec de la génération de clé")]
80 KeyGeneration,
81 #[error("Signing failed / Échec de la signature")]
82 SigningFailed,
83 #[error("Verification failed / Échec de la vérification")]
84 VerificationFailed,
85 #[error("Unsupported algorithm / Algorithme non pris en charge")]
86 UnsupportedAlgorithm,
87 #[error("Invalid key material / Matériel de clé invalide")]
88 InvalidKeyMaterial,
89}
90
91impl BilingualError for CryptoError {
92 fn message(&self, lang: Language) -> &str {
93 match (self, lang) {
94 (Self::KeyGeneration, Language::Fr) => "Échec de la génération de clé",
95 (Self::KeyGeneration, _) => "Key generation failed",
96 (Self::SigningFailed, Language::Fr) => "Échec de la signature",
97 (Self::SigningFailed, _) => "Signing failed",
98 (Self::VerificationFailed, Language::Fr) => "Échec de la vérification",
99 (Self::VerificationFailed, _) => "Verification failed",
100 (Self::UnsupportedAlgorithm, Language::Fr) => "Algorithme non pris en charge",
101 (Self::UnsupportedAlgorithm, _) => "Unsupported algorithm",
102 (Self::InvalidKeyMaterial, Language::Fr) => "Matériel de clé invalide",
103 (Self::InvalidKeyMaterial, _) => "Invalid key material",
104 }
105 }
106}
107
108#[derive(Debug, thiserror::Error)]
109pub enum DidError {
110 #[error("Invalid DID / DID invalide")]
111 InvalidDid,
112 #[error("Resolution failed / Échec de la résolution")]
113 ResolutionFailed,
114 #[error("Unsupported DID method / Méthode DID non prise en charge")]
115 UnsupportedMethod,
116 #[error("DID document not found / Document DID introuvable")]
117 NotFound,
118}
119
120impl BilingualError for DidError {
121 fn message(&self, lang: Language) -> &str {
122 match (self, lang) {
123 (Self::InvalidDid, Language::Fr) => "DID invalide",
124 (Self::InvalidDid, _) => "Invalid DID",
125 (Self::ResolutionFailed, Language::Fr) => "Échec de la résolution",
126 (Self::ResolutionFailed, _) => "Resolution failed",
127 (Self::UnsupportedMethod, Language::Fr) => "Méthode DID non prise en charge",
128 (Self::UnsupportedMethod, _) => "Unsupported DID method",
129 (Self::NotFound, Language::Fr) => "Document DID introuvable",
130 (Self::NotFound, _) => "DID document not found",
131 }
132 }
133}
134
135#[derive(Debug, thiserror::Error)]
136pub enum CredentialError {
137 #[error("Invalid credential / Justificatif invalide")]
138 InvalidCredential,
139 #[error("Credential expired / Justificatif expiré")]
140 Expired,
141 #[error("Credential revoked / Justificatif révoqué")]
142 Revoked,
143 #[error("Unsupported format / Format non pris en charge")]
144 UnsupportedFormat,
145 #[error("Predicate disclosure not supported by this format / Divulgation par prédicat non prise en charge par ce format")]
146 UnsupportedPredicate,
147 #[error("Missing required claims / Revendications requises manquantes")]
148 MissingClaims,
149}
150
151impl BilingualError for CredentialError {
152 fn message(&self, lang: Language) -> &str {
153 match (self, lang) {
154 (Self::InvalidCredential, Language::Fr) => "Justificatif invalide",
155 (Self::InvalidCredential, _) => "Invalid credential",
156 (Self::Expired, Language::Fr) => "Justificatif expiré",
157 (Self::Expired, _) => "Credential expired",
158 (Self::Revoked, Language::Fr) => "Justificatif révoqué",
159 (Self::Revoked, _) => "Credential revoked",
160 (Self::UnsupportedFormat, Language::Fr) => "Format non pris en charge",
161 (Self::UnsupportedFormat, _) => "Unsupported format",
162 (Self::UnsupportedPredicate, Language::Fr) => {
163 "Divulgation par prédicat non prise en charge par ce format"
164 }
165 (Self::UnsupportedPredicate, _) => "Predicate disclosure not supported by this format",
166 (Self::MissingClaims, Language::Fr) => "Revendications requises manquantes",
167 (Self::MissingClaims, _) => "Missing required claims",
168 }
169 }
170}
171
172#[derive(Debug, thiserror::Error)]
173pub enum SerializationError {
174 #[error("JSON error: {0}")]
175 Json(#[from] serde_json::Error),
176 #[error("CBOR encoding/decoding failed / Échec de l'encodage/décodage CBOR")]
177 Cbor,
178}
179
180impl BilingualError for SerializationError {
181 fn message(&self, lang: Language) -> &str {
182 match (self, lang) {
183 (Self::Json(_), Language::Fr) => "Erreur de sérialisation JSON",
184 (Self::Json(_), _) => "JSON serialization error",
185 (Self::Cbor, Language::Fr) => "Échec de l'encodage/décodage CBOR",
186 (Self::Cbor, _) => "CBOR encoding/decoding failed",
187 }
188 }
189}
190
191#[derive(Debug, thiserror::Error)]
192pub enum StoreError {
193 #[error("Storage operation failed / Échec de l'opération de stockage")]
194 OperationFailed,
195 #[error("Item not found / Élément introuvable")]
196 NotFound,
197 #[error("Encryption failed / Échec du chiffrement")]
198 EncryptionFailed,
199}
200
201impl BilingualError for StoreError {
202 fn message(&self, lang: Language) -> &str {
203 match (self, lang) {
204 (Self::OperationFailed, Language::Fr) => "Échec de l'opération de stockage",
205 (Self::OperationFailed, _) => "Storage operation failed",
206 (Self::NotFound, Language::Fr) => "Élément introuvable",
207 (Self::NotFound, _) => "Item not found",
208 (Self::EncryptionFailed, Language::Fr) => "Échec du chiffrement",
209 (Self::EncryptionFailed, _) => "Encryption failed",
210 }
211 }
212}
213
214#[derive(Debug, thiserror::Error)]
215pub enum ProtocolError {
216 #[error("Protocol error / Erreur de protocole")]
217 General,
218 #[error("Invalid request / Requête invalide")]
219 InvalidRequest,
220 #[error("Invalid response / Réponse invalide")]
221 InvalidResponse,
222 #[error("Transport error / Erreur de transport")]
223 Transport,
224}
225
226impl BilingualError for ProtocolError {
227 fn message(&self, lang: Language) -> &str {
228 match (self, lang) {
229 (Self::General, Language::Fr) => "Erreur de protocole",
230 (Self::General, _) => "Protocol error",
231 (Self::InvalidRequest, Language::Fr) => "Requête invalide",
232 (Self::InvalidRequest, _) => "Invalid request",
233 (Self::InvalidResponse, Language::Fr) => "Réponse invalide",
234 (Self::InvalidResponse, _) => "Invalid response",
235 (Self::Transport, Language::Fr) => "Erreur de transport",
236 (Self::Transport, _) => "Transport error",
237 }
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn bilingual_crypto_errors() {
247 let err = CryptoError::VerificationFailed;
248 assert_eq!(err.message(Language::En), "Verification failed");
249 assert_eq!(err.message(Language::Fr), "Échec de la vérification");
250 }
251
252 #[test]
253 fn bilingual_did_errors() {
254 let err = DidError::NotFound;
255 assert_eq!(err.message(Language::En), "DID document not found");
256 assert_eq!(err.message(Language::Fr), "Document DID introuvable");
257 }
258
259 #[test]
260 fn bilingual_credential_errors() {
261 let err = CredentialError::Expired;
262 assert_eq!(err.message(Language::En), "Credential expired");
263 assert_eq!(err.message(Language::Fr), "Justificatif expiré");
264 }
265
266 #[test]
267 fn error_conversion_crypto() {
268 let crypto_err = CryptoError::InvalidKeyMaterial;
269 let err: Error = crypto_err.into();
270 assert!(matches!(
271 err,
272 Error::Crypto(CryptoError::InvalidKeyMaterial)
273 ));
274 }
275
276 #[test]
277 fn error_conversion_did() {
278 let did_err = DidError::InvalidDid;
279 let err: Error = did_err.into();
280 assert!(matches!(err, Error::Did(DidError::InvalidDid)));
281 }
282
283 #[test]
284 fn error_conversion_credential() {
285 let cred_err = CredentialError::Revoked;
286 let err: Error = cred_err.into();
287 assert!(matches!(err, Error::Credential(CredentialError::Revoked)));
288 }
289
290 #[test]
291 fn error_display() {
292 let err = Error::Crypto(CryptoError::VerificationFailed);
294 let msg = format!("{err}");
295 assert!(msg.contains("Verification failed"), "got: {msg}");
296 }
297
298 #[test]
299 fn error_with_context() {
300 let err: Error = DidError::ResolutionFailed.into();
301 let err = err.with_context("did:web:example.com");
302 let msg = format!("{err}");
303 assert!(msg.contains("did:web:example.com"), "got: {msg}");
304 assert!(msg.contains("Resolution failed"), "got: {msg}");
305 }
306
307 #[test]
308 fn error_context_on_result() {
309 let result: Result<()> = Err(CryptoError::InvalidKeyMaterial.into());
310 let result = result.context("parsing JWK for did:key:z6Mk...");
311 let err = result.unwrap_err();
312 let msg = format!("{err}");
313 assert!(msg.contains("parsing JWK"), "got: {msg}");
314 assert!(msg.contains("Invalid key material"), "got: {msg}");
315 }
316
317 #[test]
318 fn error_context_preserves_ok() {
319 let result: Result<i32> = Ok(42);
320 let result = result.context("should not appear");
321 assert_eq!(result.unwrap(), 42);
322 }
323
324 #[test]
325 fn all_bilingual_variants_have_both_languages() {
326 for variant in [
328 CryptoError::KeyGeneration,
329 CryptoError::SigningFailed,
330 CryptoError::VerificationFailed,
331 CryptoError::UnsupportedAlgorithm,
332 CryptoError::InvalidKeyMaterial,
333 ] {
334 assert!(!variant.message(Language::En).is_empty());
335 assert!(!variant.message(Language::Fr).is_empty());
336 assert_ne!(variant.message(Language::En), variant.message(Language::Fr));
337 }
338
339 for variant in [
341 CredentialError::InvalidCredential,
342 CredentialError::Expired,
343 CredentialError::Revoked,
344 CredentialError::UnsupportedFormat,
345 CredentialError::UnsupportedPredicate,
346 CredentialError::MissingClaims,
347 ] {
348 assert!(!variant.message(Language::En).is_empty());
349 assert!(!variant.message(Language::Fr).is_empty());
350 }
351 }
352}