use std::collections::BTreeMap;
use serde::de::DeserializeOwned;
use crate::config::{LoadOptions, MergeStrategy};
use crate::domain::{ParsedContent, Result, Source, SourceError};
#[derive(Debug, Clone)]
pub struct LoadResult {
pub content: ParsedContent,
pub loaded_count: usize,
pub loaded_sources: Vec<String>,
pub failed_sources: Vec<(String, String)>,
pub processing_time_ms: u64,
}
impl LoadResult {
#[must_use]
pub fn new(content: ParsedContent) -> Self {
Self {
content,
loaded_count: 0,
loaded_sources: Vec::new(),
failed_sources: Vec::new(),
processing_time_ms: 0,
}
}
#[must_use]
pub fn content(&self) -> &ParsedContent {
&self.content
}
#[must_use]
pub fn loaded_count(&self) -> usize {
self.loaded_count
}
#[must_use]
pub fn has_failures(&self) -> bool {
!self.failed_sources.is_empty()
}
pub fn to_type<T: DeserializeOwned>(&self) -> Result<T> {
self.content.to_type()
}
}
type BoxedSource = Box<dyn Source>;
struct SourceEntry {
source: BoxedSource,
priority: i32,
order: usize,
}
impl std::fmt::Debug for SourceEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SourceEntry")
.field("priority", &self.priority)
.field("order", &self.order)
.field("display_name", &self.source.display_name())
.finish()
}
}
pub struct SourceCoordinator {
sources: Vec<SourceEntry>,
options: LoadOptions,
cache: BTreeMap<String, ParsedContent>,
cache_enabled: bool,
}
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 fn source_count(&self) -> usize {
self.sources.len()
}
#[must_use]
pub 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 load(&mut self) -> Result<LoadResult> {
let start = std::time::Instant::now();
let mut sorted_sources: Vec<_> = self.sources.iter().collect();
sorted_sources.sort_by(|a, b| {
b.priority
.cmp(&a.priority)
.then_with(|| a.order.cmp(&b.order))
});
let mut contents = Vec::new();
let mut loaded_sources = Vec::new();
let mut failed_sources = Vec::new();
let mut loaded_count = 0;
for entry in sorted_sources {
let name = entry.source.display_name();
if self.cache_enabled {
if let Some(cached) = self.cache.get(&name) {
contents.push(cached.clone());
loaded_sources.push(name.clone());
loaded_count += 1;
continue;
}
}
match self.load_source(&*entry.source) {
Ok(content) => {
if self.cache_enabled {
self.cache.insert(name.clone(), content.clone());
}
contents.push(content);
loaded_sources.push(name.clone());
loaded_count += 1;
}
Err(e) => {
if entry.source.is_optional() && self.options.ignore_optional_missing {
continue;
}
if self.options.fail_fast {
return Err(e);
}
failed_sources.push((name, e.to_string()));
}
}
}
if contents.is_empty() && !failed_sources.is_empty() {
let error_messages: Vec<String> = failed_sources
.iter()
.map(|(name, e)| format!("{}: {}", name, e))
.collect();
return Err(SourceError::custom(&error_messages.join(", ")));
}
let merged = self.merge_contents(contents);
Ok(LoadResult {
content: merged,
loaded_count,
loaded_sources,
failed_sources,
processing_time_ms: start.elapsed().as_millis() as u64,
})
}
fn load_source(&self, source: &dyn Source) -> Result<ParsedContent> {
source.validate()?;
let raw = source.load_raw()?;
let format = source.detect_format();
if let Some(fmt) = format {
let content = raw.as_str()?;
return fmt.parse(content.as_ref());
}
let content = raw.as_str()?;
if let Some(fmt) = crate::domain::Format::from_content(content.as_ref()) {
return fmt.parse(content.as_ref());
}
Err(SourceError::unsupported("cannot detect format"))
}
fn merge_contents(&self, contents: Vec<ParsedContent>) -> ParsedContent {
if contents.is_empty() {
return ParsedContent::Null;
}
let strategy = self.options.merge_strategy;
contents
.into_iter()
.reduce(|acc, content| match strategy {
MergeStrategy::Replace => content,
MergeStrategy::Deep | MergeStrategy::Shallow | MergeStrategy::Strict => {
acc.merge(&content)
}
})
.unwrap_or(ParsedContent::Null)
}
pub fn load_as<T: DeserializeOwned>(&mut self) -> Result<T> {
let result = self.load()?;
result.to_type()
}
pub fn reload(&mut self) -> Result<LoadResult> {
self.clear_cache();
self.load()
}
}
impl Default for SourceCoordinator {
fn default() -> Self {
Self {
sources: Vec::new(),
options: LoadOptions::default(),
cache: BTreeMap::new(),
cache_enabled: true,
}
}
}
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_enabled", &self.cache_enabled)
.finish()
}
}
#[derive(Default)]
pub struct SourceCoordinatorBuilder {
sources: Vec<SourceEntry>,
options: Option<LoadOptions>,
cache_enabled: Option<bool>,
}
impl SourceCoordinatorBuilder {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn add_source<S: Source + 'static>(mut self, source: S, priority: i32) -> Self {
let entry = SourceEntry {
source: Box::new(source),
priority,
order: self.sources.len(),
};
self.sources.push(entry);
self
}
#[must_use]
pub fn options(mut self, options: LoadOptions) -> Self {
self.options = Some(options);
self
}
#[must_use]
pub fn merge_strategy(mut self, strategy: MergeStrategy) -> Self {
let mut options = self.options.unwrap_or_default();
options.merge_strategy = strategy;
self.options = Some(options);
self
}
#[must_use]
pub fn fail_fast(mut self, fail_fast: bool) -> Self {
let mut options = self.options.unwrap_or_default();
options.fail_fast = fail_fast;
self.options = Some(options);
self
}
#[must_use]
pub fn cache_enabled(mut self, enabled: bool) -> Self {
self.cache_enabled = Some(enabled);
self
}
#[must_use]
pub fn build(self) -> SourceCoordinator {
SourceCoordinator {
sources: self.sources,
options: self.options.unwrap_or_default(),
cache: BTreeMap::new(),
cache_enabled: self.cache_enabled.unwrap_or(true),
}
}
}
impl std::fmt::Debug for SourceCoordinatorBuilder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SourceCoordinatorBuilder")
.field("source_count", &self.sources.len())
.field("options", &self.options)
.field("cache_enabled", &self.cache_enabled)
.finish()
}
}
#[cfg(feature = "async")]
mod async_impl {
use super::*;
impl SourceCoordinator {
pub async fn load_async(&mut self) -> Result<LoadResult> {
self.load()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::{Format, RawContent, SourceKind, SourceMetadata};
struct TestSource {
content: String,
format: Format,
optional: bool,
name: String,
priority_val: i32,
}
impl TestSource {
fn new(name: &str, content: &str, format: Format) -> Self {
Self {
name: name.to_string(),
content: content.to_string(),
format,
optional: false,
priority_val: 0,
}
}
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.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_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_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);
}
#[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 mut obj = std::collections::BTreeMap::new();
obj.insert(
"key".to_string(),
ParsedContent::String("value".to_string()),
);
let content = ParsedContent::Object(obj);
let result = LoadResult::new(content);
let config: Config = result.to_type().unwrap();
assert_eq!(config.key, "value");
}
}