breb 0.5.0

the blog/reblog library and command-line tool
Documentation
//! defining the structure of a blog in an unrepetitive, high-level way
//!
//! this is explicitly not meant to be used from rust code,
//! as evidenced by how much of a pain it would be to try to construct.
//! it's designed to parse config files and that's it.
//! if you're doing things programmatically,
//! it'll almost always make more sense to just directly use [`crate::raw`].
#![doc(hidden)]

mod convert;
mod example;

use std::{collections::HashMap, path::PathBuf};

use url::Url;

use crate::raw::Author;

#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Config {
	blog: BlogInfo,
	#[serde(default, skip_serializing_if = "Vec::is_empty")]
	authors: Vec<Author>,

	// and then the *complicated* bit. we want to allow for recursive sections
	// with things like subblogs, translations, etc. etc. --
	// that all lives in a separate type, because it's an ugly nested thing.
	#[serde(flatten)]
	sections: SectionList,
}

#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(untagged, rename_all = "kebab-case")]
pub enum SectionList {
	One(Box<ConfigSection>),
	Many {
		sections: Vec<ConfigSection>,
		#[serde(flatten)]
		adjacent: AdjacentInfo,
	},
}

#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct ConfigSectionBase {
	#[serde(flatten)]
	serves: ServeList,
	#[serde(default, skip_serializing_if = "Vec::is_empty")]
	feeds: Vec<FeedInfo>,
}

#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(untagged, rename_all = "kebab-case")]
pub enum ConfigSection {
	Translated {
		#[serde(flatten)]
		base: ConfigSectionBase,
		languages: Vec<Translation>,
	},
	// TODO: subblog w/ a full SectionList

	// this has to be the last so it matches last
	Normal(ConfigSectionBase),
}

#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Translation {
	locale: String,
	#[serde(default, rename = "on")]
	url_prefix: PathBuf,
	#[serde(default, rename = "from")]
	path_prefix: Option<PathBuf>,
	#[serde(flatten)]
	adjacent: AdjacentInfo,
}

/// basic site-wide metadata
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct BlogInfo {
	name: String,
	url: String,
}

#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(untagged, rename_all = "kebab-case")]
pub enum FeedInfo {
	Atom {
		#[serde(rename = "atom")]
		serve_at: PathBuf,
		index: String,
		length: Option<usize>,
		link: Option<String>,
	},
}

#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct ServeList {
	serves: Vec<ServeInfo>,
	#[serde(flatten)]
	adjacent: AdjacentInfo,
}

#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(untagged, rename_all = "kebab-case")]
pub enum ServeInfo {
	AsIs {
		#[serde(rename = "as-is")]
		serve_at: PathBuf,
		#[serde(default, skip_serializing_if = "Option::is_none")]
		from: Option<PathBuf>,
		#[serde(default, skip_serializing_if = "Option::is_none")]
		metafree: Option<bool>,
	},
	Pages {
		#[serde(rename = "pages")]
		serve_at: PathBuf,
		#[serde(default, skip_serializing_if = "Option::is_none")]
		from: Option<PathBuf>,
		#[serde(default, skip_serializing_if = "Option::is_none")]
		template: Option<PathBuf>,
		#[serde(default, skip_serializing_if = "HashMap::is_empty")]
		bases: HashMap<String, PathBuf>,
		#[serde(default, skip_serializing_if = "Option::is_none")]
		styles: Option<Vec<String>>,
		#[serde(default, skip_serializing_if = "Option::is_none")]
		nav: Option<Vec<HashMap<String, String>>>,
		#[serde(default, skip_serializing_if = "Option::is_none")]
		locale: Option<String>,
	},
	Posts {
		#[serde(rename = "posts")]
		serve_at: PathBuf,
		#[serde(default, skip_serializing_if = "Option::is_none")]
		from: Option<PathBuf>,
		#[serde(default, skip_serializing_if = "Option::is_none")]
		template: Option<PathBuf>,
		#[serde(default, skip_serializing_if = "HashMap::is_empty")]
		bases: HashMap<String, PathBuf>,
		#[serde(default, skip_serializing_if = "Option::is_none")]
		styles: Option<Vec<String>>,
		#[serde(default, skip_serializing_if = "Option::is_none")]
		nav: Option<Vec<HashMap<String, String>>>,
		#[serde(default, skip_serializing_if = "Option::is_none")]
		locale: Option<String>,
		#[serde(default, skip_serializing_if = "Option::is_none")]
		index: Option<String>,
	},
	LocalGitRepo {
		#[serde(rename = "git")]
		serve_at: PathBuf,
		#[serde(default)]
		source: Option<PathBuf>,
	},
	RemoteGitRepo {
		#[serde(rename = "git")]
		serve_at: PathBuf,
		url: Url,
	},
}

#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct AdjacentInfo {
	#[serde(default, skip_serializing_if = "Option::is_none")]
	metafree: Option<bool>,
	#[serde(default, skip_serializing_if = "Option::is_none")]
	locale: Option<String>,
	#[serde(default, skip_serializing_if = "Option::is_none")]
	template: Option<PathBuf>,
	/// this is **merged** with anything from higher levels, not overwritten --
	/// unlike the other fields.
	#[serde(default, skip_serializing_if = "HashMap::is_empty")]
	bases: HashMap<String, PathBuf>,
	#[serde(default, skip_serializing_if = "Option::is_none")]
	styles: Option<Vec<String>>,
	/// this is a `Vec<HashMap<...>>` to support the ux i want, i.e.
	///
	/// ```yaml
	/// nav:
	/// - foo: /foo
	/// - bar: /bar
	/// ```
	///
	/// the conversion to raw will error
	/// if you try to provide anything other than exactly one entry.
	#[serde(default, skip_serializing_if = "Option::is_none")]
	nav: Option<Vec<HashMap<String, String>>>,
}