hocon_linked/lib.rs
1#![deny(
2 warnings,
3 missing_debug_implementations,
4 missing_copy_implementations,
5 trivial_casts,
6 trivial_numeric_casts,
7 unsafe_code,
8 unstable_features,
9 unused_import_braces,
10 unused_qualifications,
11 missing_docs
12)]
13
14//! HOCON
15//!
16//! Parse HOCON configuration files in Rust following the
17//! [HOCON Specifications](https://github.com/lightbend/config/blob/master/HOCON.md).
18//!
19//! This implementation goal is to be as permissive as possible, returning a valid document
20//! with all errors wrapped in [`Hocon::BadValue`](enum.Hocon.html#variant.BadValue) when a
21//! correct value cannot be computed. [`strict`](struct.HoconLoader.html#method.strict) mode
22//! can be enabled to return the first [`Error`](enum.Error.html) encountered instead.
23//!
24//! # Examples
25//!
26//! ## Parsing a string to a struct using serde
27//!
28//! ```rust
29//! use serde::Deserialize;
30//! use hocon::Error;
31//!
32//! #[derive(Deserialize)]
33//! struct Configuration {
34//! host: String,
35//! port: u8,
36//! auto_connect: bool,
37//! }
38//!
39//! # fn main() -> Result<(), Error> {
40//! let s = r#"{
41//! host: 127.0.0.1
42//! port: 80
43//! auto_connect: false
44//! }"#;
45//!
46//! # #[cfg(feature = "serde-support")]
47//! let conf: Configuration = hocon::de::from_str(s)?;
48//!
49//! # Ok(())
50//! # }
51//! ```
52//!
53//! ## Reading from a string and getting value directly
54//!
55//! ```rust
56//! use hocon::{HoconLoader,Error};
57//!
58//! # fn main() -> Result<(), Error> {
59//! let s = r#"{ a: 7 }"#;
60//!
61//! let doc = HoconLoader::new()
62//! .load_str(s)?
63//! .hocon()?;
64//!
65//! let a = doc["a"].as_i64();
66//! assert_eq!(a, Some(7));
67//! # Ok(())
68//! # }
69//! ```
70//!
71//! ## Deserializing to a struct using `serde`
72//!
73//! ```rust
74//! use serde::Deserialize;
75//!
76//! use hocon::{HoconLoader,Error};
77//!
78//! #[derive(Deserialize)]
79//! struct Configuration {
80//! host: String,
81//! port: u8,
82//! auto_connect: bool,
83//! }
84//!
85//! # fn main() -> Result<(), Error> {
86//! let s = r#"{
87//! host: 127.0.0.1
88//! port: 80
89//! auto_connect: false
90//! }"#;
91//!
92//! # #[cfg(feature = "serde-support")]
93//! let conf: Configuration = HoconLoader::new()
94//! .load_str(s)?
95//! .resolve()?;
96//! # Ok(())
97//! # }
98//! ```
99//!
100//! ## Reading from a file
101//!
102//! Example file:
103//! [tests/data/basic.conf](https://raw.githubusercontent.com/mockersf/hocon.rs/master/tests/data/basic.conf)
104//!
105//! ```rust
106//! use hocon::{HoconLoader,Error};
107//!
108//! # fn main() -> Result<(), Error> {
109//! let doc = HoconLoader::new()
110//! .load_file("tests/data/basic.conf")?
111//! .hocon()?;
112//!
113//! let a = doc["a"].as_i64();
114//! assert_eq!(a, Some(5));
115//! # Ok(())
116//! # }
117//! ```
118//!
119//! ## Reading from several documents
120//!
121//! Example file:
122//! [tests/data/basic.conf](https://raw.githubusercontent.com/mockersf/hocon.rs/master/tests/data/basic.conf)
123//!
124//! ```rust
125//! use hocon::{HoconLoader,Error};
126//!
127//! # fn main() -> Result<(), Error> {
128//! let s = r#"{
129//! a: will be changed
130//! unchanged: original value
131//! }"#;
132//!
133//! let doc = HoconLoader::new()
134//! .load_str(s)?
135//! .load_file("tests/data/basic.conf")?
136//! .hocon()?;
137//!
138//! let a = doc["a"].as_i64();
139//! assert_eq!(a, Some(5));
140//! let unchanged = doc["unchanged"].as_string();
141//! assert_eq!(unchanged, Some(String::from("original value")));
142//! # Ok(())
143//! # }
144//! ```
145//!
146//! # Features
147//!
148//! All features are enabled by default. They can be disabled to reduce dependencies.
149//!
150//! ### `url-support`
151//!
152//! This feature enable fetching URLs in includes with `include url("http://mydomain.com/myfile.conf")` (see
153//! [spec](https://github.com/lightbend/config/blob/master/HOCON.md#include-syntax)). If disabled,
154//! includes will only load local files specified with `include "path/to/file.conf"` or
155//! `include file("path/to/file.conf")`.
156//!
157//! ### `serde-support`
158//!
159//! This feature enable deserializing to a `struct` implementing `Deserialize` using `serde`
160//!
161//! ```rust
162//! use serde::Deserialize;
163//!
164//! use hocon::{HoconLoader,Error};
165//!
166//! #[derive(Deserialize)]
167//! struct Configuration {
168//! host: String,
169//! port: u8,
170//! auto_connect: bool,
171//! }
172//!
173//! # fn main() -> Result<(), Error> {
174//! let s = r#"{host: 127.0.0.1, port: 80, auto_connect: false}"#;
175//!
176//! # #[cfg(feature = "serde-support")]
177//! let conf: Configuration = HoconLoader::new().load_str(s)?.resolve()?;
178//! # Ok(())
179//! # }
180//! ```
181//!
182
183use std::path::Path;
184
185mod internals;
186mod parser;
187mod value;
188pub use value::Hocon;
189mod error;
190pub use error::Error;
191pub(crate) mod helper;
192mod loader_config;
193pub(crate) use loader_config::*;
194
195#[cfg(feature = "serde-support")]
196mod serde;
197#[cfg(feature = "serde-support")]
198pub use crate::serde::de;
199
200/// Helper to load an HOCON file. This is used to set up the HOCON loader's option,
201/// like strict mode, disabling system environment, and to buffer several documents.
202///
203/// # Strict mode
204///
205/// If strict mode is enabled with [`strict()`](struct.HoconLoader.html#method.strict),
206/// loading a document will return the first error encountered. Otherwise, most errors
207/// will be wrapped in a [`Hocon::BadValue`](enum.Hocon.html#variant.BadValue).
208///
209/// # Usage
210///
211/// ```rust
212/// # use hocon::{HoconLoader,Error};
213/// # fn main() -> Result<(), Error> {
214/// # #[cfg(not(feature = "url-support"))]
215/// # let mut loader = HoconLoader::new() // Creating new loader with default configuration
216/// # .no_system(); // Disable substituting from system environment
217///
218/// # #[cfg(feature = "url-support")]
219/// let mut loader = HoconLoader::new() // Creating new loader with default configuration
220/// .no_system() // Disable substituting from system environment
221/// .no_url_include(); // Disable including files from URLs
222///
223/// let default_values = r#"{ a = 7 }"#;
224/// loader = loader.load_str(default_values)? // Load values from a string
225/// .load_file("tests/data/basic.conf")? // Load first file
226/// .load_file("tests/data/test01.conf")?; // Load another file
227///
228/// let hocon = loader.hocon()?; // Create the Hocon document from the loaded sources
229/// # Ok(())
230/// # }
231/// ```
232#[derive(Debug, Clone)]
233pub struct HoconLoader {
234 config: HoconLoaderConfig,
235 internal: internals::HoconInternal,
236}
237
238impl Default for HoconLoader {
239 fn default() -> Self {
240 Self::new()
241 }
242}
243
244impl HoconLoader {
245 /// New `HoconLoader` with default configuration
246 pub fn new() -> Self {
247 Self {
248 config: HoconLoaderConfig::default(),
249 internal: internals::HoconInternal::empty(),
250 }
251 }
252
253 /// Disable System environment substitutions
254 ///
255 /// # Example HOCON document
256 ///
257 /// ```no_test
258 /// "system" : {
259 /// "home" : ${HOME},
260 /// "pwd" : ${PWD},
261 /// "shell" : ${SHELL},
262 /// "lang" : ${LANG},
263 /// }
264 /// ```
265 ///
266 /// with system:
267 /// ```rust
268 /// # use hocon::{Hocon, HoconLoader, Error};
269 /// # fn main() -> Result<(), Error> {
270 /// # std::env::set_var("SHELL", "/bin/bash");
271 /// # let example = r#"{system.shell: ${SHELL}}"#;
272 /// assert_eq!(
273 /// HoconLoader::new().load_str(example)?.hocon()?["system"]["shell"],
274 /// Hocon::String(String::from("/bin/bash"))
275 /// );
276 /// # Ok(())
277 /// # }
278 /// ```
279 ///
280 /// without system:
281 /// ```rust
282 /// # use hocon::{Hocon, HoconLoader, Error};
283 /// # fn main() -> Result<(), Error> {
284 /// # let example = r#"{system.shell: ${SHELL}}"#;
285 /// assert_eq!(
286 /// HoconLoader::new().no_system().load_str(example)?.hocon()?["system"]["shell"],
287 /// Hocon::BadValue(Error::KeyNotFound { key: String::from("SHELL") })
288 /// );
289 /// # Ok(())
290 /// # }
291 /// ```
292 pub fn no_system(&self) -> Self {
293 Self {
294 config: HoconLoaderConfig {
295 system: false,
296 ..self.config.clone()
297 },
298 ..self.clone()
299 }
300 }
301
302 /// Disable loading included files from external urls.
303 ///
304 /// # Example HOCON document
305 ///
306 /// ```no_test
307 /// include url("https://raw.githubusercontent.com/mockersf/hocon.rs/master/tests/data/basic.conf")
308 /// ```
309 ///
310 /// with url include:
311 /// ```rust
312 /// # use hocon::{Hocon, HoconLoader, Error};
313 /// # fn main() -> Result<(), Error> {
314 /// assert_eq!(
315 /// HoconLoader::new().load_file("tests/data/include_url.conf")?.hocon()?["d"],
316 /// Hocon::Boolean(true)
317 /// );
318 /// # Ok(())
319 /// # }
320 /// ```
321 ///
322 /// without url include:
323 /// ```rust
324 /// # use hocon::{Hocon, HoconLoader, Error};
325 /// # fn main() -> Result<(), Error> {
326 /// assert_eq!(
327 /// HoconLoader::new().no_url_include().load_file("tests/data/include_url.conf")?.hocon()?["d"],
328 /// Hocon::BadValue(Error::MissingKey)
329 /// );
330 /// # Ok(())
331 /// # }
332 /// ```
333 ///
334 /// # Feature
335 ///
336 /// This method depends on feature `url-support`
337 #[cfg(feature = "url-support")]
338 pub fn no_url_include(&self) -> Self {
339 Self {
340 config: HoconLoaderConfig {
341 external_url: false,
342 ..self.config.clone()
343 },
344 ..self.clone()
345 }
346 }
347
348 /// Sets the HOCON loader to return the first [`Error`](enum.Error.html) encoutered instead
349 /// of wrapping it in a [`Hocon::BadValue`](enum.Hocon.html#variant.BadValue) and
350 /// continuing parsing
351 ///
352 /// # Example HOCON document
353 ///
354 /// ```no_test
355 /// {
356 /// a = ${b}
357 /// }
358 /// ```
359 ///
360 /// in permissive mode:
361 /// ```rust
362 /// # use hocon::{Hocon, HoconLoader, Error};
363 /// # fn main() -> Result<(), Error> {
364 /// # let example = r#"{ a = ${b} }"#;
365 /// assert_eq!(
366 /// HoconLoader::new().load_str(example)?.hocon()?["a"],
367 /// Hocon::BadValue(Error::KeyNotFound { key: String::from("b") })
368 /// );
369 /// # Ok(())
370 /// # }
371 /// ```
372 ///
373 /// in strict mode:
374 /// ```rust
375 /// # use hocon::{Hocon, HoconLoader, Error};
376 /// # fn main() -> Result<(), Error> {
377 /// # let example = r#"{ a = ${b} }"#;
378 /// assert_eq!(
379 /// HoconLoader::new().strict().load_str(example)?.hocon(),
380 /// Err(Error::KeyNotFound { key: String::from("b") })
381 /// );
382 /// # Ok(())
383 /// # }
384 /// ```
385 pub fn strict(&self) -> Self {
386 Self {
387 config: HoconLoaderConfig {
388 strict: true,
389 ..self.config.clone()
390 },
391 ..self.clone()
392 }
393 }
394
395 /// Set a new maximum include depth, by default 10
396 pub fn max_include_depth(&self, new_max_depth: u8) -> Self {
397 Self {
398 config: HoconLoaderConfig {
399 max_include_depth: new_max_depth,
400 ..self.config.clone()
401 },
402 ..self.clone()
403 }
404 }
405
406 pub(crate) fn load_from_str_of_conf_file(self, s: FileRead) -> Result<Self, Error> {
407 Ok(Self {
408 internal: self.internal.add(self.config.parse_str_to_internal(s)?),
409 config: self.config,
410 })
411 }
412
413 /// Load a string containing an `Hocon` document. Includes are not supported when
414 /// loading from a string
415 ///
416 /// # Errors
417 ///
418 /// * [`Error::Parse`](enum.Error.html#variant.Parse) if the document is invalid
419 ///
420 /// # Additional errors in strict mode
421 ///
422 /// * [`Error::IncludeNotAllowedFromStr`](enum.Error.html#variant.IncludeNotAllowedFromStr)
423 /// if there is an include in the string
424 pub fn load_str(self, s: &str) -> Result<Self, Error> {
425 self.load_from_str_of_conf_file(FileRead {
426 hocon: Some(String::from(s)),
427 ..Default::default()
428 })
429 }
430
431 /// Load the HOCON configuration file containing an `Hocon` document
432 ///
433 /// # Errors
434 ///
435 /// * [`Error::File`](enum.Error.html#variant.File) if there was an error reading the
436 /// file content
437 /// * [`Error::Parse`](enum.Error.html#variant.Parse) if the document is invalid
438 ///
439 /// # Additional errors in strict mode
440 ///
441 /// * [`Error::TooManyIncludes`](enum.Error.html#variant.TooManyIncludes)
442 /// if there are too many included files within included files. The limit can be
443 /// changed with [`max_include_depth`](struct.HoconLoader.html#method.max_include_depth)
444 pub fn load_file<P: AsRef<Path>>(&self, path: P) -> Result<Self, Error> {
445 let mut file_path = path.as_ref().to_path_buf();
446 // pub fn load_file(&self, path: &str) -> Result<Self, Error> {
447 // let mut file_path = Path::new(path).to_path_buf();
448 if !file_path.has_root() {
449 let mut current_path = std::env::current_dir().map_err(|_| Error::File {
450 path: String::from(path.as_ref().to_str().unwrap_or("invalid path")),
451 })?;
452 current_path.push(path.as_ref());
453 file_path = current_path;
454 }
455 let conf = self.config.with_file(file_path);
456 let contents = conf.read_file().map_err(|err| {
457 let path = match err {
458 Error::File { path } => path,
459 Error::Include { path } => path,
460 Error::Io { message } => message,
461 _ => "unmatched error".to_string(),
462 };
463 Error::File { path }
464 })?;
465 Self {
466 config: conf,
467 ..self.clone()
468 }
469 .load_from_str_of_conf_file(contents)
470 }
471
472 /// Load the documents as HOCON
473 ///
474 /// # Errors in strict mode
475 ///
476 /// * [`Error::Include`](enum.Error.html#variant.Include) if there was an issue with an
477 /// included file
478 /// * [`Error::KeyNotFound`](enum.Error.html#variant.KeyNotFound) if there is a substitution
479 /// with a key that is not present in the document
480 /// * [`Error::DisabledExternalUrl`](enum.Error.html#variant.DisabledExternalUrl) if crate
481 /// was built without feature `url-support` and an `include url("...")` was found
482 pub fn hocon(self) -> Result<Hocon, Error> {
483 let config = &self.config;
484 self.internal.merge(config)?.finalize(config)
485 }
486
487 /// Deserialize the loaded documents to the target type
488 ///
489 /// # Errors
490 ///
491 /// * [`Error::Deserialization`](enum.Error.html#variant.Deserialization) if there was a
492 /// serde error during deserialization (missing required field, type issue, ...)
493 ///
494 /// # Additional errors in strict mode
495 ///
496 /// * [`Error::Include`](enum.Error.html#variant.Include) if there was an issue with an
497 /// included file
498 /// * [`Error::KeyNotFound`](enum.Error.html#variant.KeyNotFound) if there is a substitution
499 /// with a key that is not present in the document
500 /// * [`Error::DisabledExternalUrl`](enum.Error.html#variant.DisabledExternalUrl) if crate
501 /// was built without feature `url-support` and an `include url("...")` was found
502 #[cfg(feature = "serde-support")]
503 pub fn resolve<'de, T>(self) -> Result<T, Error>
504 where
505 T: ::serde::Deserialize<'de>,
506 {
507 self.hocon()?.resolve()
508 }
509}
510
511#[cfg(test)]
512mod tests {
513 use super::{ConfFileMeta, Hocon, HoconLoader, HoconLoaderConfig};
514 use std::path::Path;
515
516 #[test]
517 fn read_from_properties() {
518 let s = r#"a.b:c"#;
519 let loader = dbg!(HoconLoader {
520 config: HoconLoaderConfig {
521 file_meta: Some(ConfFileMeta::from_path(
522 Path::new("file.properties").to_path_buf()
523 )),
524 ..Default::default()
525 },
526 ..Default::default()
527 }
528 .load_str(s));
529 assert!(loader.is_ok());
530
531 let doc = loader.expect("during test").hocon().expect("during test");
532 assert_eq!(doc["a"]["b"].as_string(), Some(String::from("c")));
533 }
534
535 #[test]
536 fn read_from_hocon() {
537 let s = r#"a.b:c"#;
538 let loader = dbg!(HoconLoader {
539 config: HoconLoaderConfig {
540 file_meta: Some(ConfFileMeta::from_path(
541 Path::new("file.conf").to_path_buf()
542 )),
543 ..Default::default()
544 },
545 ..Default::default()
546 }
547 .load_str(s));
548 assert!(loader.is_ok());
549
550 let doc: Hocon = loader.expect("during test").hocon().expect("during test");
551 assert_eq!(doc["a"]["b"].as_string(), Some(String::from("c")));
552 }
553
554 use serde::Deserialize;
555
556 #[derive(Deserialize, Debug)]
557 struct Simple {
558 int: i64,
559 float: f64,
560 option_int: Option<u64>,
561 }
562 #[derive(Deserialize, Debug)]
563 struct WithSubStruct {
564 vec_sub: Vec<Simple>,
565 int: i32,
566 float: f32,
567 boolean: bool,
568 string: String,
569 }
570
571 #[cfg(feature = "serde-support")]
572 #[test]
573 fn can_deserialize_struct() {
574 let doc = r#"{int:56, float:543.12, boolean:false, string: test,
575 vec_sub:[
576 {int:8, float:1.5, option_int:1919},
577 {int:8, float:0 },
578 {int:1, float:2, option_int:null},
579]}"#;
580
581 let res: Result<WithSubStruct, _> = dbg!(HoconLoader::new().load_str(doc))
582 .expect("during test")
583 .resolve();
584 assert!(res.is_ok());
585 let res = res.expect("during test");
586 assert_eq!(res.int, 56);
587 assert_eq!(res.float, 543.12);
588 assert_eq!(res.boolean, false);
589 assert_eq!(res.string, "test");
590 assert_eq!(res.vec_sub[0].int, 8);
591 assert_eq!(res.vec_sub[0].float, 1.5);
592 assert_eq!(res.vec_sub[0].option_int, Some(1919));
593 assert_eq!(res.vec_sub[1].int, 8);
594 assert_eq!(res.vec_sub[1].float, 0.0);
595 assert_eq!(res.vec_sub[1].option_int, None);
596 assert_eq!(res.vec_sub[2].int, 1);
597 assert_eq!(res.vec_sub[2].float, 2.0);
598 assert_eq!(res.vec_sub[2].option_int, None);
599 }
600
601 #[cfg(feature = "serde-support")]
602 #[test]
603 fn can_deserialize_struct2() {
604 let doc = r#"{int:56, float:543.12, boolean:false, string: test,
605 vec_sub.1 = {int:8, float:1.5, option_int:1919},
606 vec_sub.5 = {int:8, float:0 },
607 vec_sub.8 = {int:1, float:2, option_int:null},
608 }"#;
609
610 let res: Result<WithSubStruct, _> = dbg!(HoconLoader::new().load_str(doc))
611 .expect("during test")
612 .resolve();
613 assert!(res.is_ok());
614 let res = res.expect("during test");
615 assert_eq!(res.int, 56);
616 assert_eq!(res.float, 543.12);
617 assert_eq!(res.boolean, false);
618 assert_eq!(res.string, "test");
619 assert_eq!(res.vec_sub[0].int, 8);
620 assert_eq!(res.vec_sub[0].float, 1.5);
621 assert_eq!(res.vec_sub[0].option_int, Some(1919));
622 assert_eq!(res.vec_sub[1].int, 8);
623 assert_eq!(res.vec_sub[1].float, 0.0);
624 assert_eq!(res.vec_sub[1].option_int, None);
625 assert_eq!(res.vec_sub[2].int, 1);
626 assert_eq!(res.vec_sub[2].float, 2.0);
627 assert_eq!(res.vec_sub[2].option_int, None);
628 }
629
630 #[cfg(feature = "serde-support")]
631 #[test]
632 fn error_deserializing_struct() {
633 let doc = r#"{
634 int:"not an int", float:543.12, boolean:false, string: test,
635 vec_sub:[]
636 }"#;
637
638 let res: Result<WithSubStruct, _> = dbg!(HoconLoader::new().load_str(doc))
639 .expect("during test")
640 .resolve();
641 assert!(res.is_err());
642 assert_eq!(
643 res.unwrap_err(),
644 super::Error::Deserialization {
645 message: String::from("int: Invalid type for field \"int\", expected integer")
646 }
647 );
648 }
649
650 #[cfg(feature = "url-support")]
651 #[test]
652 fn can_disable_url_include() {
653 let doc = dbg!(HoconLoader::new()
654 .no_url_include()
655 .load_file("tests/data/include_url.conf")
656 .unwrap()
657 .hocon())
658 .unwrap();
659 assert_eq!(doc["d"], Hocon::BadValue(super::Error::MissingKey));
660 assert_eq!(
661 doc["https://raw.githubusercontent.com/mockersf/hocon.rs/master/tests/data/basic.conf"],
662 Hocon::BadValue(
663 super::Error::Include {
664 path: String::from("https://raw.githubusercontent.com/mockersf/hocon.rs/master/tests/data/basic.conf")
665 }
666 )
667 );
668 }
669}