cfgmatic_source/application/
loader.rs1use serde::de::DeserializeOwned;
21
22use crate::config::{LoadOptions, MergeStrategy};
23use crate::domain::{Format, ParsedContent, RawContent, Result, Source, SourceError};
24
25#[derive(Debug, Clone)]
32pub struct Loader {
33 options: LoadOptions,
35
36 default_format: Option<Format>,
38}
39
40impl Loader {
41 #[must_use]
43 pub fn new() -> Self {
44 Self::default()
45 }
46
47 #[must_use]
49 pub fn builder() -> LoaderBuilder {
50 LoaderBuilder::new()
51 }
52
53 #[must_use]
55 pub fn options(&self) -> &LoadOptions {
56 &self.options
57 }
58
59 pub fn load_raw<S: Source>(&self, source: &S) -> Result<RawContent> {
65 source.validate()?;
66 source.load_raw()
67 }
68
69 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 pub fn load_as<S: Source, T: DeserializeOwned>(&self, source: &S) -> Result<T> {
85 let raw = self.load_raw(source)?;
86 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 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 pub fn parse_raw(
114 &self,
115 raw: RawContent,
116 detected_format: Option<Format>,
117 ) -> Result<ParsedContent> {
118 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 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 #[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 pub fn to_type<T: DeserializeOwned>(&self, content: ParsedContent) -> Result<T> {
164 content.to_type()
165 }
166
167 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#[derive(Debug, Clone, Default)]
217pub struct LoaderBuilder {
218 options: Option<LoadOptions>,
219 default_format: Option<Format>,
220}
221
222impl LoaderBuilder {
223 #[must_use]
225 pub fn new() -> Self {
226 Self::default()
227 }
228
229 #[must_use]
231 pub fn options(mut self, options: LoadOptions) -> Self {
232 self.options = Some(options);
233 self
234 }
235
236 #[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 #[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 #[must_use]
256 pub fn default_format(mut self, format: Format) -> Self {
257 self.default_format = Some(format);
258 self
259 }
260
261 #[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 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 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}