use crate::tool::helpers::{SeenKeys, expr_str, parse_attr_exprs};
use crate::tool::ir::{FormatterIr, ResultIr};
use syn::spanned::Spanned;
use syn::{Attribute, Error, Expr};
pub fn parse_result(attr: &Attribute) -> Result<ResultIr, Error> {
let exprs = parse_attr_exprs(attr)?;
let mut ir = ResultIr::default();
let mut seen = SeenKeys::default();
for expr in exprs.iter() {
let Expr::Assign(assign) = expr else {
return Err(Error::new(
expr.span(),
"expected `formatters = [...]` or `default = \"...\"` in #[result(...)]",
));
};
let key = assign_left_ident(&assign.left)?;
seen.insert(&key)?;
match key.to_string().as_str() {
"formatters" => ir.formatters = parse_formatters(&assign.right)?,
"default" => ir.default_formatter = Some(expr_str(&assign.right, "default")?),
"doc" => ir.doc = Some(expr_str(&assign.right, "doc")?),
other => {
return Err(Error::new(
key.span(),
format!("unknown #[result] key `{other}`"),
));
}
}
}
Ok(ir)
}
fn parse_formatters(expr: &Expr) -> Result<Vec<FormatterIr>, Error> {
let Expr::Array(array) = expr else {
return Err(Error::new(expr.span(), "formatters must be an array"));
};
array
.elems
.iter()
.map(|formatter| match formatter {
Expr::Tuple(tuple) if tuple.elems.len() == 2 => {
let mut elems = tuple.elems.iter();
Ok(FormatterIr {
name: expr_str(elems.next().unwrap(), "formatter name")?,
doc: expr_str(elems.next().unwrap(), "formatter doc")?,
})
}
other => Ok(FormatterIr {
name: expr_str(other, "formatter")?,
doc: String::new(),
}),
})
.collect()
}
fn assign_left_ident(left: &Expr) -> Result<syn::Ident, Error> {
if let Expr::Path(p) = left
&& let Some(ident) = p.path.get_ident()
{
return Ok(ident.clone());
}
Err(Error::new(
left.span(),
"expected an identifier on the left of `=`",
))
}
#[cfg(test)]
mod tests {
use super::*;
use test_r::test;
fn result(src: &str) -> Result<ResultIr, Error> {
let item: syn::ItemStruct = syn::parse_str(&format!("{src}\nstruct S;")).unwrap();
parse_result(&item.attrs[0])
}
#[test]
fn formatters_and_default() {
let r =
result(r#"#[result(formatters = ["human", "porcelain", "json"], default = "human")]"#)
.unwrap();
assert_eq!(
r.formatters
.iter()
.map(|f| f.name.as_str())
.collect::<Vec<_>>(),
vec!["human", "porcelain", "json"]
);
assert_eq!(r.default_formatter.as_deref(), Some("human"));
}
#[test]
fn formatters_only() {
let r = result(r#"#[result(formatters = ["oneline", "short"])]"#).unwrap();
assert_eq!(r.formatters.len(), 2);
assert_eq!(r.default_formatter, None);
}
#[test]
fn formatter_and_result_docs() {
let r = result(
r#"#[result(formatters = [("json", "JSON report"), ("table", "Tabular report")], default = "json", doc = "Artifact report")]"#,
)
.unwrap();
assert_eq!(r.formatters[0].name, "json");
assert_eq!(r.formatters[0].doc, "JSON report");
assert_eq!(r.doc.as_deref(), Some("Artifact report"));
}
#[test]
fn unknown_key_is_error() {
let err = result(r#"#[result(bogus = "x")]"#).unwrap_err();
assert!(err.to_string().contains("unknown #[result] key"));
}
#[test]
fn duplicate_result_key_is_error() {
let err = result(r#"#[result(default = "human", default = "json")]"#).unwrap_err();
assert!(err.to_string().contains("duplicate"));
}
}