use serde::de::DeserializeOwned;
use crate::config::{LoadOptions, MergeStrategy};
use crate::domain::{Format, ParsedContent, RawContent, Result, Source, SourceError};
#[derive(Debug, Clone)]
pub struct Loader {
options: LoadOptions,
default_format: Option<Format>,
}
impl Loader {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn builder() -> LoaderBuilder {
LoaderBuilder::new()
}
#[must_use]
pub fn options(&self) -> &LoadOptions {
&self.options
}
pub fn load_raw<S: Source>(&self, source: &S) -> Result<RawContent> {
source.validate()?;
source.load_raw()
}
pub fn load<S: Source>(&self, source: &S) -> Result<ParsedContent> {
let raw = self.load_raw(source)?;
self.parse_raw(raw, source.detect_format())
}
pub fn load_as<S: Source, T: DeserializeOwned>(&self, source: &S) -> Result<T> {
let raw = self.load_raw(source)?;
let format = source.detect_format().or(self.default_format);
match format {
Some(fmt) => fmt.parse_as(raw.as_str()?.as_ref()),
None => Err(SourceError::unsupported("cannot detect format")),
}
}
pub fn parse(&self, raw: &RawContent, format: Format) -> Result<ParsedContent> {
let content = raw.as_str()?;
format.parse(content.as_ref())
}
pub fn parse_raw(
&self,
raw: RawContent,
detected_format: Option<Format>,
) -> Result<ParsedContent> {
let format = detected_format.or(self.default_format);
match format {
Some(fmt) => self.parse(&raw, fmt),
None => {
let content = raw.as_str()?;
if let Some(fmt) = Format::from_content(content.as_ref()) {
return fmt.parse(content.as_ref());
}
Err(SourceError::unsupported("cannot detect format"))
}
}
}
#[must_use]
pub fn merge(&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 to_type<T: DeserializeOwned>(&self, content: ParsedContent) -> Result<T> {
content.to_type()
}
pub fn load_multiple<S: Source>(&self, sources: &[S]) -> Result<ParsedContent> {
let mut contents = Vec::new();
let mut errors: Vec<(String, SourceError)> = Vec::new();
for source in sources {
match self.load(source) {
Ok(content) => contents.push(content),
Err(e) => {
if source.is_optional() && self.options.ignore_optional_missing {
continue;
}
if self.options.fail_fast {
return Err(e);
}
errors.push((source.display_name(), e));
}
}
}
if !errors.is_empty() && contents.is_empty() {
let error_messages: Vec<String> = errors
.into_iter()
.map(|(name, e)| format!("{}: {}", name, e))
.collect();
return Err(SourceError::custom(&error_messages.join(", ")));
}
Ok(self.merge(contents))
}
}
impl Default for Loader {
fn default() -> Self {
Self {
options: LoadOptions::default(),
default_format: None,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct LoaderBuilder {
options: Option<LoadOptions>,
default_format: Option<Format>,
}
impl LoaderBuilder {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[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 default_format(mut self, format: Format) -> Self {
self.default_format = Some(format);
self
}
#[must_use]
pub fn build(self) -> Loader {
Loader {
options: self.options.unwrap_or_default(),
default_format: self.default_format,
}
}
}
#[cfg(feature = "async")]
mod async_impl {
use super::*;
impl Loader {
pub async fn load_async<S: crate::domain::AsyncSource>(
&self,
source: &S,
) -> Result<ParsedContent> {
source.validate()?;
source.load_async().await
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::{SourceKind, SourceMetadata};
use std::collections::BTreeMap;
struct TestSource {
content: String,
format: Format,
optional: bool,
}
impl TestSource {
fn new(content: &str, format: Format) -> Self {
Self {
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("test")
}
fn load_raw(&self) -> Result<RawContent> {
Ok(RawContent::from_string(&self.content))
}
fn detect_format(&self) -> Option<Format> {
Some(self.format)
}
fn is_optional(&self) -> bool {
self.optional
}
}
#[test]
fn test_loader_new() {
let loader = Loader::new();
assert_eq!(loader.options().merge_strategy, MergeStrategy::Deep);
}
#[test]
fn test_loader_builder() {
let loader = Loader::builder()
.merge_strategy(MergeStrategy::Replace)
.fail_fast(false)
.build();
assert_eq!(loader.options().merge_strategy, MergeStrategy::Replace);
assert!(!loader.options().fail_fast);
}
#[test]
fn test_loader_load_raw() {
let source = TestSource::new(r#"key = "value""#, Format::Toml);
let loader = Loader::new();
let raw = loader.load_raw(&source).unwrap();
assert!(!raw.is_empty());
}
#[test]
fn test_loader_load() {
let source = TestSource::new(r#"key = "value""#, Format::Toml);
let loader = Loader::new();
let content = loader.load(&source).unwrap();
assert!(content.is_object());
}
#[test]
fn test_loader_parse() {
let raw = RawContent::from_string(r#"{"key": "value"}"#);
let loader = Loader::new();
let content = loader.parse(&raw, Format::Json).unwrap();
assert!(content.is_object());
}
#[test]
fn test_loader_merge_empty() {
let loader = Loader::new();
let result = loader.merge(vec![]);
assert!(result.is_null());
}
#[test]
fn test_loader_merge_single() {
let loader = Loader::new();
let mut obj = BTreeMap::new();
obj.insert(
"key".to_string(),
ParsedContent::String("value".to_string()),
);
let content = ParsedContent::Object(obj);
let result = loader.merge(vec![content.clone()]);
assert_eq!(result, content);
}
#[test]
fn test_loader_merge_multiple() {
let loader = Loader::builder()
.merge_strategy(MergeStrategy::Deep)
.build();
let mut obj1 = BTreeMap::new();
obj1.insert("a".to_string(), ParsedContent::Integer(1));
let mut obj2 = BTreeMap::new();
obj2.insert("b".to_string(), ParsedContent::Integer(2));
let result = loader.merge(vec![
ParsedContent::Object(obj1),
ParsedContent::Object(obj2),
]);
assert!(result.get("a").is_some());
assert!(result.get("b").is_some());
}
#[test]
fn test_loader_to_type() {
use serde::Deserialize;
#[derive(Debug, Deserialize, PartialEq)]
struct Config {
name: String,
}
let loader = Loader::new();
let mut obj = BTreeMap::new();
obj.insert(
"name".to_string(),
ParsedContent::String("test".to_string()),
);
let content = ParsedContent::Object(obj);
let config: Config = loader.to_type(content).unwrap();
assert_eq!(config.name, "test");
}
#[test]
fn test_loader_load_multiple() {
let source1 = TestSource::new(r#"{"a": 1}"#, Format::Json);
let source2 = TestSource::new(r#"{"b": 2}"#, Format::Json);
let loader = Loader::builder()
.merge_strategy(MergeStrategy::Deep)
.build();
let result = loader.load_multiple(&[source1, source2]).unwrap();
assert!(result.get("a").is_some());
assert!(result.get("b").is_some());
}
#[test]
fn test_loader_load_multiple_with_optional() {
let source1 = TestSource::new(r#"{"a": 1}"#, Format::Json);
let source2 = TestSource::new(r#"invalid"#, Format::Toml).with_optional(true);
let loader = Loader::builder().fail_fast(false).build();
let result = loader.load_multiple(&[source1, source2]).unwrap();
assert!(result.get("a").is_some());
}
#[test]
fn test_loader_default_format() {
let source = TestSource::new(r#"{"key": "value"}"#, Format::Unknown);
let loader = Loader::builder().default_format(Format::Json).build();
let content = loader.load(&source).unwrap();
assert!(content.is_object());
}
}