1#![deny(missing_docs)]
51
52use log::{debug, trace, warn};
53use serde::{Deserialize, Deserializer, Serialize, Serializer};
54use std::collections::HashMap;
55use std::env;
56use std::fs::File;
57use std::io::Read;
58use std::path::{Path, PathBuf};
59use std::str::FromStr;
60use toml::value::Table;
61use toml::{self, Value};
62
63use crate::errors::*;
64use crate::utils::{self, toml_ext::TomlExt};
65
66#[derive(Debug, Clone, PartialEq)]
69pub struct Config {
70 pub book: BookConfig,
72 pub build: BuildConfig,
74 pub rust: RustConfig,
76 rest: Value,
77}
78
79impl FromStr for Config {
80 type Err = Error;
81
82 fn from_str(src: &str) -> Result<Self> {
84 toml::from_str(src).with_context(|| "Invalid configuration file")
85 }
86}
87
88impl Config {
89 pub fn from_disk<P: AsRef<Path>>(config_file: P) -> Result<Config> {
91 let mut buffer = String::new();
92 File::open(config_file)
93 .with_context(|| "Unable to open the configuration file")?
94 .read_to_string(&mut buffer)
95 .with_context(|| "Couldn't read the file")?;
96
97 Config::from_str(&buffer)
98 }
99
100 pub fn update_from_env(&mut self) {
134 debug!("Updating the config from environment variables");
135
136 let overrides =
137 env::vars().filter_map(|(key, value)| parse_env(&key).map(|index| (index, value)));
138
139 for (key, value) in overrides {
140 trace!("{} => {}", key, value);
141 let parsed_value = serde_json::from_str(&value)
142 .unwrap_or_else(|_| serde_json::Value::String(value.to_string()));
143
144 if key == "book" || key == "build" {
145 if let serde_json::Value::Object(ref map) = parsed_value {
146 for (k, v) in map {
148 let full_key = format!("{}.{}", key, k);
149 self.set(&full_key, v).expect("unreachable");
150 }
151 return;
152 }
153 }
154
155 self.set(key, parsed_value).expect("unreachable");
156 }
157 }
158
159 pub fn get(&self, key: &str) -> Option<&Value> {
165 self.rest.read(key)
166 }
167
168 pub fn get_mut(&mut self, key: &str) -> Option<&mut Value> {
170 self.rest.read_mut(key)
171 }
172
173 #[doc(hidden)]
180 pub fn html_config(&self) -> Option<HtmlConfig> {
181 match self
182 .get_deserialized_opt("output.html")
183 .with_context(|| "Parsing configuration [output.html]")
184 {
185 Ok(Some(config)) => Some(config),
186 Ok(None) => None,
187 Err(e) => {
188 utils::log_backtrace(&e);
189 None
190 }
191 }
192 }
193
194 #[deprecated = "use get_deserialized_opt instead"]
196 pub fn get_deserialized<'de, T: Deserialize<'de>, S: AsRef<str>>(&self, name: S) -> Result<T> {
197 let name = name.as_ref();
198 match self.get_deserialized_opt(name)? {
199 Some(value) => Ok(value),
200 None => bail!("Key not found, {:?}", name),
201 }
202 }
203
204 pub fn get_deserialized_opt<'de, T: Deserialize<'de>, S: AsRef<str>>(
207 &self,
208 name: S,
209 ) -> Result<Option<T>> {
210 let name = name.as_ref();
211 self.get(name)
212 .map(|value| {
213 value
214 .clone()
215 .try_into()
216 .with_context(|| "Couldn't deserialize the value")
217 })
218 .transpose()
219 }
220
221 pub fn set<S: Serialize, I: AsRef<str>>(&mut self, index: I, value: S) -> Result<()> {
226 let index = index.as_ref();
227
228 let value = Value::try_from(value)
229 .with_context(|| "Unable to represent the item as a JSON Value")?;
230
231 if let Some(key) = index.strip_prefix("book.") {
232 self.book.update_value(key, value);
233 } else if let Some(key) = index.strip_prefix("build.") {
234 self.build.update_value(key, value);
235 } else {
236 self.rest.insert(index, value);
237 }
238
239 Ok(())
240 }
241
242 pub fn get_renderer<I: AsRef<str>>(&self, index: I) -> Option<&Table> {
244 let key = format!("output.{}", index.as_ref());
245 self.get(&key).and_then(Value::as_table)
246 }
247
248 pub fn get_preprocessor<I: AsRef<str>>(&self, index: I) -> Option<&Table> {
250 let key = format!("preprocessor.{}", index.as_ref());
251 self.get(&key).and_then(Value::as_table)
252 }
253
254 fn from_legacy(mut table: Value) -> Config {
255 let mut cfg = Config::default();
256
257 macro_rules! get_and_insert {
261 ($table:expr, $key:expr => $out:expr) => {
262 let got = $table
263 .as_table_mut()
264 .and_then(|t| t.remove($key))
265 .and_then(|v| v.try_into().ok());
266 if let Some(value) = got {
267 $out = value;
268 }
269 };
270 }
271
272 get_and_insert!(table, "title" => cfg.book.title);
273 get_and_insert!(table, "authors" => cfg.book.authors);
274 get_and_insert!(table, "source" => cfg.book.src);
275 get_and_insert!(table, "description" => cfg.book.description);
276
277 if let Some(dest) = table.delete("output.html.destination") {
278 if let Ok(destination) = dest.try_into() {
279 cfg.build.build_dir = destination;
280 }
281 }
282
283 cfg.rest = table;
284 cfg
285 }
286}
287
288impl Default for Config {
289 fn default() -> Config {
290 Config {
291 book: BookConfig::default(),
292 build: BuildConfig::default(),
293 rust: RustConfig::default(),
294 rest: Value::Table(Table::default()),
295 }
296 }
297}
298
299impl<'de> serde::Deserialize<'de> for Config {
300 fn deserialize<D: Deserializer<'de>>(de: D) -> std::result::Result<Self, D::Error> {
301 let raw = Value::deserialize(de)?;
302
303 if is_legacy_format(&raw) {
304 warn!("It looks like you are using the legacy book.toml format.");
305 warn!("We'll parse it for now, but you should probably convert to the new format.");
306 warn!("See the mdbook documentation for more details, although as a rule of thumb");
307 warn!("just move all top level configuration entries like `title`, `author` and");
308 warn!("`description` under a table called `[book]`, move the `destination` entry");
309 warn!("from `[output.html]`, renamed to `build-dir`, under a table called");
310 warn!("`[build]`, and it should all work.");
311 warn!("Documentation: http://rust-lang.github.io/mdBook/format/config.html");
312 return Ok(Config::from_legacy(raw));
313 }
314
315 use serde::de::Error;
316 let mut table = match raw {
317 Value::Table(t) => t,
318 _ => {
319 return Err(D::Error::custom(
320 "A config file should always be a toml table",
321 ));
322 }
323 };
324
325 let book: BookConfig = table
326 .remove("book")
327 .map(|book| book.try_into().map_err(D::Error::custom))
328 .transpose()?
329 .unwrap_or_default();
330
331 let build: BuildConfig = table
332 .remove("build")
333 .map(|build| build.try_into().map_err(D::Error::custom))
334 .transpose()?
335 .unwrap_or_default();
336
337 let rust: RustConfig = table
338 .remove("rust")
339 .map(|rust| rust.try_into().map_err(D::Error::custom))
340 .transpose()?
341 .unwrap_or_default();
342
343 Ok(Config {
344 book,
345 build,
346 rust,
347 rest: Value::Table(table),
348 })
349 }
350}
351
352impl Serialize for Config {
353 fn serialize<S: Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
354 let mut table = self.rest.clone();
356
357 let book_config = Value::try_from(&self.book).expect("should always be serializable");
358 table.insert("book", book_config);
359
360 if self.build != BuildConfig::default() {
361 let build_config = Value::try_from(&self.build).expect("should always be serializable");
362 table.insert("build", build_config);
363 }
364
365 if self.rust != RustConfig::default() {
366 let rust_config = Value::try_from(&self.rust).expect("should always be serializable");
367 table.insert("rust", rust_config);
368 }
369
370 table.serialize(s)
371 }
372}
373
374fn parse_env(key: &str) -> Option<String> {
375 key.strip_prefix("MDBOOK_")
376 .map(|key| key.to_lowercase().replace("__", ".").replace('_', "-"))
377}
378
379fn is_legacy_format(table: &Value) -> bool {
380 let legacy_items = [
381 "title",
382 "authors",
383 "source",
384 "description",
385 "output.html.destination",
386 ];
387
388 for item in &legacy_items {
389 if table.read(item).is_some() {
390 return true;
391 }
392 }
393
394 false
395}
396
397#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
400#[serde(default, rename_all = "kebab-case")]
401pub struct BookConfig {
402 pub title: Option<String>,
404 pub authors: Vec<String>,
406 pub description: Option<String>,
408 pub src: PathBuf,
410 pub multilingual: bool,
412 pub language: Option<String>,
414}
415
416impl Default for BookConfig {
417 fn default() -> BookConfig {
418 BookConfig {
419 title: None,
420 authors: Vec::new(),
421 description: None,
422 src: PathBuf::from("src"),
423 multilingual: false,
424 language: Some(String::from("en")),
425 }
426 }
427}
428
429#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
431#[serde(default, rename_all = "kebab-case")]
432pub struct BuildConfig {
433 pub build_dir: PathBuf,
435 pub create_missing: bool,
438 pub use_default_preprocessors: bool,
441 pub extra_watch_dirs: Vec<PathBuf>,
443}
444
445impl Default for BuildConfig {
446 fn default() -> BuildConfig {
447 BuildConfig {
448 build_dir: PathBuf::from("book"),
449 create_missing: true,
450 use_default_preprocessors: true,
451 extra_watch_dirs: Vec::new(),
452 }
453 }
454}
455
456#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
458#[serde(default, rename_all = "kebab-case")]
459pub struct RustConfig {
460 pub edition: Option<RustEdition>,
462}
463
464#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
465pub enum RustEdition {
467 #[serde(rename = "2021")]
469 E2021,
470 #[serde(rename = "2018")]
472 E2018,
473 #[serde(rename = "2015")]
475 E2015,
476}
477
478#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
480#[serde(default, rename_all = "kebab-case")]
481pub struct HtmlConfig {
482 pub theme: Option<PathBuf>,
484 pub default_theme: Option<String>,
486 pub preferred_dark_theme: Option<String>,
489 pub curly_quotes: bool,
491 pub mathjax_support: bool,
493 pub copy_fonts: bool,
495 pub google_analytics: Option<String>,
497 pub additional_css: Vec<PathBuf>,
499 pub additional_js: Vec<PathBuf>,
502 pub fold: Fold,
504 #[serde(alias = "playpen")]
506 pub playground: Playground,
507 pub print: Print,
509 pub no_section_label: bool,
511 pub search: Option<Search>,
513 pub git_repository_url: Option<String>,
515 pub git_repository_icon: Option<String>,
518 pub input_404: Option<String>,
520 pub site_url: Option<String>,
522 pub cname: Option<String>,
529 pub edit_url_template: Option<String>,
533 #[doc(hidden)]
539 pub live_reload_endpoint: Option<String>,
540 pub redirect: HashMap<String, String>,
543}
544
545impl Default for HtmlConfig {
546 fn default() -> HtmlConfig {
547 HtmlConfig {
548 theme: None,
549 default_theme: None,
550 preferred_dark_theme: None,
551 curly_quotes: false,
552 mathjax_support: false,
553 copy_fonts: true,
554 google_analytics: None,
555 additional_css: Vec::new(),
556 additional_js: Vec::new(),
557 fold: Fold::default(),
558 playground: Playground::default(),
559 print: Print::default(),
560 no_section_label: false,
561 search: None,
562 git_repository_url: None,
563 git_repository_icon: None,
564 edit_url_template: None,
565 input_404: None,
566 site_url: None,
567 cname: None,
568 live_reload_endpoint: None,
569 redirect: HashMap::new(),
570 }
571 }
572}
573
574impl HtmlConfig {
575 pub fn theme_dir(&self, root: &Path) -> PathBuf {
578 match self.theme {
579 Some(ref d) => root.join(d),
580 None => root.join("theme"),
581 }
582 }
583}
584
585#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
587#[serde(default, rename_all = "kebab-case")]
588pub struct Print {
589 pub enable: bool,
591 pub page_break: bool,
593}
594
595impl Default for Print {
596 fn default() -> Self {
597 Self {
598 enable: true,
599 page_break: true,
600 }
601 }
602}
603
604#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
606#[serde(default, rename_all = "kebab-case")]
607pub struct Fold {
608 pub enable: bool,
610 pub level: u8,
614}
615
616#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
618#[serde(default, rename_all = "kebab-case")]
619pub struct Playground {
620 pub editable: bool,
622 pub copyable: bool,
624 pub copy_js: bool,
627 pub line_numbers: bool,
629 pub runnable: bool,
631 pub languages: Vec<PlaygroundLanguage>,
635}
636
637impl Default for Playground {
638 fn default() -> Playground {
639 Playground {
640 editable: false,
641 copyable: true,
642 copy_js: true,
643 line_numbers: false,
644 runnable: true,
645 languages: vec![],
646 }
647 }
648}
649
650#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
652#[serde(default, rename_all = "kebab-case")]
653pub struct PlaygroundLanguage {
654 pub language: String,
656 pub hidelines: String,
659 pub endpoint: String,
661}
662
663impl Default for PlaygroundLanguage {
664 fn default() -> PlaygroundLanguage {
665 PlaygroundLanguage {
666 language: "rust".to_string(),
667 hidelines: "#".to_string(),
668 endpoint: "https://play.rust-lang.org/evaluate.json".to_string()
669 }
670 }
671}
672
673#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
675#[serde(default, rename_all = "kebab-case")]
676pub struct Search {
677 pub enable: bool,
679 pub limit_results: u32,
681 pub teaser_word_count: u32,
683 pub use_boolean_and: bool,
686 pub boost_title: u8,
689 pub boost_hierarchy: u8,
693 pub boost_paragraph: u8,
696 pub expand: bool,
698 pub heading_split_level: u8,
701 pub copy_js: bool,
704}
705
706impl Default for Search {
707 fn default() -> Search {
708 Search {
710 enable: true,
711 limit_results: 30,
712 teaser_word_count: 30,
713 use_boolean_and: false,
714 boost_title: 2,
715 boost_hierarchy: 1,
716 boost_paragraph: 1,
717 expand: true,
718 heading_split_level: 3,
719 copy_js: true,
720 }
721 }
722}
723
724trait Updateable<'de>: Serialize + Deserialize<'de> {
730 fn update_value<S: Serialize>(&mut self, key: &str, value: S) {
731 let mut raw = Value::try_from(&self).expect("unreachable");
732
733 if let Ok(value) = Value::try_from(value) {
734 let _ = raw.insert(key, value);
735 } else {
736 return;
737 }
738
739 if let Ok(updated) = raw.try_into() {
740 *self = updated;
741 }
742 }
743}
744
745impl<'de, T> Updateable<'de> for T where T: Serialize + Deserialize<'de> {}
746
747#[cfg(test)]
748mod tests {
749 use super::*;
750 use crate::utils::fs::get_404_output_file;
751 use serde_json::json;
752
753 const COMPLEX_CONFIG: &str = r##"
754 [book]
755 title = "Some Book"
756 authors = ["Michael-F-Bryan <michaelfbryan@gmail.com>"]
757 description = "A completely useless book"
758 multilingual = true
759 src = "source"
760 language = "ja"
761
762 [build]
763 build-dir = "outputs"
764 create-missing = false
765 use-default-preprocessors = true
766
767 [output.html]
768 theme = "./themedir"
769 default-theme = "rust"
770 curly-quotes = true
771 google-analytics = "123456"
772 additional-css = ["./foo/bar/baz.css"]
773 git-repository-url = "https://foo.com/"
774 git-repository-icon = "fa-code-fork"
775
776 [output.html.playground]
777 editable = true
778 editor = "ace"
779
780 [[output.html.playground.languages]]
781 language = "rust"
782 hidelines = "#"
783 endpoint = "https://play.rust-lang.org/evaluate.json"
784
785 [[output.html.playground.languages]]
786 language = "c"
787 hidelines = "$"
788 endpoint = "https://some-endpoint.com/evaluate"
789
790 [output.html.redirect]
791 "index.html" = "overview.html"
792 "nexted/page.md" = "https://rust-lang.org/"
793
794 [preprocessor.first]
795
796 [preprocessor.second]
797 "##;
798
799 #[test]
800 fn load_a_complex_config_file() {
801 let src = COMPLEX_CONFIG;
802
803 let book_should_be = BookConfig {
804 title: Some(String::from("Some Book")),
805 authors: vec![String::from("Michael-F-Bryan <michaelfbryan@gmail.com>")],
806 description: Some(String::from("A completely useless book")),
807 multilingual: true,
808 src: PathBuf::from("source"),
809 language: Some(String::from("ja")),
810 };
811 let build_should_be = BuildConfig {
812 build_dir: PathBuf::from("outputs"),
813 create_missing: false,
814 use_default_preprocessors: true,
815 extra_watch_dirs: Vec::new(),
816 };
817 let rust_should_be = RustConfig { edition: None };
818 let playground_should_be = Playground {
819 editable: true,
820 copyable: true,
821 copy_js: true,
822 line_numbers: false,
823 runnable: true,
824 languages: vec![PlaygroundLanguage::default(), PlaygroundLanguage {
825 language: String::from("c"),
826 hidelines:String::from("$"),
827 endpoint: String::from("https://some-endpoint.com/evaluate"),
828 }],
829 };
830 let html_should_be = HtmlConfig {
831 curly_quotes: true,
832 google_analytics: Some(String::from("123456")),
833 additional_css: vec![PathBuf::from("./foo/bar/baz.css")],
834 theme: Some(PathBuf::from("./themedir")),
835 default_theme: Some(String::from("rust")),
836 playground: playground_should_be,
837 git_repository_url: Some(String::from("https://foo.com/")),
838 git_repository_icon: Some(String::from("fa-code-fork")),
839 redirect: vec![
840 (String::from("index.html"), String::from("overview.html")),
841 (
842 String::from("nexted/page.md"),
843 String::from("https://rust-lang.org/"),
844 ),
845 ]
846 .into_iter()
847 .collect(),
848 ..Default::default()
849 };
850
851 let got = Config::from_str(src).unwrap();
852
853 assert_eq!(got.book, book_should_be);
854 assert_eq!(got.build, build_should_be);
855 assert_eq!(got.rust, rust_should_be);
856 assert_eq!(got.html_config().unwrap(), html_should_be);
857 }
858
859 #[test]
860 fn disable_runnable() {
861 let src = r#"
862 [book]
863 title = "Some Book"
864 description = "book book book"
865 authors = ["Shogo Takata"]
866
867 [output.html.playground]
868 runnable = false
869 "#;
870
871 let got = Config::from_str(src).unwrap();
872 assert!(!got.html_config().unwrap().playground.runnable);
873 }
874
875 #[test]
876 fn edition_2015() {
877 let src = r#"
878 [book]
879 title = "mdBook Documentation"
880 description = "Create book from markdown files. Like Gitbook but implemented in Rust"
881 authors = ["Mathieu David"]
882 src = "./source"
883 [rust]
884 edition = "2015"
885 "#;
886
887 let book_should_be = BookConfig {
888 title: Some(String::from("mdBook Documentation")),
889 description: Some(String::from(
890 "Create book from markdown files. Like Gitbook but implemented in Rust",
891 )),
892 authors: vec![String::from("Mathieu David")],
893 src: PathBuf::from("./source"),
894 ..Default::default()
895 };
896
897 let got = Config::from_str(src).unwrap();
898 assert_eq!(got.book, book_should_be);
899
900 let rust_should_be = RustConfig {
901 edition: Some(RustEdition::E2015),
902 };
903 let got = Config::from_str(src).unwrap();
904 assert_eq!(got.rust, rust_should_be);
905 }
906
907 #[test]
908 fn edition_2018() {
909 let src = r#"
910 [book]
911 title = "mdBook Documentation"
912 description = "Create book from markdown files. Like Gitbook but implemented in Rust"
913 authors = ["Mathieu David"]
914 src = "./source"
915 [rust]
916 edition = "2018"
917 "#;
918
919 let rust_should_be = RustConfig {
920 edition: Some(RustEdition::E2018),
921 };
922
923 let got = Config::from_str(src).unwrap();
924 assert_eq!(got.rust, rust_should_be);
925 }
926
927 #[test]
928 fn edition_2021() {
929 let src = r#"
930 [book]
931 title = "mdBook Documentation"
932 description = "Create book from markdown files. Like Gitbook but implemented in Rust"
933 authors = ["Mathieu David"]
934 src = "./source"
935 [rust]
936 edition = "2021"
937 "#;
938
939 let rust_should_be = RustConfig {
940 edition: Some(RustEdition::E2021),
941 };
942
943 let got = Config::from_str(src).unwrap();
944 assert_eq!(got.rust, rust_should_be);
945 }
946
947 #[test]
948 fn load_arbitrary_output_type() {
949 #[derive(Debug, Deserialize, PartialEq)]
950 struct RandomOutput {
951 foo: u32,
952 bar: String,
953 baz: Vec<bool>,
954 }
955
956 let src = r#"
957 [output.random]
958 foo = 5
959 bar = "Hello World"
960 baz = [true, true, false]
961 "#;
962
963 let should_be = RandomOutput {
964 foo: 5,
965 bar: String::from("Hello World"),
966 baz: vec![true, true, false],
967 };
968
969 let cfg = Config::from_str(src).unwrap();
970 let got: RandomOutput = cfg.get_deserialized_opt("output.random").unwrap().unwrap();
971
972 assert_eq!(got, should_be);
973
974 let got_baz: Vec<bool> = cfg
975 .get_deserialized_opt("output.random.baz")
976 .unwrap()
977 .unwrap();
978 let baz_should_be = vec![true, true, false];
979
980 assert_eq!(got_baz, baz_should_be);
981 }
982
983 #[test]
984 fn mutate_some_stuff() {
985 let src = COMPLEX_CONFIG;
988 let mut config = Config::from_str(src).unwrap();
989 let key = "output.html.playground.editable";
990
991 assert_eq!(config.get(key).unwrap(), &Value::Boolean(true));
992 *config.get_mut(key).unwrap() = Value::Boolean(false);
993 assert_eq!(config.get(key).unwrap(), &Value::Boolean(false));
994 }
995
996 #[test]
1001 fn can_still_load_the_previous_format() {
1002 let src = r#"
1003 title = "mdBook Documentation"
1004 description = "Create book from markdown files. Like Gitbook but implemented in Rust"
1005 authors = ["Mathieu David"]
1006 source = "./source"
1007
1008 [output.html]
1009 destination = "my-book" # the output files will be generated in `root/my-book` instead of `root/book`
1010 theme = "my-theme"
1011 curly-quotes = true
1012 google-analytics = "123456"
1013 additional-css = ["custom.css", "custom2.css"]
1014 additional-js = ["custom.js"]
1015 "#;
1016
1017 let book_should_be = BookConfig {
1018 title: Some(String::from("mdBook Documentation")),
1019 description: Some(String::from(
1020 "Create book from markdown files. Like Gitbook but implemented in Rust",
1021 )),
1022 authors: vec![String::from("Mathieu David")],
1023 src: PathBuf::from("./source"),
1024 ..Default::default()
1025 };
1026
1027 let build_should_be = BuildConfig {
1028 build_dir: PathBuf::from("my-book"),
1029 create_missing: true,
1030 use_default_preprocessors: true,
1031 extra_watch_dirs: Vec::new(),
1032 };
1033
1034 let html_should_be = HtmlConfig {
1035 theme: Some(PathBuf::from("my-theme")),
1036 curly_quotes: true,
1037 google_analytics: Some(String::from("123456")),
1038 additional_css: vec![PathBuf::from("custom.css"), PathBuf::from("custom2.css")],
1039 additional_js: vec![PathBuf::from("custom.js")],
1040 ..Default::default()
1041 };
1042
1043 let got = Config::from_str(src).unwrap();
1044 assert_eq!(got.book, book_should_be);
1045 assert_eq!(got.build, build_should_be);
1046 assert_eq!(got.html_config().unwrap(), html_should_be);
1047 }
1048
1049 #[test]
1050 fn set_a_config_item() {
1051 let mut cfg = Config::default();
1052 let key = "foo.bar.baz";
1053 let value = "Something Interesting";
1054
1055 assert!(cfg.get(key).is_none());
1056 cfg.set(key, value).unwrap();
1057
1058 let got: String = cfg.get_deserialized_opt(key).unwrap().unwrap();
1059 assert_eq!(got, value);
1060 }
1061
1062 #[test]
1063 fn parse_env_vars() {
1064 let inputs = vec![
1065 ("FOO", None),
1066 ("MDBOOK_foo", Some("foo")),
1067 ("MDBOOK_FOO__bar__baz", Some("foo.bar.baz")),
1068 ("MDBOOK_FOO_bar__baz", Some("foo-bar.baz")),
1069 ];
1070
1071 for (src, should_be) in inputs {
1072 let got = parse_env(src);
1073 let should_be = should_be.map(ToString::to_string);
1074
1075 assert_eq!(got, should_be);
1076 }
1077 }
1078
1079 fn encode_env_var(key: &str) -> String {
1080 format!(
1081 "MDBOOK_{}",
1082 key.to_uppercase().replace('.', "__").replace('-', "_")
1083 )
1084 }
1085
1086 #[test]
1087 fn update_config_using_env_var() {
1088 let mut cfg = Config::default();
1089 let key = "foo.bar";
1090 let value = "baz";
1091
1092 assert!(cfg.get(key).is_none());
1093
1094 let encoded_key = encode_env_var(key);
1095 env::set_var(encoded_key, value);
1096
1097 cfg.update_from_env();
1098
1099 assert_eq!(
1100 cfg.get_deserialized_opt::<String, _>(key).unwrap().unwrap(),
1101 value
1102 );
1103 }
1104
1105 #[test]
1106 fn update_config_using_env_var_and_complex_value() {
1107 let mut cfg = Config::default();
1108 let key = "foo-bar.baz";
1109 let value = json!({"array": [1, 2, 3], "number": 13.37});
1110 let value_str = serde_json::to_string(&value).unwrap();
1111
1112 assert!(cfg.get(key).is_none());
1113
1114 let encoded_key = encode_env_var(key);
1115 env::set_var(encoded_key, value_str);
1116
1117 cfg.update_from_env();
1118
1119 assert_eq!(
1120 cfg.get_deserialized_opt::<serde_json::Value, _>(key)
1121 .unwrap()
1122 .unwrap(),
1123 value
1124 );
1125 }
1126
1127 #[test]
1128 fn update_book_title_via_env() {
1129 let mut cfg = Config::default();
1130 let should_be = "Something else".to_string();
1131
1132 assert_ne!(cfg.book.title, Some(should_be.clone()));
1133
1134 env::set_var("MDBOOK_BOOK__TITLE", &should_be);
1135 cfg.update_from_env();
1136
1137 assert_eq!(cfg.book.title, Some(should_be));
1138 }
1139
1140 #[test]
1141 fn file_404_default() {
1142 let src = r#"
1143 [output.html]
1144 destination = "my-book"
1145 "#;
1146
1147 let got = Config::from_str(src).unwrap();
1148 let html_config = got.html_config().unwrap();
1149 assert_eq!(html_config.input_404, None);
1150 assert_eq!(&get_404_output_file(&html_config.input_404), "404.html");
1151 }
1152
1153 #[test]
1154 fn file_404_custom() {
1155 let src = r#"
1156 [output.html]
1157 input-404= "missing.md"
1158 output-404= "missing.html"
1159 "#;
1160
1161 let got = Config::from_str(src).unwrap();
1162 let html_config = got.html_config().unwrap();
1163 assert_eq!(html_config.input_404, Some("missing.md".to_string()));
1164 assert_eq!(&get_404_output_file(&html_config.input_404), "missing.html");
1165 }
1166
1167 #[test]
1168 #[should_panic(expected = "Invalid configuration file")]
1169 fn invalid_language_type_error() {
1170 let src = r#"
1171 [book]
1172 title = "mdBook Documentation"
1173 language = ["en", "pt-br"]
1174 description = "Create book from markdown files. Like Gitbook but implemented in Rust"
1175 authors = ["Mathieu David"]
1176 src = "./source"
1177 "#;
1178
1179 Config::from_str(src).unwrap();
1180 }
1181
1182 #[test]
1183 #[should_panic(expected = "Invalid configuration file")]
1184 fn invalid_title_type() {
1185 let src = r#"
1186 [book]
1187 title = 20
1188 language = "en"
1189 description = "Create book from markdown files. Like Gitbook but implemented in Rust"
1190 authors = ["Mathieu David"]
1191 src = "./source"
1192 "#;
1193
1194 Config::from_str(src).unwrap();
1195 }
1196
1197 #[test]
1198 #[should_panic(expected = "Invalid configuration file")]
1199 fn invalid_build_dir_type() {
1200 let src = r#"
1201 [build]
1202 build-dir = 99
1203 create-missing = false
1204 "#;
1205
1206 Config::from_str(src).unwrap();
1207 }
1208
1209 #[test]
1210 #[should_panic(expected = "Invalid configuration file")]
1211 fn invalid_rust_edition() {
1212 let src = r#"
1213 [rust]
1214 edition = "1999"
1215 "#;
1216
1217 Config::from_str(src).unwrap();
1218 }
1219
1220 #[test]
1221 fn print_config() {
1222 let src = r#"
1223 [output.html.print]
1224 enable = false
1225 "#;
1226 let got = Config::from_str(src).unwrap();
1227 let html_config = got.html_config().unwrap();
1228 assert!(!html_config.print.enable);
1229 assert!(html_config.print.page_break);
1230 let src = r#"
1231 [output.html.print]
1232 page-break = false
1233 "#;
1234 let got = Config::from_str(src).unwrap();
1235 let html_config = got.html_config().unwrap();
1236 assert!(html_config.print.enable);
1237 assert!(!html_config.print.page_break);
1238 }
1239}