1use heck::ToLowerCamelCase;
29use proc_macro::TokenStream;
30use quote::quote;
31use syn::{parse_macro_input, Data, DeriveInput, Fields, Type};
32
33#[proc_macro_derive(GraphQLFields, attributes(graphql))]
45pub fn derive_graphql_fields(input: TokenStream) -> TokenStream {
46 let input = parse_macro_input!(input as DeriveInput);
47 let name = &input.ident;
48
49 let full_type_path = parse_full_type(&input.attrs);
51
52 let fields = match &input.data {
53 Data::Struct(data) => match &data.fields {
54 Fields::Named(named) => &named.named,
55 _ => {
56 return syn::Error::new_spanned(
57 &input,
58 "GraphQLFields can only be derived on structs with named fields",
59 )
60 .to_compile_error()
61 .into();
62 }
63 },
64 _ => {
65 return syn::Error::new_spanned(&input, "GraphQLFields can only be derived on structs")
66 .to_compile_error()
67 .into();
68 }
69 };
70
71 let mut selection_parts = Vec::new();
72 let mut validation_checks = Vec::new();
73
74 for field in fields {
75 let field_name = field.ident.as_ref().expect("named field should have ident");
76
77 let rust_name = field_name.to_string();
80 let clean_name = rust_name.strip_prefix("r#").unwrap_or(&rust_name);
81 let gql_name = clean_name.to_lower_camel_case();
82
83 let is_nested = field.attrs.iter().any(|attr| {
84 if !attr.path().is_ident("graphql") {
85 return false;
86 }
87 let mut found = false;
88 let _ = attr.parse_nested_meta(|meta| {
89 if meta.path.is_ident("nested") {
90 found = true;
91 }
92 Ok(())
93 });
94 found
95 });
96
97 if is_nested {
98 let inner_ty = unwrap_type(&field.ty);
100 selection_parts.push(quote! {
101 {
102 let nested = <#inner_ty as GraphQLFields>::selection();
103 format!("{} {{ {} }}", #gql_name, nested)
104 }
105 });
106 if full_type_path.is_some() {
109 validation_checks.push(quote! {
110 { let _ = &__v.#field_name; }
111 });
112 }
113 } else {
114 selection_parts.push(quote! {
115 #gql_name.to_string()
116 });
117 if full_type_path.is_some() {
119 let field_ty = &field.ty;
120 validation_checks.push(quote! {
121 {
122 fn __check<__F: ::lineark_sdk::FieldCompatible<__C>, __C>(_: &__F) {}
123 __check::<_, #field_ty>(&__v.#field_name);
124 }
125 });
126 }
127 }
128 }
129
130 let full_type_assoc = if let Some(ref path) = full_type_path {
131 quote! { type FullType = #path; }
132 } else {
133 quote! { type FullType = Self; }
134 };
135
136 let validation_block = if !validation_checks.is_empty() {
137 let full_type = full_type_path.as_ref().unwrap();
138 quote! {
139 const _: () = {
140 #[allow(unused)]
141 fn __graphql_validate(__v: &#full_type) {
142 #(#validation_checks)*
143 }
144 };
145 }
146 } else {
147 quote! {}
148 };
149
150 let expanded = quote! {
151 impl GraphQLFields for #name {
152 #full_type_assoc
153
154 fn selection() -> String {
155 let parts: Vec<String> = vec![
156 #(#selection_parts),*
157 ];
158 parts.join(" ")
159 }
160 }
161
162 #validation_block
163 };
164
165 expanded.into()
166}
167
168fn parse_full_type(attrs: &[syn::Attribute]) -> Option<syn::Path> {
170 for attr in attrs {
171 if !attr.path().is_ident("graphql") {
172 continue;
173 }
174 let mut full_type = None;
175 let _ = attr.parse_nested_meta(|meta| {
176 if meta.path.is_ident("full_type") {
177 let value = meta.value()?;
178 full_type = Some(value.parse::<syn::Path>()?);
179 }
180 Ok(())
181 });
182 if full_type.is_some() {
183 return full_type;
184 }
185 }
186 None
187}
188
189fn unwrap_type(ty: &Type) -> &Type {
192 if let Type::Path(type_path) = ty {
193 if let Some(segment) = type_path.path.segments.last() {
194 let ident = segment.ident.to_string();
195 if ident == "Option" || ident == "Vec" || ident == "Box" {
196 if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
197 if let Some(syn::GenericArgument::Type(inner)) = args.args.first() {
198 return unwrap_type(inner);
199 }
200 }
201 }
202 }
203 }
204 ty
205}