Skip to main content

cfgmatic_source/application/
loader.rs

1//! Loader service for loading configuration from sources.
2//!
3//! This module provides the [`Loader`] service which is responsible for
4//! loading configuration from various sources and parsing it.
5//!
6//! # Example
7//!
8//! ```rust,ignore
9//! use cfgmatic_source::application::Loader;
10//! use cfgmatic_source::infrastructure::FileSource;
11//!
12//! let loader = Loader::builder()
13//!     .fail_fast(false)
14//!     .build();
15//!
16//! let content = loader.load_from_source(&source)?;
17//! let config: MyConfig = loader.parse_content(content)?;
18//! ```
19
20use serde::de::DeserializeOwned;
21
22use crate::config::{LoadOptions, MergeStrategy};
23use crate::domain::{Format, ParsedContent, RawContent, Result, Source, SourceError};
24
25/// Service for loading configuration from sources.
26///
27/// The Loader is responsible for:
28/// - Loading raw content from sources
29/// - Detecting and parsing formats
30/// - Converting to typed configuration
31#[derive(Debug, Clone)]
32pub struct Loader {
33    /// Loading options.
34    options: LoadOptions,
35
36    /// Default format to use when detection fails.
37    default_format: Option<Format>,
38}
39
40impl Loader {
41    /// Create a new Loader with default options.
42    #[must_use]
43    pub fn new() -> Self {
44        Self::default()
45    }
46
47    /// Create a builder for constructing a Loader.
48    #[must_use]
49    pub fn builder() -> LoaderBuilder {
50        LoaderBuilder::new()
51    }
52
53    /// Get the load options.
54    #[must_use]
55    pub fn options(&self) -> &LoadOptions {
56        &self.options
57    }
58
59    /// Load raw content from a source.
60    ///
61    /// # Errors
62    ///
63    /// Returns an error if the source cannot be read.
64    pub fn load_raw<S: Source>(&self, source: &S) -> Result<RawContent> {
65        source.validate()?;
66        source.load_raw()
67    }
68
69    /// Load and parse content from a source.
70    ///
71    /// # Errors
72    ///
73    /// Returns an error if loading or parsing fails.
74    pub fn load<S: Source>(&self, source: &S) -> Result<ParsedContent> {
75        let raw = self.load_raw(source)?;
76        self.parse_raw(raw, source.detect_format())
77    }
78
79    /// Load and parse content from a source into a specific type.
80    ///
81    /// # Errors
82    ///
83    /// Returns an error if loading, parsing, or deserialization fails.
84    pub fn load_as<S: Source, T: DeserializeOwned>(&self, source: &S) -> Result<T> {
85        let raw = self.load_raw(source)?;
86        // Treat Unknown as None to allow fallback to default_format
87        let format = source
88            .detect_format()
89            .and_then(|f| if f == Format::Unknown { None } else { Some(f) })
90            .or(self.default_format);
91
92        match format {
93            Some(fmt) => fmt.parse_as(raw.as_str()?.as_ref()),
94            None => Err(SourceError::unsupported("cannot detect format")),
95        }
96    }
97
98    /// Parse raw content using a specific format.
99    ///
100    /// # Errors
101    ///
102    /// Returns an error if parsing fails.
103    pub fn parse(&self, raw: &RawContent, format: Format) -> Result<ParsedContent> {
104        let content = raw.as_str()?;
105        format.parse(content.as_ref())
106    }
107
108    /// Parse raw content, detecting the format.
109    ///
110    /// # Errors
111    ///
112    /// Returns an error if parsing fails or format cannot be detected.
113    pub fn parse_raw(
114        &self,
115        raw: RawContent,
116        detected_format: Option<Format>,
117    ) -> Result<ParsedContent> {
118        // Treat Unknown as None to allow fallback to default_format
119        let format = detected_format
120            .and_then(|f| if f == Format::Unknown { None } else { Some(f) })
121            .or(self.default_format);
122
123        match format {
124            Some(fmt) => self.parse(&raw, fmt),
125            None => {
126                // Try to detect from content
127                let content = raw.as_str()?;
128                if let Some(fmt) = Format::from_content(content.as_ref()) {
129                    return fmt.parse(content.as_ref());
130                }
131                Err(SourceError::unsupported("cannot detect format"))
132            }
133        }
134    }
135
136    /// Merge multiple parsed contents.
137    ///
138    /// Uses the merge strategy from options.
139    #[must_use]
140    pub fn merge(&self, contents: Vec<ParsedContent>) -> ParsedContent {
141        if contents.is_empty() {
142            return ParsedContent::Null;
143        }
144
145        let strategy = self.options.merge_strategy;
146
147        contents
148            .into_iter()
149            .reduce(|acc, content| match strategy {
150                MergeStrategy::Replace => content,
151                MergeStrategy::Deep | MergeStrategy::Shallow | MergeStrategy::Strict => {
152                    acc.merge(&content)
153                }
154            })
155            .unwrap_or(ParsedContent::Null)
156    }
157
158    /// Convert parsed content to a specific type.
159    ///
160    /// # Errors
161    ///
162    /// Returns an error if deserialization fails.
163    pub fn to_type<T: DeserializeOwned>(&self, content: ParsedContent) -> Result<T> {
164        content.to_type()
165    }
166
167    /// Load from multiple sources and merge.
168    ///
169    /// Sources are loaded in order and merged according to the merge strategy.
170    /// Optional sources that fail are skipped if `ignore_optional_missing` is true.
171    ///
172    /// # Errors
173    ///
174    /// Returns an error if a required source fails and `fail_fast` is true.
175    pub fn load_multiple<S: Source>(&self, sources: &[S]) -> Result<ParsedContent> {
176        let mut contents = Vec::new();
177        let mut errors: Vec<(String, SourceError)> = Vec::new();
178
179        for source in sources {
180            match self.load(source) {
181                Ok(content) => contents.push(content),
182                Err(e) => {
183                    if source.is_optional() && self.options.ignore_optional_missing {
184                        continue;
185                    }
186                    if self.options.fail_fast {
187                        return Err(e);
188                    }
189                    errors.push((source.display_name(), e));
190                }
191            }
192        }
193
194        if !errors.is_empty() && contents.is_empty() {
195            let error_messages: Vec<String> = errors
196                .into_iter()
197                .map(|(name, e)| format!("{}: {}", name, e))
198                .collect();
199            return Err(SourceError::custom(&error_messages.join(", ")));
200        }
201
202        Ok(self.merge(contents))
203    }
204}
205
206impl Default for Loader {
207    fn default() -> Self {
208        Self {
209            options: LoadOptions::default(),
210            default_format: None,
211        }
212    }
213}
214
215/// Builder for [`Loader`].
216#[derive(Debug, Clone, Default)]
217pub struct LoaderBuilder {
218    options: Option<LoadOptions>,
219    default_format: Option<Format>,
220}
221
222impl LoaderBuilder {
223    /// Create a new builder.
224    #[must_use]
225    pub fn new() -> Self {
226        Self::default()
227    }
228
229    /// Set the load options.
230    #[must_use]
231    pub fn options(mut self, options: LoadOptions) -> Self {
232        self.options = Some(options);
233        self
234    }
235
236    /// Set the merge strategy.
237    #[must_use]
238    pub fn merge_strategy(mut self, strategy: MergeStrategy) -> Self {
239        let mut options = self.options.unwrap_or_default();
240        options.merge_strategy = strategy;
241        self.options = Some(options);
242        self
243    }
244
245    /// Set whether to fail fast on errors.
246    #[must_use]
247    pub fn fail_fast(mut self, fail_fast: bool) -> Self {
248        let mut options = self.options.unwrap_or_default();
249        options.fail_fast = fail_fast;
250        self.options = Some(options);
251        self
252    }
253
254    /// Set the default format when detection fails.
255    #[must_use]
256    pub fn default_format(mut self, format: Format) -> Self {
257        self.default_format = Some(format);
258        self
259    }
260
261    /// Build the Loader.
262    #[must_use]
263    pub fn build(self) -> Loader {
264        Loader {
265            options: self.options.unwrap_or_default(),
266            default_format: self.default_format,
267        }
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274    use crate::domain::{SourceKind, SourceMetadata};
275    use std::collections::BTreeMap;
276
277    /// Test source implementation.
278    struct TestSource {
279        content: String,
280        format: Format,
281        optional: bool,
282    }
283
284    impl TestSource {
285        fn new(content: &str, format: Format) -> Self {
286            Self {
287                content: content.to_string(),
288                format,
289                optional: false,
290            }
291        }
292
293        fn with_optional(mut self, optional: bool) -> Self {
294            self.optional = optional;
295            self
296        }
297    }
298
299    impl Source for TestSource {
300        fn kind(&self) -> SourceKind {
301            SourceKind::Memory
302        }
303
304        fn metadata(&self) -> SourceMetadata {
305            SourceMetadata::new("test")
306        }
307
308        fn load_raw(&self) -> Result<RawContent> {
309            Ok(RawContent::from_string(&self.content))
310        }
311
312        fn detect_format(&self) -> Option<Format> {
313            Some(self.format)
314        }
315
316        fn is_optional(&self) -> bool {
317            self.optional
318        }
319    }
320
321    #[test]
322    fn test_loader_new() {
323        let loader = Loader::new();
324        assert_eq!(loader.options().merge_strategy, MergeStrategy::Deep);
325    }
326
327    #[test]
328    fn test_loader_builder() {
329        let loader = Loader::builder()
330            .merge_strategy(MergeStrategy::Replace)
331            .fail_fast(false)
332            .build();
333
334        assert_eq!(loader.options().merge_strategy, MergeStrategy::Replace);
335        assert!(!loader.options().fail_fast);
336    }
337
338    #[test]
339    fn test_loader_load_raw() {
340        let source = TestSource::new(r#"key = "value""#, Format::Toml);
341        let loader = Loader::new();
342
343        let raw = loader.load_raw(&source).unwrap();
344        assert!(!raw.is_empty());
345    }
346
347    #[test]
348    fn test_loader_load() {
349        let source = TestSource::new(r#"key = "value""#, Format::Toml);
350        let loader = Loader::new();
351
352        let content = loader.load(&source).unwrap();
353        assert!(content.is_object());
354    }
355
356    #[test]
357    fn test_loader_parse() {
358        let raw = RawContent::from_string(r#"{"key": "value"}"#);
359        let loader = Loader::new();
360
361        let content = loader.parse(&raw, Format::Json).unwrap();
362        assert!(content.is_object());
363    }
364
365    #[test]
366    fn test_loader_merge_empty() {
367        let loader = Loader::new();
368        let result = loader.merge(vec![]);
369        assert!(result.is_null());
370    }
371
372    #[test]
373    fn test_loader_merge_single() {
374        let loader = Loader::new();
375        let mut obj = BTreeMap::new();
376        obj.insert(
377            "key".to_string(),
378            ParsedContent::String("value".to_string()),
379        );
380        let content = ParsedContent::Object(obj);
381
382        let result = loader.merge(vec![content.clone()]);
383        assert_eq!(result, content);
384    }
385
386    #[test]
387    fn test_loader_merge_multiple() {
388        let loader = Loader::builder()
389            .merge_strategy(MergeStrategy::Deep)
390            .build();
391
392        let mut obj1 = BTreeMap::new();
393        obj1.insert("a".to_string(), ParsedContent::Integer(1));
394
395        let mut obj2 = BTreeMap::new();
396        obj2.insert("b".to_string(), ParsedContent::Integer(2));
397
398        let result = loader.merge(vec![
399            ParsedContent::Object(obj1),
400            ParsedContent::Object(obj2),
401        ]);
402
403        assert!(result.get("a").is_some());
404        assert!(result.get("b").is_some());
405    }
406
407    #[test]
408    fn test_loader_to_type() {
409        use serde::Deserialize;
410
411        #[derive(Debug, Deserialize, PartialEq)]
412        struct Config {
413            name: String,
414        }
415
416        let loader = Loader::new();
417
418        let mut obj = BTreeMap::new();
419        obj.insert(
420            "name".to_string(),
421            ParsedContent::String("test".to_string()),
422        );
423        let content = ParsedContent::Object(obj);
424
425        let config: Config = loader.to_type(content).unwrap();
426        assert_eq!(config.name, "test");
427    }
428
429    #[test]
430    fn test_loader_load_multiple() {
431        let source1 = TestSource::new(r#"{"a": 1}"#, Format::Json);
432        let source2 = TestSource::new(r#"{"b": 2}"#, Format::Json);
433
434        let loader = Loader::builder()
435            .merge_strategy(MergeStrategy::Deep)
436            .build();
437
438        let result = loader.load_multiple(&[source1, source2]).unwrap();
439        assert!(result.get("a").is_some());
440        assert!(result.get("b").is_some());
441    }
442
443    #[test]
444    fn test_loader_load_multiple_with_optional() {
445        let source1 = TestSource::new(r#"{"a": 1}"#, Format::Json);
446        let source2 = TestSource::new(r"invalid", Format::Toml).with_optional(true);
447
448        let loader = Loader::builder().fail_fast(false).build();
449
450        // Should succeed because optional source failure is ignored
451        let result = loader.load_multiple(&[source1, source2]).unwrap();
452        assert!(result.get("a").is_some());
453    }
454
455    #[test]
456    fn test_loader_default_format() {
457        let source = TestSource::new(r#"{"key": "value"}"#, Format::Unknown);
458
459        let loader = Loader::builder().default_format(Format::Json).build();
460
461        let content = loader.load(&source).unwrap();
462        assert!(content.is_object());
463    }
464}