Skip to main content

fmtstruct/loader/
dyn_loader.rs

1/* src/loader/dyn_loader.rs */
2
3#[cfg(feature = "alloc")]
4use crate::{FmtError, Format, LoadResult, PreProcess, Source, ValidateConfig, format::AnyFormat};
5#[cfg(feature = "alloc")]
6use alloc::boxed::Box;
7#[cfg(feature = "alloc")]
8use alloc::vec::Vec;
9#[cfg(feature = "alloc")]
10use serde::de::DeserializeOwned;
11
12#[cfg(feature = "alloc")]
13pub struct DynLoader {
14	source: Box<dyn Source>,
15	formats: Vec<AnyFormat>,
16}
17
18#[cfg(feature = "alloc")]
19impl core::fmt::Debug for DynLoader {
20	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
21		f.debug_struct("DynLoader")
22			.field("source", &"<dyn Source>")
23			.field("formats", &self.formats)
24			.finish()
25	}
26}
27
28#[cfg(feature = "alloc")]
29pub struct DynLoaderBuilder {
30	source: Option<Box<dyn Source>>,
31	formats: Vec<AnyFormat>,
32}
33
34#[cfg(feature = "alloc")]
35impl DynLoaderBuilder {
36	pub fn new() -> Self {
37		Self {
38			source: None,
39			formats: Vec::new(),
40		}
41	}
42
43	pub fn source(mut self, source: impl Source + 'static) -> Self {
44		self.source = Some(Box::new(source));
45		self
46	}
47
48	pub fn format(mut self, format: AnyFormat) -> Self {
49		self.formats.push(format);
50		self
51	}
52
53	pub fn build(self) -> Result<DynLoader, &'static str> {
54		let source = self.source.ok_or("source is required")?;
55		if self.formats.is_empty() {
56			return Err("at least one format is required");
57		}
58		Ok(DynLoader {
59			source,
60			formats: self.formats,
61		})
62	}
63}
64
65#[cfg(feature = "alloc")]
66impl DynLoader {
67	pub fn new(source: Box<dyn Source>, formats: Vec<AnyFormat>) -> Self {
68		Self { source, formats }
69	}
70
71	pub fn builder() -> DynLoaderBuilder {
72		DynLoaderBuilder::new()
73	}
74
75	/// Automatically detects and loads the configuration based on registered formats.
76	pub async fn load<T>(&self, base_name: &str) -> LoadResult<T>
77	where
78		T: DeserializeOwned + PreProcess + ValidateConfig,
79	{
80		let mut found: Option<(alloc::string::String, &AnyFormat)> = None;
81		let mut conflicts = Vec::new();
82
83		for format in &self.formats {
84			for ext in format.extensions() {
85				let key = alloc::format!("{}.{}", base_name, ext);
86				if self.source.exists(&key).await {
87					if found.is_some() {
88						conflicts.push(key);
89					} else {
90						found = Some((key, format));
91					}
92				}
93			}
94		}
95
96		if let Some((key, format)) = found {
97			self.load_explicit(&key, format, conflicts).await
98		} else {
99			LoadResult::NotFound
100		}
101	}
102
103	/// Directly loads a specific path, selecting parser by extension.
104	pub async fn load_file<T>(&self, path: &str) -> LoadResult<T>
105	where
106		T: DeserializeOwned + PreProcess + ValidateConfig,
107	{
108		let ext = if let Some(idx) = path.rfind('.') {
109			&path[idx + 1..]
110		} else {
111			#[cfg(feature = "alloc")]
112			return LoadResult::Invalid(FmtError::ParseError(alloc::string::String::from(
113				"missing extension",
114			)));
115			#[cfg(not(feature = "alloc"))]
116			return LoadResult::Invalid(FmtError::ParseError);
117		};
118
119		for format in &self.formats {
120			if format.extensions().contains(&ext) {
121				return self.load_explicit(path, format, Vec::new()).await;
122			}
123		}
124		LoadResult::NotFound
125	}
126
127	/// Dry-run mode, validates without returning data.
128	#[cfg(feature = "validate")]
129	pub async fn validate<T>(&self, base_name: &str) -> Result<(), FmtError>
130	where
131		T: DeserializeOwned + PreProcess + validator::Validate,
132	{
133		match self.load::<T>(base_name).await {
134			LoadResult::Ok { mut value, .. } => {
135				value.set_context(base_name);
136				value.validate_config()
137			}
138			LoadResult::Invalid(e) => Err(e),
139			LoadResult::NotFound => Err(FmtError::NotFound),
140		}
141	}
142
143	/// Loads the configuration using a specific key and format.
144	async fn load_explicit<T>(
145		&self,
146		key: &str,
147		format: &AnyFormat,
148		conflicts: Vec<alloc::string::String>,
149	) -> LoadResult<T>
150	where
151		T: DeserializeOwned + PreProcess + ValidateConfig,
152	{
153		let bytes = match self.source.read(key).await {
154			Ok(b) => b,
155			Err(FmtError::NotFound) => return LoadResult::NotFound,
156			Err(e) => return LoadResult::Invalid(e),
157		};
158
159		match format.parse::<T>(&bytes) {
160			Ok(mut obj) => {
161				obj.pre_process();
162
163				LoadResult::Ok {
164					value: obj,
165					info: crate::LoadInfo {
166						#[cfg(feature = "std")]
167						path: std::path::PathBuf::from(key),
168						#[cfg(not(feature = "std"))]
169						key: alloc::string::String::from(key),
170						format: format.extensions().first().copied().unwrap_or("unknown"),
171						#[cfg(feature = "std")]
172						conflicts: conflicts
173							.into_iter()
174							.map(std::path::PathBuf::from)
175							.collect(),
176						#[cfg(not(feature = "std"))]
177						conflicts,
178					},
179				}
180			}
181			Err(e) => LoadResult::Invalid(e),
182		}
183	}
184}