TIL about serde default

This commit is contained in:
Vincent Prouillet 2018-03-12 20:11:03 +01:00
parent bb5cdc0b6c
commit b3b2527c5d
8 changed files with 121 additions and 122 deletions

View file

@ -24,7 +24,12 @@ mod theme;
use theme::Theme;
// We want a default base url for tests
static DEFAULT_BASE_URL: &'static str = "http://a-website.com";
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct Config {
/// Base URL of the site, the only required config argument
pub base_url: String,
@ -33,48 +38,47 @@ pub struct Config {
pub theme: Option<String>,
/// Title of the site. Defaults to None
pub title: Option<String>,
/// Whether to highlight all code blocks found in markdown files. Defaults to false
pub highlight_code: Option<bool>,
/// Which themes to use for code highlighting. See Readme for supported themes
pub highlight_theme: Option<String>,
/// Description of the site
pub description: Option<String>,
/// The language used in the site. Defaults to "en"
pub default_language: Option<String>,
pub default_language: String,
/// Languages list and translated strings
pub translations: HashMap<String, Toml>,
/// Whether to highlight all code blocks found in markdown files. Defaults to false
pub highlight_code: bool,
/// Which themes to use for code highlighting. See Readme for supported themes
/// Defaults to "base16-ocean-dark"
pub highlight_theme: String,
/// Whether to generate RSS. Defaults to false
pub generate_rss: Option<bool>,
/// The number of articles to include in the RSS feed. Defaults to unlimited
pub rss_limit: Option<usize>,
pub generate_rss: bool,
/// The number of articles to include in the RSS feed. Defaults to 10_000
pub rss_limit: usize,
/// Whether to generate tags and individual tag pages if some pages have them. Defaults to true
pub generate_tags_pages: Option<bool>,
pub generate_tags_pages: bool,
/// Whether to generate categories and individual tag categories if some pages have them. Defaults to true
pub generate_categories_pages: Option<bool>,
pub generate_categories_pages: bool,
/// Whether to compile the `sass` directory and output the css files into the static folder
pub compile_sass: Option<bool>,
pub compile_sass: bool,
/// Whether to build the search index for the content
pub build_search_index: bool,
/// A list of file glob patterns to ignore when processing the content folder. Defaults to none.
/// Had to remove the PartialEq derive because GlobSet does not implement it. No impact
/// because it's unused anyway (who wants to sort Configs?).
pub ignored_content: Option<Vec<String>>,
pub ignored_content: Vec<String>,
#[serde(skip_serializing, skip_deserializing)] // not a typo, 2 are needed
pub ignored_content_globber: Option<GlobSet>,
/// Languages list and translated strings
pub translations: Option<HashMap<String, Toml>>,
pub ignored_content_globset: Option<GlobSet>,
/// All user params set in [extra] in the config
pub extra: Option<HashMap<String, Toml>>,
pub extra: HashMap<String, Toml>,
/// Set automatically when instantiating the config. Used for cachebusting
pub build_timestamp: Option<i64>,
}
macro_rules! set_default {
($key: expr, $default: expr) => {
if $key.is_none() {
$key = Some($default);
}
}
}
impl Config {
/// Parses a string containing TOML to our Config struct
@ -85,45 +89,33 @@ impl Config {
Err(e) => bail!(e)
};
set_default!(config.default_language, "en".to_string());
set_default!(config.highlight_code, false);
set_default!(config.generate_rss, false);
set_default!(config.rss_limit, 20);
set_default!(config.generate_tags_pages, false);
set_default!(config.generate_categories_pages, false);
set_default!(config.compile_sass, false);
set_default!(config.ignored_content, Vec::new());
set_default!(config.translations, HashMap::new());
set_default!(config.extra, HashMap::new());
if config.base_url.is_empty() || config.base_url == DEFAULT_BASE_URL {
bail!("A base URL is required in config.toml with key `base_url`");
}
match config.highlight_theme {
Some(ref t) => {
if !THEME_SET.themes.contains_key(t) {
bail!("Theme {} not available", t)
}
}
None => config.highlight_theme = Some("base16-ocean-dark".to_string())
};
if !THEME_SET.themes.contains_key(&config.highlight_theme) {
bail!("Highlight theme {} not available", config.highlight_theme)
}
config.build_timestamp = Some(Utc::now().timestamp());
// Convert the file glob strings into a compiled glob set matcher. We want to do this once,
// at program initialization, rather than for every page, for example. We arrange for the
// globset matcher to always exist (even though it has to be an inside an Option at the
// moment because of the TOML serializer); if the glob set is empty the `is_match` function
// of the globber always returns false.
let mut glob_set_builder = GlobSetBuilder::new();
if let Some(ref v) = config.ignored_content {
for pat in v {
if !config.ignored_content.is_empty() {
// Convert the file glob strings into a compiled glob set matcher. We want to do this once,
// at program initialization, rather than for every page, for example. We arrange for the
// globset matcher to always exist (even though it has to be an inside an Option at the
// moment because of the TOML serializer); if the glob set is empty the `is_match` function
// of the globber always returns false.
let mut glob_set_builder = GlobSetBuilder::new();
for pat in &config.ignored_content {
let glob = match Glob::new(pat) {
Ok(g) => g,
Err(e) => bail!("Invalid ignored_content glob pattern: {}, error = {}", pat, e)
};
glob_set_builder.add(glob);
}
config.ignored_content_globset = Some(glob_set_builder.build().expect("Bad ignored_content in config file."));
}
config.ignored_content_globber = Some(glob_set_builder.build().expect("Bad ignored_content in config file."));
Ok(config)
}
@ -161,19 +153,17 @@ impl Config {
/// Merges the extra data from the theme with the config extra data
fn add_theme_extra(&mut self, theme: &Theme) -> Result<()> {
if let Some(ref mut config_extra) = self.extra {
// 3 pass merging
// 1. save config to preserve user
let original = config_extra.clone();
// 2. inject theme extra values
for (key, val) in &theme.extra {
config_extra.entry(key.to_string()).or_insert_with(|| val.clone());
}
// 3 pass merging
// 1. save config to preserve user
let original = self.extra.clone();
// 2. inject theme extra values
for (key, val) in &theme.extra {
self.extra.entry(key.to_string()).or_insert_with(|| val.clone());
}
// 3. overwrite with original config
for (key, val) in &original {
config_extra.entry(key.to_string()).or_insert_with(|| val.clone());
}
// 3. overwrite with original config
for (key, val) in &original {
self.extra.entry(key.to_string()).or_insert_with(|| val.clone());
}
Ok(())
@ -187,27 +177,26 @@ impl Config {
}
}
/// Exists only for testing purposes
#[doc(hidden)]
impl Default for Config {
fn default() -> Config {
Config {
title: Some("".to_string()),
theme: None,
base_url: "http://a-website.com/".to_string(),
highlight_code: Some(true),
highlight_theme: Some("base16-ocean-dark".to_string()),
base_url: DEFAULT_BASE_URL.to_string(),
title: None,
description: None,
default_language: Some("en".to_string()),
generate_rss: Some(false),
rss_limit: Some(10_000),
generate_tags_pages: Some(true),
generate_categories_pages: Some(true),
compile_sass: Some(false),
ignored_content: Some(Vec::new()),
ignored_content_globber: Some(GlobSetBuilder::new().build().unwrap()),
translations: None,
extra: None,
theme: None,
highlight_code: true,
highlight_theme: "base16-ocean-dark".to_string(),
default_language: "en".to_string(),
generate_rss: false,
rss_limit: 10_000,
generate_tags_pages: true,
generate_categories_pages: true,
compile_sass: false,
build_search_index: false,
ignored_content: Vec::new(),
ignored_content_globset: None,
translations: HashMap::new(),
extra: HashMap::new(),
build_timestamp: Some(1),
}
}
@ -277,7 +266,7 @@ hello = "world"
let config = Config::parse(config);
assert!(config.is_ok());
assert_eq!(config.unwrap().extra.unwrap().get("hello").unwrap().as_str().unwrap(), "world");
assert_eq!(config.unwrap().extra.get("hello").unwrap().as_str().unwrap(), "world");
}
#[test]
@ -333,7 +322,7 @@ a_value = 10
"#;
let theme = Theme::parse(theme_str).unwrap();
assert!(config.add_theme_extra(&theme).is_ok());
let extra = config.extra.unwrap();
let extra = config.extra;
assert_eq!(extra["hello"].as_str().unwrap(), "world".to_string());
assert_eq!(extra["a_value"].as_integer().unwrap(), 10);
}
@ -355,26 +344,26 @@ title = "A title"
let config = Config::parse(config);
assert!(config.is_ok());
let translations = config.unwrap().translations.unwrap();
let translations = config.unwrap().translations;
assert_eq!(translations["fr"]["title"].as_str().unwrap(), "Un titre");
assert_eq!(translations["en"]["title"].as_str().unwrap(), "A title");
}
#[test]
fn missing_ignored_content_results_in_empty_vector_and_empty_globber() {
fn missing_ignored_content_results_in_empty_vector_and_empty_globset() {
let config_str = r#"
title = "My site"
base_url = "example.com"
"#;
let config = Config::parse(config_str).unwrap();
let v = config.ignored_content.unwrap();
let v = config.ignored_content;
assert_eq!(v.len(), 0);
assert!(config.ignored_content_globber.unwrap().is_empty());
assert!(config.ignored_content_globset.is_none());
}
#[test]
fn empty_ignored_content_results_in_empty_vector_and_empty_globber() {
fn empty_ignored_content_results_in_empty_vector_and_empty_globset() {
let config_str = r#"
title = "My site"
base_url = "example.com"
@ -382,12 +371,12 @@ ignored_content = []
"#;
let config = Config::parse(config_str).unwrap();
assert_eq!(config.ignored_content.unwrap().len(), 0);
assert!(config.ignored_content_globber.unwrap().is_empty());
assert_eq!(config.ignored_content.len(), 0);
assert!(config.ignored_content_globset.is_none());
}
#[test]
fn non_empty_ignored_content_results_in_vector_of_patterns_and_configured_globber() {
fn non_empty_ignored_content_results_in_vector_of_patterns_and_configured_globset() {
let config_str = r#"
title = "My site"
base_url = "example.com"
@ -395,10 +384,10 @@ ignored_content = ["*.{graphml,iso}", "*.py?"]
"#;
let config = Config::parse(config_str).unwrap();
let v = config.ignored_content.unwrap();
let v = config.ignored_content;
assert_eq!(v, vec!["*.{graphml,iso}", "*.py?"]);
let g = config.ignored_content_globber.unwrap();
let g = config.ignored_content_globset.unwrap();
assert_eq!(g.len(), 2);
assert!(g.is_match("foo.graphml"));
assert!(g.is_match("foo.iso"));

View file

@ -130,23 +130,27 @@ impl Page {
let mut page = Page::parse(path, &content, config)?;
if page.file.name == "index" {
// `find_related_assets` only scans the immediate directory (it is not recursive) so our
// filtering only needs to work against the file_name component, not the full suffix. If
// `find_related_assets` was changed to also return files in subdirectories, we could
// use `PathBuf.strip_prefix` to remove the parent directory and then glob-filter
// against the remaining path. Note that the current behaviour effectively means that
// the `ignored_content` setting in the config file is limited to single-file glob
// patterns (no "**" patterns).
let globber = config.ignored_content_globber.as_ref().unwrap();
let parent_dir = path.parent().unwrap();
page.assets = find_related_assets(parent_dir).into_iter()
.filter(|path|
match path.file_name() {
None => true,
Some(file) => !globber.is_match(file)
}
).collect();
let assets = find_related_assets(parent_dir);
if let Some(ref globset) = config.ignored_content_globset {
// `find_related_assets` only scans the immediate directory (it is not recursive) so our
// filtering only needs to work against the file_name component, not the full suffix. If
// `find_related_assets` was changed to also return files in subdirectories, we could
// use `PathBuf.strip_prefix` to remove the parent directory and then glob-filter
// against the remaining path. Note that the current behaviour effectively means that
// the `ignored_content` setting in the config file is limited to single-file glob
// patterns (no "**" patterns).
page.assets = assets.into_iter()
.filter(|path|
match path.file_name() {
None => true,
Some(file) => !globset.is_match(file)
}
).collect();
} else {
page.assets = assets;
}
} else {
page.assets = vec![];
}
@ -160,8 +164,8 @@ impl Page {
pub fn render_markdown(&mut self, permalinks: &HashMap<String, String>, tera: &Tera, config: &Config, anchor_insert: InsertAnchor) -> Result<()> {
let context = Context::new(
tera,
config.highlight_code.unwrap(),
config.highlight_theme.clone().unwrap(),
config.highlight_code,
config.highlight_theme.clone(),
&self.permalink,
permalinks,
anchor_insert
@ -450,7 +454,7 @@ Hello world
let mut gsb = GlobSetBuilder::new();
gsb.add(Glob::new("*.{js,png}").unwrap());
let mut config = Config::default();
config.ignored_content_globber = Some(gsb.build().unwrap());
config.ignored_content_globset = Some(gsb.build().unwrap());
let res = Page::from_file(
nested_path.join("index.md").as_path(),

View file

@ -100,8 +100,8 @@ impl Section {
pub fn render_markdown(&mut self, permalinks: &HashMap<String, String>, tera: &Tera, config: &Config) -> Result<()> {
let context = Context::new(
tera,
config.highlight_code.unwrap(),
config.highlight_theme.clone().unwrap(),
config.highlight_code,
config.highlight_theme.clone(),
&self.permalink,
permalinks,
self.meta.insert_anchor_links.unwrap()

View file

@ -376,8 +376,8 @@ impl Site {
/// Find all the tags and categories if it's asked in the config
pub fn populate_tags_and_categories(&mut self) {
let generate_tags_pages = self.config.generate_tags_pages.unwrap();
let generate_categories_pages = self.config.generate_categories_pages.unwrap();
let generate_tags_pages = self.config.generate_tags_pages;
let generate_categories_pages = self.config.generate_categories_pages;
if !generate_tags_pages && !generate_categories_pages {
return;
}
@ -505,7 +505,7 @@ impl Site {
self.render_sections()?;
self.render_orphan_pages()?;
self.render_sitemap()?;
if self.config.generate_rss.unwrap() {
if self.config.generate_rss {
self.render_rss_feed()?;
}
self.render_robots()?;
@ -521,7 +521,7 @@ impl Site {
}
}
if self.config.compile_sass.unwrap() {
if self.config.compile_sass {
self.compile_sass(&self.base_path)?;
}
@ -703,7 +703,7 @@ impl Site {
let (sorted_pages, _) = sort_pages(pages, SortBy::Date);
context.add("last_build_date", &sorted_pages[0].meta.date.clone().map(|d| d.to_string()));
// limit to the last n elements)
context.add("pages", &sorted_pages.iter().take(self.config.rss_limit.unwrap()).collect::<Vec<_>>());
context.add("pages", &sorted_pages.iter().take(self.config.rss_limit).collect::<Vec<_>>());
context.add("config", &self.config);
let rss_feed_url = if self.config.base_url.ends_with('/') {

View file

@ -193,7 +193,7 @@ fn can_build_site_with_categories() {
let mut path = env::current_dir().unwrap().parent().unwrap().parent().unwrap().to_path_buf();
path.push("test_site");
let mut site = Site::new(&path, "config.toml").unwrap();
site.config.generate_categories_pages = Some(true);
site.config.generate_categories_pages = true;
site.load().unwrap();
for (i, page) in site.pages.values_mut().enumerate() {
@ -247,7 +247,7 @@ fn can_build_site_with_tags() {
let mut path = env::current_dir().unwrap().parent().unwrap().parent().unwrap().to_path_buf();
path.push("test_site");
let mut site = Site::new(&path, "config.toml").unwrap();
site.config.generate_tags_pages = Some(true);
site.config.generate_tags_pages = true;
site.load().unwrap();
for (i, page) in site.pages.values_mut().enumerate() {

View file

@ -23,8 +23,8 @@ macro_rules! required_string_arg {
pub fn make_trans(config: Config) -> GlobalFn {
let translations_config = config.translations.unwrap();
let default_lang = to_value(config.default_language.unwrap()).unwrap();
let translations_config = config.translations;
let default_lang = to_value(config.default_language).unwrap();
Box::new(move |args| -> Result<Value> {
let key = required_string_arg!(args.get("key"), "`trans` requires a `key` argument.");

View file

@ -19,6 +19,9 @@ compile_sass = %COMPILE_SASS%
# Theme can be customised by setting the `highlight_theme` variable to a theme supported by Gutenberg
highlight_code = %HIGHLIGHT%
# Whether to build a search index to be used later on by a JavaScript library
build_search_index = %SEARCH%
[extra]
# Put all your custom variables here
"#;
@ -37,11 +40,13 @@ pub fn create_new_project(name: &str) -> Result<()> {
let base_url = ask_url("> What is the URL of your site?", "https://example.com")?;
let compile_sass = ask_bool("> Do you want to enable Sass compilation?", true)?;
let highlight = ask_bool("> Do you want to enable syntax highlighting?", false)?;
let search = ask_bool("> Do you want to build a search index of the content?", false)?;
let config = CONFIG
.trim_left()
.replace("%BASE_URL%", &base_url)
.replace("%COMPILE_SASS%", &format!("{}", compile_sass))
.replace("%SEARCH%", &format!("{}", search))
.replace("%HIGHLIGHT%", &format!("{}", highlight));
create_file(&path.join("config.toml"), &config)?;
@ -53,6 +58,7 @@ pub fn create_new_project(name: &str) -> Result<()> {
if compile_sass {
create_dir(path.join("sass"))?;
}
// TODO: if search == true, copy a lunr js file embedded in gutenberg
println!();
console::success(&format!("Done! Your site was created in {:?}", canonicalize(path).unwrap()));

View file

@ -163,7 +163,7 @@ pub fn serve(interface: &str, port: &str, output_dir: &str, base_url: &str, conf
if watching_static {
watchers.push("static");
}
if site.config.compile_sass.unwrap() {
if site.config.compile_sass {
watchers.push("sass");
}