Skip to main content

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}