es_fluent_manager_core/localization/
registry.rs1use super::I18nModuleRegistration;
2use crate::asset_localization::ModuleData;
3use std::collections::HashMap;
4
5#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6pub enum ModuleRegistrationKind {
7 MetadataOnly,
8 RuntimeLocalizer,
9}
10
11impl std::fmt::Display for ModuleRegistrationKind {
12 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
13 match self {
14 Self::MetadataOnly => f.write_str("metadata-only"),
15 Self::RuntimeLocalizer => f.write_str("runtime-localizer"),
16 }
17 }
18}
19
20#[derive(Clone, Debug, Eq, PartialEq)]
21pub enum ModuleDiscoveryError {
22 InvalidMetadata(crate::asset_localization::ModuleRegistryError),
23 InconsistentModuleMetadata {
24 name: String,
25 owner: String,
26 },
27 DuplicateModuleRegistration {
28 name: String,
29 owner: String,
30 kind: ModuleRegistrationKind,
31 count: usize,
32 },
33}
34
35impl std::fmt::Display for ModuleDiscoveryError {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 match self {
38 Self::InvalidMetadata(error) => write!(f, "{error}"),
39 Self::InconsistentModuleMetadata { name, owner } => write!(
40 f,
41 "module '{name}' (owner '{owner}') has mismatched metadata between registrations",
42 ),
43 Self::DuplicateModuleRegistration {
44 name,
45 owner,
46 kind,
47 count,
48 } => write!(
49 f,
50 "module '{name}' (owner '{owner}') has {count} duplicate {kind} registrations",
51 ),
52 }
53 }
54}
55
56impl std::error::Error for ModuleDiscoveryError {
57 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
58 match self {
59 Self::InvalidMetadata(error) => Some(error),
60 Self::InconsistentModuleMetadata { .. } => None,
61 Self::DuplicateModuleRegistration { .. } => None,
62 }
63 }
64}
65
66#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
67struct RegistrationCounts {
68 metadata_only: usize,
69 runtime_localizer: usize,
70}
71
72#[derive(Clone, Copy, Debug, Default)]
73struct IdentityInspection {
74 chosen_data: Option<&'static ModuleData>,
75 metadata_only: Option<&'static ModuleData>,
76 runtime_localizer: Option<&'static ModuleData>,
77 counts: RegistrationCounts,
78}
79
80fn validate_single_module_data(data: &'static ModuleData, errors: &mut Vec<ModuleDiscoveryError>) {
81 if let Err(validation_errors) = crate::asset_localization::validate_module_registry([data]) {
82 errors.extend(
83 validation_errors
84 .into_iter()
85 .map(ModuleDiscoveryError::InvalidMetadata),
86 );
87 }
88}
89
90pub fn try_filter_module_registry(
101 modules: impl IntoIterator<Item = &'static dyn I18nModuleRegistration>,
102) -> Result<Vec<&'static dyn I18nModuleRegistration>, Vec<ModuleDiscoveryError>> {
103 let modules = modules.into_iter().collect::<Vec<_>>();
104 let inspections = inspect_module_registry(&modules);
105 let discovered_data = inspections
106 .values()
107 .filter_map(|inspection| inspection.chosen_data)
108 .collect::<Vec<_>>();
109
110 let mut errors = Vec::new();
111 if let Err(validation_errors) =
112 crate::asset_localization::validate_module_registry(discovered_data.iter().copied())
113 {
114 errors.extend(
115 validation_errors
116 .into_iter()
117 .map(ModuleDiscoveryError::InvalidMetadata),
118 );
119 }
120
121 for ((name, owner), inspection) in inspections {
122 if let (Some(metadata_only), Some(runtime_localizer)) =
123 (inspection.metadata_only, inspection.runtime_localizer)
124 && metadata_only != runtime_localizer
125 {
126 errors.push(ModuleDiscoveryError::InconsistentModuleMetadata {
127 name: name.to_string(),
128 owner: owner.to_string(),
129 });
130
131 if inspection.chosen_data != Some(metadata_only) {
132 validate_single_module_data(metadata_only, &mut errors);
133 }
134 if inspection.chosen_data != Some(runtime_localizer) {
135 validate_single_module_data(runtime_localizer, &mut errors);
136 }
137 }
138
139 let counts = inspection.counts;
140 if counts.metadata_only > 1 {
141 errors.push(ModuleDiscoveryError::DuplicateModuleRegistration {
142 name: name.to_string(),
143 owner: owner.to_string(),
144 kind: ModuleRegistrationKind::MetadataOnly,
145 count: counts.metadata_only,
146 });
147 }
148 if counts.runtime_localizer > 1 {
149 errors.push(ModuleDiscoveryError::DuplicateModuleRegistration {
150 name: name.to_string(),
151 owner: owner.to_string(),
152 kind: ModuleRegistrationKind::RuntimeLocalizer,
153 count: counts.runtime_localizer,
154 });
155 }
156 }
157
158 if errors.is_empty() {
159 Ok(modules)
160 } else {
161 Err(errors)
162 }
163}
164
165fn inspect_module_registry(
166 modules: &[&'static dyn I18nModuleRegistration],
167) -> HashMap<(&'static str, &'static str), IdentityInspection> {
168 let mut inspections: HashMap<(&'static str, &'static str), IdentityInspection> = HashMap::new();
169
170 for module in modules {
171 let data = module.data();
172 let inspection = inspections
173 .entry((data.name, data.owner.as_str()))
174 .or_default();
175 inspection.chosen_data.get_or_insert(data);
176
177 match module.registration_kind() {
178 ModuleRegistrationKind::MetadataOnly => {
179 inspection.counts.metadata_only += 1;
180 inspection.metadata_only.get_or_insert(data);
181 },
182 ModuleRegistrationKind::RuntimeLocalizer => {
183 inspection.counts.runtime_localizer += 1;
184 inspection.runtime_localizer.get_or_insert(data);
185 },
186 }
187 }
188
189 inspections
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use crate::asset_localization::{I18nModuleDescriptor, StaticModuleDescriptor};
196 use crate::localization::{I18nModule, LocalizationError, Localizer};
197 use std::error::Error as _;
198 use unic_langid::{LanguageIdentifier, langid};
199
200 static REGISTRY_TEST_LANGUAGES: &[LanguageIdentifier] = &[langid!("en")];
201 static REGISTRY_TEST_DATA: ModuleData = ModuleData {
202 name: "registry-test",
203 owner: crate::__macro::static_domain("registry-domain"),
204 supported_languages: REGISTRY_TEST_LANGUAGES,
205 domains: &[crate::ModuleDomain {
206 domain: crate::__macro::static_domain("registry-domain"),
207 namespaces: &[],
208 }],
209 };
210 static REGISTRY_INVALID_DATA: ModuleData = ModuleData {
211 name: "registry-invalid",
212 owner: crate::__macro::static_domain("registry-invalid"),
213 supported_languages: &[],
214 domains: &[crate::ModuleDomain {
215 domain: crate::__macro::static_domain("registry-invalid"),
216 namespaces: &[" ../escape "],
217 }],
218 };
219 static REGISTRY_METADATA: StaticModuleDescriptor =
220 StaticModuleDescriptor::new(®ISTRY_TEST_DATA);
221 static REGISTRY_METADATA_TWO: StaticModuleDescriptor =
222 StaticModuleDescriptor::new(®ISTRY_TEST_DATA);
223 static REGISTRY_INVALID_METADATA: StaticModuleDescriptor =
224 StaticModuleDescriptor::new(®ISTRY_INVALID_DATA);
225
226 struct RegistryRuntimeModule;
227 struct RegistryRuntimeModuleTwo;
228 struct RegistryNoopLocalizer;
229
230 impl Localizer for RegistryNoopLocalizer {
231 fn select_language(&self, _lang: &LanguageIdentifier) -> Result<(), LocalizationError> {
232 Ok(())
233 }
234
235 fn localize<'a>(
236 &self,
237 _key: crate::StaticFluentMessageKey,
238 _args: Option<&crate::FluentArgumentMap<'a>>,
239 ) -> Option<String> {
240 None
241 }
242 }
243
244 impl I18nModuleDescriptor for RegistryRuntimeModule {
245 fn data(&self) -> &'static ModuleData {
246 ®ISTRY_TEST_DATA
247 }
248 }
249
250 impl I18nModule for RegistryRuntimeModule {
251 fn create_localizer(&self) -> Box<dyn Localizer> {
252 Box::new(RegistryNoopLocalizer)
253 }
254 }
255
256 impl I18nModuleDescriptor for RegistryRuntimeModuleTwo {
257 fn data(&self) -> &'static ModuleData {
258 ®ISTRY_TEST_DATA
259 }
260 }
261
262 impl I18nModule for RegistryRuntimeModuleTwo {
263 fn create_localizer(&self) -> Box<dyn Localizer> {
264 Box::new(RegistryNoopLocalizer)
265 }
266 }
267
268 static REGISTRY_RUNTIME: RegistryRuntimeModule = RegistryRuntimeModule;
269 static REGISTRY_RUNTIME_TWO: RegistryRuntimeModuleTwo = RegistryRuntimeModuleTwo;
270
271 #[test]
272 fn inspect_module_registry_records_counts_for_each_registration_kind() {
273 let modules: [&'static dyn I18nModuleRegistration; 2] = [
274 ®ISTRY_METADATA as &dyn I18nModuleRegistration,
275 ®ISTRY_RUNTIME as &dyn I18nModuleRegistration,
276 ];
277
278 let inspections = inspect_module_registry(&modules);
279 let inspection = inspections
280 .get(&("registry-test", "registry-domain"))
281 .expect("module identity should be inspected");
282
283 assert_eq!(inspection.chosen_data, Some(®ISTRY_TEST_DATA));
284 assert_eq!(inspection.metadata_only, Some(®ISTRY_TEST_DATA));
285 assert_eq!(inspection.runtime_localizer, Some(®ISTRY_TEST_DATA));
286 assert_eq!(
287 inspection.counts,
288 RegistrationCounts {
289 metadata_only: 1,
290 runtime_localizer: 1,
291 }
292 );
293 }
294
295 #[test]
296 fn try_filter_module_registry_allows_empty_and_exact_metadata_runtime_pair() {
297 let empty: [&'static dyn I18nModuleRegistration; 0] = [];
298 assert!(
299 try_filter_module_registry(empty)
300 .expect("empty registry is valid")
301 .is_empty()
302 );
303
304 let filtered = try_filter_module_registry([
305 ®ISTRY_METADATA as &dyn I18nModuleRegistration,
306 ®ISTRY_RUNTIME as &dyn I18nModuleRegistration,
307 ])
308 .expect("exact metadata and runtime registrations should be valid");
309
310 assert_eq!(filtered.len(), 2);
311 }
312
313 #[test]
314 fn try_filter_module_registry_reports_duplicate_registration_kinds() {
315 let metadata_errors = match try_filter_module_registry([
316 ®ISTRY_METADATA as &dyn I18nModuleRegistration,
317 ®ISTRY_METADATA_TWO as &dyn I18nModuleRegistration,
318 ]) {
319 Ok(_) => panic!("duplicate metadata registrations should fail"),
320 Err(errors) => errors,
321 };
322 assert!(metadata_errors.iter().any(|error| {
323 matches!(
324 error,
325 ModuleDiscoveryError::DuplicateModuleRegistration {
326 kind: ModuleRegistrationKind::MetadataOnly,
327 count: 2,
328 ..
329 }
330 )
331 }));
332
333 let runtime_errors = match try_filter_module_registry([
334 ®ISTRY_RUNTIME as &dyn I18nModuleRegistration,
335 ®ISTRY_RUNTIME_TWO as &dyn I18nModuleRegistration,
336 ]) {
337 Ok(_) => panic!("duplicate runtime registrations should fail"),
338 Err(errors) => errors,
339 };
340 assert!(runtime_errors.iter().any(|error| {
341 matches!(
342 error,
343 ModuleDiscoveryError::DuplicateModuleRegistration {
344 kind: ModuleRegistrationKind::RuntimeLocalizer,
345 count: 2,
346 ..
347 }
348 )
349 }));
350 }
351
352 #[test]
353 fn validate_single_module_data_wraps_registry_validation_errors() {
354 let mut errors = Vec::new();
355 validate_single_module_data(®ISTRY_INVALID_DATA, &mut errors);
356
357 let invalid = errors
358 .iter()
359 .find(|error| matches!(error, ModuleDiscoveryError::InvalidMetadata(_)))
360 .expect("invalid metadata should be wrapped");
361 assert!(invalid.source().is_some());
362 assert!(invalid.to_string().contains("namespace"));
363 }
364
365 #[test]
366 fn module_discovery_errors_format_diagnostics_and_sources() {
367 assert_eq!(
368 ModuleRegistrationKind::MetadataOnly.to_string(),
369 "metadata-only"
370 );
371 assert_eq!(
372 ModuleRegistrationKind::RuntimeLocalizer.to_string(),
373 "runtime-localizer"
374 );
375
376 let invalid = match try_filter_module_registry([
377 ®ISTRY_INVALID_METADATA as &dyn I18nModuleRegistration
378 ]) {
379 Ok(_) => panic!("invalid metadata should fail"),
380 Err(mut errors) => errors.remove(0),
381 };
382 assert!(invalid.source().is_some());
383
384 let inconsistent = ModuleDiscoveryError::InconsistentModuleMetadata {
385 name: "name".to_string(),
386 owner: "owner".to_string(),
387 };
388 assert_eq!(
389 inconsistent.to_string(),
390 "module 'name' (owner 'owner') has mismatched metadata between registrations"
391 );
392 assert!(inconsistent.source().is_none());
393
394 let duplicate = ModuleDiscoveryError::DuplicateModuleRegistration {
395 name: "name".to_string(),
396 owner: "owner".to_string(),
397 kind: ModuleRegistrationKind::RuntimeLocalizer,
398 count: 3,
399 };
400 assert_eq!(
401 duplicate.to_string(),
402 "module 'name' (owner 'owner') has 3 duplicate runtime-localizer registrations"
403 );
404 assert!(duplicate.source().is_none());
405 }
406}