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}