hocon_rs/
config.rs

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