use crate::logic::{
args::{AttrArgs, ModelAttrArgs, OptionType},
*,
};
use proc_macro2::{Ident, TokenStream};
use proc_macro_error::abort;
use quote::quote;
use syn::{Attribute, DeriveInput, Type};
pub fn impl_patch_model(
ast: &DeriveInput,
attr: &Attribute,
model_args: &ModelAttrArgs,
) -> TokenStream {
let (args, mut remainder) = AttrArgs::parse(attr, model_args, false);
let AttrArgs {
name,
fields: _,
derive,
preset,
attributes_with,
} = args.clone();
let skip_serializing_double_option =
take_ident_bool("skip_serializing_double_option", &mut remainder).unwrap_or_default();
let option = OptionType::parse(&mut remainder).unwrap_or_else(|| preset.option());
AttrArgs::abort_unexpected(&remainder, &["option"]);
let original_name = &ast.ident;
let mut fields_and_is_option: Vec<(&Ident, bool)> = vec![];
let mut fields: Vec<_> = vec![];
match &ast.data {
syn::Data::Struct(data) => data
.fields
.iter()
.filter(|f| {
preset.predicate(f)
&& args
.fields
.predicate(f.ident.as_ref().expect("Field must be named"))
})
.for_each(|field| {
let field_name = &field.ident.as_ref().unwrap();
let docs = extract_docs(&field.attrs);
let field_ty = &field.ty;
let option_ty = extract_type_from_option(field_ty);
fields_and_is_option.push((field_name, option_ty.is_some()));
fields.push(impl_struct_fields(
field_name,
field_ty,
option_ty,
&docs,
option,
skip_serializing_double_option,
));
}),
_ => abort!(attr, "Patch Models can only be derived for structs"),
};
let attributes = attributes_with.gen_top_attributes(ast);
let derives = gen_derive(derive.as_ref());
let impl_from_derived = impl_from_derived(&fields_and_is_option, option);
let impl_merge = impl_merge(&fields_and_is_option, option);
let impl_extras = impl_extras(original_name, &name, model_args);
quote! {
#derives
#(#attributes)*
pub struct #name {
#(#fields),*
}
impl #name {
pub fn from_derived(value: #original_name) -> Self {
Self {
#impl_from_derived
}
}
pub fn merge(self, mut value: #original_name) -> #original_name {
self.merge_mut(&mut value);
value
}
pub fn merge_mut(self, mut value: &mut #original_name) {
#impl_merge
}
}
impl ::core::convert::From<#original_name> for #name {
fn from(value: #original_name) -> Self {
#name::from_derived(value)
}
}
#(#impl_extras)*
}
}
fn impl_merge(fields: &[(&Ident, bool)], option: OptionType) -> TokenStream {
match option {
OptionType::MaybeUndefined => {
let option_field: Vec<_> = fields
.iter()
.filter_map(|(ident, is_option)| is_option.then_some(ident))
.collect();
let required_field: Vec<_> = fields
.iter()
.filter_map(|(ident, is_option)| (!is_option).then_some(ident))
.collect();
quote! {
#(
match self.#required_field {
::core::option::Option::Some(v) => value.#required_field = v,
::core::option::Option::None => {},
}
)*
#(
match self.#option_field {
::poem_openapi::types::MaybeUndefined::Value(v) => value.#option_field = ::core::option::Option::Some(v),
::poem_openapi::types::MaybeUndefined::Null => value.#option_field = ::core::option::Option::None,
::poem_openapi::types::MaybeUndefined::Undefined => {},
}
)*
}
}
OptionType::Option => {
let field: Vec<_> = fields.iter().map(|v| v.0).collect();
quote! {
#(
match self.#field {
::core::option::Option::Some(v) => value.#field = v,
::core::option::Option::None => {},
}
)*
}
}
}
}
fn impl_from_derived(fields: &[(&Ident, bool)], option: OptionType) -> TokenStream {
match option {
OptionType::MaybeUndefined => {
let option_field: Vec<_> = fields
.iter()
.filter_map(|(ident, is_option)| is_option.then_some(ident))
.collect();
let required_field: Vec<_> = fields
.iter()
.filter_map(|(ident, is_option)| (!is_option).then_some(ident))
.collect();
quote! {
#(
#option_field: ::poem_openapi::types::MaybeUndefined::from_opt_undefined(value.#option_field),
)*
#(
#required_field: ::core::option::Option::Some(value.#required_field),
)*
}
}
OptionType::Option => {
let field: Vec<_> = fields.iter().map(|v| v.0).collect();
quote! {
#(
#field: ::core::option::Option::Some(value.#field),
)*
}
}
}
}
fn impl_struct_fields(
field_name: &Ident,
field_ty: &Type,
#[allow(unused_variables)] option_ty: Option<&Type>,
docs: &TokenStream,
option: OptionType,
skip_serializing_double_option: bool,
) -> TokenStream {
match option {
OptionType::MaybeUndefined => match option_ty {
Some(t) => {
quote! {
#docs
pub #field_name: ::poem_openapi::types::MaybeUndefined<#t>
}
}
_ => {
quote! {
#docs
pub #field_name: core::option::Option<#field_ty>
}
}
},
OptionType::Option => {
let skip_serializing_if = match skip_serializing_double_option && option_ty.is_some()
{
true => quote! {
#[serde(skip_serializing_if = "::core::option::Option::is_none")]
},
false => quote! {},
};
quote! {
#docs
#skip_serializing_if
pub #field_name: core::option::Option<#field_ty>
}
}
}
}
fn extract_type_from_option(ty: &Type) -> Option<&Type> {
use syn::{GenericArgument, Path, PathArguments, PathSegment};
fn extract_type_path(ty: &syn::Type) -> Option<&Path> {
match *ty {
syn::Type::Path(ref typepath) if typepath.qself.is_none() => Some(&typepath.path),
_ => None,
}
}
fn extract_option_segment(path: &Path) -> Option<&PathSegment> {
let idents_of_path = path.segments.iter().fold(String::new(), |mut acc, v| {
acc.push_str(&v.ident.to_string());
acc.push('|');
acc
});
vec!["Option|", "std|option|Option|", "core|option|Option|"]
.into_iter()
.find(|s| idents_of_path == *s)
.and_then(|_| path.segments.last())
}
extract_type_path(ty)
.and_then(|path| extract_option_segment(path))
.and_then(|path_seg| {
let type_params = &path_seg.arguments;
match *type_params {
PathArguments::AngleBracketed(ref params) => params.args.first(),
_ => None,
}
})
.and_then(|generic_arg| match *generic_arg {
GenericArgument::Type(ref ty) => Some(ty),
_ => None,
})
}