Skip to main content

irx_config/
parsers.rs

1//! This module define base structures ([`FileParser`] and [`FileParserBuilder`]) which help to implement file based
2//! parsers. All embedded file based parsers is using that base structures.
3
4#[cfg(feature = "cmd")]
5pub mod cmd;
6#[cfg(feature = "env")]
7pub mod env;
8#[cfg(feature = "json")]
9pub mod json;
10#[cfg(feature = "json5-parser")]
11pub mod json5;
12#[cfg(test)]
13mod tests;
14#[cfg(feature = "toml-parser")]
15pub mod toml;
16#[cfg(feature = "yaml")]
17pub mod yaml;
18
19use crate::{AnyResult, Case, Parse, Value, DEFAULT_KEYS_SEPARATOR};
20use derive_builder::Builder;
21use std::{
22    borrow::Cow,
23    fs::File,
24    io::{BufReader, Error as IoError, Read},
25    path::{Path, PathBuf},
26    result::Result as StdResult,
27};
28
29/// A result type for file-based parsers errors.
30pub type Result<T> = StdResult<T, Error>;
31
32type CowPath<'a> = Cow<'a, Path>;
33
34/// All errors for file-based parsers.
35#[non_exhaustive]
36#[derive(thiserror::Error, Debug)]
37pub enum Error {
38    #[error("Failed to get file path by option: '{1}'")]
39    PathOption(#[source] crate::Error, String),
40    #[error("Failed to open file: '{1}'")]
41    Open(#[source] IoError, PathBuf),
42    #[error("Failed to get meta data for file: '{1}'")]
43    Meta(#[source] IoError, PathBuf),
44    #[error("Is not a file: '{0}'")]
45    NotAFile(PathBuf),
46}
47
48/// The trait to be used by [`FileParser`] to load data from file in specific format.
49pub trait Load: Case {
50    /// Load data from reader in specific format to [`Value`] structure.
51    ///
52    /// # Errors
53    ///
54    /// If any errors will occur during load then error will be returned.
55    fn load(&mut self, reader: impl Read) -> AnyResult<Value>;
56}
57
58/// The base structure to implement file based parsers.
59#[derive(Builder)]
60#[builder(setter(into, strip_option))]
61pub struct FileParser<L: Load + Default> {
62    /// Set default path to the file to be parsed.
63    default_path: PathBuf,
64    /// Set path option name which could be used to get path value from previous parsing [`Value`] results.
65    #[builder(default = "None")]
66    path_option: Option<String>,
67    /// Set delimiter used to separate keys levels in path value. Default is [`DEFAULT_KEYS_SEPARATOR`].
68    #[builder(default = "DEFAULT_KEYS_SEPARATOR.to_string()")]
69    keys_delimiter: String,
70    /// If file does not exists do not try to load it. The default [`Value`] will be returned. Default is `false`.
71    #[builder(default = "false")]
72    ignore_missing_file: bool,
73    /// Set the loader structure which implements [`Load`] trait.
74    #[builder(default)]
75    loader: L,
76}
77
78impl<L: Load + Default> Case for FileParser<L> {
79    #[inline]
80    fn is_case_sensitive(&self) -> bool {
81        self.loader.is_case_sensitive()
82    }
83}
84
85impl<L: Load + Default> Parse for FileParser<L> {
86    fn parse(&mut self, value: &Value) -> AnyResult<Value> {
87        let path = get_path(
88            value,
89            &self.path_option,
90            &self.default_path,
91            &self.keys_delimiter,
92        )?;
93
94        let file = match try_open_file(path.as_ref()) {
95            Ok(f) => f,
96            Err(_) if self.ignore_missing_file => return Ok(Value::default()),
97            Err(e) => return Err(e.into()),
98        };
99
100        self.loader.load(BufReader::new(file))
101    }
102}
103
104fn get_path<'a>(
105    value: &Value,
106    path_option: &Option<String>,
107    default: &'a Path,
108    delim: &str,
109) -> Result<CowPath<'a>> {
110    let default = default.into();
111    let Some(option) = path_option else {
112        return Ok(default);
113    };
114
115    let path: Option<String> = value
116        .get_by_key_path_with_delim(option, delim)
117        .map_err(|e| Error::PathOption(e, option.into()))?;
118    Ok(path.map_or(default, |p| PathBuf::from(p).into()))
119}
120
121fn try_open_file(path: &Path) -> Result<File> {
122    let file = File::open(path).map_err(|e| Error::Open(e, path.into()))?;
123    if file
124        .metadata()
125        .map_err(|e| Error::Meta(e, path.into()))?
126        .is_file()
127    {
128        return Ok(file);
129    }
130
131    Err(Error::NotAFile(path.into()))
132}