]> git.proxmox.com Git - rustc.git/blame - src/vendor/mdbook/src/config.rs
New upstream version 1.27.1+dfsg1
[rustc.git] / src / vendor / mdbook / src / config.rs
CommitLineData
2c00a5a8 1//! Mdbook's configuration system.
83c7162d 2//!
2c00a5a8
XL
3//! The main entrypoint of the `config` module is the `Config` struct. This acts
4//! essentially as a bag of configuration information, with a couple
83c7162d 5//! pre-determined tables (`BookConfig` and `BuildConfig`) as well as support
2c00a5a8 6//! for arbitrary data which is exposed to plugins and alternate backends.
83c7162d
XL
7//!
8//!
2c00a5a8 9//! # Examples
83c7162d 10//!
2c00a5a8
XL
11//! ```rust
12//! # extern crate mdbook;
13//! # use mdbook::errors::*;
14//! # extern crate toml;
15//! use std::path::PathBuf;
16//! use mdbook::Config;
17//! use toml::Value;
83c7162d 18//!
2c00a5a8
XL
19//! # fn run() -> Result<()> {
20//! let src = r#"
21//! [book]
22//! title = "My Book"
23//! authors = ["Michael-F-Bryan"]
83c7162d 24//!
2c00a5a8
XL
25//! [build]
26//! src = "out"
83c7162d 27//!
2c00a5a8
XL
28//! [other-table.foo]
29//! bar = 123
30//! "#;
83c7162d 31//!
2c00a5a8
XL
32//! // load the `Config` from a toml string
33//! let mut cfg = Config::from_str(src)?;
83c7162d 34//!
2c00a5a8
XL
35//! // retrieve a nested value
36//! let bar = cfg.get("other-table.foo.bar").cloned();
37//! assert_eq!(bar, Some(Value::Integer(123)));
83c7162d 38//!
2c00a5a8
XL
39//! // Set the `output.html.theme` directory
40//! assert!(cfg.get("output.html").is_none());
41//! cfg.set("output.html.theme", "./themes");
83c7162d 42//!
2c00a5a8
XL
43//! // then load it again, automatically deserializing to a `PathBuf`.
44//! let got: PathBuf = cfg.get_deserialized("output.html.theme")?;
45//! assert_eq!(got, PathBuf::from("./themes"));
46//! # Ok(())
47//! # }
48//! # fn main() { run().unwrap() }
49//! ```
50
51#![deny(missing_docs)]
52
53use std::path::{Path, PathBuf};
54use std::fs::File;
55use std::io::Read;
56use std::env;
57use toml::{self, Value};
58use toml::value::Table;
59use toml_query::read::TomlValueReadExt;
60use toml_query::insert::TomlValueInsertExt;
61use toml_query::delete::TomlValueDeleteExt;
62use serde::{Deserialize, Deserializer, Serialize, Serializer};
63use serde_json;
64
65use errors::*;
66
67/// The overall configuration object for MDBook, essentially an in-memory
68/// representation of `book.toml`.
69#[derive(Debug, Clone, PartialEq)]
70pub struct Config {
71 /// Metadata about the book.
72 pub book: BookConfig,
73 /// Information about the build environment.
74 pub build: BuildConfig,
75 rest: Value,
76}
77
78impl Config {
79 /// Load a `Config` from some string.
80 pub fn from_str(src: &str) -> Result<Config> {
81 toml::from_str(src).chain_err(|| Error::from("Invalid configuration file"))
82 }
83
84 /// Load the configuration file from disk.
85 pub fn from_disk<P: AsRef<Path>>(config_file: P) -> Result<Config> {
86 let mut buffer = String::new();
87 File::open(config_file)
88 .chain_err(|| "Unable to open the configuration file")?
89 .read_to_string(&mut buffer)
90 .chain_err(|| "Couldn't read the file")?;
91
92 Config::from_str(&buffer)
93 }
94
95 /// Updates the `Config` from the available environment variables.
96 ///
97 /// Variables starting with `MDBOOK_` are used for configuration. The key is
98 /// created by removing the `MDBOOK_` prefix and turning the resulting
99 /// string into `kebab-case`. Double underscores (`__`) separate nested
100 /// keys, while a single underscore (`_`) is replaced with a dash (`-`).
101 ///
102 /// For example:
103 ///
104 /// - `MDBOOK_foo` -> `foo`
105 /// - `MDBOOK_FOO` -> `foo`
106 /// - `MDBOOK_FOO__BAR` -> `foo.bar`
107 /// - `MDBOOK_FOO_BAR` -> `foo-bar`
108 /// - `MDBOOK_FOO_bar__baz` -> `foo-bar.baz`
109 ///
110 /// So by setting the `MDBOOK_BOOK__TITLE` environment variable you can
111 /// override the book's title without needing to touch your `book.toml`.
112 ///
113 /// > **Note:** To facilitate setting more complex config items, the value
114 /// > of an environment variable is first parsed as JSON, falling back to a
115 /// > string if the parse fails.
116 /// >
117 /// > This means, if you so desired, you could override all book metadata
118 /// > when building the book with something like
119 /// >
120 /// > ```text
121 /// > $ export MDBOOK_BOOK="{'title': 'My Awesome Book', authors: ['Michael-F-Bryan']}"
122 /// > $ mdbook build
123 /// > ```
124 ///
125 /// The latter case may be useful in situations where `mdbook` is invoked
126 /// from a script or CI, where it sometimes isn't possible to update the
127 /// `book.toml` before building.
128 pub fn update_from_env(&mut self) {
129 debug!("Updating the config from environment variables");
130
131 let overrides = env::vars().filter_map(|(key, value)| match parse_env(&key) {
132 Some(index) => Some((index, value)),
133 None => None,
134 });
135
136 for (key, value) in overrides {
137 trace!("{} => {}", key, value);
138 let parsed_value = serde_json::from_str(&value)
139 .unwrap_or_else(|_| serde_json::Value::String(value.to_string()));
140
141 self.set(key, parsed_value).expect("unreachable");
142 }
143 }
144
145 /// Fetch an arbitrary item from the `Config` as a `toml::Value`.
146 ///
147 /// You can use dotted indices to access nested items (e.g.
148 /// `output.html.playpen` will fetch the "playpen" out of the html output
149 /// table).
150 pub fn get(&self, key: &str) -> Option<&Value> {
151 match self.rest.read(key) {
152 Ok(inner) => inner,
153 Err(_) => None,
154 }
155 }
156
157 /// Fetch a value from the `Config` so you can mutate it.
158 pub fn get_mut<'a>(&'a mut self, key: &str) -> Option<&'a mut Value> {
159 match self.rest.read_mut(key) {
160 Ok(inner) => inner,
161 Err(_) => None,
162 }
163 }
164
165 /// Convenience method for getting the html renderer's configuration.
166 ///
167 /// # Note
168 ///
169 /// This is for compatibility only. It will be removed completely once the
170 /// HTML renderer is refactored to be less coupled to `mdbook` internals.
171 #[doc(hidden)]
172 pub fn html_config(&self) -> Option<HtmlConfig> {
173 self.get_deserialized("output.html").ok()
174 }
175
176 /// Convenience function to fetch a value from the config and deserialize it
177 /// into some arbitrary type.
178 pub fn get_deserialized<'de, T: Deserialize<'de>, S: AsRef<str>>(&self, name: S) -> Result<T> {
179 let name = name.as_ref();
180
181 if let Some(value) = self.get(name) {
182 value
183 .clone()
184 .try_into()
185 .chain_err(|| "Couldn't deserialize the value")
186 } else {
187 bail!("Key not found, {:?}", name)
188 }
189 }
190
191 /// Set a config key, clobbering any existing values along the way.
192 ///
193 /// The only way this can fail is if we can't serialize `value` into a
194 /// `toml::Value`.
195 pub fn set<S: Serialize, I: AsRef<str>>(&mut self, index: I, value: S) -> Result<()> {
196 let index = index.as_ref();
197
198 let value =
199 Value::try_from(value).chain_err(|| "Unable to represent the item as a JSON Value")?;
200
201 if index.starts_with("book.") {
202 self.book.update_value(&index[5..], value);
203 } else if index.starts_with("build.") {
204 self.build.update_value(&index[6..], value);
205 } else {
206 self.rest.insert(index, value)?;
207 }
208
209 Ok(())
210 }
211
212 fn from_legacy(mut table: Value) -> Config {
213 let mut cfg = Config::default();
214
215 // we use a macro here instead of a normal loop because the $out
216 // variable can be different types. This way we can make type inference
217 // figure out what try_into() deserializes to.
218 macro_rules! get_and_insert {
219 ($table:expr, $key:expr => $out:expr) => {
220 let got = $table.as_table_mut()
221 .and_then(|t| t.remove($key))
222 .and_then(|v| v.try_into().ok());
223 if let Some(value) = got {
224 $out = value;
225 }
226 };
227 }
228
229 get_and_insert!(table, "title" => cfg.book.title);
230 get_and_insert!(table, "authors" => cfg.book.authors);
231 get_and_insert!(table, "source" => cfg.book.src);
232 get_and_insert!(table, "description" => cfg.book.description);
233
234 if let Ok(Some(dest)) = table.delete("output.html.destination") {
235 if let Ok(destination) = dest.try_into() {
236 cfg.build.build_dir = destination;
237 }
238 }
239
240 cfg.rest = table;
241 cfg
242 }
243}
244
245impl Default for Config {
246 fn default() -> Config {
247 Config {
248 book: BookConfig::default(),
249 build: BuildConfig::default(),
250 rest: Value::Table(Table::default()),
251 }
252 }
253}
254impl<'de> Deserialize<'de> for Config {
255 fn deserialize<D: Deserializer<'de>>(de: D) -> ::std::result::Result<Self, D::Error> {
256 let raw = Value::deserialize(de)?;
257
258 if is_legacy_format(&raw) {
259 warn!("It looks like you are using the legacy book.toml format.");
260 warn!("We'll parse it for now, but you should probably convert to the new format.");
261 warn!("See the mdbook documentation for more details, although as a rule of thumb");
262 warn!("just move all top level configuration entries like `title`, `author` and");
263 warn!("`description` under a table called `[book]`, move the `destination` entry");
264 warn!("from `[output.html]`, renamed to `build-dir`, under a table called");
265 warn!("`[build]`, and it should all work.");
266 warn!("Documentation: http://rust-lang-nursery.github.io/mdBook/format/config.html");
267 return Ok(Config::from_legacy(raw));
268 }
269
270 let mut table = match raw {
271 Value::Table(t) => t,
272 _ => {
273 use serde::de::Error;
274 return Err(D::Error::custom(
275 "A config file should always be a toml table",
276 ));
277 }
278 };
279
280 let book: BookConfig = table
281 .remove("book")
282 .and_then(|value| value.try_into().ok())
283 .unwrap_or_default();
284
285 let build: BuildConfig = table
286 .remove("build")
287 .and_then(|value| value.try_into().ok())
288 .unwrap_or_default();
289
290 Ok(Config {
291 book: book,
292 build: build,
293 rest: Value::Table(table),
294 })
295 }
296}
297
298impl Serialize for Config {
299 fn serialize<S: Serializer>(&self, s: S) -> ::std::result::Result<S::Ok, S::Error> {
300 use serde::ser::Error;
301
302 let mut table = self.rest.clone();
303
304 let book_config = match Value::try_from(self.book.clone()) {
305 Ok(cfg) => cfg,
306 Err(_) => {
307 return Err(S::Error::custom("Unable to serialize the BookConfig"));
308 }
309 };
310
311 table.insert("book", book_config).expect("unreachable");
312 table.serialize(s)
313 }
314}
315
316fn parse_env(key: &str) -> Option<String> {
317 const PREFIX: &str = "MDBOOK_";
318
319 if key.starts_with(PREFIX) {
320 let key = &key[PREFIX.len()..];
321
322 Some(key.to_lowercase().replace("__", ".").replace("_", "-"))
323 } else {
324 None
325 }
326}
327
328fn is_legacy_format(table: &Value) -> bool {
329 let legacy_items = [
330 "title",
331 "authors",
332 "source",
333 "description",
334 "output.html.destination",
335 ];
336
337 for item in &legacy_items {
338 if let Ok(Some(_)) = table.read(item) {
339 return true;
340 }
341 }
342
343 false
344}
345
346/// Configuration options which are specific to the book and required for
347/// loading it from disk.
348#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
349#[serde(default, rename_all = "kebab-case")]
350pub struct BookConfig {
351 /// The book's title.
352 pub title: Option<String>,
353 /// The book's authors.
354 pub authors: Vec<String>,
355 /// An optional description for the book.
356 pub description: Option<String>,
357 /// Location of the book source relative to the book's root directory.
358 pub src: PathBuf,
359 /// Does this book support more than one language?
360 pub multilingual: bool,
361}
362
363impl Default for BookConfig {
364 fn default() -> BookConfig {
365 BookConfig {
366 title: None,
367 authors: Vec::new(),
368 description: None,
369 src: PathBuf::from("src"),
370 multilingual: false,
371 }
372 }
373}
374
375/// Configuration for the build procedure.
376#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
377#[serde(default, rename_all = "kebab-case")]
378pub struct BuildConfig {
379 /// Where to put built artefacts relative to the book's root directory.
380 pub build_dir: PathBuf,
381 /// Should non-existent markdown files specified in `SETTINGS.md` be created
382 /// if they don't exist?
383 pub create_missing: bool,
384 /// Which preprocessors should be applied
385 pub preprocess: Option<Vec<String>>,
2c00a5a8
XL
386}
387
388impl Default for BuildConfig {
389 fn default() -> BuildConfig {
390 BuildConfig {
391 build_dir: PathBuf::from("book"),
392 create_missing: true,
393 preprocess: None,
394 }
395 }
396}
397
398/// Configuration for the HTML renderer.
399#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
400#[serde(default, rename_all = "kebab-case")]
401pub struct HtmlConfig {
402 /// The theme directory, if specified.
403 pub theme: Option<PathBuf>,
404 /// Use "smart quotes" instead of the usual `"` character.
405 pub curly_quotes: bool,
406 /// Should mathjax be enabled?
407 pub mathjax_support: bool,
408 /// An optional google analytics code.
409 pub google_analytics: Option<String>,
410 /// Additional CSS stylesheets to include in the rendered page's `<head>`.
411 pub additional_css: Vec<PathBuf>,
83c7162d 412 /// Additional JS scripts to include at the bottom of the rendered page's
2c00a5a8
XL
413 /// `<body>`.
414 pub additional_js: Vec<PathBuf>,
415 /// Playpen settings.
416 pub playpen: Playpen,
417 /// This is used as a bit of a workaround for the `mdbook serve` command.
418 /// Basically, because you set the websocket port from the command line, the
419 /// `mdbook serve` command needs a way to let the HTML renderer know where
420 /// to point livereloading at, if it has been enabled.
421 ///
422 /// This config item *should not be edited* by the end user.
423 #[doc(hidden)]
424 pub livereload_url: Option<String>,
425 /// Should section labels be rendered?
426 pub no_section_label: bool,
83c7162d
XL
427 /// Search settings. If `None`, the default will be used.
428 pub search: Option<Search>,
429}
430
431impl HtmlConfig {
432 /// Returns the directory of theme from the provided root directory. If the
433 /// directory is not present it will append the default directory of "theme"
434 pub fn theme_dir(&self, root: &PathBuf) -> PathBuf {
435 match self.theme {
436 Some(ref d) => root.join(d),
437 None => root.join("theme"),
438 }
439 }
2c00a5a8
XL
440}
441
442/// Configuration for tweaking how the the HTML renderer handles the playpen.
443#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
444#[serde(default, rename_all = "kebab-case")]
445pub struct Playpen {
83c7162d 446 /// Should playpen snippets be editable? Default: `false`.
2c00a5a8 447 pub editable: bool,
83c7162d
XL
448 /// Copy JavaScript files for the editor to the output directory?
449 /// Default: `true`.
450 pub copy_js: bool,
2c00a5a8
XL
451}
452
453impl Default for Playpen {
454 fn default() -> Playpen {
455 Playpen {
2c00a5a8 456 editable: false,
83c7162d
XL
457 copy_js: true,
458 }
459 }
460}
461
462/// Configuration of the search functionality of the HTML renderer.
463#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
464#[serde(default, rename_all = "kebab-case")]
465pub struct Search {
466 /// Maximum number of visible results. Default: `30`.
467 pub limit_results: u32,
468 /// The number of words used for a search result teaser. Default: `30`,
469 pub teaser_word_count: u32,
470 /// Define the logical link between multiple search words.
471 /// If true, all search words must appear in each result. Default: `true`.
472 pub use_boolean_and: bool,
473 /// Boost factor for the search result score if a search word appears in the header.
474 /// Default: `2`.
475 pub boost_title: u8,
476 /// Boost factor for the search result score if a search word appears in the hierarchy.
477 /// The hierarchy contains all titles of the parent documents and all parent headings.
478 /// Default: `1`.
479 pub boost_hierarchy: u8,
480 /// Boost factor for the search result score if a search word appears in the text.
481 /// Default: `1`.
482 pub boost_paragraph: u8,
483 /// True if the searchword `micro` should match `microwave`. Default: `true`.
484 pub expand: bool,
485 /// Documents are split into smaller parts, seperated by headings. This defines, until which
486 /// level of heading documents should be split. Default: `3`. (`### This is a level 3 heading`)
487 pub heading_split_level: u8,
488 /// Copy JavaScript files for the search functionality to the output directory?
489 /// Default: `true`.
490 pub copy_js: bool,
491}
492
493impl Default for Search {
494 fn default() -> Search {
495 // Please update the documentation of `Search` when changing values!
496 Search {
497 limit_results: 30,
498 teaser_word_count: 30,
499 use_boolean_and: false,
500 boost_title: 2,
501 boost_hierarchy: 1,
502 boost_paragraph: 1,
503 expand: true,
504 heading_split_level: 3,
505 copy_js: true,
2c00a5a8
XL
506 }
507 }
508}
509
510/// Allows you to "update" any arbitrary field in a struct by round-tripping via
511/// a `toml::Value`.
512///
513/// This is definitely not the most performant way to do things, which means you
514/// should probably keep it away from tight loops...
515trait Updateable<'de>: Serialize + Deserialize<'de> {
516 fn update_value<S: Serialize>(&mut self, key: &str, value: S) {
517 let mut raw = Value::try_from(&self).expect("unreachable");
518
519 {
520 if let Ok(value) = Value::try_from(value) {
521 let _ = raw.insert(key, value);
522 } else {
523 return;
524 }
525 }
526
527 if let Ok(updated) = raw.try_into() {
528 *self = updated;
529 }
530 }
531}
532
533impl<'de, T> Updateable<'de> for T
534where
535 T: Serialize + Deserialize<'de>,
536{
537}
538
539#[cfg(test)]
540mod tests {
541 use super::*;
542
543 const COMPLEX_CONFIG: &'static str = r#"
544 [book]
545 title = "Some Book"
546 authors = ["Michael-F-Bryan <michaelfbryan@gmail.com>"]
547 description = "A completely useless book"
548 multilingual = true
549 src = "source"
550
551 [build]
552 build-dir = "outputs"
553 create-missing = false
554 preprocess = ["first_preprocessor", "second_preprocessor"]
555
556 [output.html]
557 theme = "./themedir"
558 curly-quotes = true
559 google-analytics = "123456"
560 additional-css = ["./foo/bar/baz.css"]
561
562 [output.html.playpen]
563 editable = true
564 editor = "ace"
565 "#;
566
567 #[test]
568 fn load_a_complex_config_file() {
569 let src = COMPLEX_CONFIG;
570
571 let book_should_be = BookConfig {
572 title: Some(String::from("Some Book")),
573 authors: vec![String::from("Michael-F-Bryan <michaelfbryan@gmail.com>")],
574 description: Some(String::from("A completely useless book")),
575 multilingual: true,
576 src: PathBuf::from("source"),
577 ..Default::default()
578 };
579 let build_should_be = BuildConfig {
580 build_dir: PathBuf::from("outputs"),
581 create_missing: false,
83c7162d
XL
582 preprocess: Some(vec![
583 "first_preprocessor".to_string(),
584 "second_preprocessor".to_string(),
585 ]),
2c00a5a8
XL
586 };
587 let playpen_should_be = Playpen {
588 editable: true,
83c7162d 589 copy_js: true,
2c00a5a8
XL
590 };
591 let html_should_be = HtmlConfig {
592 curly_quotes: true,
593 google_analytics: Some(String::from("123456")),
594 additional_css: vec![PathBuf::from("./foo/bar/baz.css")],
595 theme: Some(PathBuf::from("./themedir")),
596 playpen: playpen_should_be,
597 ..Default::default()
598 };
599
600 let got = Config::from_str(src).unwrap();
601
602 assert_eq!(got.book, book_should_be);
603 assert_eq!(got.build, build_should_be);
604 assert_eq!(got.html_config().unwrap(), html_should_be);
605 }
606
607 #[test]
608 fn load_arbitrary_output_type() {
609 #[derive(Debug, Deserialize, PartialEq)]
610 struct RandomOutput {
611 foo: u32,
612 bar: String,
613 baz: Vec<bool>,
614 }
615
616 let src = r#"
617 [output.random]
618 foo = 5
619 bar = "Hello World"
620 baz = [true, true, false]
621 "#;
622
623 let should_be = RandomOutput {
624 foo: 5,
625 bar: String::from("Hello World"),
626 baz: vec![true, true, false],
627 };
628
629 let cfg = Config::from_str(src).unwrap();
630 let got: RandomOutput = cfg.get_deserialized("output.random").unwrap();
631
632 assert_eq!(got, should_be);
633
634 let baz: Vec<bool> = cfg.get_deserialized("output.random.baz").unwrap();
635 let baz_should_be = vec![true, true, false];
636
637 assert_eq!(baz, baz_should_be);
638 }
639
640 #[test]
641 fn mutate_some_stuff() {
642 // really this is just a sanity check to make sure the borrow checker
643 // is happy...
644 let src = COMPLEX_CONFIG;
645 let mut config = Config::from_str(src).unwrap();
646 let key = "output.html.playpen.editable";
647
648 assert_eq!(config.get(key).unwrap(), &Value::Boolean(true));
649 *config.get_mut(key).unwrap() = Value::Boolean(false);
650 assert_eq!(config.get(key).unwrap(), &Value::Boolean(false));
651 }
652
653 /// The config file format has slightly changed (metadata stuff is now under
654 /// the `book` table instead of being at the top level) so we're adding a
655 /// **temporary** compatibility check. You should be able to still load the
656 /// old format, emitting a warning.
657 #[test]
658 fn can_still_load_the_previous_format() {
659 let src = r#"
660 title = "mdBook Documentation"
661 description = "Create book from markdown files. Like Gitbook but implemented in Rust"
662 authors = ["Mathieu David"]
663 source = "./source"
664
665 [output.html]
666 destination = "my-book" # the output files will be generated in `root/my-book` instead of `root/book`
667 theme = "my-theme"
668 curly-quotes = true
669 google-analytics = "123456"
670 additional-css = ["custom.css", "custom2.css"]
671 additional-js = ["custom.js"]
672 "#;
673
674 let book_should_be = BookConfig {
675 title: Some(String::from("mdBook Documentation")),
676 description: Some(String::from(
677 "Create book from markdown files. Like Gitbook but implemented in Rust",
678 )),
679 authors: vec![String::from("Mathieu David")],
680 src: PathBuf::from("./source"),
681 ..Default::default()
682 };
683
684 let build_should_be = BuildConfig {
685 build_dir: PathBuf::from("my-book"),
686 create_missing: true,
687 preprocess: None,
688 };
689
690 let html_should_be = HtmlConfig {
691 theme: Some(PathBuf::from("my-theme")),
692 curly_quotes: true,
693 google_analytics: Some(String::from("123456")),
694 additional_css: vec![PathBuf::from("custom.css"), PathBuf::from("custom2.css")],
695 additional_js: vec![PathBuf::from("custom.js")],
696 ..Default::default()
697 };
698
699 let got = Config::from_str(src).unwrap();
700 assert_eq!(got.book, book_should_be);
701 assert_eq!(got.build, build_should_be);
702 assert_eq!(got.html_config().unwrap(), html_should_be);
703 }
704
705 #[test]
706 fn set_a_config_item() {
707 let mut cfg = Config::default();
708 let key = "foo.bar.baz";
709 let value = "Something Interesting";
710
711 assert!(cfg.get(key).is_none());
712 cfg.set(key, value).unwrap();
713
714 let got: String = cfg.get_deserialized(key).unwrap();
715 assert_eq!(got, value);
716 }
717
718 #[test]
719 fn parse_env_vars() {
720 let inputs = vec![
721 ("FOO", None),
722 ("MDBOOK_foo", Some("foo")),
723 ("MDBOOK_FOO__bar__baz", Some("foo.bar.baz")),
724 ("MDBOOK_FOO_bar__baz", Some("foo-bar.baz")),
725 ];
726
727 for (src, should_be) in inputs {
728 let got = parse_env(src);
729 let should_be = should_be.map(|s| s.to_string());
730
731 assert_eq!(got, should_be);
732 }
733 }
734
735 fn encode_env_var(key: &str) -> String {
736 format!(
737 "MDBOOK_{}",
738 key.to_uppercase().replace('.', "__").replace("-", "_")
739 )
740 }
741
742 #[test]
743 fn update_config_using_env_var() {
744 let mut cfg = Config::default();
745 let key = "foo.bar";
746 let value = "baz";
747
748 assert!(cfg.get(key).is_none());
749
750 let encoded_key = encode_env_var(key);
751 env::set_var(encoded_key, value);
752
753 cfg.update_from_env();
754
755 assert_eq!(cfg.get_deserialized::<String, _>(key).unwrap(), value);
756 }
757
758 #[test]
759 fn update_config_using_env_var_and_complex_value() {
760 let mut cfg = Config::default();
761 let key = "foo-bar.baz";
762 let value = json!({"array": [1, 2, 3], "number": 3.14});
763 let value_str = serde_json::to_string(&value).unwrap();
764
765 assert!(cfg.get(key).is_none());
766
767 let encoded_key = encode_env_var(key);
768 env::set_var(encoded_key, value_str);
769
770 cfg.update_from_env();
771
772 assert_eq!(
773 cfg.get_deserialized::<serde_json::Value, _>(key).unwrap(),
774 value
775 );
776 }
777
778 #[test]
779 fn update_book_title_via_env() {
780 let mut cfg = Config::default();
781 let should_be = "Something else".to_string();
782
783 assert_ne!(cfg.book.title, Some(should_be.clone()));
784
785 env::set_var("MDBOOK_BOOK__TITLE", &should_be);
786 cfg.update_from_env();
787
788 assert_eq!(cfg.book.title, Some(should_be));
789 }
790}