use crate::words;
use proc_macro2::{Span, TokenStream};
use quote::{format_ident, quote};
use syn::spanned::Spanned;
use syn::visit::{self, Visit};
use syn::visit_mut::{self as visit_mut, VisitMut};
use syn::{
Error, Expr, ExprMethodCall, FnArg, ImplItem, ImplItemFn, ItemImpl, LitStr, Result, Type, Visibility, parse2,
};
struct Member {
name: syn::Ident,
carrier: Type,
merged: bool,
}
struct Members {
found: Vec<Result<Member>>,
}
impl Visit<'_> for Members {
fn visit_expr_method_call(&mut self, call: &ExprMethodCall) {
let merged = call.method == "merge";
if call.method == "field" || merged {
self.found.push(read_member(call, merged));
}
visit::visit_expr_method_call(self, call);
}
}
fn read_member(call: &ExprMethodCall, merged: bool) -> Result<Member> {
let carrier = match &call.turbofish {
Some(turbofish) => match turbofish.args.first() {
Some(syn::GenericArgument::Type(carrier)) => carrier.clone(),
_ => {
return Err(Error::new(
turbofish.span(),
"a member states the type carrying it, as `field::<Carrier>(name, shape)`",
));
}
},
None => {
return Err(Error::new(
call.span(),
"a member states the type carrying it, as `field::<Carrier>(name, shape)`; without \
one there is nothing to emit a field from",
));
}
};
if merged {
return Ok(Member { name: name_of_type(&carrier), carrier, merged });
}
match call.args.first() {
Some(Expr::Path(path)) => match path.path.get_ident() {
Some(name) => Ok(Member { name: name.clone(), carrier, merged }),
None => Err(Error::new(path.span(), "a member is named by an identifier")),
},
Some(other) => Err(Error::new(other.span(), "a member is named by an identifier")),
None => Err(Error::new(call.span(), "a member states a name")),
}
}
fn name_of_type(carrier: &Type) -> syn::Ident {
let text = quote!(#carrier).to_string();
let last = text.rsplit(':').next().unwrap_or(&text).trim().to_owned();
let words = words::of_camel(last.split('<').next().unwrap_or(&last).trim());
format_ident!("{}", words.join("_"), span = carrier.span())
}
fn layout(method: &ImplItemFn, ext: &Option<syn::Path>, spelling: &str) -> Result<TokenStream> {
if !method.sig.inputs.iter().any(|input| matches!(input, FnArg::Receiver(_))) {
return Ok(TokenStream::new());
}
let name = method.sig.ident.to_string();
let Some(stem) = name.strip_suffix("_shape") else {
return Ok(TokenStream::new());
};
let mut members = Members { found: Vec::new() };
members.visit_block(&method.block);
let members = members.found.into_iter().collect::<Result<Vec<_>>>()?;
let fields = members.iter().map(|member| {
let name = syn::Ident::new(&member.name.to_string(), member.name.span());
let carrier = &member.carrier;
if member.merged { quote!(#[serde(flatten)] pub #name: #carrier) } else { quote!(pub #name: #carrier) }
});
let name = words::of_snake(stem).iter().map(|word| pascal(word)).collect::<String>();
let layout = format_ident!("{name}", span = method.sig.ident.span());
let declaration = &method.sig.ident;
let spelling = LitStr::new(spelling, Span::call_site());
let reach = ext.as_ref().map(|ext| quote!(use #ext as _;));
Ok(quote! {
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = #spelling)]
pub struct #layout {
#(#fields,)*
}
impl<Alg> ::alux_shape::ShapeOf<Alg> for #layout
where
Alg: ::alux_shape::ShapeAlg + ::alux_shape::FieldAlg,
{
type Shape = <Alg as ::alux_shape::Sorts>::Ty;
fn shape_of(alg: &Alg) -> Self::Shape {
use ::alux_shape::ShapeProgramExt as _;
#reach
alg.compile_shape(alg.#declaration())
}
}
})
}
fn pascal(word: &str) -> String {
let mut chars = word.chars();
match chars.next() {
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
}
fn extension(attrs: &[syn::Attribute]) -> Option<syn::Path> {
let mut found = None;
for attr in attrs.iter().filter(|attr| attr.path().is_ident("ext")) {
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("name") {
found = meta.value().and_then(|value| value.parse()).ok();
} else if meta.input.peek(syn::Token![=]) {
let _: TokenStream = meta.value()?.parse()?;
}
Ok(())
});
}
found
}
struct Constructors;
impl VisitMut for Constructors {
fn visit_expr_method_call_mut(&mut self, call: &mut ExprMethodCall) {
if call.method == "field" {
if let Some(Expr::Path(path)) = call.args.first()
&& let Some(name) = path.path.get_ident()
{
let words = words::of_snake(&name.to_string());
let words = words.iter().map(|word| LitStr::new(word, name.span()));
call.args[0] = syn::parse_quote!(&[#(#words),*]);
}
match (call.turbofish.is_some(), call.args.len()) {
(true, 1) => call.method = format_ident!("field_of", span = call.method.span()),
(true, _) => call.turbofish = None,
(false, _) => (),
}
}
if call.method == "merge" {
call.turbofish = None;
}
visit_mut::visit_expr_method_call_mut(self, call);
}
}
pub(crate) fn expand(attr: TokenStream, item: TokenStream) -> Result<TokenStream> {
let spelling = if attr.is_empty() { "camelCase".to_owned() } else { parse2::<LitStr>(attr)?.value() };
let (attrs, visibility, rest) = split_prefix(item)?;
let mut block = parse2::<ItemImpl>(rest)?;
let ext = extension(&attrs);
let layouts = block
.items
.iter()
.filter_map(|item| match item {
ImplItem::Fn(method) => Some(layout(method, &ext, &spelling)),
_ => None,
})
.collect::<Result<Vec<_>>>()?;
Constructors.visit_item_impl_mut(&mut block);
Ok(quote! {
#(#layouts)*
#(#attrs)*
#visibility #block
})
}
fn split_prefix(item: TokenStream) -> Result<(Vec<syn::Attribute>, Option<Visibility>, TokenStream)> {
let mut tokens = item.into_iter().peekable();
let mut attrs = TokenStream::new();
while let Some(proc_macro2::TokenTree::Punct(punct)) = tokens.peek() {
if punct.as_char() != '#' {
break;
}
attrs.extend(tokens.next());
attrs.extend(tokens.next());
}
let mut visibility = TokenStream::new();
if let Some(proc_macro2::TokenTree::Ident(ident)) = tokens.peek()
&& ident == "pub"
{
visibility.extend(tokens.next());
if let Some(proc_macro2::TokenTree::Group(group)) = tokens.peek()
&& group.delimiter() == proc_macro2::Delimiter::Parenthesis
{
visibility.extend(tokens.next());
}
}
let attrs = syn::parse::Parser::parse2(syn::Attribute::parse_outer, attrs)?;
let visibility = if visibility.is_empty() { None } else { Some(parse2(visibility)?) };
Ok((attrs, visibility, tokens.collect()))
}