use cfgmatic_merge::Merge;
use serde_json::Value;
use crate::domain::{Format, RawContent, Result, Source, SourceError, SourceKind, SourceMetadata};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct SourcePriority(pub u32);
impl SourcePriority {
pub const LOWEST: Self = Self(0);
pub const DEFAULT: Self = Self(100);
pub const HIGH: Self = Self(200);
pub const HIGHEST: Self = Self(255);
#[must_use]
pub fn new(level: u32) -> Self {
Self(level)
}
#[must_use]
pub fn level(&self) -> u32 {
self.0
}
}
impl Default for SourcePriority {
fn default() -> Self {
Self::DEFAULT
}
}
struct SourceEntry {
source: Box<dyn Source>,
priority: SourcePriority,
required: bool,
}
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("required", &self.required)
.field("metadata", &self.source.metadata())
.finish()
}
}
#[derive(Default)]
pub struct CompositeSourceBuilder {
sources: Vec<SourceEntry>,
}
impl CompositeSourceBuilder {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn add<S: Source + 'static>(mut self, source: S) -> Self {
self.sources.push(SourceEntry {
source: Box::new(source),
priority: SourcePriority::DEFAULT,
required: false,
});
self
}
#[must_use]
pub fn add_with_priority<S: Source + 'static>(
mut self,
source: S,
priority: SourcePriority,
) -> Self {
self.sources.push(SourceEntry {
source: Box::new(source),
priority,
required: false,
});
self
}
#[must_use]
pub fn add_required<S: Source + 'static>(mut self, source: S) -> Self {
self.sources.push(SourceEntry {
source: Box::new(source),
priority: SourcePriority::DEFAULT,
required: true,
});
self
}
#[must_use]
pub fn build(self) -> CompositeSource {
let mut sources = self.sources;
sources.sort_by_key(|e| e.priority);
CompositeSource { sources }
}
}
#[derive(Debug)]
pub struct CompositeSource {
sources: Vec<SourceEntry>,
}
impl CompositeSource {
#[must_use]
pub fn new() -> Self {
Self {
sources: Vec::new(),
}
}
#[must_use]
pub fn builder() -> CompositeSourceBuilder {
CompositeSourceBuilder::new()
}
#[must_use]
pub fn len(&self) -> usize {
self.sources.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.sources.is_empty()
}
fn load_merged(&self) -> Result<Value> {
let mut merged = Value::Object(serde_json::Map::new());
let mut any_loaded = false;
for entry in &self.sources {
match entry.source.load_raw() {
Ok(raw) => {
let content = raw
.as_str()
.map_err(|e| SourceError::serialization(&e.to_string()))?;
let value: Value = serde_json::from_str(content.as_ref()).map_err(|e| {
SourceError::parse_failed("composite", "json", &e.to_string())
})?;
merged = merged
.merge_deep(value)
.map_err(|e| SourceError::serialization(&e.to_string()))?;
any_loaded = true;
}
Err(e) if entry.required => {
return Err(e);
}
Err(_) => {
continue;
}
}
}
if !any_loaded && self.sources.iter().any(|e| e.required) {
return Err(SourceError::not_found(
"No required sources could be loaded",
));
}
Ok(merged)
}
}
impl Default for CompositeSource {
fn default() -> Self {
Self::new()
}
}
impl Source for CompositeSource {
fn kind(&self) -> SourceKind {
SourceKind::Custom
}
fn metadata(&self) -> SourceMetadata {
SourceMetadata::new("composite").with_priority(0)
}
fn load_raw(&self) -> Result<RawContent> {
let merged = self.load_merged()?;
let content = serde_json::to_string(&merged)
.map_err(|e| SourceError::serialization(&e.to_string()))?;
Ok(RawContent::from_string(content))
}
fn detect_format(&self) -> Option<Format> {
Some(Format::Json)
}
fn is_required(&self) -> bool {
self.sources.iter().any(|e| e.required)
}
}
#[cfg(feature = "async")]
mod async_impl {
use super::*;
use async_trait::async_trait;
#[async_trait]
impl Source for CompositeSource {
async fn load_raw_async(&self) -> Result<RawContent> {
self.load_raw()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::infrastructure::MemorySource;
#[test]
fn test_source_priority_ordering() {
assert!(SourcePriority::HIGHEST > SourcePriority::HIGH);
assert!(SourcePriority::HIGH > SourcePriority::DEFAULT);
assert!(SourcePriority::DEFAULT > SourcePriority::LOWEST);
}
#[test]
fn test_source_priority_level() {
let priority = SourcePriority::new(42);
assert_eq!(priority.level(), 42);
}
#[test]
fn test_composite_source_empty() {
let composite = CompositeSource::new();
assert!(composite.is_empty());
assert_eq!(composite.len(), 0);
}
#[test]
fn test_composite_source_builder() {
let source1 = MemorySource::builder().set("a", "1").build();
let source2 = MemorySource::builder().set("b", "2").build();
let composite = CompositeSource::builder().add(source1).add(source2).build();
assert_eq!(composite.len(), 2);
}
#[test]
fn test_composite_source_merge() {
let defaults = MemorySource::builder()
.set("host", "localhost")
.set("port", "8080")
.build();
let overrides = MemorySource::builder().set("port", "3000").build();
let composite = CompositeSource::builder()
.add_with_priority(defaults, SourcePriority::LOWEST)
.add_with_priority(overrides, SourcePriority::HIGH)
.build();
let raw = composite.load_raw().unwrap();
let content = raw.as_str().unwrap();
#[derive(Debug, serde::Deserialize, PartialEq)]
struct TestConfig {
host: String,
port: String,
}
let config: TestConfig = serde_json::from_str(content.as_ref()).unwrap();
assert_eq!(config.host, "localhost"); assert_eq!(config.port, "3000"); }
#[test]
fn test_composite_source_nested_merge() {
let base = MemorySource::builder()
.insert(
"database",
serde_json::json!({
"host": "localhost",
"port": 5432,
"name": "mydb"
}),
)
.build();
let override_config = MemorySource::builder()
.insert(
"database",
serde_json::json!({
"port": 5433,
"user": "admin"
}),
)
.build();
let composite = CompositeSource::builder()
.add_with_priority(base, SourcePriority::LOWEST)
.add_with_priority(override_config, SourcePriority::HIGH)
.build();
let raw = composite.load_raw().unwrap();
let content = raw.as_str().unwrap();
#[derive(Debug, serde::Deserialize, PartialEq)]
struct Database {
host: String,
port: u16,
name: String,
user: String,
}
#[derive(Debug, serde::Deserialize, PartialEq)]
struct TestConfig {
database: Database,
}
let config: TestConfig = serde_json::from_str(content.as_ref()).unwrap();
assert_eq!(config.database.host, "localhost"); assert_eq!(config.database.port, 5433); assert_eq!(config.database.name, "mydb"); assert_eq!(config.database.user, "admin"); }
#[test]
fn test_composite_source_required_failure() {
let failing_source = MemorySource::builder().required(true).build();
let composite = CompositeSource::builder()
.add_required(failing_source)
.build();
let result = composite.load_raw();
assert!(result.is_err());
}
#[test]
fn test_composite_source_optional_failure() {
let empty_source = MemorySource::builder().required(false).build();
let working_source = MemorySource::builder().set("name", "test").build();
let composite = CompositeSource::builder()
.add(empty_source)
.add(working_source)
.build();
let raw = composite.load_raw().unwrap();
let content = raw.as_str().unwrap();
#[derive(Debug, serde::Deserialize, PartialEq)]
struct TestConfig {
name: String,
}
let config: TestConfig = serde_json::from_str(content.as_ref()).unwrap();
assert_eq!(config.name, "test");
}
#[test]
fn test_composite_source_metadata() {
let composite = CompositeSource::new();
let meta = composite.metadata();
assert_eq!(meta.name, "composite");
assert_eq!(composite.kind(), SourceKind::Custom);
assert_eq!(meta.priority, 0);
}
#[test]
fn test_composite_source_is_required() {
let composite_required = CompositeSource::builder()
.add_required(MemorySource::new())
.build();
assert!(composite_required.is_required());
let composite_optional = CompositeSource::builder().add(MemorySource::new()).build();
assert!(!composite_optional.is_required());
}
}