hocon_rs/
config.rs

1use crate::config_options::ConfigOptions;
2use crate::merge::object::Object as MObject;
3use crate::merge::value::Value as MValue;
4use crate::parser::loader::{self, load_from_path, parse_hocon};
5use crate::parser::read::{StrRead, StreamRead};
6use crate::raw::raw_object::RawObject;
7use crate::raw::raw_string::RawString;
8use crate::raw::raw_value::RawValue;
9use crate::raw::{field::ObjectField, include::Inclusion};
10use crate::value::Value;
11use derive_more::{Deref, DerefMut};
12use serde::de::DeserializeOwned;
13
14#[derive(Debug, Clone, PartialEq, Deref, DerefMut)]
15pub struct Config {
16    #[deref]
17    #[deref_mut]
18    object: RawObject,
19    options: ConfigOptions,
20}
21
22impl Config {
23    pub fn new(options: Option<ConfigOptions>) -> Self {
24        Self {
25            object: Default::default(),
26            options: options.unwrap_or_default(),
27        }
28    }
29
30    pub fn load<T>(
31        path: impl AsRef<std::path::Path>,
32        options: Option<ConfigOptions>,
33    ) -> crate::Result<T>
34    where
35        T: DeserializeOwned,
36    {
37        let raw = loader::load(&path, options.unwrap_or_default())?;
38        tracing::debug!("path: {} raw obj: {}", path.as_ref().display(), raw);
39        Self::resolve_object::<T>(raw)
40    }
41
42    pub fn add_kv<K, V>(&mut self, key: K, value: V) -> &mut Self
43    where
44        K: Into<RawString>,
45        V: Into<RawValue>,
46    {
47        let field = ObjectField::key_value(key, value);
48        self.object.push(field);
49        self
50    }
51
52    pub fn add_include(&mut self, inclusion: Inclusion) -> &mut Self {
53        let field = ObjectField::inclusion(inclusion);
54        self.object.push(field);
55        self
56    }
57
58    pub fn add_kvs<I, V>(&mut self, kvs: I) -> &mut Self
59    where
60        I: IntoIterator<Item = (String, V)>,
61        V: Into<RawValue>,
62    {
63        let fields = kvs
64            .into_iter()
65            .map(|(key, value)| ObjectField::key_value(key, value));
66        self.object.extend(fields);
67        self
68    }
69
70    pub fn add_object(&mut self, object: RawObject) -> &mut Self {
71        self.object.extend(object.0);
72        self
73    }
74
75    pub fn resolve<T>(self) -> crate::Result<T>
76    where
77        T: DeserializeOwned,
78    {
79        Self::resolve_object(self.object)
80    }
81
82    pub fn parse_file<T>(
83        path: impl AsRef<std::path::Path>,
84        opts: Option<ConfigOptions>,
85    ) -> crate::Result<T>
86    where
87        T: DeserializeOwned,
88    {
89        let raw = load_from_path(path, opts.unwrap_or_default())?;
90        Self::resolve_object::<T>(raw)
91    }
92
93    #[cfg(feature = "urls_includes")]
94    pub fn parse_url<T>(url: impl AsRef<str>, opts: Option<ConfigOptions>) -> crate::Result<T>
95    where
96        T: DeserializeOwned,
97    {
98        use std::str::FromStr;
99        let url = url::Url::from_str(url.as_ref())?;
100        let raw = loader::load_from_url(url, opts.unwrap_or_default().into())?;
101        Self::resolve_object::<T>(raw)
102    }
103
104    pub fn parse_map<T>(values: std::collections::HashMap<String, Value>) -> crate::Result<T>
105    where
106        T: DeserializeOwned,
107    {
108        fn into_raw(value: Value) -> RawValue {
109            match value {
110                Value::Object(object) => {
111                    let len = object.len();
112                    let fields = object.into_iter().fold(
113                        Vec::with_capacity(len),
114                        |mut acc, (key, value)| {
115                            let field = ObjectField::key_value(key, into_raw(value));
116                            acc.push(field);
117                            acc
118                        },
119                    );
120                    RawValue::Object(RawObject::new(fields))
121                }
122                Value::Array(array) => RawValue::array(array.into_iter().map(into_raw).collect()),
123                Value::Boolean(boolean) => RawValue::Boolean(boolean),
124                Value::Null => RawValue::Null,
125                Value::String(string) => {
126                    let s = RawString::path_expression(
127                        string.split('.').map(RawString::quoted).collect(),
128                    );
129                    RawValue::String(s)
130                }
131                Value::Number(number) => RawValue::Number(number),
132            }
133        }
134        let raw = into_raw(Value::Object(ahash::HashMap::from_iter(values)));
135        if let RawValue::Object(raw_obj) = raw {
136            Self::resolve_object::<T>(raw_obj)
137        } else {
138            unreachable!("raw should always be an object");
139        }
140    }
141
142    pub fn parse_str<T>(s: &str, options: Option<ConfigOptions>) -> crate::Result<T>
143    where
144        T: DeserializeOwned,
145    {
146        let read = StrRead::new(s);
147        let raw = parse_hocon(read, options.unwrap_or_default())?;
148        Self::resolve_object::<T>(raw)
149    }
150
151    pub fn parse_reader<R, T>(rdr: R, options: Option<ConfigOptions>) -> crate::Result<T>
152    where
153        R: std::io::Read,
154        T: DeserializeOwned,
155    {
156        use crate::parser::read::DEFAULT_BUFFER;
157        let read: StreamRead<_, DEFAULT_BUFFER> = StreamRead::new(std::io::BufReader::new(rdr));
158        let raw = parse_hocon(read, options.unwrap_or_default())?;
159        Self::resolve_object::<T>(raw)
160    }
161
162    fn resolve_object<T>(object: RawObject) -> crate::Result<T>
163    where
164        T: DeserializeOwned,
165    {
166        let object = MObject::from_raw(None, object)?;
167        let mut value = MValue::Object(object);
168        value.resolve()?;
169        if value.is_unmerged() {
170            return Err(crate::error::Error::ResolveIncomplete);
171        }
172        let value: Value = value.try_into()?;
173        T::deserialize(value)
174    }
175}
176
177impl From<RawObject> for Config {
178    fn from(value: RawObject) -> Self {
179        Config {
180            object: value,
181            options: Default::default(),
182        }
183    }
184}
185
186/// Constructs a [Config] from a [std::collections::HashMap].
187///
188/// Keys are treated as literal values, not path expressions.
189/// For example, a key `"foo.bar"` in the map will result in a single entry
190/// with the key `"foo.bar"`, rather than creating a nested object
191/// with `"foo"` containing another object `"bar"`.
192impl From<std::collections::HashMap<String, Value>> for Config {
193    fn from(value: std::collections::HashMap<String, Value>) -> Self {
194        let fields = value
195            .into_iter()
196            .map(|(k, v)| ObjectField::key_value(k, v))
197            .collect();
198        Config {
199            object: RawObject::new(fields),
200            options: Default::default(),
201        }
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use crate::Result;
208    use crate::error::Error;
209    use crate::{config::Config, config_options::ConfigOptions, value::Value};
210    use rstest::rstest;
211
212    impl Value {
213        pub fn assert_deep_eq(&self, other: &Value, path: &str) {
214            match (self, other) {
215                (Value::Object(map1), Value::Object(map2)) => {
216                    for (k, v1) in map1 {
217                        let new_path = format!("{}/{}", path, k);
218                        if let Some(v2) = map2.get(k) {
219                            v1.assert_deep_eq(v2, &new_path);
220                        } else {
221                            panic!("Key missing in right: {}", new_path);
222                        }
223                    }
224                    for k in map2.keys() {
225                        if !map1.contains_key(k) {
226                            panic!("Key missing in left: {}/{}", path, k);
227                        }
228                    }
229                }
230                (Value::Array(arr1), Value::Array(arr2)) => {
231                    let len = arr1.len().max(arr2.len());
232                    for i in 0..len {
233                        let new_path = format!("{}/[{}]", path, i);
234                        match (arr1.get(i), arr2.get(i)) {
235                            (Some(v1), Some(v2)) => v1.assert_deep_eq(v2, &new_path),
236                            (Some(_), None) => panic!("Index missing in right: {}", new_path),
237                            (None, Some(_)) => panic!("Index missing in left: {}", new_path),
238                            _ => {}
239                        }
240                    }
241                }
242                _ => {
243                    assert_eq!(
244                        self, other,
245                        "Difference at {}: left={:?}, right={:?}",
246                        path, self, other
247                    );
248                }
249            }
250        }
251    }
252
253    #[rstest]
254    #[case("resources/empty.conf", "resources/empty.json")]
255    #[case("resources/base.conf", "resources/base.json")]
256    #[case("resources/add_assign.conf", "resources/add_assign_expected.json")]
257    #[case("resources/concat.conf", "resources/concat.json")]
258    #[case("resources/concat2.conf", "resources/concat2.json")]
259    #[case("resources/concat3.conf", "resources/concat3.json")]
260    #[case("resources/concat4.conf", "resources/concat4.json")]
261    #[case("resources/concat5.conf", "resources/concat5.json")]
262    #[case("resources/include.conf", "resources/include.json")]
263    #[case("resources/comment.conf", "resources/comment.json")]
264    #[case("resources/substitution.conf", "resources/substitution.json")]
265    #[case("resources/self_referential.conf", "resources/self_referential.json")]
266    fn test_hocon(
267        #[case] hocon: impl AsRef<std::path::Path>,
268        #[case] json: impl AsRef<std::path::Path>,
269    ) -> Result<()> {
270        let mut options = ConfigOptions::default();
271        options.classpath = vec!["resources".to_string()].into();
272        let value = Config::load::<Value>(hocon, Some(options))?;
273        let f = std::fs::File::open(json)?;
274        let expected_value: serde_json::Value = serde_json::from_reader(f)?;
275        let expected_value: Value = expected_value.into();
276        value.assert_deep_eq(&expected_value, "$");
277        Ok(())
278    }
279
280    #[test]
281    fn test_max_depth() -> Result<()> {
282        let error = Config::load::<Value>("resources/max_depth.conf", None)
283            .err()
284            .unwrap();
285        assert!(matches!(error, Error::RecursionDepthExceeded { .. }));
286        Ok(())
287    }
288
289    #[test]
290    fn test_include_cycle() -> Result<()> {
291        let mut options = ConfigOptions::default();
292        options.classpath = vec!["resources".to_string()].into();
293        let error = Config::load::<Value>("resources/include_cycle.conf", Some(options))
294            .err()
295            .unwrap();
296        assert!(matches!(error, Error::Include { .. }));
297        Ok(())
298    }
299
300    #[test]
301    fn test_substitution_cycle() -> Result<()> {
302        let mut options = ConfigOptions::default();
303        options.classpath = vec!["resources".to_string()].into();
304        let error = Config::load::<Value>("resources/substitution_cycle.conf", Some(options))
305            .err()
306            .unwrap();
307        assert!(matches!(error, Error::SubstitutionCycle { .. }));
308        Ok(())
309    }
310
311    #[test]
312    fn test_substitution_not_found() -> Result<()> {
313        let mut options = ConfigOptions::default();
314        options.classpath = vec!["resources".to_string()].into();
315        let error = Config::load::<Value>("resources/substitution2.conf", Some(options))
316            .err()
317            .unwrap();
318        assert!(matches!(error, Error::SubstitutionNotFound { .. }));
319        Ok(())
320    }
321}