configbuilder-pattern-loader 0.1.0

configbuilder-pattern: loader domain, trait contract + tech-agnostic reference implementation.
Documentation
use crate::{
    ConfigError, FeatureRecord, FeatureRecordBuilder, FeatureState, FeatureSummary, LoadedFeature,
    OnError, OptionalSection, OverrideSource,
};

type FeatureObserver = Box<dyn Fn(&FeatureRecord)>;

/// Collects feature-load metadata at startup for all optional TOML sections.
pub struct FeatureRegistry {
    records: Vec<FeatureRecord>,
    observers: Vec<FeatureObserver>,
}

/// Resolve the `OnError` policy for `key`: env var override takes precedence
/// over the trait default. Pure env-var/trait logic -- no filesystem dependency,
/// so it lives here rather than in any real, technology-specific `LoaderOps`
/// implementor.
fn resolve_feature_on_error<T: OptionalSection>(key: &str) -> OnError {
    let var_name = format!(
        "CONFIGBUILDER_FEATURE_{}_ON_ERROR",
        key.to_uppercase().replace('.', "_")
    );
    match std::env::var(&var_name).as_deref() {
        Ok("disable") => OnError::Disable,
        Ok("fail") => OnError::Fail,
        _ => T::on_error(),
    }
}

impl FeatureRegistry {
    /// Create an empty registry.
    pub(crate) fn new() -> Self {
        Self {
            records: Vec::new(),
            observers: Vec::new(),
        }
    }

    /// Register a callback to observe loaded records.
    pub(crate) fn on_load(&mut self, observer: impl Fn(&FeatureRecord) + 'static) {
        self.observers.push(Box::new(observer));
    }

    /// Load a feature section and record its state.
    pub(crate) fn load<T>(
        &mut self,
        loader: &crate::DefaultLoader,
    ) -> Result<FeatureState<T>, ConfigError>
    where
        T: OptionalSection,
    {
        let loaded: LoadedFeature<T> = loader.load_feature(T::section_name())?;
        let LoadedFeature { state, record } = loaded;
        let record = *record;

        let validation_result = if let FeatureState::Enabled(ref value) = state {
            Some(value.validate_enabled())
        } else {
            None
        };

        let (final_state, final_override) = match validation_result {
            Some(Ok(())) | None => (state, record.override_source),
            Some(Err(e)) => match resolve_feature_on_error::<T>(T::section_name()) {
                OnError::Fail => return Err(e),
                OnError::Disable => (
                    FeatureState::Disabled,
                    Some(OverrideSource::ValidationError {
                        reason: e.to_string(),
                    }),
                ),
            },
        };

        let mut built = FeatureRecordBuilder::new(record.section_name)
            .enabled(final_state.is_enabled())
            .requires(T::requires())
            .metadata(T::metadata());
        if let Some(override_source) = final_override {
            built = built.override_source(override_source);
        }
        self.records.push(built.build());

        if let Some(record) = self.records.last() {
            for observer in &self.observers {
                observer(record);
            }
        }

        Ok(final_state)
    }

    /// Validate recorded dependencies.
    pub(crate) fn validate_dependencies(&self) -> Result<(), ConfigError> {
        let enabled: std::collections::HashSet<&str> = self
            .records
            .iter()
            .filter(|r| r.enabled)
            .map(|r| r.section_name.as_str())
            .collect();

        let violations: Vec<String> = self
            .records
            .iter()
            .filter(|r| r.enabled)
            .flat_map(|r| {
                r.requires.iter().filter_map(|dep| {
                    if enabled.contains(dep) {
                        None
                    } else {
                        Some(format!(
                            "'{}' requires '{}' but '{}' is not enabled",
                            r.section_name, dep, dep
                        ))
                    }
                })
            })
            .collect();

        if violations.is_empty() {
            Ok(())
        } else {
            Err(ConfigError::Validation {
                section: String::from("feature_dependencies"),
                reason: violations.join("; "),
            })
        }
    }

    /// Borrow the recorded feature records.
    pub(crate) fn records(&self) -> &[FeatureRecord] {
        &self.records
    }

    /// Build a snapshot summary of the recorded features.
    pub(crate) fn summary(&self) -> FeatureSummary {
        FeatureSummary {
            records: self.records.clone(),
        }
    }
}

impl crate::FeatureRegistryOps for FeatureRegistry {
    fn new() -> Self {
        FeatureRegistry::new()
    }

    fn on_load(&mut self, observer: impl Fn(&FeatureRecord) + 'static) {
        FeatureRegistry::on_load(self, observer)
    }

    fn load<T>(&mut self, loader: &crate::DefaultLoader) -> Result<FeatureState<T>, ConfigError>
    where
        T: OptionalSection,
    {
        FeatureRegistry::load(self, loader)
    }

    fn validate_dependencies(&self) -> Result<(), ConfigError> {
        FeatureRegistry::validate_dependencies(self)
    }

    fn records(&self) -> &[FeatureRecord] {
        FeatureRegistry::records(self)
    }

    fn summary(&self) -> FeatureSummary {
        FeatureRegistry::summary(self)
    }
}

impl Default for FeatureRegistry {
    fn default() -> Self {
        Self::new()
    }
}