use crate::doc_index::DocIndex;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tera::{Tera, Value};
const KNOWN_KEYS: &[&str] = &["name", "omit", "limit"];
pub fn register(env: &mut Tera, index: Arc<DocIndex>) {
env.register_function(
"collection",
move |args: &HashMap<String, Value>| -> tera::Result<Value> {
check_known_keys(args)?;
let name = collection_name(args)?;
let omit = parse_omit(args)?;
let limit = parse_limit(args)?;
let omit: HashSet<&Path> = omit.iter().map(PathBuf::as_path).collect();
let docs: Vec<&crate::doc::Doc> = index
.get_collection(&name)
.filter(|d| !omit.contains(d.id_path.as_path()))
.take(limit.unwrap_or(usize::MAX))
.collect();
tera::to_value(docs).map_err(tera::Error::from)
},
);
}
fn check_known_keys(args: &HashMap<String, Value>) -> tera::Result<()> {
for key in args.keys() {
if !KNOWN_KEYS.contains(&key.as_str()) {
return Err(tera::Error::msg(format!(
"collection: unknown argument `{}` (allowed: {})",
key,
KNOWN_KEYS.join(", ")
)));
}
}
Ok(())
}
fn collection_name(args: &HashMap<String, Value>) -> tera::Result<String> {
match args.get("name") {
Some(Value::String(s)) => Ok(s.clone()),
Some(_) => Err(tera::Error::msg("collection: `name` must be a string")),
None => Err(tera::Error::msg(
"collection: missing required `name` argument",
)),
}
}
fn parse_omit(args: &HashMap<String, Value>) -> tera::Result<Vec<PathBuf>> {
match args.get("omit") {
None => Ok(Vec::new()),
Some(v) => {
let arr = v.as_array().ok_or_else(|| {
tera::Error::msg("collection: `omit` must be an array of strings")
})?;
arr.iter()
.map(|e| {
e.as_str().map(PathBuf::from).ok_or_else(|| {
tera::Error::msg("collection: `omit` entries must be strings")
})
})
.collect()
}
}
}
fn parse_limit(args: &HashMap<String, Value>) -> tera::Result<Option<usize>> {
match args.get("limit") {
None => Ok(None),
Some(v) => {
let n = v.as_u64().ok_or_else(|| {
tera::Error::msg("collection: `limit` must be a non-negative integer")
})?;
Ok(Some(n as usize))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn val_str(s: &str) -> Value {
Value::String(s.to_string())
}
#[test]
fn collection_name_requires_string() {
assert!(collection_name(&HashMap::new()).is_err());
let mut args = HashMap::new();
args.insert("name".to_string(), Value::from(1u64));
assert!(collection_name(&args).is_err());
let mut args = HashMap::new();
args.insert("name".to_string(), val_str("posts"));
assert_eq!(collection_name(&args).unwrap(), "posts");
}
#[test]
fn check_known_keys_rejects_typo() {
let mut args = HashMap::new();
args.insert("limti".to_string(), Value::from(2u64));
assert!(check_known_keys(&args).is_err());
}
#[test]
fn parse_omit_default_is_empty() {
assert!(parse_omit(&HashMap::new()).unwrap().is_empty());
}
#[test]
fn parse_omit_parses_array() {
let mut args = HashMap::new();
args.insert(
"omit".to_string(),
Value::Array(vec![val_str("a.md"), val_str("b.md")]),
);
assert_eq!(
parse_omit(&args).unwrap(),
vec![PathBuf::from("a.md"), PathBuf::from("b.md")]
);
}
#[test]
fn parse_omit_not_array_errors() {
let mut args = HashMap::new();
args.insert("omit".to_string(), val_str("a.md"));
assert!(parse_omit(&args).is_err());
}
#[test]
fn parse_limit_default_is_none() {
assert_eq!(parse_limit(&HashMap::new()).unwrap(), None);
}
#[test]
fn parse_limit_parses_integer() {
let mut args = HashMap::new();
args.insert("limit".to_string(), Value::from(3u64));
assert_eq!(parse_limit(&args).unwrap(), Some(3));
}
#[test]
fn parse_limit_not_integer_errors() {
let mut args = HashMap::new();
args.insert("limit".to_string(), val_str("3"));
assert!(parse_limit(&args).is_err());
}
}