mod builder;
mod entry;
mod loading;
mod result;
use std::collections::BTreeMap;
use super::support::{elapsed_millis_u64, to_tracked_layer};
pub use builder::SourceCoordinatorBuilder;
use entry::SourceEntry;
use loading::LayerCollection;
pub use result::{LoadReport, LoadResult, SourceLayer};
use serde::de::DeserializeOwned;
use crate::config::LoadOptions;
use crate::domain::{ParsedContent, Result, Source, SourceError};
#[derive(Default)]
pub struct SourceCoordinator {
pub(super) sources: Vec<SourceEntry>,
pub(super) options: LoadOptions,
pub(super) cache: BTreeMap<String, ParsedContent>,
}
impl SourceCoordinator {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn builder() -> SourceCoordinatorBuilder {
SourceCoordinatorBuilder::new()
}
pub fn add_source<S: Source + 'static>(&mut self, source: S, priority: i32) -> &mut Self {
let entry = SourceEntry {
source: Box::new(source),
priority,
order: self.sources.len(),
};
self.sources.push(entry);
self
}
#[must_use]
pub const fn source_count(&self) -> usize {
self.sources.len()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.sources.is_empty()
}
pub fn clear(&mut self) {
self.sources.clear();
self.cache.clear();
}
pub fn clear_cache(&mut self) {
self.cache.clear();
}
pub fn collect_layers(&mut self) -> Result<Vec<SourceLayer>> {
loading::collect_loaded_layers(&self.sources, &self.options, &mut self.cache)
.map(|collection| collection.layers)
}
pub fn load_report(&mut self) -> Result<LoadReport> {
let start = std::time::Instant::now();
let LayerCollection {
layers,
loaded_sources,
failed_sources,
loaded_count,
} = loading::collect_loaded_layers(&self.sources, &self.options, &mut self.cache)?;
if layers.is_empty() && !failed_sources.is_empty() {
let error_messages: Vec<String> = failed_sources
.iter()
.map(|(name, error)| format!("{name}: {error}"))
.collect();
return Err(SourceError::custom(&error_messages.join(", ")));
}
let tracked_layers: Vec<_> = layers.iter().map(to_tracked_layer).collect::<Result<_>>()?;
let merge_report = cfgmatic_merge::merge_layers_with_report(
&tracked_layers,
&super::support::merge_options(self.options.merge_strategy),
)
.map_err(|error| SourceError::custom(&error.to_string()))?;
let merged = ParsedContent::from_json(merge_report.merged.clone());
Ok(LoadReport {
layers,
merged,
merge_report,
loaded_count,
loaded_sources,
failed_sources,
processing_time_ms: elapsed_millis_u64(start),
})
}
pub fn load(&mut self) -> Result<LoadResult> {
self.load_report().map(LoadReport::into_load_result)
}
pub fn load_as<T: DeserializeOwned>(&mut self) -> Result<T> {
self.load_report()?.to_type()
}
pub fn reload(&mut self) -> Result<LoadResult> {
self.clear_cache();
self.load()
}
pub fn reload_report(&mut self) -> Result<LoadReport> {
self.clear_cache();
self.load_report()
}
}
impl std::fmt::Debug for SourceCoordinator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SourceCoordinator")
.field("source_count", &self.sources.len())
.field("options", &self.options)
.field("cache_entries", &self.cache.len())
.finish()
}
}
#[cfg(feature = "async")]
#[allow(clippy::wildcard_imports, clippy::unused_async)]
mod async_impl {
use super::*;
impl SourceCoordinator {
pub async fn load_async(&mut self) -> Result<LoadResult> {
self.load()
}
pub async fn load_report_async(&mut self) -> Result<LoadReport> {
self.load_report()
}
}
}
#[cfg(test)]
#[allow(clippy::wildcard_imports, clippy::unused_async, dead_code)]
mod tests {
use std::fs;
use super::*;
use crate::config::MergeStrategy;
use crate::domain::{Format, RawContent, SourceKind, SourceMetadata};
#[cfg(feature = "file")]
use crate::infrastructure::FileSource;
#[cfg(feature = "file")]
use tempfile::tempdir;
struct TestSource {
content: String,
format: Format,
optional: bool,
name: String,
}
impl TestSource {
fn new(name: &str, content: &str, format: Format) -> Self {
Self {
name: name.to_string(),
content: content.to_string(),
format,
optional: false,
}
}
fn with_optional(mut self, optional: bool) -> Self {
self.optional = optional;
self
}
}
impl Source for TestSource {
fn kind(&self) -> SourceKind {
SourceKind::Memory
}
fn metadata(&self) -> SourceMetadata {
SourceMetadata::new(&self.name)
}
fn load_raw(&self) -> Result<RawContent> {
Ok(RawContent::from_string(&self.content))
}
fn detect_format(&self) -> Option<Format> {
if self.format == Format::Unknown {
None
} else {
Some(self.format)
}
}
fn is_optional(&self) -> bool {
self.optional
}
}
#[test]
fn test_load_result_new() {
let result = LoadResult::new(ParsedContent::Null);
assert!(result.content().is_null());
assert_eq!(result.loaded_count(), 0);
assert!(!result.has_failures());
}
#[test]
fn test_source_coordinator_new() {
let coordinator = SourceCoordinator::new();
assert!(coordinator.is_empty());
assert_eq!(coordinator.source_count(), 0);
}
#[test]
fn test_source_coordinator_builder() {
let coordinator = SourceCoordinator::builder()
.merge_strategy(MergeStrategy::Deep)
.fail_fast(false)
.cache_enabled(false)
.build();
assert_eq!(coordinator.source_count(), 0);
assert!(!coordinator.options.is_cache_enabled());
}
#[test]
fn test_source_coordinator_add_source() {
let source = TestSource::new("test", r#"{"key": "value"}"#, Format::Json);
let mut coordinator = SourceCoordinator::new();
coordinator.add_source(source, 10);
assert_eq!(coordinator.source_count(), 1);
}
#[test]
fn test_source_coordinator_load_single() {
let source = TestSource::new("test", r#"{"key": "value"}"#, Format::Json);
let mut coordinator = SourceCoordinator::builder().add_source(source, 10).build();
let result = coordinator.load().unwrap();
assert!(result.content().is_object());
assert_eq!(result.loaded_count(), 1);
assert!(!result.has_failures());
}
#[test]
fn test_source_coordinator_load_multiple() {
let source1 = TestSource::new("low", r#"{"a": 1}"#, Format::Json);
let source2 = TestSource::new("high", r#"{"b": 2}"#, Format::Json);
let mut coordinator = SourceCoordinator::builder()
.add_source(source1, 1)
.add_source(source2, 10)
.merge_strategy(MergeStrategy::Deep)
.build();
let result = coordinator.load().unwrap();
assert!(result.content().get("a").is_some());
assert!(result.content().get("b").is_some());
}
#[test]
fn test_source_coordinator_collect_layers_orders_by_priority_then_registration() {
let source1 = TestSource::new("later-low", r#"{"name": "low"}"#, Format::Json);
let source2 = TestSource::new("high", r#"{"name": "high"}"#, Format::Json);
let source3 = TestSource::new("later-high", r#"{"name": "later-high"}"#, Format::Json);
let mut coordinator = SourceCoordinator::builder()
.add_source(source1, 1)
.add_source(source2, 10)
.add_source(source3, 10)
.build();
let layers = coordinator.collect_layers().unwrap();
assert_eq!(layers.len(), 3);
assert_eq!(layers[0].registration_index, 0);
assert_eq!(layers[1].registration_index, 1);
assert_eq!(layers[2].registration_index, 2);
assert_eq!(layers[0].priority, 1);
assert_eq!(layers[1].priority, 10);
assert_eq!(layers[2].priority, 10);
}
#[test]
fn test_source_coordinator_collect_layers_skips_optional_missing_like_load_report() {
let required = TestSource::new("required", r#"{"a": 1}"#, Format::Json);
let optional = TestSource::new("optional", r"invalid", Format::Unknown).with_optional(true);
let mut coordinator = SourceCoordinator::builder()
.add_source(required, 1)
.add_source(optional, 2)
.fail_fast(false)
.build();
let layers = coordinator.collect_layers().unwrap();
assert_eq!(layers.len(), 1);
assert_eq!(layers[0].content.get("a").unwrap().as_integer(), Some(1));
}
#[test]
fn test_source_coordinator_collect_layers_preserves_negative_priority() {
let source1 = TestSource::new("low", r#"{"value": "low"}"#, Format::Json);
let source2 = TestSource::new("default", r#"{"value": "mid"}"#, Format::Json);
let source3 = TestSource::new("high", r#"{"value": "high"}"#, Format::Json);
let mut coordinator = SourceCoordinator::builder()
.add_source(source2, 0)
.add_source(source3, 10)
.add_source(source1, -10)
.build();
let layers = coordinator.collect_layers().unwrap();
assert_eq!(
layers
.iter()
.map(|layer| layer.priority)
.collect::<Vec<_>>(),
vec![-10, 0, 10]
);
}
#[test]
fn test_source_coordinator_priority() {
let source1 = TestSource::new("low", r#"{"key": "low"}"#, Format::Json);
let source2 = TestSource::new("high", r#"{"key": "high"}"#, Format::Json);
let mut coordinator = SourceCoordinator::builder()
.add_source(source1, 1)
.add_source(source2, 10)
.merge_strategy(MergeStrategy::Deep)
.build();
let result = coordinator.load().unwrap();
assert_eq!(result.content().get("key").unwrap().as_str(), Some("high"));
}
#[test]
fn test_source_coordinator_load_report_exposes_merge_details() {
let source1 = TestSource::new("defaults", r#"{"server":{"port":8080}}"#, Format::Json);
let source2 = TestSource::new("env", r#"{"server":{"port":9090}}"#, Format::Json);
let mut coordinator = SourceCoordinator::builder()
.add_source(source1, 1)
.add_source(source2, 10)
.merge_strategy(MergeStrategy::Deep)
.build();
let report = coordinator.load_report().unwrap();
let explanation = report.merge_report.explain_path("/server/port").unwrap();
assert_eq!(report.loaded_count, 2);
assert_eq!(report.layers.len(), 2);
assert_eq!(
report
.merged
.get("server")
.unwrap()
.get("port")
.unwrap()
.as_integer(),
Some(9090)
);
assert_eq!(explanation.winner.unwrap().source, "env");
}
#[test]
fn test_source_coordinator_optional_source() {
let required =
TestSource::new("required", r#"{"a": 1}"#, Format::Json).with_optional(false);
let optional = TestSource::new("optional", r"invalid", Format::Unknown).with_optional(true);
let mut coordinator = SourceCoordinator::builder()
.add_source(required, 1)
.add_source(optional, 2)
.build();
let result = coordinator.load().unwrap();
assert!(result.content().get("a").is_some());
}
#[test]
fn test_source_coordinator_cache() {
let source = TestSource::new("test", r#"{"key": "value"}"#, Format::Json);
let mut coordinator = SourceCoordinator::builder()
.add_source(source, 10)
.cache_enabled(true)
.build();
let result1 = coordinator.load().unwrap();
assert_eq!(result1.loaded_count(), 1);
let result2 = coordinator.load().unwrap();
assert_eq!(result2.loaded_count(), 1);
}
#[test]
fn test_source_coordinator_clear_cache() {
let source = TestSource::new("test", r#"{"key": "value"}"#, Format::Json);
let mut coordinator = SourceCoordinator::builder()
.add_source(source, 10)
.cache_enabled(true)
.build();
coordinator.load().unwrap();
coordinator.clear_cache();
assert!(coordinator.cache.is_empty());
}
#[test]
fn test_source_coordinator_reload() {
let source = TestSource::new("test", r#"{"key": "value"}"#, Format::Json);
let mut coordinator = SourceCoordinator::builder()
.add_source(source, 10)
.cache_enabled(true)
.build();
coordinator.load().unwrap();
let result = coordinator.reload().unwrap();
assert_eq!(result.loaded_count(), 1);
}
#[cfg(feature = "file")]
#[test]
fn test_source_coordinator_cache_uses_source_identity() {
let temp_dir = tempdir().unwrap();
let first = temp_dir.path().join("first.json");
let second = temp_dir.path().join("second.json");
fs::write(&first, r#"{"first": 1}"#).unwrap();
fs::write(&second, r#"{"second": 2}"#).unwrap();
let mut coordinator = SourceCoordinator::builder()
.add_source(FileSource::new(&first), 1)
.add_source(FileSource::new(&second), 2)
.cache_enabled(true)
.merge_strategy(MergeStrategy::Deep)
.build();
let result = coordinator.load().unwrap();
assert_eq!(result.content().get("first").unwrap().as_integer(), Some(1));
assert_eq!(
result.content().get("second").unwrap().as_integer(),
Some(2)
);
}
#[test]
fn test_source_coordinator_to_type() {
use serde::Deserialize;
#[derive(Debug, Deserialize, PartialEq)]
struct Config {
key: String,
}
let source = TestSource::new("test", r#"{"key": "value"}"#, Format::Json);
let mut coordinator = SourceCoordinator::builder().add_source(source, 10).build();
let config: Config = coordinator.load_as().unwrap();
assert_eq!(config.key, "value");
}
#[test]
fn test_source_coordinator_empty() {
let mut coordinator = SourceCoordinator::new();
let result = coordinator.load().unwrap();
assert!(result.content().is_null());
}
#[test]
fn test_source_coordinator_clear() {
let source = TestSource::new("test", r#"{"key": "value"}"#, Format::Json);
let mut coordinator = SourceCoordinator::builder().add_source(source, 10).build();
assert_eq!(coordinator.source_count(), 1);
coordinator.clear();
assert!(coordinator.is_empty());
}
#[test]
fn test_load_result_to_type() {
use serde::Deserialize;
#[derive(Debug, Deserialize, PartialEq)]
struct Config {
key: String,
}
let result = LoadResult {
content: ParsedContent::Object(
std::iter::once((
"key".to_string(),
ParsedContent::String("value".to_string()),
))
.collect(),
),
loaded_count: 1,
loaded_sources: vec!["test".to_string()],
failed_sources: Vec::new(),
processing_time_ms: 0,
};
let config: Config = result.to_type().unwrap();
assert_eq!(config.key, "value");
}
}