Skip to main content

es_fluent_shared/
error.rs

1//! Common error types shared across the es-fluent ecosystem.
2
3use std::path::PathBuf;
4use thiserror::Error;
5use unic_langid::LanguageIdentifier;
6
7/// Common error types shared across the es-fluent ecosystem.
8#[derive(Debug, Error)]
9pub enum EsFluentError {
10    /// Configuration file not found.
11    #[error("Configuration file not found: {path}")]
12    ConfigNotFound { path: PathBuf },
13
14    /// Failed to parse configuration file.
15    #[error("Failed to parse configuration file: {0}")]
16    ConfigParseError(#[from] toml::de::Error),
17
18    /// Assets directory not found.
19    #[error("Assets directory not found: {path}")]
20    AssetsNotFound { path: PathBuf },
21
22    /// Fallback language directory not found.
23    #[error("Fallback language directory not found: {language}")]
24    FallbackLanguageNotFound { language: String },
25
26    /// Invalid language identifier.
27    #[error("Invalid language identifier '{identifier}': {reason}")]
28    InvalidLanguageIdentifier { identifier: String, reason: String },
29
30    /// Invalid Fluent metadata identifier.
31    #[error("Invalid Fluent metadata '{identifier}': {reason}")]
32    InvalidFluentIdentifier { identifier: String, reason: String },
33
34    /// Duplicate generated Fluent key metadata.
35    #[error("Duplicate generated FTL key '{key}' from {first} and {second}")]
36    DuplicateGeneratedFtlKey {
37        key: String,
38        first: String,
39        second: String,
40    },
41
42    /// Language not supported.
43    #[error("Language '{0}' is not supported")]
44    LanguageNotSupported(LanguageIdentifier),
45
46    /// Fluent parsing error.
47    #[error("Fluent parsing error: {0:?}")]
48    FluentParseError(Vec<fluent_syntax::parser::ParserError>),
49
50    /// Fluent serialization error.
51    #[error("Fluent serialization error: {0}")]
52    FluentSerializeError(#[from] std::fmt::Error),
53
54    /// IO error during file operations.
55    #[error("IO error: {0}")]
56    IoError(#[from] std::io::Error),
57
58    /// Environment variable error.
59    #[error("Environment variable error: {0}")]
60    EnvVarError(#[from] std::env::VarError),
61
62    /// Generic backend error.
63    #[error("Backend error: {0}")]
64    BackendError(#[from] anyhow::Error),
65
66    /// Missing package name.
67    #[error("Missing package name")]
68    MissingPackageName,
69}
70
71impl EsFluentError {
72    /// Creates a configuration not found error.
73    pub fn config_not_found(path: impl Into<PathBuf>) -> Self {
74        Self::ConfigNotFound { path: path.into() }
75    }
76
77    /// Creates an assets not found error.
78    pub fn assets_not_found(path: impl Into<PathBuf>) -> Self {
79        Self::AssetsNotFound { path: path.into() }
80    }
81
82    /// Creates an invalid language identifier error.
83    pub fn invalid_language_identifier(
84        identifier: impl Into<String>,
85        reason: impl Into<String>,
86    ) -> Self {
87        Self::InvalidLanguageIdentifier {
88            identifier: identifier.into(),
89            reason: reason.into(),
90        }
91    }
92
93    /// Creates an invalid Fluent metadata identifier error.
94    pub fn invalid_fluent_identifier(
95        identifier: impl Into<String>,
96        reason: impl Into<String>,
97    ) -> Self {
98        Self::InvalidFluentIdentifier {
99            identifier: identifier.into(),
100            reason: reason.into(),
101        }
102    }
103
104    /// Creates a duplicate generated FTL key error.
105    pub fn duplicate_generated_ftl_key(
106        key: impl Into<String>,
107        first: impl Into<String>,
108        second: impl Into<String>,
109    ) -> Self {
110        Self::DuplicateGeneratedFtlKey {
111            key: key.into(),
112            first: first.into(),
113            second: second.into(),
114        }
115    }
116
117    /// Creates a fallback language not found error.
118    pub fn fallback_language_not_found(language: impl Into<String>) -> Self {
119        Self::FallbackLanguageNotFound {
120            language: language.into(),
121        }
122    }
123}
124
125/// A result type for common es-fluent operations.
126pub type EsFluentResult<T> = Result<T, EsFluentError>;
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use std::error::Error as _;
132
133    #[test]
134    fn helper_constructors_build_expected_variants() {
135        let config = EsFluentError::config_not_found("/tmp/i18n.toml");
136        assert!(matches!(config, EsFluentError::ConfigNotFound { .. }));
137
138        let assets = EsFluentError::assets_not_found("/tmp/i18n");
139        assert!(matches!(assets, EsFluentError::AssetsNotFound { .. }));
140
141        let invalid = EsFluentError::invalid_language_identifier("bad", "parse failure");
142        assert!(matches!(
143            invalid,
144            EsFluentError::InvalidLanguageIdentifier { .. }
145        ));
146
147        let invalid_fluent = EsFluentError::invalid_fluent_identifier("bad key", "parse failure");
148        assert!(matches!(
149            invalid_fluent,
150            EsFluentError::InvalidFluentIdentifier { .. }
151        ));
152
153        let fallback = EsFluentError::fallback_language_not_found("en-US");
154        assert!(matches!(
155            fallback,
156            EsFluentError::FallbackLanguageNotFound { .. }
157        ));
158    }
159
160    #[test]
161    fn display_messages_include_variant_context() {
162        let supported = EsFluentError::LanguageNotSupported("fr".parse().unwrap());
163        assert_eq!(supported.to_string(), "Language 'fr' is not supported");
164
165        let fluent = EsFluentError::FluentParseError(Vec::new());
166        assert_eq!(fluent.to_string(), "Fluent parsing error: []");
167
168        let serialize = EsFluentError::FluentSerializeError(std::fmt::Error);
169        assert_eq!(
170            serialize.to_string(),
171            "Fluent serialization error: an error occurred when formatting an argument"
172        );
173
174        let missing = EsFluentError::MissingPackageName;
175        assert_eq!(missing.to_string(), "Missing package name");
176    }
177
178    #[test]
179    fn source_errors_are_preserved_for_wrapped_variants() {
180        let parse_error: EsFluentError = toml::from_str::<toml::Value>("=").unwrap_err().into();
181        assert!(parse_error.source().is_some());
182
183        let io_error: EsFluentError =
184            std::io::Error::new(std::io::ErrorKind::NotFound, "missing").into();
185        assert!(io_error.source().is_some());
186
187        let env_error: EsFluentError = std::env::VarError::NotPresent.into();
188        assert!(env_error.source().is_some());
189
190        let backend_error: EsFluentError = anyhow::anyhow!("backend").into();
191        assert!(backend_error.source().is_some());
192    }
193}