baseid_core/
error.rs

1//! Bilingual error types for BaseID.
2//!
3//! All errors carry messages in both English and French to support
4//! Canadian government bilingual requirements.
5
6use crate::Language;
7
8/// The result type used throughout BaseID crates.
9pub type Result<T> = std::result::Result<T, Error>;
10
11/// Top-level error type for all BaseID operations.
12#[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    /// Attach additional context to this error.
38    ///
39    /// Wraps the error with a human-readable message that appears before
40    /// the original error in display output.
41    ///
42    /// # Example
43    /// ```
44    /// use baseid_core::error::{Error, DidError};
45    /// let err: Error = DidError::ResolutionFailed.into();
46    /// let err = err.with_context("did:web:example.com");
47    /// assert!(format!("{err}").contains("did:web:example.com"));
48    /// ```
49    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
57/// Trait for errors that provide bilingual messages.
58pub trait BilingualError {
59    /// Returns the error message in the specified language.
60    fn message(&self, lang: Language) -> &str;
61}
62
63/// Extension trait for adding context to `Result<T, Error>`.
64///
65/// Similar to `anyhow::Context` but for BaseID's error type.
66pub trait ErrorContext<T> {
67    /// Attach context to the error, if present.
68    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        // Display should use English by default
293        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        // CryptoError
327        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        // CredentialError
340        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}