use quote::{format_ident, quote};
use syn::{Fields, ItemStruct, Path, Type, parse_quote};
use crate::format::DocRec;
pub struct MagmaDocumentation {
pub ident: syn::Ident,
pub generics: syn::Generics,
pub doc: Option<String>,
pub fields: Vec<MagmaField>,
}
pub struct MagmaField {
pub name: String,
pub ty: Type,
pub doc: Option<String>,
}
impl MagmaDocumentation {
pub fn from_item(s: &ItemStruct, doc_rec: DocRec) -> syn::Result<Self> {
let struct_docs = extract_docs(&s.attrs);
if struct_docs.is_none() && doc_rec.need_struct() {
return Err(syn::Error::new_spanned(
&s.ident,
"Client structs must have documentation (///) to generate API specs.",
));
}
let fields = match &s.fields {
Fields::Named(named) => named
.named
.iter()
.map(|f| {
Ok(MagmaField {
name: f.ident.as_ref().unwrap().to_string(),
ty: f.ty.clone(),
doc: extract_docs(&f.attrs),
})
})
.collect::<syn::Result<Vec<_>>>()?,
Fields::Unnamed(unnamed) => unnamed
.unnamed
.iter()
.enumerate()
.map(|(i, f)| {
Ok(MagmaField {
name: i.to_string(),
ty: f.ty.clone(),
doc: extract_docs(&f.attrs),
})
})
.collect::<syn::Result<Vec<_>>>()?,
Fields::Unit => vec![],
};
Ok(Self {
ident: s.ident.clone(),
generics: s.generics.clone(),
doc: struct_docs,
fields,
})
}
pub fn generate(&self, import_location: &Path) -> syn::Result<proc_macro2::TokenStream> {
let values_path = quote! { #import_location::format::value };
let name = &self.ident;
let mut generics = self.generics.clone();
for param in &mut generics.params {
if let syn::GenericParam::Type(ref mut type_param) = *param {
type_param.bounds.push(parse_quote!(#values_path::Typeable));
}
}
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let field_entries: Vec<proc_macro2::TokenStream> = self
.fields
.iter()
.map(|f| {
let fname = &f.name;
let fty = &f.ty;
let doc_setter = match &f.doc {
Some(doc_str) => {
quote! (.add_docstring(::std::borrow::Cow::Borrowed(#doc_str)))
}
None => quote! {},
};
quote! {
{
let field = #values_path::PyroField::<'static>::new(
#fname,
<#fty as #values_path::Typeable>::pyro_type(),
<#fty as #values_path::Typeable>::is_nullable(),
)#doc_setter;
field
}
}
})
.collect();
let struct_doc_quote = match &self.doc {
Some(d) => quote! { Some(::std::borrow::Cow::Borrowed(#d)) },
None => quote! { None },
};
Ok(quote! {
impl #impl_generics #values_path::TypeableRow for #name #ty_generics #where_clause {
fn schema() -> #values_path::PyroSchema<'static> {
#values_path::PyroSchema {
fields: ::std::borrow::Cow::Owned(vec![
#(#field_entries),*
]),
documentation: #struct_doc_quote,
}
}
}
})
}
pub fn generate_for_ref(
&self,
import_location: &Path,
) -> syn::Result<proc_macro2::TokenStream> {
let values_path = quote! { #import_location::format::value };
let ref_name = format_ident!("{}Ref", self.ident);
let field_entries: Vec<proc_macro2::TokenStream> = self
.fields
.iter()
.map(|f| {
let fname = &f.name;
let fty = &f.ty;
let doc_setter = match &f.doc {
Some(doc_str) => {
quote! (.add_docstring(::std::borrow::Cow::Borrowed(#doc_str)))
}
None => quote! {},
};
quote! {
{
let mut field = #values_path::PyroField::<'static>::new(
#fname,
<#fty as #values_path::Typeable>::pyro_type(),
<#fty as #values_path::Typeable>::is_nullable(),
)#doc_setter;
field
}
}
})
.collect();
let struct_doc_quote = match &self.doc {
Some(d) => quote! { Some(::std::borrow::Cow::Borrowed(#d)) },
None => quote! { None },
};
Ok(quote! {
impl #values_path::TypeableRow for #ref_name<'_> {
fn schema() -> #values_path::PyroSchema<'static> {
#values_path::PyroSchema {
fields: ::std::borrow::Cow::Owned(vec![
#(#field_entries),*
]),
documentation: #struct_doc_quote,
}
}
}
})
}
}
pub fn generate_documented_impl(
item: &ItemStruct,
import_location: &Path,
doc_rec: DocRec,
) -> syn::Result<proc_macro2::TokenStream> {
let documentation = MagmaDocumentation::from_item(item, doc_rec)?;
documentation.generate(import_location)
}
pub fn ref_documentation(
item: &ItemStruct,
import_location: &Path,
doc_rec: DocRec,
) -> syn::Result<proc_macro2::TokenStream> {
let documentation = MagmaDocumentation::from_item(item, doc_rec)?;
documentation.generate_for_ref(import_location)
}
fn extract_docs(attrs: &[syn::Attribute]) -> Option<String> {
let docs: Vec<String> = attrs
.iter()
.filter(|attr| attr.path().is_ident("doc"))
.filter_map(|attr| match &attr.meta {
syn::Meta::NameValue(nv) => {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(lit),
..
}) = &nv.value
{
Some(lit.value().trim().to_string())
} else {
None
}
}
_ => None,
})
.collect();
if docs.is_empty() {
None
} else {
Some(docs.join("\n"))
}
}