use syn::{
parse::{Parse, ParseStream, Parser},
punctuated::Punctuated,
Attribute, Expr, ExprLit, ExprMacro, Ident, Lit, Result, Token,
};
pub struct CompositionBlock {
pub statements: Vec<Statement>,
}
pub enum Statement {
Let {
name: Ident,
value: TemplateCall,
},
Const {
attrs: Vec<Attribute>,
name: Ident,
value: TemplateCall,
},
}
pub struct TemplateCall {
pub source: TemplateSource,
pub params: Vec<(String, ParamValue)>,
}
pub enum TemplateSource {
Name(String),
}
pub enum ParamValue {
Literal(String),
Variable(String),
Nested(TemplateCall),
}
impl Parse for CompositionBlock {
fn parse(input: ParseStream) -> Result<Self> {
let content = input;
let mut statements = Vec::new();
while !content.is_empty() {
let attrs = content.call(Attribute::parse_outer)?;
if content.peek(Token![let]) {
if !attrs.is_empty() {
return Err(syn::Error::new_spanned(
&attrs[0],
"Attributes are not allowed on let bindings",
));
}
statements.push(parse_let_statement(&content)?);
} else if content.peek(Token![const]) {
statements.push(parse_const_statement(&content, attrs)?);
} else {
return Err(content.error("Expected 'let' or 'const' statement"));
}
if content.peek(Token![,]) {
content.parse::<Token![,]>()?;
}
}
Ok(CompositionBlock { statements })
}
}
fn parse_let_statement(input: ParseStream) -> Result<Statement> {
input.parse::<Token![let]>()?;
let name = input.parse::<Ident>()?;
input.parse::<Token![=]>()?;
let value = parse_template_call(input)?;
input.parse::<Token![;]>()?;
Ok(Statement::Let { name, value })
}
fn parse_const_statement(input: ParseStream, attrs: Vec<Attribute>) -> Result<Statement> {
input.parse::<Token![const]>()?;
let name = input.parse::<Ident>()?;
input.parse::<Token![=]>()?;
let value = parse_template_call(input)?;
input.parse::<Token![;]>()?;
Ok(Statement::Const { attrs, name, value })
}
fn parse_template_call(input: ParseStream) -> Result<TemplateCall> {
let mac: ExprMacro = input.parse()?;
if !mac.mac.path.is_ident("tomplate") {
return Err(syn::Error::new_spanned(
mac,
"Expected 'tomplate!' macro call",
));
}
parse_template_args(mac.mac.tokens)
}
fn parse_template_args(tokens: proc_macro2::TokenStream) -> Result<TemplateCall> {
let parser = |input: ParseStream| -> Result<TemplateCall> {
let first_arg = input.parse::<Expr>()?;
let source = match first_arg {
Expr::Lit(ExprLit { lit: Lit::Str(s), .. }) => {
TemplateSource::Name(s.value())
}
_ => {
return Err(syn::Error::new_spanned(
first_arg,
"Template source must be a string literal",
));
}
};
let mut params = Vec::new();
if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
let args = Punctuated::<Expr, Token![,]>::parse_terminated(input)?;
for arg in args {
match arg {
Expr::Assign(assign) => {
let param_name = match &*assign.left {
Expr::Path(path) if path.path.segments.len() == 1 => {
path.path.segments[0].ident.to_string()
}
_ => {
return Err(syn::Error::new_spanned(
assign.left,
"Parameter name must be a simple identifier",
));
}
};
let param_value = parse_param_value(&*assign.right)?;
params.push((param_name, param_value));
}
_ => {
return Err(syn::Error::new_spanned(
arg,
"Expected 'key = value' syntax",
));
}
}
}
}
Ok(TemplateCall { source, params })
};
parser.parse2(tokens)
}
fn parse_param_value(expr: &Expr) -> Result<ParamValue> {
match expr {
Expr::Lit(lit) => {
let value = match &lit.lit {
Lit::Str(s) => s.value(),
Lit::Int(i) => i.to_string(),
Lit::Float(f) => f.to_string(),
Lit::Bool(b) => b.value.to_string(),
_ => {
return Err(syn::Error::new_spanned(
lit,
"Unsupported literal type",
));
}
};
Ok(ParamValue::Literal(value))
}
Expr::Path(path) if path.path.segments.len() == 1 => {
Ok(ParamValue::Variable(path.path.segments[0].ident.to_string()))
}
Expr::Macro(mac) if mac.mac.path.is_ident("tomplate") => {
let nested = parse_template_args(mac.mac.tokens.clone())?;
Ok(ParamValue::Nested(nested))
}
_ => Err(syn::Error::new_spanned(
expr,
"Parameter value must be a literal, variable reference, or tomplate!() call",
)),
}
}