plugx_config/
configuration.rs

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}