Skip to main content

configbuilder_pattern_loader/
feature_registry.rs

1use crate::{
2    ConfigError, FeatureRecord, FeatureRecordBuilder, FeatureState, FeatureSummary, LoadedFeature,
3    OnError, OptionalSection, OverrideSource,
4};
5
6type FeatureObserver = Box<dyn Fn(&FeatureRecord)>;
7
8/// Collects feature-load metadata at startup for all optional TOML sections.
9pub struct FeatureRegistry {
10    records: Vec<FeatureRecord>,
11    observers: Vec<FeatureObserver>,
12}
13
14/// Resolve the `OnError` policy for `key`: env var override takes precedence
15/// over the trait default. Pure env-var/trait logic -- no filesystem dependency,
16/// so it lives here rather than in any real, technology-specific `LoaderOps`
17/// implementor.
18fn resolve_feature_on_error<T: OptionalSection>(key: &str) -> OnError {
19    let var_name = format!(
20        "CONFIGBUILDER_FEATURE_{}_ON_ERROR",
21        key.to_uppercase().replace('.', "_")
22    );
23    match std::env::var(&var_name).as_deref() {
24        Ok("disable") => OnError::Disable,
25        Ok("fail") => OnError::Fail,
26        _ => T::on_error(),
27    }
28}
29
30impl FeatureRegistry {
31    /// Create an empty registry.
32    pub(crate) fn new() -> Self {
33        Self {
34            records: Vec::new(),
35            observers: Vec::new(),
36        }
37    }
38
39    /// Register a callback to observe loaded records.
40    pub(crate) fn on_load(&mut self, observer: impl Fn(&FeatureRecord) + 'static) {
41        self.observers.push(Box::new(observer));
42    }
43
44    /// Load a feature section and record its state.
45    pub(crate) fn load<T>(
46        &mut self,
47        loader: &crate::DefaultLoader,
48    ) -> Result<FeatureState<T>, ConfigError>
49    where
50        T: OptionalSection,
51    {
52        let loaded: LoadedFeature<T> = loader.load_feature(T::section_name())?;
53        let LoadedFeature { state, record } = loaded;
54        let record = *record;
55
56        let validation_result = if let FeatureState::Enabled(ref value) = state {
57            Some(value.validate_enabled())
58        } else {
59            None
60        };
61
62        let (final_state, final_override) = match validation_result {
63            Some(Ok(())) | None => (state, record.override_source),
64            Some(Err(e)) => match resolve_feature_on_error::<T>(T::section_name()) {
65                OnError::Fail => return Err(e),
66                OnError::Disable => (
67                    FeatureState::Disabled,
68                    Some(OverrideSource::ValidationError {
69                        reason: e.to_string(),
70                    }),
71                ),
72            },
73        };
74
75        let mut built = FeatureRecordBuilder::new(record.section_name)
76            .enabled(final_state.is_enabled())
77            .requires(T::requires())
78            .metadata(T::metadata());
79        if let Some(override_source) = final_override {
80            built = built.override_source(override_source);
81        }
82        self.records.push(built.build());
83
84        if let Some(record) = self.records.last() {
85            for observer in &self.observers {
86                observer(record);
87            }
88        }
89
90        Ok(final_state)
91    }
92
93    /// Validate recorded dependencies.
94    pub(crate) fn validate_dependencies(&self) -> Result<(), ConfigError> {
95        let enabled: std::collections::HashSet<&str> = self
96            .records
97            .iter()
98            .filter(|r| r.enabled)
99            .map(|r| r.section_name.as_str())
100            .collect();
101
102        let violations: Vec<String> = self
103            .records
104            .iter()
105            .filter(|r| r.enabled)
106            .flat_map(|r| {
107                r.requires.iter().filter_map(|dep| {
108                    if enabled.contains(dep) {
109                        None
110                    } else {
111                        Some(format!(
112                            "'{}' requires '{}' but '{}' is not enabled",
113                            r.section_name, dep, dep
114                        ))
115                    }
116                })
117            })
118            .collect();
119
120        if violations.is_empty() {
121            Ok(())
122        } else {
123            Err(ConfigError::Validation {
124                section: String::from("feature_dependencies"),
125                reason: violations.join("; "),
126            })
127        }
128    }
129
130    /// Borrow the recorded feature records.
131    pub(crate) fn records(&self) -> &[FeatureRecord] {
132        &self.records
133    }
134
135    /// Build a snapshot summary of the recorded features.
136    pub(crate) fn summary(&self) -> FeatureSummary {
137        FeatureSummary {
138            records: self.records.clone(),
139        }
140    }
141}
142
143impl crate::FeatureRegistryOps for FeatureRegistry {
144    fn new() -> Self {
145        FeatureRegistry::new()
146    }
147
148    fn on_load(&mut self, observer: impl Fn(&FeatureRecord) + 'static) {
149        FeatureRegistry::on_load(self, observer)
150    }
151
152    fn load<T>(&mut self, loader: &crate::DefaultLoader) -> Result<FeatureState<T>, ConfigError>
153    where
154        T: OptionalSection,
155    {
156        FeatureRegistry::load(self, loader)
157    }
158
159    fn validate_dependencies(&self) -> Result<(), ConfigError> {
160        FeatureRegistry::validate_dependencies(self)
161    }
162
163    fn records(&self) -> &[FeatureRecord] {
164        FeatureRegistry::records(self)
165    }
166
167    fn summary(&self) -> FeatureSummary {
168        FeatureRegistry::summary(self)
169    }
170}
171
172impl Default for FeatureRegistry {
173    fn default() -> Self {
174        Self::new()
175    }
176}