use std::collections::{HashMap, HashSet};
use tera::{Tera, Value};
const KNOWN_KEYS: &[&str] = &["omit"];
pub fn register(env: &mut Tera) {
env.register_filter(
"omit_docs",
|value: &Value, args: &HashMap<String, Value>| -> tera::Result<Value> {
check_known_keys(args)?;
let docs = value.as_array().ok_or_else(|| {
tera::Error::msg("omit_docs filter: input must be an array of docs")
})?;
let omit = parse_omit(args)?;
let omit: HashSet<&str> = omit.iter().map(String::as_str).collect();
omit_docs(docs, &omit)
},
);
}
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!(
"omit_docs: unknown argument `{}` (allowed: {})",
key,
KNOWN_KEYS.join(", ")
)));
}
}
Ok(())
}
fn parse_omit(args: &HashMap<String, Value>) -> tera::Result<Vec<String>> {
let v = args
.get("omit")
.ok_or_else(|| tera::Error::msg("omit_docs: missing required `omit` argument"))?;
let arr = v
.as_array()
.ok_or_else(|| tera::Error::msg("omit_docs: `omit` must be an array of strings"))?;
arr.iter()
.map(|e| {
e.as_str()
.map(str::to_string)
.ok_or_else(|| tera::Error::msg("omit_docs: `omit` entries must be strings"))
})
.collect()
}
fn omit_docs(docs: &[Value], omit: &HashSet<&str>) -> tera::Result<Value> {
let mut kept: Vec<Value> = Vec::new();
for doc in docs {
let id_path = doc.get("id_path").and_then(Value::as_str).ok_or_else(|| {
tera::Error::msg("omit_docs filter: every doc must have a string `id_path`")
})?;
if !omit.contains(id_path) {
kept.push(doc.clone());
}
}
Ok(Value::Array(kept))
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeMap;
fn doc(id_path: &str, title: &str) -> Value {
let map: BTreeMap<&str, &str> = [("id_path", id_path), ("title", title)]
.into_iter()
.collect();
tera::to_value(map).unwrap()
}
fn ids(value: &Value) -> Vec<String> {
value
.as_array()
.unwrap()
.iter()
.map(|d| d["id_path"].as_str().unwrap().to_string())
.collect()
}
#[test]
fn drops_listed_docs_preserving_order() {
let docs = [doc("c.md", "C"), doc("a.md", "A"), doc("b.md", "B")];
let omit: HashSet<&str> = ["a.md"].into_iter().collect();
let out = omit_docs(&docs, &omit).unwrap();
assert_eq!(ids(&out), vec!["c.md", "b.md"]);
}
#[test]
fn empty_omit_is_passthrough() {
let docs = [doc("a.md", "A"), doc("b.md", "B")];
let out = omit_docs(&docs, &HashSet::new()).unwrap();
assert_eq!(ids(&out), vec!["a.md", "b.md"]);
}
#[test]
fn missing_id_path_is_an_error() {
let no_id = tera::to_value(BTreeMap::from([("title", "No id")])).unwrap();
assert!(omit_docs(&[no_id], &HashSet::new()).is_err());
}
#[test]
fn non_array_input_is_an_error() {
let mut env = Tera::default();
register(&mut env);
let mut ctx = tera::Context::new();
ctx.insert("x", "not an array");
assert!(
env.render_str("{{ x | omit_docs(omit=[]) }}", &ctx)
.is_err()
);
}
#[test]
fn missing_omit_argument_is_an_error() {
let mut env = Tera::default();
register(&mut env);
let mut ctx = tera::Context::new();
ctx.insert("docs", &Vec::<Value>::new());
assert!(env.render_str("{{ docs | omit_docs }}", &ctx).is_err());
}
#[test]
fn unknown_argument_is_an_error() {
let mut env = Tera::default();
register(&mut env);
let mut ctx = tera::Context::new();
ctx.insert("docs", &Vec::<Value>::new());
assert!(
env.render_str("{{ docs | omit_docs(omit=[], limit=3) }}", &ctx)
.is_err()
);
}
}