use darling::{ast::Data, FromDeriveInput, FromField};
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::{parse_macro_input, DeriveInput, Ident, Type};
#[derive(Debug, FromField)]
#[darling(attributes(payrix))]
struct PayrixField {
ident: Option<Ident>,
ty: Type,
#[darling(default)]
readonly: bool,
#[darling(default)]
create_only: bool,
#[darling(default)]
mutable: bool,
#[darling(default)]
create_required: bool,
#[darling(default)]
create_type: Option<String>,
}
#[derive(Debug, FromDeriveInput)]
#[darling(attributes(payrix), supports(struct_named))]
struct PayrixEntityArgs {
ident: Ident,
data: Data<(), PayrixField>,
#[darling(default)]
create: Option<Ident>,
#[darling(default)]
update: Option<Ident>,
}
fn get_serde_rename(attrs: &[syn::Attribute]) -> Option<String> {
for attr in attrs {
if attr.path().is_ident("serde") {
if let Ok(nested) = attr.parse_args_with(
syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated,
) {
for meta in nested {
if let syn::Meta::NameValue(nv) = meta {
if nv.path.is_ident("rename") {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(s),
..
}) = nv.value
{
return Some(s.value());
}
}
}
}
}
}
}
None
}
fn is_option_type(ty: &Type) -> bool {
if let Type::Path(type_path) = ty {
if let Some(segment) = type_path.path.segments.last() {
return segment.ident == "Option";
}
}
false
}
fn is_vec_type(ty: &Type) -> bool {
if let Type::Path(type_path) = ty {
if let Some(segment) = type_path.path.segments.last() {
return segment.ident == "Vec";
}
}
false
}
fn is_bool_type(ty: &Type) -> bool {
if let Type::Path(type_path) = ty {
if let Some(segment) = type_path.path.segments.last() {
return segment.ident == "bool";
}
}
false
}
fn wrap_in_option(ty: &Type) -> TokenStream2 {
if is_option_type(ty) {
quote! { #ty }
} else if is_bool_type(ty) {
quote! { Option<bool> }
} else {
quote! { Option<#ty> }
}
}
struct RequestField {
name: Ident,
ty: Type,
rename: Option<String>,
required: bool,
override_type: Option<String>,
}
fn generate_request_type(
type_name: &Ident,
fields: &[RequestField],
is_create: bool,
source_name: &Ident,
) -> TokenStream2 {
let field_defs: Vec<TokenStream2> = fields
.iter()
.map(|field| {
let name = &field.name;
let rename_attr = field.rename.as_ref().map(|r| {
quote! { #[serde(rename = #r)] }
});
let field_doc = format!("See [`{}`] for field documentation.", source_name);
let (field_ty, skip_attr) = if field.required {
if let Some(ref override_type) = field.override_type {
let ty: Type = syn::parse_str(override_type)
.expect("Invalid create_type value");
(quote! { #ty }, quote! {})
} else {
let ty = &field.ty;
if is_option_type(ty) {
if let Type::Path(type_path) = ty {
if let Some(segment) = type_path.path.segments.last() {
if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
if let Some(syn::GenericArgument::Type(inner)) = args.args.first() {
return quote! {
#[doc = #field_doc]
#rename_attr
pub #name: #inner
};
}
}
}
}
}
(quote! { #ty }, quote! {})
}
} else {
if let Some(ref override_type) = field.override_type {
let ty: Type = syn::parse_str(override_type)
.expect("Invalid create_type value");
(quote! { Option<#ty> }, quote! { #[serde(skip_serializing_if = "Option::is_none")] })
} else {
let wrapped_ty = wrap_in_option(&field.ty);
(wrapped_ty, quote! { #[serde(skip_serializing_if = "Option::is_none")] })
}
};
quote! {
#[doc = #field_doc]
#rename_attr
#skip_attr
pub #name: #field_ty
}
})
.collect();
let type_doc = if is_create {
format!("Request body for creating a new [`{}`].", source_name)
} else {
format!("Request body for updating an existing [`{}`].", source_name)
};
let has_required = fields.iter().any(|f| f.required);
let derives = if has_required {
quote! { #[derive(Debug, Clone, serde::Serialize)] }
} else {
quote! { #[derive(Debug, Clone, Default, serde::Serialize)] }
};
quote! {
#[doc = #type_doc]
#derives
#[serde(rename_all = "camelCase")]
pub struct #type_name {
#(#field_defs),*
}
}
}
#[proc_macro_derive(PayrixEntity, attributes(payrix))]
pub fn derive_payrix_entity(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let original_fields: Vec<_> = if let syn::Data::Struct(data) = &input.data {
if let syn::Fields::Named(fields) = &data.fields {
fields.named.iter().collect()
} else {
Vec::new()
}
} else {
Vec::new()
};
let args = match PayrixEntityArgs::from_derive_input(&input) {
Ok(args) => args,
Err(e) => return TokenStream::from(e.write_errors()),
};
let struct_name = &args.ident;
let create_name = args
.create
.unwrap_or_else(|| format_ident!("Create{}", struct_name));
let update_name = args
.update
.unwrap_or_else(|| format_ident!("Update{}", struct_name));
let mut create_fields: Vec<RequestField> = Vec::new();
let mut update_fields: Vec<RequestField> = Vec::new();
let fields = match args.data {
Data::Struct(ref fields) => fields,
_ => panic!("PayrixEntity only supports structs"),
};
for (idx, field) in fields.iter().enumerate() {
let field_name = match &field.ident {
Some(name) => name.clone(),
None => continue,
};
if is_vec_type(&field.ty) {
continue;
}
let serde_rename = original_fields
.get(idx)
.and_then(|f| get_serde_rename(&f.attrs));
if field.readonly {
continue;
} else if field.create_only {
create_fields.push(RequestField {
name: field_name,
ty: field.ty.clone(),
rename: serde_rename,
required: field.create_required,
override_type: field.create_type.clone(),
});
} else if field.mutable {
create_fields.push(RequestField {
name: field_name.clone(),
ty: field.ty.clone(),
rename: serde_rename.clone(),
required: field.create_required,
override_type: field.create_type.clone(),
});
update_fields.push(RequestField {
name: field_name,
ty: field.ty.clone(),
rename: serde_rename,
required: false,
override_type: None,
});
}
}
let create_type = generate_request_type(&create_name, &create_fields, true, struct_name);
let update_type = generate_request_type(&update_name, &update_fields, false, struct_name);
let expanded = quote! {
#create_type
#update_type
};
TokenStream::from(expanded)
}