use heck::ToLowerCamelCase;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields, Type};
#[proc_macro_derive(GraphQLFields, attributes(graphql))]
pub fn derive_graphql_fields(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let full_type_path = parse_full_type(&input.attrs);
let fields = match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(named) => &named.named,
_ => {
return syn::Error::new_spanned(
&input,
"GraphQLFields can only be derived on structs with named fields",
)
.to_compile_error()
.into();
}
},
_ => {
return syn::Error::new_spanned(&input, "GraphQLFields can only be derived on structs")
.to_compile_error()
.into();
}
};
let mut selection_parts = Vec::new();
let mut validation_checks = Vec::new();
for field in fields {
let field_name = field.ident.as_ref().expect("named field should have ident");
let rust_name = field_name.to_string();
let clean_name = rust_name.strip_prefix("r#").unwrap_or(&rust_name);
let gql_name = clean_name.to_lower_camel_case();
let is_nested = field.attrs.iter().any(|attr| {
if !attr.path().is_ident("graphql") {
return false;
}
let mut found = false;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("nested") {
found = true;
}
Ok(())
});
found
});
if is_nested {
let inner_ty = unwrap_type(&field.ty);
selection_parts.push(quote! {
{
let nested = <#inner_ty as GraphQLFields>::selection();
format!("{} {{ {} }}", #gql_name, nested)
}
});
if full_type_path.is_some() {
validation_checks.push(quote! {
{ let _ = &__v.#field_name; }
});
}
} else {
selection_parts.push(quote! {
#gql_name.to_string()
});
if full_type_path.is_some() {
let field_ty = &field.ty;
validation_checks.push(quote! {
{
fn __check<__F: ::lineark_sdk::FieldCompatible<__C>, __C>(_: &__F) {}
__check::<_, #field_ty>(&__v.#field_name);
}
});
}
}
}
let full_type_assoc = if let Some(ref path) = full_type_path {
quote! { type FullType = #path; }
} else {
quote! { type FullType = Self; }
};
let validation_block = if !validation_checks.is_empty() {
let full_type = full_type_path.as_ref().unwrap();
quote! {
const _: () = {
#[allow(unused)]
fn __graphql_validate(__v: &#full_type) {
#(#validation_checks)*
}
};
}
} else {
quote! {}
};
let expanded = quote! {
impl GraphQLFields for #name {
#full_type_assoc
fn selection() -> String {
let parts: Vec<String> = vec![
#(#selection_parts),*
];
parts.join(" ")
}
}
#validation_block
};
expanded.into()
}
fn parse_full_type(attrs: &[syn::Attribute]) -> Option<syn::Path> {
for attr in attrs {
if !attr.path().is_ident("graphql") {
continue;
}
let mut full_type = None;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("full_type") {
let value = meta.value()?;
full_type = Some(value.parse::<syn::Path>()?);
}
Ok(())
});
if full_type.is_some() {
return full_type;
}
}
None
}
fn unwrap_type(ty: &Type) -> &Type {
if let Type::Path(type_path) = ty {
if let Some(segment) = type_path.path.segments.last() {
let ident = segment.ident.to_string();
if ident == "Option" || ident == "Vec" || ident == "Box" {
if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
if let Some(syn::GenericArgument::Type(inner)) = args.args.first() {
return unwrap_type(inner);
}
}
}
}
}
ty
}