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
188impl 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}