use proc_macro2::TokenStream;
use quote::quote;
use syn::{
spanned::Spanned, Field, FieldsNamed, GenericArgument, Path, PathArguments, Result, Type,
};
use crate::{
attr::{Attr, ContainerAttr, FieldAttr, Inflection, Optional, StructAttr},
deps::Dependencies,
utils::{raw_name_to_py_field, to_py_ident},
DerivedPY,
};
pub(crate) fn named(attr: &StructAttr, name: &str, fields: &FieldsNamed) -> Result<DerivedPY> {
let crate_rename = attr.crate_rename();
let mut formatted_fields = Vec::new();
let mut flattened_fields = Vec::new();
let mut dependencies = Dependencies::new(crate_rename.clone());
if let Some(tag) = &attr.tag {
let formatted = format!("\t{}: {}\n", tag, name);
formatted_fields.push(quote! {
#formatted.to_string()
});
}
for field in &fields.named {
format_field(
&crate_rename,
&mut formatted_fields,
&mut flattened_fields,
&mut dependencies,
field,
&attr.rename_all,
)?;
}
let fields = quote!(<[String]>::join(&[#(#formatted_fields),*], "\n\t"));
let flattened = quote!(<[String]>::join(&[#(#flattened_fields),*], " & "));
let inline = match (formatted_fields.len(), flattened_fields.len()) {
(0, 0) => quote!("{ }".to_owned()),
(_, 0) => quote!(format!("{}", #fields)),
(0, 1) => quote! {{
if #flattened.starts_with('(') && #flattened.ends_with(')') {
#flattened[1..#flattened.len() - 1].trim().to_owned()
} else {
#flattened.trim().to_owned()
}
}},
(0, _) => quote!(#flattened),
(_, _) => quote!(format!("{{ {} }} & {}", #fields, #flattened)),
};
let inline_flattened = match (formatted_fields.len(), flattened_fields.len()) {
(0, 0) => quote!("{ }".to_owned()),
(_, 0) => quote!(format!("{}", #fields)),
(0, _) => quote!(#flattened),
(_, _) => quote!(format!("{{ {} }} & {}", #fields, #flattened)), };
Ok(DerivedPY {
crate_rename,
inline: quote!(#inline.replace(" } & { ", " ")),
inline_flattened: Some(quote!(#inline_flattened.replace(" } & { ", " "))),
docs: attr.docs.clone(),
dependencies,
export: attr.export,
export_to: attr.export_to.clone(),
py_name: name.to_owned(),
concrete: attr.concrete.clone(),
bound: attr.bound.clone(),
enum_def: None,
})
}
fn format_field(
crate_rename: &Path,
formatted_fields: &mut Vec<TokenStream>,
flattened_fields: &mut Vec<TokenStream>,
dependencies: &mut Dependencies,
field: &Field,
rename_all: &Option<Inflection>,
) -> Result<()> {
let field_attr = FieldAttr::from_attrs(&field.attrs)?;
field_attr.assert_validity(field)?;
if field_attr.skip {
return Ok(());
}
let parsed_ty = field_attr.type_as(&field.ty);
let (ty, optional_annotation) = match field_attr.optional {
Optional {
optional: true,
nullable,
} => {
let inner_type = extract_option_argument(&parsed_ty)?; match nullable {
true => (&parsed_ty, "?"), false => (inner_type, "?"), }
}
Optional {
optional: false, ..
} => (&parsed_ty, ""),
};
if field_attr.flatten {
flattened_fields.push(quote!(<#ty as #crate_rename::PY>::inline_flattened()));
dependencies.append_from(ty);
return Ok(());
}
let formatted_ty = field_attr
.type_override
.map(|t| quote!(#t))
.unwrap_or_else(|| {
if field_attr.inline {
dependencies.append_from(ty);
quote!(<#ty as #crate_rename::PY>::inline())
} else {
dependencies.push(ty);
quote!(<#ty as #crate_rename::PY>::name())
}
});
let field_name = to_py_ident(field.ident.as_ref().unwrap());
let name = match (field_attr.rename, rename_all) {
(Some(rn), _) => rn,
(None, Some(rn)) => rn.apply(&field_name),
(None, None) => field_name,
};
let valid_name = raw_name_to_py_field(name);
let docs = match field_attr.docs.is_empty() {
true => "".to_string(),
false => format!("\n{}", &field_attr.docs),
};
formatted_fields.push(quote! {
format!("{}{}{}: {}", #docs, #valid_name, #optional_annotation, #formatted_ty)
});
Ok(())
}
fn extract_option_argument(ty: &Type) -> Result<&Type> {
match ty {
Type::Path(type_path)
if type_path.qself.is_none()
&& type_path.path.leading_colon.is_none()
&& type_path.path.segments.len() == 1
&& type_path.path.segments[0].ident == "Option" =>
{
let segment = &type_path.path.segments[0];
match &segment.arguments {
PathArguments::AngleBracketed(args) if args.args.len() == 1 => {
match &args.args[0] {
GenericArgument::Type(inner_ty) => Ok(inner_ty),
other => syn_err!(other.span(); "`Option` argument must be a type"),
}
}
other => {
syn_err!(other.span(); "`Option` type must have a single generic argument")
}
}
}
other => syn_err!(other.span(); "`optional` can only be used on an Option<T> type"),
}
}