|
1 | | -use proc_macro2::TokenStream as TokenStream2; |
2 | | -use quote::quote; |
3 | | -use syn::{Error, Ident, spanned::Spanned}; |
| 1 | +use convert_case::{Case, Casing}; |
| 2 | +use proc_macro_crate::FoundCrate; |
| 3 | +use proc_macro2::{Span, TokenStream as TokenStream2}; |
| 4 | +use quote::{format_ident, quote}; |
| 5 | +use syn::{Data, DeriveInput, Error, LitStr, Meta, Type, spanned::Spanned}; |
4 | 6 |
|
5 | | -pub fn derive(struct_name: Ident, data: syn::Data) -> syn::Result<TokenStream2> { |
6 | | - let syn::Data::Struct(data_struct) = data else { |
7 | | - return Err(Error::new(proc_macro2::Span::call_site(), String::from("Deriving `Destruct` is currently only supported for structs"))); |
| 7 | +pub fn derive(input: DeriveInput) -> syn::Result<TokenStream2> { |
| 8 | + let struct_name = input.ident; |
| 9 | + let generics = input.generics; |
| 10 | + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); |
| 11 | + |
| 12 | + let Data::Struct(data_struct) = input.data else { |
| 13 | + return Err(Error::new( |
| 14 | + Span::call_site(), |
| 15 | + "Deriving `Destruct` is currently only supported for structs", |
| 16 | + )); |
8 | 17 | }; |
9 | 18 |
|
10 | | - let crate_name = proc_macro_crate::crate_name("graphene-core").map_err(|e| { |
| 19 | + let graphene_core = match proc_macro_crate::crate_name("graphene-core").map_err(|e| { |
11 | 20 | Error::new( |
12 | | - proc_macro2::Span::call_site(), |
13 | | - format!("Failed to find location of graphene_core. Make sure it is imported as a dependency: {}", e), |
| 21 | + Span::call_site(), |
| 22 | + format!("Failed to find location of graphene_core. Make sure it is imported as a dependency: {e}"), |
14 | 23 | ) |
15 | | - })?; |
16 | | - |
17 | | - let path = quote!(std::module_path!().rsplit_once("::").unwrap().0); |
| 24 | + })? { |
| 25 | + FoundCrate::Itself => quote!(crate), |
| 26 | + FoundCrate::Name(name) => { |
| 27 | + let ident = syn::Ident::new(&name, Span::call_site()); |
| 28 | + quote!(#ident) |
| 29 | + } |
| 30 | + }; |
18 | 31 |
|
19 | 32 | let mut node_implementations = Vec::with_capacity(data_struct.fields.len()); |
20 | | - let mut field_structs = Vec::with_capacity(data_struct.fields.len()); |
| 33 | + let mut output_fields = Vec::with_capacity(data_struct.fields.len()); |
21 | 34 |
|
22 | 35 | for field in data_struct.fields { |
23 | 36 | let Some(field_name) = field.ident else { |
24 | | - return Err(Error::new(field.span(), String::from("Destruct cant be used on tuple structs"))); |
| 37 | + return Err(Error::new(field.span(), "Destruct cannot be used on tuple structs")); |
25 | 38 | }; |
| 39 | + |
26 | 40 | let ty = field.ty; |
27 | | - let fn_name = quote::format_ident!("extract_ {field_name}"); |
28 | | - node_implementations.push(quote! { |
29 | | - #[node_macro(category(""))] |
30 | | - fn #fn_name(_: impl Ctx, data: #struct_name) -> #ty { |
31 | | - data.#field_name |
32 | | - } |
33 | | - }); |
| 41 | + let output_name = parse_output_name(&field.attrs)?.unwrap_or_else(|| field_name.to_string().to_case(Case::Title)); |
| 42 | + let output_name_lit = LitStr::new(&output_name, field_name.span()); |
34 | 43 |
|
35 | | - field_structs.push(quote! { |
36 | | - #crate_name::registry::FieldStruct { |
37 | | - name: stringify!(#field_name), |
38 | | - node_path: concat!() |
| 44 | + let fn_name = format_ident!("extract_{}_{}", struct_name.to_string().to_case(Case::Snake), field_name); |
| 45 | + let node_struct_name = format_ident!("{}Node", fn_name.to_string().to_case(Case::Pascal)); |
39 | 46 |
|
| 47 | + node_implementations.push(generate_extractor_node(&graphene_core, &fn_name, &struct_name, &field_name, &ty, &output_name_lit)); |
| 48 | + output_fields.push(quote! { |
| 49 | + #graphene_core::registry::StructField { |
| 50 | + name: #output_name_lit, |
| 51 | + node_path: concat!(std::module_path!().rsplit_once("::").unwrap().0, "::", stringify!(#node_struct_name)), |
| 52 | + ty: #graphene_core::concrete!(#ty), |
40 | 53 | } |
41 | | - }) |
| 54 | + }); |
42 | 55 | } |
43 | 56 |
|
44 | 57 | Ok(quote! { |
45 | | - impl graphene_core::registry::Destruct for #struct_name { |
46 | | - fn fields() -> &[graphene_core::registry::FieldStruct] { |
47 | | - &[ |
| 58 | + #(#node_implementations)* |
48 | 59 |
|
| 60 | + impl #impl_generics #graphene_core::registry::Destruct for #struct_name #ty_generics #where_clause { |
| 61 | + fn fields() -> &'static [#graphene_core::registry::StructField] { |
| 62 | + &[ |
| 63 | + #(#output_fields,)* |
49 | 64 | ] |
50 | 65 | } |
51 | 66 | } |
52 | | - |
53 | 67 | }) |
54 | 68 | } |
| 69 | + |
| 70 | +fn generate_extractor_node( |
| 71 | + graphene_core: &TokenStream2, |
| 72 | + fn_name: &syn::Ident, |
| 73 | + struct_name: &syn::Ident, |
| 74 | + field_name: &syn::Ident, |
| 75 | + ty: &Type, |
| 76 | + output_name: &LitStr, |
| 77 | +) -> TokenStream2 { |
| 78 | + quote! { |
| 79 | + #[node_macro::node(category(""), name(#output_name))] |
| 80 | + fn #fn_name(_: impl #graphene_core::Ctx, data: #struct_name) -> #ty { |
| 81 | + data.#field_name |
| 82 | + } |
| 83 | + } |
| 84 | +} |
| 85 | + |
| 86 | +fn parse_output_name(attrs: &[syn::Attribute]) -> syn::Result<Option<String>> { |
| 87 | + let mut output_name = None; |
| 88 | + |
| 89 | + for attr in attrs { |
| 90 | + if !attr.path().is_ident("output") { |
| 91 | + continue; |
| 92 | + } |
| 93 | + |
| 94 | + let mut this_output_name = None; |
| 95 | + match &attr.meta { |
| 96 | + Meta::Path(_) => { |
| 97 | + return Err(Error::new_spanned(attr, "Expected output metadata like #[output(name = \"Result\")]")); |
| 98 | + } |
| 99 | + Meta::NameValue(_) => { |
| 100 | + return Err(Error::new_spanned(attr, "Expected output metadata like #[output(name = \"Result\")]")); |
| 101 | + } |
| 102 | + Meta::List(_) => { |
| 103 | + attr.parse_nested_meta(|meta| { |
| 104 | + if meta.path.is_ident("name") { |
| 105 | + if this_output_name.is_some() { |
| 106 | + return Err(meta.error("Multiple output names provided for one field")); |
| 107 | + } |
| 108 | + let value = meta.value()?; |
| 109 | + let lit: LitStr = value.parse()?; |
| 110 | + this_output_name = Some(lit.value()); |
| 111 | + Ok(()) |
| 112 | + } else { |
| 113 | + Err(meta.error("Unsupported output metadata. Supported syntax is #[output(name = \"...\")]")) |
| 114 | + } |
| 115 | + })?; |
| 116 | + } |
| 117 | + } |
| 118 | + |
| 119 | + let this_output_name = this_output_name.ok_or_else(|| Error::new_spanned(attr, "Missing output name. Use #[output(name = \"...\")]"))?; |
| 120 | + if output_name.is_some() { |
| 121 | + return Err(Error::new_spanned(attr, "Multiple #[output(...)] attributes are not allowed on one field")); |
| 122 | + } |
| 123 | + output_name = Some(this_output_name); |
| 124 | + } |
| 125 | + |
| 126 | + Ok(output_name) |
| 127 | +} |
0 commit comments