hocon/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 use error::Result;
192pub(crate) mod helper;
193mod loader_config;
194pub(crate) use loader_config::*;
195
196#[cfg(feature = "serde-support")]
197mod serde;
198#[cfg(feature = "serde-support")]
199pub use crate::serde::de;
200
201/// Helper to load an HOCON file. This is used to set up the HOCON loader's option,
202/// like strict mode, disabling system environment, and to buffer several documents.
203///
204/// # Strict mode
205///
206/// If strict mode is enabled with [`strict()`](struct.HoconLoader.html#method.strict),
207/// loading a document will return the first error encountered. Otherwise, most errors
208/// will be wrapped in a [`Hocon::BadValue`](enum.Hocon.html#variant.BadValue).
209///
210/// # Usage
211///
212/// ```rust
213/// # use hocon::{HoconLoader,Error};
214/// # fn main() -> Result<(), Error> {
215/// # #[cfg(not(feature = "url-support"))]
216/// # let mut loader = HoconLoader::new() // Creating new loader with default configuration
217/// # .no_system(); // Disable substituting from system environment
218///
219/// # #[cfg(feature = "url-support")]
220/// let mut loader = HoconLoader::new() // Creating new loader with default configuration
221/// .no_system() // Disable substituting from system environment
222/// .no_url_include(); // Disable including files from URLs
223///
224/// let default_values = r#"{ a = 7 }"#;
225/// loader = loader.load_str(default_values)? // Load values from a string
226/// .load_file("tests/data/basic.conf")? // Load first file
227/// .load_file("tests/data/test01.conf")?; // Load another file
228///
229/// let hocon = loader.hocon()?; // Create the Hocon document from the loaded sources
230/// # Ok(())
231/// # }
232/// ```
233#[derive(Debug, Clone)]
234pub struct HoconLoader {
235 config: HoconLoaderConfig,
236 internal: internals::HoconInternal,
237}
238
239impl Default for HoconLoader {
240 fn default() -> Self {
241 Self::new()
242 }
243}
244
245impl HoconLoader {
246 /// New `HoconLoader` with default configuration
247 pub fn new() -> Self {
248 Self {
249 config: HoconLoaderConfig::default(),
250 internal: internals::HoconInternal::empty(),
251 }
252 }
253
254 /// Disable System environment substitutions
255 ///
256 /// # Example HOCON document
257 ///
258 /// ```no_test
259 /// "system" : {
260 /// "home" : ${HOME},
261 /// "pwd" : ${PWD},
262 /// "shell" : ${SHELL},
263 /// "lang" : ${LANG},
264 /// }
265 /// ```
266 ///
267 /// with system:
268 /// ```rust
269 /// # use hocon::{Hocon, HoconLoader, Error};
270 /// # fn main() -> Result<(), Error> {
271 /// # // SAFETY: This is a single-threaded doctest
272 /// # unsafe { std::env::set_var("SHELL", "/bin/bash") };
273 /// # let example = r#"{system.shell: ${SHELL}}"#;
274 /// assert_eq!(
275 /// HoconLoader::new().load_str(example)?.hocon()?["system"]["shell"],
276 /// Hocon::String(String::from("/bin/bash"))
277 /// );
278 /// # Ok(())
279 /// # }
280 /// ```
281 ///
282 /// without system:
283 /// ```rust
284 /// # use hocon::{Hocon, HoconLoader, Error};
285 /// # fn main() -> Result<(), Error> {
286 /// # let example = r#"{system.shell: ${SHELL}}"#;
287 /// assert_eq!(
288 /// HoconLoader::new().no_system().load_str(example)?.hocon()?["system"]["shell"],
289 /// Hocon::BadValue(Error::KeyNotFound { key: String::from("SHELL") })
290 /// );
291 /// # Ok(())
292 /// # }
293 /// ```
294 pub fn no_system(&self) -> Self {
295 Self {
296 config: HoconLoaderConfig {
297 system: false,
298 ..self.config.clone()
299 },
300 ..self.clone()
301 }
302 }
303
304 /// Disable loading included files from external urls.
305 ///
306 /// # Example HOCON document
307 ///
308 /// ```no_test
309 /// include url("https://raw.githubusercontent.com/mockersf/hocon.rs/master/tests/data/basic.conf")
310 /// ```
311 ///
312 /// with url include:
313 /// ```rust
314 /// # use hocon::{Hocon, HoconLoader, Error};
315 /// # fn main() -> Result<(), Error> {
316 /// assert_eq!(
317 /// HoconLoader::new().load_file("tests/data/include_url.conf")?.hocon()?["d"],
318 /// Hocon::Boolean(true)
319 /// );
320 /// # Ok(())
321 /// # }
322 /// ```
323 ///
324 /// without url include:
325 /// ```rust
326 /// # use hocon::{Hocon, HoconLoader, Error};
327 /// # fn main() -> Result<(), Error> {
328 /// assert_eq!(
329 /// HoconLoader::new().no_url_include().load_file("tests/data/include_url.conf")?.hocon()?["d"],
330 /// Hocon::BadValue(Error::MissingKey)
331 /// );
332 /// # Ok(())
333 /// # }
334 /// ```
335 ///
336 /// # Feature
337 ///
338 /// This method depends on feature `url-support`
339 #[cfg(feature = "url-support")]
340 pub fn no_url_include(&self) -> Self {
341 Self {
342 config: HoconLoaderConfig {
343 external_url: false,
344 ..self.config.clone()
345 },
346 ..self.clone()
347 }
348 }
349
350 /// Sets the HOCON loader to return the first [`Error`](enum.Error.html) encoutered instead
351 /// of wrapping it in a [`Hocon::BadValue`](enum.Hocon.html#variant.BadValue) and
352 /// continuing parsing
353 ///
354 /// # Example HOCON document
355 ///
356 /// ```no_test
357 /// {
358 /// a = ${b}
359 /// }
360 /// ```
361 ///
362 /// in permissive mode:
363 /// ```rust
364 /// # use hocon::{Hocon, HoconLoader, Error};
365 /// # fn main() -> Result<(), Error> {
366 /// # let example = r#"{ a = ${b} }"#;
367 /// assert_eq!(
368 /// HoconLoader::new().load_str(example)?.hocon()?["a"],
369 /// Hocon::BadValue(Error::KeyNotFound { key: String::from("b") })
370 /// );
371 /// # Ok(())
372 /// # }
373 /// ```
374 ///
375 /// in strict mode:
376 /// ```rust
377 /// # use hocon::{Hocon, HoconLoader, Error};
378 /// # fn main() -> Result<(), Error> {
379 /// # let example = r#"{ a = ${b} }"#;
380 /// assert_eq!(
381 /// HoconLoader::new().strict().load_str(example)?.hocon(),
382 /// Err(Error::KeyNotFound { key: String::from("b") })
383 /// );
384 /// # Ok(())
385 /// # }
386 /// ```
387 pub fn strict(&self) -> Self {
388 Self {
389 config: HoconLoaderConfig {
390 strict: true,
391 ..self.config.clone()
392 },
393 ..self.clone()
394 }
395 }
396
397 /// Set a new maximum include depth, by default 10
398 pub fn max_include_depth(&self, new_max_depth: u8) -> Self {
399 Self {
400 config: HoconLoaderConfig {
401 max_include_depth: new_max_depth,
402 ..self.config.clone()
403 },
404 ..self.clone()
405 }
406 }
407
408 pub(crate) fn load_from_str_of_conf_file(self, s: FileRead) -> Result<Self> {
409 Ok(Self {
410 internal: self.internal.add(self.config.parse_str_to_internal(s)?),
411 config: self.config,
412 })
413 }
414
415 /// Load a string containing an `Hocon` document. Includes are not supported when
416 /// loading from a string
417 ///
418 /// # Errors
419 ///
420 /// * [`Error::Parse`](enum.Error.html#variant.Parse) if the document is invalid
421 ///
422 /// # Additional errors in strict mode
423 ///
424 /// * [`Error::IncludeNotAllowedFromStr`](enum.Error.html#variant.IncludeNotAllowedFromStr)
425 /// if there is an include in the string
426 pub fn load_str(self, s: &str) -> Result<Self> {
427 self.load_from_str_of_conf_file(FileRead {
428 hocon: Some(String::from(s)),
429 ..Default::default()
430 })
431 }
432
433 /// Load the HOCON configuration file containing an `Hocon` document
434 ///
435 /// # Errors
436 ///
437 /// * [`Error::File`](enum.Error.html#variant.File) if there was an error reading the
438 /// file content
439 /// * [`Error::Parse`](enum.Error.html#variant.Parse) if the document is invalid
440 ///
441 /// # Additional errors in strict mode
442 ///
443 /// * [`Error::TooManyIncludes`](enum.Error.html#variant.TooManyIncludes)
444 /// if there are too many included files within included files. The limit can be
445 /// changed with [`max_include_depth`](struct.HoconLoader.html#method.max_include_depth)
446 pub fn load_file<P: AsRef<Path>>(&self, path: P) -> Result<Self> {
447 let mut file_path = path.as_ref().to_path_buf();
448 // pub fn load_file(&self, path: &str) -> Result<Self, Error> {
449 // let mut file_path = Path::new(path).to_path_buf();
450 if !file_path.has_root() {
451 let mut current_path = std::env::current_dir().map_err(|_| Error::File {
452 path: String::from(path.as_ref().to_str().unwrap_or("invalid path")),
453 })?;
454 current_path.push(path.as_ref());
455 file_path = current_path;
456 }
457 let conf = self.config.with_file(file_path);
458 let contents = conf.read_file().map_err(|err| {
459 let path = match err {
460 Error::File { path } => path,
461 Error::Include { path } => path,
462 Error::Io { message } => message,
463 _ => "unmatched error".to_string(),
464 };
465 Error::File { path }
466 })?;
467 Self {
468 config: conf,
469 ..self.clone()
470 }
471 .load_from_str_of_conf_file(contents)
472 }
473
474 /// Load the documents as HOCON
475 ///
476 /// # Errors in strict mode
477 ///
478 /// * [`Error::Include`](enum.Error.html#variant.Include) if there was an issue with an
479 /// included file
480 /// * [`Error::KeyNotFound`](enum.Error.html#variant.KeyNotFound) if there is a substitution
481 /// with a key that is not present in the document
482 /// * [`Error::DisabledExternalUrl`](enum.Error.html#variant.DisabledExternalUrl) if crate
483 /// was built without feature `url-support` and an `include url("...")` was found
484 pub fn hocon(self) -> Result<Hocon> {
485 let config = &self.config;
486 self.internal.merge(config)?.finalize(config)
487 }
488
489 /// Deserialize the loaded documents to the target type
490 ///
491 /// # Errors
492 ///
493 /// * [`Error::Deserialization`](enum.Error.html#variant.Deserialization) if there was a
494 /// serde error during deserialization (missing required field, type issue, ...)
495 ///
496 /// # Additional errors in strict mode
497 ///
498 /// * [`Error::Include`](enum.Error.html#variant.Include) if there was an issue with an
499 /// included file
500 /// * [`Error::KeyNotFound`](enum.Error.html#variant.KeyNotFound) if there is a substitution
501 /// with a key that is not present in the document
502 /// * [`Error::DisabledExternalUrl`](enum.Error.html#variant.DisabledExternalUrl) if crate
503 /// was built without feature `url-support` and an `include url("...")` was found
504 #[cfg(feature = "serde-support")]
505 pub fn resolve<'de, T>(self) -> Result<T>
506 where
507 T: ::serde::Deserialize<'de>,
508 {
509 self.hocon()?.resolve()
510 }
511}
512
513#[cfg(test)]
514mod tests {
515 use super::{ConfFileMeta, Hocon, HoconLoader, HoconLoaderConfig};
516 use std::path::Path;
517
518 #[test]
519 fn read_from_properties() {
520 let s = r#"a.b:c"#;
521 let loader = dbg!(
522 HoconLoader {
523 config: HoconLoaderConfig {
524 file_meta: Some(ConfFileMeta::from_path(
525 Path::new("file.properties").to_path_buf()
526 )),
527 ..Default::default()
528 },
529 ..Default::default()
530 }
531 .load_str(s)
532 );
533 assert!(loader.is_ok());
534
535 let doc = loader.expect("during test").hocon().expect("during test");
536 assert_eq!(doc["a"]["b"].as_string(), Some(String::from("c")));
537 }
538
539 #[test]
540 fn read_from_hocon() {
541 let s = r#"a.b:c"#;
542 let loader = dbg!(
543 HoconLoader {
544 config: HoconLoaderConfig {
545 file_meta: Some(ConfFileMeta::from_path(
546 Path::new("file.conf").to_path_buf()
547 )),
548 ..Default::default()
549 },
550 ..Default::default()
551 }
552 .load_str(s)
553 );
554 assert!(loader.is_ok());
555
556 let doc: Hocon = loader.expect("during test").hocon().expect("during test");
557 assert_eq!(doc["a"]["b"].as_string(), Some(String::from("c")));
558 }
559
560 #[cfg(feature = "serde-support")]
561 use serde::Deserialize;
562
563 #[cfg(feature = "serde-support")]
564 #[derive(Deserialize, Debug)]
565 struct Simple {
566 #[allow(dead_code)]
567 int: i64,
568 #[allow(dead_code)]
569 float: f64,
570 #[allow(dead_code)]
571 option_int: Option<u64>,
572 }
573 #[cfg(feature = "serde-support")]
574 #[derive(Deserialize, Debug)]
575 struct WithSubStruct {
576 #[allow(dead_code)]
577 vec_sub: Vec<Simple>,
578 #[allow(dead_code)]
579 int: i32,
580 #[allow(dead_code)]
581 float: f32,
582 #[allow(dead_code)]
583 boolean: bool,
584 #[allow(dead_code)]
585 string: String,
586 }
587
588 #[cfg(feature = "serde-support")]
589 #[test]
590 fn can_deserialize_struct() {
591 let doc = r#"{int:56, float:543.12, boolean:false, string: test,
592 vec_sub:[
593 {int:8, float:1.5, option_int:1919},
594 {int:8, float:0 },
595 {int:1, float:2, option_int:null},
596]}"#;
597
598 let res: Result<WithSubStruct, _> = dbg!(HoconLoader::new().load_str(doc))
599 .expect("during test")
600 .resolve();
601 assert!(res.is_ok());
602 let res = res.expect("during test");
603 assert_eq!(res.int, 56);
604 assert_eq!(res.float, 543.12);
605 assert!(!res.boolean);
606 assert_eq!(res.string, "test");
607 assert_eq!(res.vec_sub[0].int, 8);
608 assert_eq!(res.vec_sub[0].float, 1.5);
609 assert_eq!(res.vec_sub[0].option_int, Some(1919));
610 assert_eq!(res.vec_sub[1].int, 8);
611 assert_eq!(res.vec_sub[1].float, 0.0);
612 assert_eq!(res.vec_sub[1].option_int, None);
613 assert_eq!(res.vec_sub[2].int, 1);
614 assert_eq!(res.vec_sub[2].float, 2.0);
615 assert_eq!(res.vec_sub[2].option_int, None);
616 }
617
618 #[cfg(feature = "serde-support")]
619 #[test]
620 fn can_deserialize_struct2() {
621 let doc = r#"{int:56, float:543.12, boolean:false, string: test,
622 vec_sub.1 = {int:8, float:1.5, option_int:1919},
623 vec_sub.5 = {int:8, float:0 },
624 vec_sub.8 = {int:1, float:2, option_int:null},
625 }"#;
626
627 let res: Result<WithSubStruct, _> = dbg!(HoconLoader::new().load_str(doc))
628 .expect("during test")
629 .resolve();
630 assert!(res.is_ok());
631 let res = res.expect("during test");
632 assert_eq!(res.int, 56);
633 assert_eq!(res.float, 543.12);
634 assert!(!res.boolean);
635 assert_eq!(res.string, "test");
636 assert_eq!(res.vec_sub[0].int, 8);
637 assert_eq!(res.vec_sub[0].float, 1.5);
638 assert_eq!(res.vec_sub[0].option_int, Some(1919));
639 assert_eq!(res.vec_sub[1].int, 8);
640 assert_eq!(res.vec_sub[1].float, 0.0);
641 assert_eq!(res.vec_sub[1].option_int, None);
642 assert_eq!(res.vec_sub[2].int, 1);
643 assert_eq!(res.vec_sub[2].float, 2.0);
644 assert_eq!(res.vec_sub[2].option_int, None);
645 }
646
647 #[cfg(feature = "serde-support")]
648 #[test]
649 fn error_deserializing_struct() {
650 let doc = r#"{
651 int:"not an int", float:543.12, boolean:false, string: test,
652 vec_sub:[]
653 }"#;
654
655 let res: Result<WithSubStruct, _> = dbg!(HoconLoader::new().load_str(doc))
656 .expect("during test")
657 .resolve();
658 assert!(res.is_err());
659 assert_eq!(
660 res.unwrap_err(),
661 super::Error::Deserialization {
662 message: String::from("int: Invalid type for field \"int\", expected integer")
663 }
664 );
665 }
666
667 #[cfg(feature = "url-support")]
668 #[test]
669 fn can_disable_url_include() {
670 let doc = dbg!(
671 HoconLoader::new()
672 .no_url_include()
673 .load_file("tests/data/include_url.conf")
674 .unwrap()
675 .hocon()
676 )
677 .unwrap();
678 assert_eq!(doc["d"], Hocon::BadValue(super::Error::MissingKey));
679 assert_eq!(
680 doc["https://raw.githubusercontent.com/mockersf/hocon.rs/master/tests/data/basic.conf"],
681 Hocon::BadValue(super::Error::Include {
682 path: String::from(
683 "https://raw.githubusercontent.com/mockersf/hocon.rs/master/tests/data/basic.conf"
684 )
685 })
686 );
687 }
688}