use compmd::context::{RenderContext, parse_time};
use compmd::expand::Options;
use compmd::processor::Expanded;
use compmd::{Ambient, ParseOptions, Processor};
use serde_json::json;
use std::collections::HashMap;
use std::rc::Rc;
const DOCUMENT: &str = "---\ntitle: Sales\nmd:\n inputs:\n region: eu | us = eu\n data:\n - from: ./sales.json\n as: sales\n---\n\n# {{md.doc.title}}\n";
const SALES: &str = r#"[{"region": "eu", "amount": 120}, {"region": "us", "amount": 80}, {"region": "eu", "amount": 30}]"#;
fn processed(inputs: &serde_json::Value) -> Processor<Expanded> {
let files = HashMap::from([("sales.json".to_string(), SALES.to_string())]);
let context = RenderContext {
now: parse_time("2026-10-21T09:00:00Z").expect("a time"),
tz: "UTC".into(),
time: Rc::new(()),
inputs: inputs.as_object().cloned().unwrap_or_default(),
};
Processor::new("sales.comp.md", DOCUMENT)
.parse(&ParseOptions::default())
.resolve(files, (), &Ambient::default())
.validate(&[])
.load(context, (), ())
.expand(&Options::default())
}
fn value(processor: &Processor<Expanded>, expression: &str) -> serde_json::Value {
processor
.evaluate(expression)
.unwrap_or_else(|error| panic!("{expression}: {}", error.message))
}
#[test]
fn gives_the_documents_frontmatter_data_and_inputs_with_this_renders_values() {
let processor = processed(&json!({ "region": "us" }));
assert_eq!(value(&processor, "md.doc.title"), json!("Sales"));
assert_eq!(value(&processor, "md.input.region"), json!("us"));
assert_eq!(value(&processor, "md.data.sales.size()"), json!(3));
assert_eq!(
value(
&processor,
"md.data.sales.filter(s, s.region == md.input.region).map(s, s.amount)"
),
json!([80])
);
assert_eq!(
value(&processor, "md.data.sales[0]"),
json!({ "region": "eu", "amount": 120 })
);
}
#[test]
fn calls_the_md_functions_at_the_renders_time_and_in_the_documents_language() {
let processor = processed(&json!({}));
assert_eq!(value(&processor, "md.today()"), json!("2026-10-21"));
assert_eq!(
value(&processor, "md.sum(md.data.sales.map(s, s.amount))"),
json!(230)
);
assert_eq!(
value(&processor, "[md.lang, md.tz, md.version]"),
json!(["en", "UTC", "1"])
);
}
#[test]
fn reports_f024_at_its_place_in_the_expression_for_each_kind_of_error() {
let processor = processed(&json!({}));
let error = |expression: &str| {
let diagnostic = processor.evaluate(expression).expect_err(expression);
let start = diagnostic.range.start;
(
diagnostic.code.as_str(),
start.line,
start.column,
diagnostic.message,
)
};
let (code, line, column, _) = error("md.doc.title +");
assert_eq!((code, line), ("F024", 1));
assert!(column > 13);
let (_, line, column, message) = error("md.doc.titel");
assert_eq!((line, column), (1, 8));
assert!(message.contains("title"), "{message}");
let (code, line, column, _) = error("[1,\n 1 / 0]");
assert_eq!((code, line, column), ("F024", 2, 5));
let stage = |expression: &str| {
let diagnostic = processor.evaluate(expression).expect_err(expression);
serde_json::to_value(&*diagnostic).expect("serializes")["stage"].clone()
};
assert_eq!(stage("md.doc.titel"), json!("validate"));
assert_eq!(stage("1 / 0"), json!("evaluate"));
}
#[test]
fn sees_only_the_documents_top_level_scope_and_changes_nothing() {
let processor = processed(&json!({}));
let before = serde_json::to_value(processor.expanded()).expect("the tree serializes");
let diagnostics = processor.diagnostics().to_vec();
assert!(
processor.evaluate("md.loop.index").is_err(),
"no `each` around it"
);
assert!(processor.evaluate("item").is_err(), "no `as` name");
assert_eq!(
serde_json::to_value(processor.expanded()).expect("the tree serializes"),
before
);
assert_eq!(processor.diagnostics(), diagnostics);
}