1use crate::{
2 entity::ConfigurationEntity, error::Error, loader::Error as LoaderError, loader::Loader,
3 parser::Parser,
4};
5use anyhow::anyhow;
6use cfg_if::cfg_if;
7use plugx_input::{position::InputPosition, schema::InputSchemaType, Input};
8use std::env;
9use url::Url;
10
11#[derive(Debug, Default)]
12pub struct Configuration {
13 url_list: Vec<Url>,
14 loader_list: Vec<Box<dyn Loader>>,
15 parser_list: Vec<Box<dyn Parser>>,
16 maybe_whitelist: Option<Vec<String>>,
17}
18
19impl Configuration {
20 pub fn new() -> Self {
21 let new = Self {
22 parser_list: vec![
23 #[cfg(feature = "env")]
24 Box::new(crate::parser::env::Env::new()),
25 #[cfg(feature = "json")]
26 Box::new(crate::parser::json::Json::new()),
27 #[cfg(feature = "toml")]
28 Box::new(crate::parser::toml::Toml::new()),
29 #[cfg(feature = "yaml")]
30 Box::new(crate::parser::yaml::Yaml::new()),
31 ],
32 ..Default::default()
33 };
34 let parser_name_list: Vec<_> = new
35 .parser_list
36 .iter()
37 .map(|parser| format!("{parser}"))
38 .collect();
39 if parser_name_list.is_empty() {
40 cfg_if! {
41 if #[cfg(feature = "tracing")] {
42 tracing::debug!("Initialized with no parser")
43 } else if #[cfg(feature = "logging")] {
44 log::debug!("msg=\"Initialized with no parser\"")
45 }
46 }
47 } else {
48 cfg_if! {
49 if #[cfg(feature = "tracing")] {
50 tracing::debug!(parsers=?parser_name_list, "Initialized with parser(s)")
51 } else if #[cfg(feature = "logging")] {
52 log::debug!("msg=\"Initialized with parser(s)\" parsers={parser_name_list:?}")
53 }
54 }
55 }
56 new
57 }
58}
59
60impl Configuration {
61 pub fn url_list(&self) -> &[Url] {
62 self.url_list.as_slice()
63 }
64
65 pub fn has_url(&mut self, url: &Url) -> bool {
66 self.url_list.contains(url)
67 }
68
69 pub fn has_url_scheme(&mut self, url: &Url) -> bool {
70 self.url_list
71 .iter()
72 .any(|inner_url| inner_url.scheme() == url.scheme())
73 }
74
75 pub fn with_url(mut self, url: Url) -> Result<Self, Error> {
76 self.add_url(url)?;
77 Ok(self)
78 }
79
80 pub fn add_url(&mut self, url: Url) -> Result<(), Error> {
81 let scheme = url.scheme().to_string();
82 let maybe_loader_name = if let Some(loader) = self
83 .loader_list
84 .iter()
85 .find(|loader| loader.scheme_list().contains(&scheme))
86 {
87 self.url_list.push(url.clone());
88 Some(format!("{loader}"))
89 } else {
90 #[allow(unused_mut)]
91 let mut included_loader_list: Vec<Box<dyn Loader>> = Vec::new();
92
93 #[cfg(feature = "env")]
94 included_loader_list.push(Box::new(crate::loader::env::Env::new()));
95
96 #[cfg(feature = "fs")]
97 included_loader_list.push(Box::new(crate::loader::fs::Fs::new()));
98
99 included_loader_list
100 .into_iter()
101 .find(|loader| loader.scheme_list().contains(&scheme))
102 .map(|loader| {
103 let name = format!("{loader}");
104 self.add_boxed_loader(loader);
105 self.url_list.push(url.clone());
106 name
107 })
108 };
109 maybe_loader_name.map(|_loader_name| {
110 cfg_if! {
111 if #[cfg(feature = "tracing")] {
112 tracing::debug!(url=%url, loader=_loader_name, "Added configuration URL");
113 } else if #[cfg(feature = "logging")] {
114 log::debug!("msg=\"Added configuration URL\", url=\"{url}\" loader={_loader_name:?}");
115 }
116 }
117 Ok(())
118 }).unwrap_or(Err(LoaderError::LoaderNotFound { scheme, url }.into()))
119 }
120
121 pub fn remove_url(&mut self, url: &Url) -> bool {
122 let mut result = false;
123 while let Some(index) = self.url_list.iter().position(|inner_url| inner_url == url) {
124 self.url_list.remove(index);
125 cfg_if! {
126 if #[cfg(feature = "tracing")] {
127 tracing::debug!(url=%url, "Removed URL")
128 } else if #[cfg(feature = "logging")] {
129 log::debug!("msg=\"Removed URL\" url=\"{url}\"")
130 }
131 }
132 result = true;
133 }
134 result
135 }
136
137 pub fn remove_scheme<S: AsRef<str>>(&mut self, scheme: S) -> Vec<Url> {
138 let mut url_list = Vec::new();
139 while let Some(url) = self
140 .url_list
141 .iter()
142 .find(|url| url.scheme() == scheme.as_ref())
143 {
144 url_list.push(url.clone())
145 }
146 url_list.iter().for_each(|url| {
147 self.remove_url(url);
148 });
149 url_list
150 }
151}
152
153impl Configuration {
154 pub fn has_loader(&mut self, url: &Url) -> bool {
155 let scheme = url.scheme().to_string();
156 self.loader_list
157 .iter()
158 .any(|loader| loader.scheme_list().contains(&scheme))
159 }
160
161 pub fn with_loader<L>(mut self, loader: L) -> Self
162 where
163 L: Loader + 'static,
164 {
165 self.add_boxed_loader(Box::new(loader));
166 self
167 }
168
169 pub fn add_loader<L>(&mut self, loader: L)
170 where
171 L: Loader + 'static,
172 {
173 self.add_boxed_loader(Box::new(loader));
174 }
175
176 pub fn with_boxed_loader(mut self, loader: Box<dyn Loader>) -> Self {
177 self.add_boxed_loader(loader);
178 self
179 }
180
181 pub fn add_boxed_loader(&mut self, loader: Box<dyn Loader>) {
182 cfg_if! {
183 if #[cfg(feature = "tracing")] {
184 tracing::debug!(
185 loader=%loader,
186 schema_list=?loader.scheme_list(),
187 "Added configuration loader"
188 );
189 } else if #[cfg(feature = "logging")] {
190 log::debug!(
191 "msg=\"Added configuration loader\" loader=\"{loader}\" schema_list={:?}",
192 loader.scheme_list()
193 );
194 }
195 }
196 self.loader_list.push(loader);
197 }
198
199 pub fn remove_loader_and_urls<S: AsRef<str>>(
200 &mut self,
201 scheme: S,
202 ) -> Option<(Box<dyn Loader>, Vec<Url>)> {
203 let scheme_string = scheme.as_ref().to_string();
204 if let Some(index) = self
205 .loader_list
206 .iter()
207 .position(|loader| loader.scheme_list().contains(&scheme_string))
208 {
209 let loader = self.loader_list.swap_remove(index);
210 cfg_if! {
211 if #[cfg(feature = "tracing")] {
212 tracing::debug!(
213 loader=%loader,
214 schema_list=?loader.scheme_list(),
215 "Removed configuration loader"
216 );
217 } else if #[cfg(feature = "logging")] {
218 log::debug!(
219 "message=\"Removed configuration loader\" loader=\"{loader}\" schema_list={:?}",
220 loader.scheme_list()
221 );
222 }
223 }
224 Some((loader, self.remove_scheme(scheme)))
225 } else {
226 None
227 }
228 }
229
230 pub fn load(
231 &self,
232 skip_soft_errors: bool,
233 ) -> Result<Vec<(String, Vec<ConfigurationEntity>)>, Error> {
234 load(
235 self.url_list.as_slice(),
236 self.loader_list.as_slice(),
237 self.maybe_whitelist.as_deref(),
238 skip_soft_errors,
239 )
240 .map_err(Error::from)
241 }
242}
243
244impl Configuration {
245 pub fn has_parser<F: AsRef<str>>(&self, format: F) -> bool {
246 let format = format.as_ref().to_lowercase();
247 self.parser_list
248 .iter()
249 .any(|parser| parser.supported_format_list().contains(&format))
250 }
251
252 pub fn with_parser<P>(mut self, parser: P) -> Self
253 where
254 P: Parser + 'static,
255 {
256 self.add_parser(parser);
257 self
258 }
259
260 pub fn add_parser<P>(&mut self, parser: P)
261 where
262 P: Parser + 'static,
263 {
264 self.add_boxed_parser(Box::new(parser));
265 }
266
267 pub fn with_boxed_parser(mut self, parser: Box<dyn Parser>) -> Self {
268 self.add_boxed_parser(parser);
269 self
270 }
271
272 pub fn add_boxed_parser(&mut self, parser: Box<dyn Parser>) {
273 cfg_if! {
274 if #[cfg(feature = "tracing")] {
275 tracing::debug!(
276 parser=%parser,
277 format_list=?parser.supported_format_list(),
278 "Added configuration parser"
279 );
280 } else if #[cfg(feature = "logging")] {
281 log::debug!(
282 "msg=\"Added configuration parser\" parser=\"{parser}\" format_list={:?}",
283 parser.supported_format_list()
284 );
285 }
286 }
287 self.parser_list.push(parser);
288 }
289
290 pub fn remove_parser<F: AsRef<str>>(&mut self, format: F) -> Vec<Box<dyn Parser>> {
291 let format = format.as_ref().to_lowercase();
292 let mut parser_list = Vec::new();
293 while let Some(index) = self
294 .parser_list
295 .iter()
296 .position(|parser| parser.supported_format_list().contains(&format))
297 {
298 let parser = self.parser_list.swap_remove(index);
299 cfg_if! {
300 if #[cfg(feature = "tracing")] {
301 tracing::debug!(
302 parser=%parser,
303 format_list=?parser.supported_format_list(),
304 "Removed configuration parser"
305 );
306 } else if #[cfg(feature = "logging")] {
307 log::debug!(
308 "msg=\"Removed configuration parser\" parser=\"{parser}\" format_list={:?}",
309 parser.supported_format_list()
310 );
311 }
312 }
313 parser_list.push(parser)
314 }
315 parser_list
316 }
317
318 pub fn load_and_parse(
319 &self,
320 skip_soft_errors: bool,
321 ) -> Result<Vec<(String, Vec<ConfigurationEntity>)>, Error> {
322 let mut load_result = self.load(skip_soft_errors)?;
323 parse(load_result.as_mut(), self.parser_list.as_slice())?;
324 Ok(load_result)
325 }
326}
327
328impl Configuration {
329 pub fn is_in_whitelist<P: AsRef<str>>(&self, name: P) -> bool {
330 let name = name.as_ref().to_lowercase();
331 self.maybe_whitelist
332 .as_ref()
333 .map(|whitelist| whitelist.contains(&name))
334 .unwrap_or(false)
335 }
336
337 pub fn load_whitelist_from_env<K: AsRef<str>>(&mut self, key: K) -> Result<(), Error> {
338 let whitelist = env::var(key.as_ref())
339 .map(|value| value.trim().to_lowercase())
340 .map(|value| {
341 if value.is_empty() {
342 Vec::new()
343 } else {
344 value.split([' ', ',', ';']).map(String::from).collect()
345 }
346 })
347 .map_err(|error| {
348 Error::Other(anyhow!("Invalid key or the value is not set: {}", error))
349 })?;
350 if whitelist.is_empty() {
351 cfg_if! {
352 if #[cfg(feature = "tracing")] {
353 tracing::warn!(key=key.as_ref(), "Whitelist environment-variable is set to empty")
354 } else if #[cfg(feature = "logging")] {
355 log::warn!("msg=\"Whitelist environment-variable is set to empty\" key={:?}", key.as_ref())
356 }
357 }
358 } else {
359 cfg_if! {
360 if #[cfg(feature = "tracing")] {
361 tracing::info!(key=key.as_ref(), "Set whitelist from environment-variable")
362 } else if #[cfg(feature = "logging")] {
363 log::info!("msg=\"Set whitelist from environment-variable\" key={:?}", key.as_ref())
364 }
365 }
366 }
367 self.set_whitelist(whitelist.as_ref());
368 Ok(())
369 }
370
371 pub fn set_whitelist_from_env<K: AsRef<str>>(mut self, key: K) -> Result<Self, Error> {
372 self.load_whitelist_from_env(key)?;
373 Ok(self)
374 }
375
376 pub fn set_whitelist<N: AsRef<str>>(&mut self, whitelist: &[N]) {
377 whitelist
378 .iter()
379 .for_each(|name| self.add_to_whitelist(name));
380 }
381
382 pub fn with_whitelist<N: AsRef<str>>(mut self, whitelist: &[N]) -> Self {
383 self.set_whitelist(whitelist);
384 self
385 }
386
387 pub fn add_to_whitelist<N: AsRef<str>>(&mut self, name: N) {
388 let name = name.as_ref().to_lowercase();
389 cfg_if! {
390 if #[cfg(feature = "tracing")] {
391 tracing::debug!(name=name, "Added to whitelist")
392 } else if #[cfg(feature = "logging")] {
393 log::debug!("msg=\"Added to whitelist\" name={name:?}")
394 }
395 }
396 if let Some(whitelist) = self.maybe_whitelist.as_mut() {
397 if !whitelist.contains(&name) {
398 whitelist.push(name);
399 }
400 } else {
401 self.maybe_whitelist = Some(Vec::from([name]));
402 }
403 }
404}
405
406impl Configuration {
407 pub fn load_parse_merge(&self, skip_soft_errors: bool) -> Result<Vec<(String, Input)>, Error> {
408 let mut parsed = self.load_and_parse(skip_soft_errors)?;
409 merge(parsed.as_mut())
410 }
411
412 pub fn load_parse_merge_validate(
413 &self,
414 schema_list: &[(String, InputSchemaType)],
415 skip_soft_errors: bool,
416 ) -> Result<Vec<(String, Input)>, Error> {
417 let mut merged = self.load_parse_merge(skip_soft_errors)?;
418 validate(merged.as_mut(), schema_list)
419 }
420}
421
422pub fn load(
423 url_list: &[Url],
424 loader_list: &[Box<dyn Loader>],
425 maybe_whitelist: Option<&[String]>,
426 skip_soft_errors: bool,
427) -> Result<Vec<(String, Vec<ConfigurationEntity>)>, LoaderError> {
428 let mut result: Vec<(String, Vec<_>)> = Vec::with_capacity(url_list.len());
429 url_list
430 .iter()
431 .try_for_each(|url| {
432 let scheme_string = url.scheme().to_string();
433 if let Some(loader) = loader_list
434 .iter()
435 .find(|loader| loader.scheme_list().contains(&scheme_string))
436 {
437 loader
438 .load(url, maybe_whitelist, skip_soft_errors)
439 .map(|loaded_list| {
440 loaded_list
441 .into_iter()
442 .for_each(|(plugin_name, configuration)| {
443 if let Some((_, configuration_list)) =
444 result.iter_mut().find(|(loaded_plugin_name, _)| {
445 loaded_plugin_name == &plugin_name
446 })
447 {
448 configuration_list.push(configuration);
449 } else {
450 result.push((plugin_name.clone(), [configuration].to_vec()))
451 }
452 });
453 })
454 } else {
455 Err(LoaderError::LoaderNotFound {
456 scheme: scheme_string,
457 url: url.clone(),
458 })
459 }
460 })
461 .map(|_| result)
462}
463
464pub fn parse(
465 plugin_configuration_list: &mut [(String, Vec<ConfigurationEntity>)],
466 parser_list: &[Box<dyn Parser>],
467) -> Result<(), Error> {
468 plugin_configuration_list
469 .iter_mut()
470 .try_for_each(|(plugin_name, configuration_list)| {
471 configuration_list
472 .iter_mut()
473 .try_for_each(|configuration| {
474 if configuration.maybe_parsed_contents().is_none() {
475 let parsed =
476 configuration.parse_contents(parser_list).map_err(|error| {
477 Error::Parse {
478 plugin_name: plugin_name.to_string(),
479 url: configuration.url().clone(),
480 item: configuration.item().clone().into(),
481 source: error,
482 }
483 })?;
484 configuration.set_parsed_contents(parsed);
485 }
486 Ok::<_, Error>(())
487 })?;
488 Ok::<_, Error>(())
489 })
490}
491
492pub fn merge(
493 plugin_configuration_list: &[(String, Vec<ConfigurationEntity>)],
494) -> Result<Vec<(String, Input)>, Error> {
495 let mut result = Vec::with_capacity(plugin_configuration_list.len());
496 plugin_configuration_list
497 .iter()
498 .for_each(|(plugin_name, configuration_list)| {
499 let mut first = Input::new_map();
500 configuration_list
501 .iter()
502 .filter(|configuration| configuration.maybe_parsed_contents().is_some())
503 .for_each(|configuration| {
504 plugx_input::merge::merge_with_positions(
505 &mut first,
506 plugx_input::position::new().new_with_key(plugin_name),
507 configuration.maybe_parsed_contents().unwrap(),
508 plugx_input::position::new().new_with_key(configuration.url().as_str()),
509 )
510 });
511 result.push((plugin_name.to_string(), first));
512 });
513 Ok(result)
514}
515
516pub fn validate(
517 plugin_configuration_list: &[(String, Input)],
518 schema_list: &[(String, InputSchemaType)],
519) -> Result<Vec<(String, Input)>, Error> {
520 let mut result = Vec::with_capacity(plugin_configuration_list.len());
521 plugin_configuration_list
522 .iter()
523 .try_for_each(|(plugin_name, configuration)| {
524 let mut configuration = configuration.clone();
525 if let Some((_, schema_type)) = schema_list
526 .iter()
527 .find(|(schema_plugin_name, _)| schema_plugin_name == plugin_name)
528 {
529 schema_type.validate(
530 &mut configuration,
531 Some(InputPosition::new().new_with_key(plugin_name)),
532 )
533 } else {
534 Ok(())
535 }?;
536 result.push((plugin_name.to_string(), configuration));
537 Ok::<_, Error>(())
538 })
539 .map(|_| result)
540}