1use std::collections::{BTreeMap, BTreeSet, HashMap};
9use std::hash::BuildHasher;
10
11use crate::{
12 ConfigCoreError, ConfigSource, ConfigValue, ConfigValueType, ConfigVisibility, Result,
13 StoredConfig, validate_storage_value,
14};
15
16fn empty_default_value() -> String {
17 String::new()
18}
19
20pub trait ConfigValueLookup {
22 fn get_config_value(&self, key: &str) -> Option<String>;
24}
25
26impl<S: BuildHasher> ConfigValueLookup for HashMap<String, String, S> {
27 fn get_config_value(&self, key: &str) -> Option<String> {
28 self.get(key).cloned()
29 }
30}
31
32impl ConfigValueLookup for BTreeMap<String, String> {
33 fn get_config_value(&self, key: &str) -> Option<String> {
34 self.get(key).cloned()
35 }
36}
37
38impl<F> ConfigValueLookup for F
39where
40 F: Fn(&str) -> Option<String>,
41{
42 fn get_config_value(&self, key: &str) -> Option<String> {
43 self(key)
44 }
45}
46
47pub type ConfigNormalizer =
49 fn(lookup: &dyn ConfigValueLookup, key: &str, value: &str) -> Result<String>;
50
51pub type ConfigDependencyValidator =
53 fn(lookup: &dyn ConfigValueLookup, key: &str, normalized_value: &str) -> Result<()>;
54
55#[derive(Debug, Clone, Copy)]
57pub struct ConfigDefinition {
58 pub key: &'static str,
60 pub label_i18n_key: &'static str,
62 pub description_i18n_key: &'static str,
64 pub value_type: ConfigValueType,
66 pub default_fn: fn() -> String,
68 pub normalize_fn: Option<ConfigNormalizer>,
70 pub dependency_validator_fn: Option<ConfigDependencyValidator>,
72 pub requires_restart: bool,
74 pub is_sensitive: bool,
76 pub visibility: ConfigVisibility,
78 pub category: &'static str,
80 pub description: &'static str,
82}
83
84impl ConfigDefinition {
85 #[must_use]
93 pub const fn private_system() -> Self {
94 Self {
95 key: "",
96 label_i18n_key: "",
97 description_i18n_key: "",
98 value_type: ConfigValueType::String,
99 default_fn: empty_default_value,
100 normalize_fn: None,
101 dependency_validator_fn: None,
102 requires_restart: false,
103 is_sensitive: false,
104 visibility: ConfigVisibility::Private,
105 category: "",
106 description: "",
107 }
108 }
109}
110
111#[derive(Debug, Clone, PartialEq, Eq)]
113pub struct ConfigSeedRecord {
114 pub key: String,
116 pub value: String,
118 pub value_type: ConfigValueType,
120 pub requires_restart: bool,
122 pub is_sensitive: bool,
124 pub source: ConfigSource,
126 pub visibility: ConfigVisibility,
128 pub category: String,
130 pub description: String,
132}
133
134#[derive(Debug)]
136pub struct ConfigRegistry {
137 definitions: &'static [ConfigDefinition],
138}
139
140impl ConfigRegistry {
141 #[must_use]
143 pub const fn new(definitions: &'static [ConfigDefinition]) -> Self {
144 Self { definitions }
145 }
146
147 #[must_use]
149 pub const fn definitions(&self) -> &'static [ConfigDefinition] {
150 self.definitions
151 }
152
153 #[must_use]
155 pub fn get(&self, key: &str) -> Option<&'static ConfigDefinition> {
156 self.definitions
157 .iter()
158 .find(|definition| definition.key == key)
159 }
160
161 #[must_use]
163 pub fn contains_key(&self, key: &str) -> bool {
164 self.get(key).is_some()
165 }
166
167 pub fn require(&self, key: &str) -> Result<&'static ConfigDefinition> {
173 self.get(key)
174 .ok_or_else(|| ConfigCoreError::UnknownKey(key.to_string()))
175 }
176
177 pub fn validate_unique_keys(&self) -> Result<()> {
183 let mut seen = BTreeSet::new();
184 for definition in self.definitions {
185 if definition.key.trim().is_empty() {
186 return Err(ConfigCoreError::invalid_value(
187 "config definition key cannot be empty",
188 ));
189 }
190 if !seen.insert(definition.key) {
191 return Err(ConfigCoreError::invalid_value(format!(
192 "duplicate config definition key '{}'",
193 definition.key
194 )));
195 }
196 }
197 Ok(())
198 }
199
200 pub fn validate_categories(&self, allowed_categories: &[&str]) -> Result<()> {
206 for definition in self.definitions {
207 if !allowed_categories.contains(&definition.category) {
208 return Err(ConfigCoreError::invalid_value(format!(
209 "config key '{}' uses unknown category '{}'",
210 definition.key, definition.category
211 )));
212 }
213 }
214 Ok(())
215 }
216
217 pub fn validate_value_structure(&self, key: &str, value: &str) -> Result<()> {
227 let definition = self.require(key)?;
228 validate_storage_value(definition.value_type, value)
229 }
230
231 pub fn normalize_value(
248 &self,
249 lookup: &dyn ConfigValueLookup,
250 key: &str,
251 value: &str,
252 ) -> Result<String> {
253 let definition = self.require(key)?;
254 validate_storage_value(definition.value_type, value)?;
255
256 let normalized = match definition.normalize_fn {
257 Some(normalize) => normalize(lookup, key, value)?,
258 None => value.to_string(),
259 };
260 validate_storage_value(definition.value_type, &normalized)?;
261
262 if let Some(validate) = definition.dependency_validator_fn {
263 validate(lookup, key, &normalized)?;
264 }
265
266 Ok(normalized)
267 }
268
269 pub fn value_to_normalized_storage(
279 &self,
280 lookup: &dyn ConfigValueLookup,
281 key: &str,
282 value: &ConfigValue,
283 ) -> Result<String> {
284 let definition = self.require(key)?;
285 let storage = value.to_storage_for_type(definition.value_type)?;
286 self.normalize_value(lookup, key, &storage)
287 }
288
289 pub fn value_to_storage_for_key(
304 &self,
305 lookup: &dyn ConfigValueLookup,
306 key: &str,
307 value: &ConfigValue,
308 ) -> Result<String> {
309 match self.get(key) {
310 Some(definition) => {
311 let storage = value.to_storage_for_type(definition.value_type)?;
312 self.normalize_value(lookup, key, &storage)
313 }
314 None => value.to_storage_for_type(ConfigValueType::String),
315 }
316 }
317
318 #[must_use]
320 pub fn apply_definition(&self, mut config: StoredConfig) -> StoredConfig {
321 if config.source != ConfigSource::System {
322 return config;
323 }
324
325 let Some(definition) = self.get(&config.key) else {
326 return config;
327 };
328
329 config.value_type = definition.value_type;
330 config.requires_restart = definition.requires_restart;
331 config.is_sensitive = definition.is_sensitive;
332 config.visibility = definition.visibility;
333 config.category = definition.category.to_string();
334 config.description = definition.description.to_string();
335 config
336 }
337
338 pub fn default_seed_records(&self) -> Result<Vec<ConfigSeedRecord>> {
349 self.validate_unique_keys()?;
350
351 let mut lookup = BTreeMap::<String, String>::new();
352 let mut rows = Vec::with_capacity(self.definitions.len());
353
354 for definition in self.definitions {
355 let raw = (definition.default_fn)();
356 let normalized = self.normalize_value(&lookup, definition.key, &raw)?;
357 lookup.insert(definition.key.to_string(), normalized.clone());
358 rows.push(ConfigSeedRecord {
359 key: definition.key.to_string(),
360 value: normalized,
361 value_type: definition.value_type,
362 requires_restart: definition.requires_restart,
363 is_sensitive: definition.is_sensitive,
364 source: ConfigSource::System,
365 visibility: definition.visibility,
366 category: definition.category.to_string(),
367 description: definition.description.to_string(),
368 });
369 }
370
371 Ok(rows)
372 }
373}
374
375#[cfg(test)]
376mod tests {
377 use std::collections::HashMap;
378 use std::sync::atomic::{AtomicUsize, Ordering};
379
380 use super::{
381 ConfigDefinition, ConfigRegistry, ConfigSeedRecord, ConfigValueLookup, ConfigValueType,
382 };
383 use crate::{ConfigSource, ConfigValue, ConfigVisibility, StoredConfig};
384
385 fn default_value() -> String {
386 "default".to_string()
387 }
388
389 fn default_toggle() -> String {
390 "true".to_string()
391 }
392
393 #[expect(
394 clippy::unnecessary_wraps,
395 reason = "This test normalizer must match the fallible ConfigNormalizer function-pointer contract."
396 )]
397 fn trim_value(
398 _lookup: &dyn ConfigValueLookup,
399 _key: &str,
400 value: &str,
401 ) -> crate::Result<String> {
402 Ok(value.trim().to_string())
403 }
404
405 fn require_enabled(
406 lookup: &dyn ConfigValueLookup,
407 _key: &str,
408 _value: &str,
409 ) -> crate::Result<()> {
410 match lookup.get_config_value("enabled").as_deref() {
411 Some("true") => Ok(()),
412 Some(_) => Err(crate::ConfigCoreError::invalid_value(
413 "feature requires enabled=true",
414 )),
415 None => Err(crate::ConfigCoreError::invalid_value(
416 "feature requires enabled to be present",
417 )),
418 }
419 }
420
421 const PRIMARY: ConfigDefinition = ConfigDefinition {
422 key: "primary",
423 label_i18n_key: "primary_label",
424 description_i18n_key: "primary_desc",
425 value_type: ConfigValueType::String,
426 default_fn: default_value,
427 normalize_fn: Some(trim_value),
428 dependency_validator_fn: None,
429 requires_restart: false,
430 is_sensitive: false,
431 visibility: ConfigVisibility::Private,
432 category: "general",
433 description: "primary setting",
434 };
435
436 const ENABLED: ConfigDefinition = ConfigDefinition {
437 key: "enabled",
438 label_i18n_key: "enabled_label",
439 description_i18n_key: "enabled_desc",
440 value_type: ConfigValueType::Boolean,
441 default_fn: default_toggle,
442 normalize_fn: None,
443 dependency_validator_fn: None,
444 requires_restart: false,
445 is_sensitive: false,
446 visibility: ConfigVisibility::Private,
447 category: "general",
448 description: "enabled flag",
449 };
450
451 const DEPENDENT: ConfigDefinition = ConfigDefinition {
452 key: "dependent",
453 label_i18n_key: "dependent_label",
454 description_i18n_key: "dependent_desc",
455 value_type: ConfigValueType::String,
456 default_fn: default_value,
457 normalize_fn: Some(trim_value),
458 dependency_validator_fn: Some(require_enabled),
459 requires_restart: true,
460 is_sensitive: true,
461 visibility: ConfigVisibility::Authenticated,
462 category: "general",
463 description: "dependent setting",
464 };
465
466 const DUPLICATE: ConfigDefinition = ConfigDefinition {
467 key: "primary",
468 ..PRIMARY
469 };
470
471 static DEFAULT_CALLS: AtomicUsize = AtomicUsize::new(0);
472
473 fn counting_default() -> String {
474 DEFAULT_CALLS.fetch_add(1, Ordering::Relaxed);
475 "counted".to_string()
476 }
477
478 #[test]
479 fn registry_finds_definitions_by_key() {
480 let registry = ConfigRegistry::new(&[PRIMARY]);
481
482 let definition = registry.require("primary").unwrap();
483
484 assert_eq!(definition.key, "primary");
485 assert!(registry.contains_key("primary"));
486 assert!(!registry.contains_key("missing"));
487 }
488
489 #[test]
490 fn static_key_validation_accepts_empty_and_single_definition_lists() {
491 const _: () = crate::__assert_unique_config_definition_keys(&[]);
492 const _: () = crate::__assert_unique_config_definition_keys(&[PRIMARY]);
493 crate::__assert_unique_config_definition_keys(&[]);
494 crate::__assert_unique_config_definition_keys(&[PRIMARY]);
495 }
496
497 #[test]
498 fn static_key_validation_distinguishes_lengths_bytes_and_unicode() {
499 const DEFINITIONS: &[ConfigDefinition] = &[
500 ConfigDefinition {
501 key: "a",
502 ..PRIMARY
503 },
504 ConfigDefinition {
505 key: "ab",
506 ..PRIMARY
507 },
508 ConfigDefinition {
509 key: "ac",
510 ..PRIMARY
511 },
512 ConfigDefinition {
513 key: "aB",
514 ..PRIMARY
515 },
516 ConfigDefinition {
517 key: "\u{e9}",
518 ..PRIMARY
519 },
520 ConfigDefinition {
521 key: "\u{ea}",
522 ..PRIMARY
523 },
524 ];
525 const _: () = crate::__assert_unique_config_definition_keys(DEFINITIONS);
526 crate::__assert_unique_config_definition_keys(DEFINITIONS);
527 }
528
529 #[test]
530 #[should_panic(expected = "duplicate config definition key")]
531 fn static_key_validation_rejects_adjacent_duplicates() {
532 crate::__assert_unique_config_definition_keys(&[PRIMARY, DUPLICATE]);
533 }
534
535 #[test]
536 #[should_panic(expected = "duplicate config definition key")]
537 fn static_key_validation_rejects_non_adjacent_duplicates() {
538 crate::__assert_unique_config_definition_keys(&[ENABLED, PRIMARY, DEPENDENT, DUPLICATE]);
539 }
540
541 #[test]
542 #[should_panic(expected = "config definition key cannot be empty")]
543 fn static_key_validation_rejects_empty_single_key() {
544 crate::__assert_unique_config_definition_keys(&[ConfigDefinition { key: "", ..PRIMARY }]);
545 }
546
547 #[test]
548 #[should_panic(expected = "config definition key cannot be empty")]
549 fn static_key_validation_rejects_empty_last_key() {
550 crate::__assert_unique_config_definition_keys(&[
551 PRIMARY,
552 ENABLED,
553 ConfigDefinition { key: "", ..PRIMARY },
554 ]);
555 }
556
557 #[test]
558 fn registry_rejects_duplicate_keys_and_unknown_categories() {
559 let duplicate_registry = ConfigRegistry::new(&[PRIMARY, DUPLICATE]);
560 assert!(duplicate_registry.validate_unique_keys().is_err());
561
562 let category_registry = ConfigRegistry::new(&[PRIMARY]);
563 assert!(category_registry.validate_categories(&["general"]).is_ok());
564 assert!(category_registry.validate_categories(&["other"]).is_err());
565 }
566
567 #[test]
568 fn default_seed_records_rejects_invalid_keys_before_generating_rows() {
569 const DUPLICATE_WITH_COUNTING_DEFAULT: ConfigDefinition = ConfigDefinition {
570 key: "primary",
571 default_fn: counting_default,
572 ..PRIMARY
573 };
574 const EMPTY: ConfigDefinition = ConfigDefinition { key: "", ..PRIMARY };
575 const WHITESPACE_ONLY: ConfigDefinition = ConfigDefinition {
576 key: " ",
577 ..PRIMARY
578 };
579 const TABBED: ConfigDefinition = ConfigDefinition {
580 key: "\t\n",
581 ..PRIMARY
582 };
583
584 DEFAULT_CALLS.store(0, Ordering::Relaxed);
585 let duplicate_registry = ConfigRegistry::new(&[PRIMARY, DUPLICATE]);
586 assert!(duplicate_registry.default_seed_records().is_err());
587
588 let duplicate_with_counting_default =
589 ConfigRegistry::new(&[PRIMARY, DUPLICATE_WITH_COUNTING_DEFAULT]);
590 assert!(
591 duplicate_with_counting_default
592 .default_seed_records()
593 .is_err()
594 );
595 assert_eq!(DEFAULT_CALLS.load(Ordering::Relaxed), 0);
596
597 let empty_registry = ConfigRegistry::new(&[EMPTY]);
598 assert!(empty_registry.default_seed_records().is_err());
599
600 let whitespace_registry = ConfigRegistry::new(&[WHITESPACE_ONLY]);
601 assert!(whitespace_registry.default_seed_records().is_err());
602
603 let tabbed_registry = ConfigRegistry::new(&[TABBED]);
604 assert!(tabbed_registry.default_seed_records().is_err());
605 }
606
607 #[test]
608 fn registry_normalizes_and_validates_known_values() {
609 let registry = ConfigRegistry::new(&[ENABLED, DEPENDENT]);
610 let lookup = HashMap::from([("enabled".to_string(), "true".to_string())]);
611
612 assert_eq!(
613 registry
614 .normalize_value(&lookup, "dependent", " demo ")
615 .unwrap(),
616 "demo"
617 );
618 assert!(registry.normalize_value(&lookup, "enabled", "yes").is_err());
619 }
620
621 #[test]
622 fn registry_dependency_validation_uses_lookup() {
623 let registry = ConfigRegistry::new(&[ENABLED, DEPENDENT]);
624 let failing_lookup = HashMap::from([("enabled".to_string(), "false".to_string())]);
625
626 assert!(
627 registry
628 .normalize_value(&failing_lookup, "dependent", "value")
629 .is_err()
630 );
631 }
632
633 #[test]
634 fn function_lookup_can_back_runtime_readers() {
635 let lookup = |key: &str| (key == "enabled").then_some("true".to_string());
636
637 assert_eq!(lookup.get_config_value("enabled"), Some("true".to_string()));
638 assert_eq!(lookup.get_config_value("missing"), None);
639 }
640
641 #[test]
642 fn registry_converts_api_values_into_normalized_storage() {
643 let registry = ConfigRegistry::new(&[PRIMARY]);
644
645 assert_eq!(
646 registry
647 .value_to_normalized_storage(
648 &HashMap::new(),
649 "primary",
650 &ConfigValue::from(" x ")
651 )
652 .unwrap(),
653 "x"
654 );
655 }
656
657 #[test]
658 fn registry_converts_registered_and_custom_values_for_storage() {
659 let registry = ConfigRegistry::new(&[PRIMARY]);
660
661 assert_eq!(
662 registry
663 .value_to_storage_for_key(&HashMap::new(), "primary", &ConfigValue::from(" x "))
664 .unwrap(),
665 "x"
666 );
667 assert_eq!(
668 registry
669 .value_to_storage_for_key(
670 &HashMap::new(),
671 "custom.banner",
672 &ConfigValue::from(" x ")
673 )
674 .unwrap(),
675 " x "
676 );
677 assert!(
678 registry
679 .value_to_storage_for_key(
680 &HashMap::new(),
681 "custom.list",
682 &ConfigValue::StringArray(vec!["x".to_string()])
683 )
684 .is_err()
685 );
686 }
687
688 #[test]
689 fn registry_applies_definition_metadata_to_system_rows() {
690 let registry = ConfigRegistry::new(&[DEPENDENT]);
691 let row = StoredConfig {
692 id: 7,
693 key: "dependent".to_string(),
694 value: "value".to_string(),
695 value_type: ConfigValueType::String,
696 requires_restart: false,
697 is_sensitive: false,
698 source: ConfigSource::System,
699 visibility: ConfigVisibility::Private,
700 category: String::new(),
701 description: String::new(),
702 };
703
704 let applied = registry.apply_definition(row);
705 assert_eq!(applied.value_type, ConfigValueType::String);
706 assert!(applied.requires_restart);
707 assert!(applied.is_sensitive);
708 assert_eq!(applied.visibility, ConfigVisibility::Authenticated);
709 assert_eq!(applied.category, "general");
710 assert_eq!(applied.description, "dependent setting");
711 }
712
713 #[test]
714 fn registry_builds_normalized_default_seed_records() {
715 let registry = ConfigRegistry::new(&[ENABLED, DEPENDENT]);
716
717 assert_eq!(
718 registry.default_seed_records().unwrap(),
719 vec![
720 ConfigSeedRecord {
721 key: "enabled".to_string(),
722 value: "true".to_string(),
723 value_type: ConfigValueType::Boolean,
724 requires_restart: false,
725 is_sensitive: false,
726 source: ConfigSource::System,
727 visibility: ConfigVisibility::Private,
728 category: "general".to_string(),
729 description: "enabled flag".to_string(),
730 },
731 ConfigSeedRecord {
732 key: "dependent".to_string(),
733 value: "default".to_string(),
734 value_type: ConfigValueType::String,
735 requires_restart: true,
736 is_sensitive: true,
737 source: ConfigSource::System,
738 visibility: ConfigVisibility::Authenticated,
739 category: "general".to_string(),
740 description: "dependent setting".to_string(),
741 },
742 ]
743 );
744 }
745}