hocon/
loader_config.rs

1use crate::Error;
2use std::ffi::OsStr;
3use std::fs::File;
4use std::io::prelude::*;
5use std::path::{Path, PathBuf};
6
7#[derive(Debug, Clone)]
8pub(crate) enum FileType {
9    Properties,
10    Hocon,
11    Json,
12    All,
13}
14
15#[derive(Default, Debug)]
16pub(crate) struct FileRead {
17    pub(crate) properties: Option<String>,
18    pub(crate) json: Option<String>,
19    pub(crate) hocon: Option<String>,
20}
21impl FileRead {
22    fn from_file_type(ft: &FileType, s: String) -> Self {
23        match ft {
24            FileType::Properties => Self {
25                properties: Some(s),
26                ..Default::default()
27            },
28            FileType::Json => Self {
29                json: Some(s),
30                ..Default::default()
31            },
32            FileType::Hocon => Self {
33                hocon: Some(s),
34                ..Default::default()
35            },
36            FileType::All => unimplemented!(),
37        }
38    }
39}
40
41#[derive(Debug, Clone)]
42pub(crate) struct ConfFileMeta {
43    path: PathBuf,
44    full_path: PathBuf,
45    file_type: FileType,
46}
47impl ConfFileMeta {
48    pub(crate) fn from_path(path: PathBuf) -> Self {
49        let file = path
50            .file_name()
51            .expect("got a path without a filename")
52            .to_str()
53            .expect("got invalid UTF-8 path");
54        let mut parent_path = path.clone();
55        parent_path.pop();
56
57        Self {
58            path: parent_path,
59            full_path: path.clone(),
60            file_type: match Path::new(file).extension().and_then(OsStr::to_str) {
61                Some("properties") => FileType::Properties,
62                Some("json") => FileType::Json,
63                Some("conf") => FileType::Hocon,
64                _ => FileType::All,
65            },
66        }
67    }
68}
69
70#[derive(Debug, Clone)]
71pub(crate) struct HoconLoaderConfig {
72    pub(crate) include_depth: u8,
73    pub(crate) file_meta: Option<ConfFileMeta>,
74    pub(crate) system: bool,
75    #[cfg(feature = "url-support")]
76    pub(crate) external_url: bool,
77    pub(crate) strict: bool,
78    pub(crate) max_include_depth: u8,
79}
80
81impl Default for HoconLoaderConfig {
82    fn default() -> Self {
83        Self {
84            include_depth: 0,
85            file_meta: None,
86            system: true,
87            #[cfg(feature = "url-support")]
88            external_url: true,
89            strict: false,
90            max_include_depth: 10,
91        }
92    }
93}
94
95impl HoconLoaderConfig {
96    pub(crate) fn included_from(&self) -> Self {
97        Self {
98            include_depth: self.include_depth + 1,
99            ..self.clone()
100        }
101    }
102
103    pub(crate) fn with_file(&self, path: PathBuf) -> Self {
104        match self.file_meta.as_ref() {
105            Some(file_meta) => Self {
106                file_meta: Some(ConfFileMeta::from_path(file_meta.clone().path.join(path))),
107                ..self.clone()
108            },
109            None => Self {
110                file_meta: Some(ConfFileMeta::from_path(path)),
111                ..self.clone()
112            },
113        }
114    }
115
116    pub(crate) fn parse_str_to_internal(
117        &self,
118        s: FileRead,
119    ) -> Result<crate::internals::HoconInternal, Error> {
120        let mut internal = crate::internals::HoconInternal::empty();
121        if let Some(properties) = s.properties {
122            internal = internal.add(
123                java_properties::read(properties.as_bytes())
124                    .map(crate::internals::HoconInternal::from_properties)
125                    .map_err(|_| crate::Error::Parse)?,
126            );
127        };
128        if let Some(json) = s.json {
129            internal = internal.add(
130                crate::parser::root(format!("{}\n\0", json.replace('\r', "\n")).as_bytes(), self)
131                    .map_err(|_| crate::Error::Parse)
132                    .and_then(|(remaining, parsed)| {
133                        if Self::remaining_only_whitespace(remaining) {
134                            parsed
135                        } else if self.strict {
136                            Err(crate::Error::Deserialization {
137                                message: String::from("file could not be parsed completely"),
138                            })
139                        } else {
140                            parsed
141                        }
142                    })?,
143            );
144        };
145        if let Some(hocon) = s.hocon {
146            internal = internal.add(
147                crate::parser::root(
148                    format!("{}\n\0", hocon.replace('\r', "\n")).as_bytes(),
149                    self,
150                )
151                .map_err(|_| crate::Error::Parse)
152                .and_then(|(remaining, parsed)| {
153                    if Self::remaining_only_whitespace(remaining) {
154                        parsed
155                    } else if self.strict {
156                        Err(crate::Error::Deserialization {
157                            message: String::from("file could not be parsed completely"),
158                        })
159                    } else {
160                        parsed
161                    }
162                })?,
163            );
164        };
165
166        Ok(internal)
167    }
168
169    fn remaining_only_whitespace(remaining: &[u8]) -> bool {
170        remaining
171            .iter()
172            .find(|c| {
173                **c != 10 // \n
174                && **c != 13 // \r
175                && **c != 0
176            })
177            .map(|_| false)
178            .unwrap_or(true)
179    }
180
181    pub(crate) fn read_file_to_string(path: PathBuf) -> Result<String, Error> {
182        let mut file = File::open(path.as_os_str())?;
183        let mut contents = String::new();
184        file.read_to_string(&mut contents)?;
185        Ok(contents)
186    }
187
188    pub(crate) fn read_file(&self) -> Result<FileRead, Error> {
189        let full_path = self
190            .file_meta
191            .clone()
192            .expect("missing file metadata")
193            .full_path;
194        match self.file_meta.as_ref().map(|fm| &fm.file_type) {
195            Some(FileType::All) => Ok(FileRead {
196                hocon: Self::read_file_to_string({
197                    let mut path = full_path.clone();
198                    if !path.exists() {
199                        path.set_extension("conf");
200                    }
201                    path
202                })
203                .ok(),
204                json: Self::read_file_to_string({
205                    let mut path = full_path.clone();
206                    path.set_extension("json");
207                    path
208                })
209                .ok(),
210                properties: Self::read_file_to_string({
211                    let mut path = full_path;
212                    path.set_extension("properties");
213                    path
214                })
215                .ok(),
216            }),
217            Some(ft) => Ok(FileRead::from_file_type(
218                ft,
219                Self::read_file_to_string(full_path)?,
220            )),
221            _ => unimplemented!(),
222        }
223    }
224
225    #[cfg(feature = "url-support")]
226    pub(crate) fn load_url(&self, url: &str) -> Result<crate::internals::HoconInternal, Error> {
227        if let Ok(parsed_url) = reqwest::Url::parse(url) {
228            if parsed_url.scheme() == "file" {
229                if let Ok(path) = parsed_url.to_file_path() {
230                    let include_config = self.included_from().with_file(path);
231                    let s = include_config.read_file()?;
232                    Ok(include_config.parse_str_to_internal(s).map_err(|_| {
233                        crate::Error::Include {
234                            path: String::from(url),
235                        }
236                    })?)
237                } else {
238                    Err(crate::Error::Include {
239                        path: String::from(url),
240                    })
241                }
242            } else if self.external_url {
243                let body = reqwest::blocking::get(parsed_url)
244                    .and_then(reqwest::blocking::Response::text)
245                    .map_err(|_| crate::Error::Include {
246                        path: String::from(url),
247                    })?;
248
249                Ok(self.parse_str_to_internal(FileRead {
250                    hocon: Some(body),
251                    ..Default::default()
252                })?)
253            } else {
254                Err(crate::Error::Include {
255                    path: String::from(url),
256                })
257            }
258        } else {
259            Err(crate::Error::Include {
260                path: String::from(url),
261            })
262        }
263    }
264}