use std::collections::HashSet;
use proc_macro::TokenStream as TokenStream1;
use attribute_derive::FromAttr;
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::{
parse_macro_input, parse_str, punctuated::Punctuated, Attribute, Data, DataEnum, DataStruct, DeriveInput, Expr, Fields, GenericArgument, Ident, PathArguments, Type
};
use crate::Attributes;
macro_rules! new_ambiguous_ident {
($e:expr, $($vars:expr),+ $(,)?) => {
Ident::new(
&format!($e, $($vars,)+),
Span::call_site()
)
};
($e:expr) => {
Ident::new($e, Span::call_site())
}
}
#[derive(FromAttr, Debug)]
#[attribute(ident = match_path)]
#[attribute(error(missing_field = "`{field}` not specified"))]
struct MatchPatternAttrs {
path: String,
requires: Option<String>,
}
#[derive(Debug, PartialEq, Eq)]
pub struct MatchPatternAttrsParsed {
pub path: String,
pub params: Vec<ParamAttrsParsed>,
pub requires: Option<Expr>
}
#[derive(FromAttr, Debug, Default)]
#[attribute(ident = param, aliases=[parent, root])]
#[attribute(error(missing_field = "`{field}` not specified"))]
struct ParamAttrs {
pub name: Option<String>,
pub map_from: Option<String>,
pub requires: Option<String>,
pub is_parent: Option<bool>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ParamAttrsParsed {
pub field_name: Ident,
pub name: String,
pub kind: Type,
pub map_from: Option<Expr>,
pub requires: Option<Expr>,
pub is_option: bool,
pub is_param: bool,
pub is_parent: bool,
}
pub fn build(input: TokenStream1) -> TokenStream1 {
let input = parse_macro_input!(input as DeriveInput);
match &input.data {
Data::Struct(data) => build_struct(&input, data),
Data::Enum(data) => build_enum(&input, data),
_ => panic!("unions are not supported")
}.into()
}
fn build_enum(input: &DeriveInput, data: &DataEnum) -> TokenStream {
let mut match_arms = quote! {};
data.variants.iter().for_each(|variant| {
let params = parse_params(&variant.fields);
let match_paths = parse_paths(&variant.attrs, ¶ms);
validate_paths_by_params(&match_paths, ¶ms);
let ident = &variant.ident;
if params.is_empty() {
let default_uri = ident.to_string().to_lowercase();
match_arms.extend(quote! { Self::#ident => #default_uri.to_string(), });
return;
}
for pattern in match_paths {
let path = &pattern.path;
let mut lhs = quote! {};
let rhs = quote! { format!(#path) };
params.iter().enumerate().for_each(|(idx, p)| {
let index_name = new_ambiguous_ident!("p{}", idx);
if p.is_option && pattern.params.contains(p) {
lhs.extend(quote! { Some(#index_name), })
} else if p.is_option {
lhs.extend(quote! { None, })
} else {
lhs.extend(quote! { #index_name, })
}
});
match_arms.extend(quote! { Self::#ident(#lhs) => #rhs, })
}
});
let (ident, generics) = (&input.ident, &input.generics);
let where_clause = &generics.where_clause;
let crate_ident = crate::get_crate_ident();
let gen = quote! {
impl #generics #crate_ident::UriBuilder for #ident #generics #where_clause {
fn build(&self) -> crate::BuildResult {
Ok(self.to_string().into())
}
}
impl #generics std::fmt::Display for #ident #generics #where_clause {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
{
write!(f, "{}", match self {
#match_arms
})
}
}
};
gen
}
fn build_struct(input: &DeriveInput, data: &DataStruct) -> TokenStream {
let crate_ident = crate::get_crate_ident();
let where_clause = &input.generics.where_clause;
let params = parse_params(&data.fields);
let match_paths = parse_paths(&input.attrs, ¶ms);
validate_paths_by_params(&match_paths, ¶ms);
let (ident, generics) = (&input.ident, &input.generics);
let match_arms = build_matches(&match_paths, ¶ms);
let mut gen = quote! {
impl #generics #crate_ident::UriBuilder for #ident #generics #where_clause {
fn build(&self) -> #crate_ident::BuildResult {
match self {
#match_arms
}
}
}
};
gen.extend(build_methods(input, ¶ms));
gen.extend(quote! {
impl #generics std::fmt::Display for #ident #generics #where_clause {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self
.build()
.expect("build must produce a string"))
}
}
});
gen
}
fn build_matches(paths: &[MatchPatternAttrsParsed], params: &[ParamAttrsParsed]) -> TokenStream {
let mut match_arms = quote! {};
for pattern in paths {
if pattern.params.is_empty() {
match_arms.extend(build_match_arm_shallow(pattern, params));
} else {
match_arms.extend(build_match_arm(pattern, params));
}
}
match_arms.extend(quote! {
_ => {
Err(crate::uri::UriBuildError::UnrecognizedPattern.into())
}
});
match_arms
}
fn build_match_arm(pattern: &MatchPatternAttrsParsed, params: &[ParamAttrsParsed]) -> TokenStream {
let path = &pattern.path;
let mut lhs = quote! {};
let mut rhs = quote! {};
let mut rhs_inner = quote! {};
params.iter().filter(|p| p.is_param).enumerate().for_each(|(idx, p)| {
let field_name = &p.field_name;
let param_name = new_ambiguous_ident!(&p.name);
let index_name = new_ambiguous_ident!("p{}", idx);
if !pattern.params.contains(p) && p.is_option {
lhs.extend(quote! { #field_name: None, });
return
} else if !pattern.params.contains(p) {
return
}
if let Some(mf) = &p.map_from {
lhs.extend(quote! { #field_name: Some(#index_name), });
rhs_inner.extend(quote! {
let mapper = #mf; let #param_name = mapper(#index_name);
});
} else {
lhs.extend(quote! { #field_name: Some(#param_name), });
}
if let Some(rq) = &p.requires {
rhs_inner.extend(quote! {
if !#rq(#param_name) {
return Err(crate::uri::UriBuildError::Validation.into())
}
})
}
});
let mut conditional = quote! {};
if let Some(rq) = &pattern.requires {
conditional.extend(quote! { if #rq(self) })
}
rhs.extend(quote! {
{
#rhs_inner format!(#path)
}
});
lhs.extend(quote! { .. });
quote! { Self { #lhs } #conditional => Ok(#rhs), }
}
fn build_match_arm_shallow(pattern: &MatchPatternAttrsParsed, params: &[ParamAttrsParsed]) -> TokenStream {
let path = &pattern.path;
let mut lhs = quote! {};
let rhs = quote! { String::from(#path) };
params.iter().filter(|p| p.is_param).for_each(|p| {
let field_name = &p.field_name;
lhs.extend(quote! { #field_name: None, })
});
lhs.extend(quote! { .. });
quote! { Self { #lhs } => Ok(#rhs), }
}
fn build_methods(input: &DeriveInput, params: &[ParamAttrsParsed]) -> TokenStream {
let mut with_methods = quote! {};
params.iter().for_each(|param| {
let method_name = new_ambiguous_ident!("with_{}", ¶m.name);
let field_name = ¶m.field_name;
let kind = ¶m.kind;
if param.is_parent {
with_methods.extend(quote! {
pub fn with_parent(mut self, value: #kind) -> Self {
self.#field_name = Some(value);
self
}
pub fn from_parent(value: #kind) -> Self
where
Self: Default,
{
Self::default().with_parent(value)
}
})
} else if param.is_option {
with_methods.extend(quote! {
pub fn #method_name<V: Clone + Into<#kind>>(mut self, value: V) -> Self {
self.#field_name = Some(value.to_owned().into());
self
}
})
} else {
with_methods.extend(quote! {
pub fn #method_name<V: Clone + Into<#kind>>(mut self, value: V) -> Self {
self.#field_name = value.to_owned().into();
self
}
})
}
});
let (ident, generics) = (&input.ident, &input.generics);
let where_clause = &generics.where_clause;
quote! {
impl #generics #ident #generics #where_clause {
#with_methods
}
}
}
fn filter_match_paths(attr: &&Attribute) -> bool {
attr.meta.path().segments[0].ident == "match_path"
}
fn filter_params(attr: &&Attribute) -> bool {
["param", "parent"]
.contains(&attr.meta.path().segments[0].ident.to_string().as_str())
}
fn is_parent(attr: &Attribute) -> bool {
attr.meta.path().segments[0].ident == "parent"
}
fn is_optional_type(kind: &Type) -> bool {
match &kind {
Type::Path(tp) => tp.path.segments[0].ident == "Option",
_ => panic!("expected a type path")
}
}
fn parse_optional_type(kind: &Type) -> &Type {
match &kind {
Type::Path(tp) => {
match &tp.path.segments[0].arguments {
PathArguments::AngleBracketed(ab) => {
match &ab.args[0] {
GenericArgument::Type(ty) => ty,
_ => panic!("expected a type")
}
},
_ => panic!("unexpected syntax")
}
},
_ => unreachable!()
}
}
fn parse_params(fields: &Fields) -> Vec<ParamAttrsParsed> {
match fields {
Fields::Named(f) => {
f.named.to_owned()
},
Fields::Unit => Punctuated::new(),
Fields::Unnamed(f) => {
f.unnamed.to_owned()
}
}
.iter()
.enumerate()
.map(|(idx, f)| {
let ident = f
.ident
.clone()
.unwrap_or(new_ambiguous_ident!("p{}", idx));
let is_param = !f
.attrs
.iter()
.filter(filter_params)
.collect::<Vec<_>>()
.is_empty();
let attrs = f
.attrs
.iter()
.find(filter_params)
.and_then(|a| {
let mut parsed = ParamAttrs::from_attribute(a)
.unwrap();
parsed.is_parent = is_parent(a).into();
parsed.into()
});
let attrs = attrs.unwrap_or_default();
let kind = f.ty.clone();
let is_parent = attrs.is_parent.unwrap_or_default();
let is_option = is_optional_type(&kind);
let name = attrs
.name
.as_ref().unwrap_or(&ident.to_string())
.to_owned();
let kind = if is_option {
parse_optional_type(&kind)
} else {
&kind
}.to_owned();
let map_from = attrs
.map_from
.as_ref()
.map(|mf| parse_str::<Expr>(mf).expect("must be a parsable expression"));
let requires = attrs
.requires
.as_ref()
.map(|rq| parse_str::<Expr>(rq).expect("must be a parsable expression"));
ParamAttrsParsed {
field_name: ident,
name,
kind,
map_from,
requires,
is_option,
is_parent,
is_param,
}
})
.collect()
}
fn parse_paths(attrs: &Attributes, params: &[ParamAttrsParsed]) -> Vec<MatchPatternAttrsParsed> {
attrs
.iter()
.filter(filter_match_paths)
.map(MatchPatternAttrs::from_attribute)
.map(|a| {
let a = a.unwrap();
let requires = a
.requires
.map(|rq| parse_str::<Expr>(&rq).expect("must be a parsable expression"));
let mut parsed = MatchPatternAttrsParsed{
path: a.path,
params: vec![],
requires
};
parsed
.path
.split('/')
.filter(|p| p.contains(['{', '}']))
.map(|param_name| param_name.replace(['{', '}'], ""))
.for_each(|param_name| {
let found = params
.iter()
.find(|p| p.name == param_name);
if let Some(f) = found {
parsed.params.push(f.to_owned())
}
});
parsed
})
.collect()
}
fn validate_paths_by_params(paths: &[MatchPatternAttrsParsed], params: &[ParamAttrsParsed]) {
let mut param_map = HashSet::new();
for match_path in paths.iter() {
match_path.path.split('/').for_each(|p| {
if p.contains(['{', '}']) {
param_map.insert(p.replace(['{', '}'], ""));
}
})
}
if !param_map.iter().all(|n| params.iter().any(|p| p.name == *n)) {
let fields = param_map
.iter()
.filter(|n| !params.iter().any(|p| p.name == **n))
.map(|p| p.to_owned())
.collect::<Vec<_>>()
.join(", ");
panic!("missing parameter(s) declared in path patterns: ({fields})")
}
}