feat(documentation): add polymorphic generate_schema macro (#1183)

Co-authored-by: pixincreate@work <69745008+pixincreate@users.noreply.github.com>
This commit is contained in:
Narayan Bhat
2023-05-19 16:37:54 +05:30
committed by GitHub
parent bd0069e2a8
commit 53aa5ac92d
14 changed files with 2568 additions and 1218 deletions

View File

@ -0,0 +1,140 @@
use std::collections::{HashMap, HashSet};
use syn::{self, parse_quote, punctuated::Punctuated, Token};
use crate::macros::helpers;
/// Parse schemas from attribute
/// Example
///
/// #[mandatory_in(PaymentsCreateRequest, PaymentsUpdateRequest)]
/// would return
///
/// [PaymentsCreateRequest, PaymentsUpdateRequest]
fn get_inner_path_ident(attribute: &syn::Attribute) -> syn::Result<Vec<syn::Ident>> {
Ok(attribute
.parse_args_with(Punctuated::<syn::Ident, Token![,]>::parse_terminated)?
.into_iter()
.collect::<Vec<_>>())
}
fn get_struct_fields(data: syn::Data) -> syn::Result<Punctuated<syn::Field, syn::token::Comma>> {
if let syn::Data::Struct(syn::DataStruct {
fields: syn::Fields::Named(syn::FieldsNamed { ref named, .. }),
..
}) = data
{
Ok(named.to_owned())
} else {
Err(syn::Error::new(
proc_macro2::Span::call_site(),
"This macro cannot be used on structs with no fields",
))
}
}
pub fn polymorphic_macro_derive_inner(
input: syn::DeriveInput,
) -> syn::Result<proc_macro2::TokenStream> {
let schemas_to_create =
helpers::get_metadata_inner::<syn::Ident>("generate_schemas", &input.attrs)?;
let fields = get_struct_fields(input.data)
.map_err(|error| syn::Error::new(proc_macro2::Span::call_site(), error))?;
// Go through all the fields and create a mapping of required fields for a schema
// PaymentsCreate -> ["amount","currency"]
// This will be stored in a hashset
// mandatory_hashset -> ((PaymentsCreate, amount), (PaymentsCreate,currency))
let mut mandatory_hashset = HashSet::<(syn::Ident, syn::Ident)>::new();
let mut other_fields_hm = HashMap::<syn::Field, Vec<syn::Attribute>>::new();
fields.iter().for_each(|field| {
// Partition the attributes of a field into two vectors
// One with #[mandatory_in] attributes present
// Rest of the attributes ( include only the schema attribute, serde is not required)
let (mandatory_attribute, other_attributes) = field
.attrs
.iter()
.partition::<Vec<_>, _>(|attribute| attribute.path.is_ident("mandatory_in"));
// Other attributes ( schema ) are to be printed as is
other_attributes
.iter()
.filter(|attribute| attribute.path.is_ident("schema") || attribute.path.is_ident("doc"))
.for_each(|attribute| {
// Since attributes will be modified, the field should not contain any attributes
// So create a field, with previous attributes removed
let mut field_without_attributes = field.clone();
field_without_attributes.attrs.clear();
other_fields_hm
.entry(field_without_attributes.to_owned())
.or_insert(vec![])
.push(attribute.to_owned().to_owned());
});
// Mandatory attributes are to be inserted into hashset
// The hashset will store it in this format
// (PaymentsCreateRequest, "amount")
// (PaymentsConfirmRequest, "currency")
//
// For these attributes, we need to later add #[schema(required = true)] attribute
_ = mandatory_attribute
.iter()
// Filter only #[mandatory_in] attributes
.map(|&attribute| get_inner_path_ident(attribute))
.try_for_each(|schemas| {
let res = schemas
.map_err(|error| syn::Error::new(proc_macro2::Span::call_site(), error))?
.iter()
.filter_map(|schema| field.ident.to_owned().zip(Some(schema.to_owned())))
.collect::<HashSet<_>>();
mandatory_hashset.extend(res);
Ok::<_, syn::Error>(())
});
});
let schemas = schemas_to_create
.iter()
.map(|schema| {
let fields = other_fields_hm
.iter()
.flat_map(|(field, value)| {
let mut attributes = value
.iter()
.map(|attribute| quote::quote!(#attribute))
.collect::<Vec<_>>();
// If the field is required for this schema, then add
// #[schema(required = true)] for this field
let required_attribute: syn::Attribute =
parse_quote!(#[schema(required = true)]);
// Can be none, because tuple fields have no ident
field.ident.to_owned().and_then(|field_ident| {
mandatory_hashset
.contains(&(field_ident, schema.to_owned()))
.then(|| attributes.push(quote::quote!(#required_attribute)))
});
quote::quote! {
#(#attributes)*
#field,
}
})
.collect::<Vec<_>>();
quote::quote! {
#[derive(utoipa::ToSchema)]
pub struct #schema {
#(#fields)*
}
}
})
.collect::<Vec<_>>();
Ok(quote::quote! {
#(#schemas)*
})
}