shopify_function_macro/
lib.rs

1use std::collections::HashMap;
2
3use bluejay_core::{
4    definition::{
5        EnumTypeDefinition, EnumValueDefinition, InputObjectTypeDefinition, InputValueDefinition,
6    },
7    AsIter,
8};
9use bluejay_typegen_codegen::{
10    generate_schema, names, CodeGenerator, ExecutableStruct, Input as BluejayInput,
11    KnownCustomScalarType, WrappedExecutableType,
12};
13use convert_case::{Case, Casing};
14use proc_macro2::Span;
15use quote::{format_ident, quote, ToTokens};
16use syn::{parse_macro_input, parse_quote, FnArg};
17
18fn extract_shopify_function_return_type(ast: &syn::ItemFn) -> Result<&syn::Ident, syn::Error> {
19    use syn::*;
20
21    let ReturnType::Type(_arrow, ty) = &ast.sig.output else {
22        return Err(Error::new_spanned(
23            &ast.sig,
24            "Shopify Functions require an explicit return type",
25        ));
26    };
27    let Type::Path(path) = ty.as_ref() else {
28        return Err(Error::new_spanned(
29            &ast.sig,
30            "Shopify Functions must return a Result",
31        ));
32    };
33    let result = path.path.segments.last().unwrap();
34    if result.ident != "Result" {
35        return Err(Error::new_spanned(
36            result,
37            "Shopify Functions must return a Result",
38        ));
39    }
40    let PathArguments::AngleBracketed(generics) = &result.arguments else {
41        return Err(Error::new_spanned(
42            result,
43            "Shopify Function Result is missing generic arguments",
44        ));
45    };
46    if generics.args.len() != 1 {
47        return Err(Error::new_spanned(
48            generics,
49            "Shopify Function Result takes exactly one generic argument",
50        ));
51    }
52    let GenericArgument::Type(ty) = generics.args.first().unwrap() else {
53        return Err(Error::new_spanned(
54            generics,
55            "Shopify Function Result expects a type",
56        ));
57    };
58    let Type::Path(path) = ty else {
59        return Err(Error::new_spanned(
60            result,
61            "Unexpected result type for Shopify Function Result",
62        ));
63    };
64    Ok(&path.path.segments.last().as_ref().unwrap().ident)
65}
66
67/// Generates code for a Function. This will define a wrapper function that is exported to Wasm.
68/// The wrapper handles deserializing the input and serializing the output.
69#[proc_macro_attribute]
70pub fn shopify_function(
71    attr: proc_macro::TokenStream,
72    item: proc_macro::TokenStream,
73) -> proc_macro::TokenStream {
74    let ast = parse_macro_input!(item as syn::ItemFn);
75    if !attr.is_empty() {
76        return quote! {compile_error!("Shopify functions don't accept attributes");}.into();
77    }
78
79    let function_name = &ast.sig.ident;
80    let function_name_string = function_name.to_string();
81    let export_function_name = format_ident!("{}_export", function_name);
82
83    if ast.sig.inputs.len() != 1 {
84        return quote! {compile_error!("Shopify functions need exactly one input parameter");}
85            .into();
86    }
87
88    let input_type = match &ast.sig.inputs.first().unwrap() {
89        FnArg::Typed(input) => input.ty.as_ref(),
90        FnArg::Receiver(_) => {
91            return quote! {compile_error!("Shopify functions can't have a receiver");}.into()
92        }
93    };
94
95    if let Err(error) = extract_shopify_function_return_type(&ast) {
96        return error.to_compile_error().into();
97    }
98
99    quote! {
100        #[export_name = #function_name_string]
101        pub extern "C" fn #export_function_name() {
102            shopify_function::wasm_api::init_panic_handler();
103            let mut context = shopify_function::wasm_api::Context::new();
104            let root_value = context.input_get().expect("Failed to get input");
105            let mut input: #input_type = shopify_function::wasm_api::Deserialize::deserialize(&root_value).expect("Failed to deserialize input");
106            let result = #function_name(input).expect("Failed to call function");
107            shopify_function::wasm_api::Serialize::serialize(&result, &mut context).expect("Failed to serialize output");
108        }
109
110        #ast
111    }
112    .into()
113}
114
115const DEFAULT_EXTERN_ENUMS: &[&str] = &["LanguageCode", "CountryCode", "CurrencyCode"];
116
117mod kw {
118    syn::custom_keyword!(input_stream);
119    syn::custom_keyword!(output_stream);
120}
121
122/// Generates Rust types from GraphQL schema definitions and queries.
123///
124/// ### Arguments
125///
126/// **Positional:**
127///
128/// 1. String literal with path to the file containing the schema definition. If relative, should be with respect to
129///    the project root (wherever `Cargo.toml` is located).
130///
131/// **Optional keyword:**
132///
133/// _enums_as_str_: Optional list of enum names for which the generated code should use string types instead of
134/// a fully formed enum. Defaults to `["LanguageCode", "CountryCode", "CurrencyCode"]`.
135///
136/// ### Trait implementations
137///
138/// By default, will implement `PartialEq`, and `Debug` for all input and enum types. Enums will also implement `Copy`.
139/// For types corresponding to values returned from queries,  the `shopify_function::wasm_api::Deserialize` trait
140/// is implemented. For types that would
141/// be arguments to a query, the `shopify_function::wasm_api::Serialize` trait is implemented.
142///
143/// ### Usage
144///
145/// Must be used with a module. Inside the module, type aliases must be defined for any custom scalars in the schema.
146///
147/// #### Queries
148///
149/// To use a query, define a module within the aforementioned module, and annotate it with
150/// `#[query("path/to/query.graphql")]`, where the argument is a string literal path to the query document, or the
151/// query contents enclosed in square brackets.
152///
153/// ##### Custom scalar overrides
154///
155/// To override the type of a custom scalar for a path within a query, use the `custom_scalar_overrides` named argument
156/// inside of the `#[query(...)]` attribute. The argument is a map from a path to a type, where the path is a string literal
157/// path to the field in the query, and the type is the type to override the field with.
158///
159/// For example, with the following query:
160/// ```graphql
161/// query MyQuery {
162///     myField: myScalar!
163/// }
164/// ```
165/// do something like the following:
166/// ```ignore
167/// #[query("path/to/query.graphql", custom_scalar_overrides = {
168///     "MyQuery.myField" => ::std::primitive::i32,
169/// })]
170/// ```
171/// Any type path that does not start with `::` is assumed to be relative to the schema definition module.
172///
173/// ### Naming
174///
175/// To generate idiomatic Rust code, some renaming of types, enum variants, and fields is performed. Types are
176/// renamed with `PascalCase`, as are enum variants. Fields are renamed with `snake_case`.
177///
178/// ### Query restrictions
179///
180/// In order to keep the type generation code relatively simple, there are some restrictions on the queries that are
181/// permitted. This may be relaxed in future versions.
182/// * Selection sets on object and interface types must contain either a single fragment spread, or entirely field
183///   selections.
184/// * Selection sets on union types must contain either a single fragment spread, or both an unaliased `__typename`
185///   selection and inline fragments for all or a subset of the objects contained in the union.
186#[proc_macro_attribute]
187pub fn typegen(
188    attr: proc_macro::TokenStream,
189    item: proc_macro::TokenStream,
190) -> proc_macro::TokenStream {
191    let mut input = syn::parse_macro_input!(attr as BluejayInput);
192    let mut module = syn::parse_macro_input!(item as syn::ItemMod);
193
194    if let Some(borrow) = input.borrow.as_ref() {
195        if borrow.value() {
196            let error = syn::Error::new_spanned(
197                borrow,
198                "`borrow` attribute must be `false` or omitted for Shopify Functions",
199            );
200            return error.to_compile_error().into();
201        }
202    }
203
204    if input.enums_as_str.is_empty() {
205        let enums_as_str = DEFAULT_EXTERN_ENUMS
206            .iter()
207            .map(|enum_name| syn::LitStr::new(enum_name, Span::mixed_site()))
208            .collect::<Vec<_>>();
209        input.enums_as_str = syn::parse_quote! { #(#enums_as_str),* };
210    }
211
212    let string_known_custom_scalar_type = KnownCustomScalarType {
213        type_for_borrowed: None, // we disallow borrowing
214        type_for_owned: syn::parse_quote! { ::std::string::String },
215    };
216
217    let known_custom_scalar_types = HashMap::from([
218        (String::from("Id"), string_known_custom_scalar_type.clone()),
219        (String::from("Url"), string_known_custom_scalar_type.clone()),
220        (
221            String::from("Handle"),
222            string_known_custom_scalar_type.clone(),
223        ),
224        (
225            String::from("Date"),
226            string_known_custom_scalar_type.clone(),
227        ),
228        (
229            String::from("DateTime"),
230            string_known_custom_scalar_type.clone(),
231        ),
232        (
233            String::from("DateTimeWithoutTimezone"),
234            string_known_custom_scalar_type.clone(),
235        ),
236        (
237            String::from("TimeWithoutTimezone"),
238            string_known_custom_scalar_type.clone(),
239        ),
240        (
241            String::from("Void"),
242            KnownCustomScalarType {
243                type_for_borrowed: None,
244                type_for_owned: syn::parse_quote! { () },
245            },
246        ),
247        (
248            String::from("Json"),
249            KnownCustomScalarType {
250                type_for_borrowed: None,
251                type_for_owned: syn::parse_quote! { ::shopify_function::scalars::JsonValue },
252            },
253        ),
254        (
255            String::from("Decimal"),
256            KnownCustomScalarType {
257                type_for_borrowed: None,
258                type_for_owned: syn::parse_quote! { ::shopify_function::scalars::Decimal },
259            },
260        ),
261    ]);
262
263    if let Err(error) = generate_schema(
264        input,
265        &mut module,
266        known_custom_scalar_types,
267        ShopifyFunctionCodeGenerator,
268    ) {
269        return error.to_compile_error().into();
270    }
271
272    module.to_token_stream().into()
273}
274
275struct ShopifyFunctionCodeGenerator;
276
277impl CodeGenerator for ShopifyFunctionCodeGenerator {
278    fn fields_for_executable_struct(
279        &self,
280        executable_struct: &bluejay_typegen_codegen::ExecutableStruct,
281    ) -> syn::Fields {
282        let once_cell_fields: Vec<syn::Field> = executable_struct
283            .fields()
284            .iter()
285            .map(|field| {
286                let field_name_ident = names::field_ident(field.graphql_name());
287                let field_type = Self::type_for_field(executable_struct, field.r#type(), false);
288
289                parse_quote! {
290                    #field_name_ident: ::std::cell::OnceCell<#field_type>
291                }
292            })
293            .collect();
294
295        let fields_named: syn::FieldsNamed = parse_quote! {
296            {
297                __wasm_value: shopify_function::wasm_api::Value,
298                #(#once_cell_fields),*
299            }
300        };
301        fields_named.into()
302    }
303
304    fn additional_impls_for_executable_struct(
305        &self,
306        executable_struct: &bluejay_typegen_codegen::ExecutableStruct,
307    ) -> Vec<syn::ItemImpl> {
308        let name_ident = names::type_ident(executable_struct.parent_name());
309
310        let once_cell_field_values: Vec<syn::FieldValue> = executable_struct
311            .fields()
312            .iter()
313            .map(|field| {
314                let field_name_ident = names::field_ident(field.graphql_name());
315
316                parse_quote! {
317                    #field_name_ident: ::std::cell::OnceCell::new()
318                }
319            })
320            .collect();
321
322        let deserialize_impl = parse_quote! {
323            impl shopify_function::wasm_api::Deserialize for #name_ident {
324                fn deserialize(value: &shopify_function::wasm_api::Value) -> ::std::result::Result<Self, shopify_function::wasm_api::read::Error> {
325                    Ok(Self {
326                        __wasm_value: *value,
327                        #(#once_cell_field_values),*
328                    })
329                }
330            }
331        };
332
333        let accessors: Vec<syn::ImplItemFn> = executable_struct
334            .fields()
335            .iter()
336            .map(|field| {
337                let field_name_ident = names::field_ident(field.graphql_name());
338                let field_name_lit_str = syn::LitStr::new(field.graphql_name(), Span::mixed_site());
339                let field_type = Self::type_for_field(executable_struct, field.r#type(), true);
340
341                let properly_referenced_value =
342                    Self::reference_variable_for_type(field.r#type(), &format_ident!("value_ref"));
343
344                let description: Option<syn::Attribute> = field.description().map(|description| {
345                    let description_lit_str = syn::LitStr::new(description, Span::mixed_site());
346                    parse_quote! { #[doc = #description_lit_str] }
347                });
348
349                parse_quote! {
350                    #description
351                    pub fn #field_name_ident(&self) -> #field_type {
352                        static INTERNED_FIELD_NAME: shopify_function::wasm_api::CachedInternedStringId = shopify_function::wasm_api::CachedInternedStringId::new(#field_name_lit_str, );
353                        let interned_string_id = INTERNED_FIELD_NAME.load();
354
355                        let value = self.#field_name_ident.get_or_init(|| {
356                            let value = self.__wasm_value.get_interned_obj_prop(interned_string_id);
357                            shopify_function::wasm_api::Deserialize::deserialize(&value).unwrap()
358                        });
359                        let value_ref = &value;
360                        #properly_referenced_value
361                    }
362                }
363            })
364            .collect();
365
366        let accessor_impl = parse_quote! {
367            impl #name_ident {
368                #(#accessors)*
369            }
370        };
371
372        vec![deserialize_impl, accessor_impl]
373    }
374
375    fn additional_impls_for_executable_enum(
376        &self,
377        executable_enum: &bluejay_typegen_codegen::ExecutableEnum,
378    ) -> Vec<syn::ItemImpl> {
379        let name_ident = names::type_ident(executable_enum.parent_name());
380
381        let match_arms: Vec<syn::Arm> = executable_enum
382            .variants()
383            .iter()
384            .map(|variant| {
385                let variant_name_ident = names::enum_variant_ident(variant.parent_name());
386                let variant_name_lit_str = syn::LitStr::new(variant.parent_name(), Span::mixed_site());
387
388                parse_quote! {
389                    #variant_name_lit_str => shopify_function::wasm_api::Deserialize::deserialize(value).map(Self::#variant_name_ident),
390                }
391            }).collect();
392
393        vec![parse_quote! {
394            impl shopify_function::wasm_api::Deserialize for #name_ident {
395                fn deserialize(value: &shopify_function::wasm_api::Value) -> ::std::result::Result<Self, shopify_function::wasm_api::read::Error> {
396                    let typename = value.get_obj_prop("__typename");
397                    let typename_str: ::std::string::String = shopify_function::wasm_api::Deserialize::deserialize(&typename)?;
398
399                    match typename_str.as_str() {
400                        #(#match_arms)*
401                        _ => ::std::result::Result::Ok(Self::Other),
402                    }
403                }
404            }
405        }]
406    }
407
408    fn additional_impls_for_enum(
409        &self,
410        enum_type_definition: &impl EnumTypeDefinition,
411    ) -> Vec<syn::ItemImpl> {
412        let name_ident = names::type_ident(enum_type_definition.name());
413
414        let from_str_match_arms: Vec<syn::Arm> = enum_type_definition
415            .enum_value_definitions()
416            .iter()
417            .map(|evd| {
418                let variant_name_ident = names::enum_variant_ident(evd.name());
419                let variant_name_lit_str = syn::LitStr::new(evd.name(), Span::mixed_site());
420
421                parse_quote! {
422                    #variant_name_lit_str => Self::#variant_name_ident,
423                }
424            })
425            .collect();
426
427        let as_str_match_arms: Vec<syn::Arm> = enum_type_definition
428            .enum_value_definitions()
429            .iter()
430            .map(|evd| {
431                let variant_name_ident = names::enum_variant_ident(evd.name());
432                let variant_name_lit_str = syn::LitStr::new(evd.name(), Span::mixed_site());
433
434                parse_quote! {
435                    Self::#variant_name_ident => #variant_name_lit_str,
436                }
437            })
438            .collect();
439
440        let non_trait_method_impls = parse_quote! {
441            impl #name_ident {
442                pub fn from_str(s: &str) -> Self {
443                    match s {
444                        #(#from_str_match_arms)*
445                        _ => Self::Other,
446                    }
447                }
448
449                fn as_str(&self) -> &::std::primitive::str {
450                    match self {
451                        #(#as_str_match_arms)*
452                        Self::Other => panic!("Cannot serialize `Other` variant"),
453                    }
454                }
455            }
456        };
457
458        let serialize_impl = parse_quote! {
459            impl shopify_function::wasm_api::Serialize for #name_ident {
460                fn serialize(&self, context: &mut shopify_function::wasm_api::Context) -> ::std::result::Result<(), shopify_function::wasm_api::write::Error> {
461                    let str_value = self.as_str();
462                    context.write_utf8_str(str_value)
463                }
464            }
465        };
466
467        let deserialize_impl = parse_quote! {
468            impl shopify_function::wasm_api::Deserialize for #name_ident {
469                fn deserialize(value: &shopify_function::wasm_api::Value) -> ::std::result::Result<Self, shopify_function::wasm_api::read::Error> {
470                    let str_value: ::std::string::String = shopify_function::wasm_api::Deserialize::deserialize(value)?;
471
472                    ::std::result::Result::Ok(Self::from_str(&str_value))
473                }
474            }
475        };
476
477        let display_impl = parse_quote! {
478            impl std::fmt::Display for #name_ident {
479                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
480                    write!(f, "{}", self.as_str())
481                }
482            }
483        };
484
485        vec![
486            non_trait_method_impls,
487            serialize_impl,
488            deserialize_impl,
489            display_impl,
490        ]
491    }
492
493    fn additional_impls_for_input_object(
494        &self,
495        #[allow(unused_variables)] input_object_type_definition: &impl InputObjectTypeDefinition,
496    ) -> Vec<syn::ItemImpl> {
497        let name_ident = names::type_ident(input_object_type_definition.name());
498
499        let field_statements: Vec<syn::Stmt> = input_object_type_definition
500            .input_field_definitions()
501            .iter()
502            .flat_map(|ivd| {
503                let field_name_ident = names::field_ident(ivd.name());
504                let field_name_lit_str = syn::LitStr::new(ivd.name(), Span::mixed_site());
505
506                vec![
507                    parse_quote! {
508                        context.write_utf8_str(#field_name_lit_str)?;
509                    },
510                    parse_quote! {
511                        self.#field_name_ident.serialize(context)?;
512                    },
513                ]
514            })
515            .collect();
516
517        let num_fields = input_object_type_definition.input_field_definitions().len();
518
519        let serialize_impl = parse_quote! {
520            impl shopify_function::wasm_api::Serialize for #name_ident {
521                fn serialize(&self, context: &mut shopify_function::wasm_api::Context) -> ::std::result::Result<(), shopify_function::wasm_api::write::Error> {
522                    context.write_object(
523                        |context| {
524                            #(#field_statements)*
525                            ::std::result::Result::Ok(())
526                        },
527                        #num_fields,
528                    )
529                }
530            }
531        };
532
533        let field_values: Vec<syn::FieldValue> = input_object_type_definition
534            .input_field_definitions()
535            .iter()
536            .map(|ivd| {
537                let field_name_ident = names::field_ident(ivd.name());
538                let field_name_lit_str = syn::LitStr::new(ivd.name(), Span::mixed_site());
539                parse_quote! { #field_name_ident: shopify_function::wasm_api::Deserialize::deserialize(&value.get_obj_prop(#field_name_lit_str))? }
540            })
541            .collect();
542
543        let deserialize_impl = parse_quote! {
544            impl shopify_function::wasm_api::Deserialize for #name_ident {
545                fn deserialize(value: &shopify_function::wasm_api::Value) -> ::std::result::Result<Self, shopify_function::wasm_api::read::Error> {
546                    ::std::result::Result::Ok(Self {
547                        #(#field_values),*
548                    })
549                }
550            }
551        };
552
553        vec![serialize_impl, deserialize_impl]
554    }
555
556    fn additional_impls_for_one_of_input_object(
557        &self,
558        input_object_type_definition: &impl InputObjectTypeDefinition,
559    ) -> Vec<syn::ItemImpl> {
560        let name_ident = names::type_ident(input_object_type_definition.name());
561
562        let match_arms: Vec<syn::Arm> = input_object_type_definition
563            .input_field_definitions()
564            .iter()
565            .map(|ivd| {
566                let variant_ident = names::enum_variant_ident(ivd.name());
567                let field_name_lit_str = syn::LitStr::new(ivd.name(), Span::mixed_site());
568
569                parse_quote! {
570                    Self::#variant_ident(value) => {
571                        context.write_utf8_str(#field_name_lit_str)?;
572                        shopify_function::wasm_api::Serialize::serialize(value, context)?;
573                    }
574                }
575            })
576            .collect();
577
578        let serialize_impl = parse_quote! {
579            impl shopify_function::wasm_api::Serialize for #name_ident {
580                fn serialize(&self, context: &mut shopify_function::wasm_api::Context) -> ::std::result::Result<(), shopify_function::wasm_api::write::Error> {
581                    context.write_object(|context| {
582                        match self {
583                            #(#match_arms)*
584                        }
585                        ::std::result::Result::Ok(())
586                    }, 1)
587                }
588            }
589        };
590
591        let deserialize_match_arms: Vec<syn::Arm> = input_object_type_definition
592            .input_field_definitions()
593            .iter()
594            .map(|ivd| {
595                let field_name_lit_str = syn::LitStr::new(ivd.name(), Span::mixed_site());
596                let variant_ident = names::enum_variant_ident(ivd.name());
597
598                parse_quote! {
599                    #field_name_lit_str => {
600                        let value = shopify_function::wasm_api::Deserialize::deserialize(&field_value)?;
601                        ::std::result::Result::Ok(Self::#variant_ident(value))
602                    }
603                }
604            })
605            .collect();
606
607        let deserialize_impl = parse_quote! {
608            impl shopify_function::wasm_api::Deserialize for #name_ident {
609                fn deserialize(value: &shopify_function::wasm_api::Value) -> ::std::result::Result<Self, shopify_function::wasm_api::read::Error> {
610                    let ::std::option::Option::Some(obj_len) = value.obj_len() else {
611                        return ::std::result::Result::Err(shopify_function::wasm_api::read::Error::InvalidType);
612                    };
613
614                    if obj_len != 1 {
615                        return ::std::result::Result::Err(shopify_function::wasm_api::read::Error::InvalidType);
616                    }
617
618                    let ::std::option::Option::Some(field_name) = value.get_obj_key_at_index(0) else {
619                        return ::std::result::Result::Err(shopify_function::wasm_api::read::Error::InvalidType);
620                    };
621                    let field_value = value.get_at_index(0);
622
623                    match field_name.as_str() {
624                        #(#deserialize_match_arms)*
625                        _ => ::std::result::Result::Err(shopify_function::wasm_api::read::Error::InvalidType),
626                    }
627                }
628            }
629        };
630
631        vec![serialize_impl, deserialize_impl]
632    }
633
634    fn attributes_for_enum(
635        &self,
636        _enum_type_definition: &impl EnumTypeDefinition,
637    ) -> Vec<syn::Attribute> {
638        vec![
639            parse_quote! { #[derive(::std::fmt::Debug, ::std::cmp::PartialEq, ::std::clone::Clone, ::std::marker::Copy)] },
640        ]
641    }
642
643    fn attributes_for_input_object(
644        &self,
645        _input_object_type_definition: &impl InputObjectTypeDefinition,
646    ) -> Vec<syn::Attribute> {
647        vec![
648            parse_quote! { #[derive(::std::fmt::Debug, ::std::cmp::PartialEq, ::std::clone::Clone)] },
649        ]
650    }
651
652    fn attributes_for_one_of_input_object(
653        &self,
654        _input_object_type_definition: &impl InputObjectTypeDefinition,
655    ) -> Vec<syn::Attribute> {
656        vec![
657            parse_quote! { #[derive(::std::fmt::Debug, ::std::cmp::PartialEq, ::std::clone::Clone)] },
658        ]
659    }
660}
661
662impl ShopifyFunctionCodeGenerator {
663    fn type_for_field(
664        executable_struct: &ExecutableStruct,
665        r#type: &WrappedExecutableType,
666        reference: bool,
667    ) -> syn::Type {
668        match r#type {
669            WrappedExecutableType::Base(base) => {
670                let base_type = executable_struct.compute_base_type(base);
671                if reference {
672                    parse_quote! { &#base_type }
673                } else {
674                    base_type
675                }
676            }
677            WrappedExecutableType::Optional(inner) => {
678                let inner_type = Self::type_for_field(executable_struct, inner, reference);
679                parse_quote! { ::std::option::Option<#inner_type> }
680            }
681            WrappedExecutableType::Vec(inner) => {
682                let inner_type = Self::type_for_field(executable_struct, inner, false);
683                if reference {
684                    parse_quote! { &[#inner_type] }
685                } else {
686                    parse_quote! { ::std::vec::Vec<#inner_type> }
687                }
688            }
689        }
690    }
691
692    fn reference_variable_for_type(
693        r#type: &WrappedExecutableType,
694        variable: &syn::Ident,
695    ) -> syn::Expr {
696        match r#type {
697            WrappedExecutableType::Base(_) => {
698                parse_quote! { #variable }
699            }
700            WrappedExecutableType::Vec(_) => {
701                parse_quote! { #variable.as_slice()}
702            }
703            WrappedExecutableType::Optional(inner) => {
704                let inner_variable = format_ident!("v_inner");
705                let inner_reference = Self::reference_variable_for_type(inner, &inner_variable);
706                parse_quote! { ::std::option::Option::as_ref(#variable).map(|#inner_variable| #inner_reference) }
707            }
708        }
709    }
710}
711
712/// Derives the `Deserialize` trait for structs to deserialize values from shopify_function_wasm_api::Value.
713///
714/// The derive macro supports the following attributes:
715///
716/// - `#[shopify_function(rename_all = "camelCase")]` - Converts field names from snake_case in Rust
717///   to the specified case style ("camelCase", "snake_case", or "kebab-case") when deserializing.
718///
719/// - `#[shopify_function(default)]` - When applied to a field, uses the `Default` implementation for
720///   that field's type if either:
721///   1. The field's value is explicitly `null` in the JSON
722///   2. The field is missing entirely from the JSON object
723///
724/// - `#[shopify_function(rename = "custom_name")]` - When applied to a field, uses the specified
725///   custom name for deserialization instead of the field's Rust name. This takes precedence over
726///   any struct-level `rename_all` attribute.
727///
728/// This is similar to serde's `#[serde(default)]` attribute, allowing structs to handle missing or null
729/// fields gracefully by using their default values instead of returning an error.
730///
731/// Note: Fields that use `#[shopify_function(default)]` must be a type that implements the `Default` trait.
732#[proc_macro_derive(Deserialize, attributes(shopify_function))]
733pub fn derive_deserialize(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
734    let input = syn::parse_macro_input!(input as syn::DeriveInput);
735
736    derive_deserialize_for_derive_input(&input)
737        .map(|impl_item| impl_item.to_token_stream().into())
738        .unwrap_or_else(|error| error.to_compile_error().into())
739}
740
741#[derive(Default)]
742struct FieldAttributes {
743    rename: Option<String>,
744    has_default: bool,
745}
746
747fn parse_field_attributes(field: &syn::Field) -> syn::Result<FieldAttributes> {
748    let mut attributes = FieldAttributes::default();
749
750    for attr in field.attrs.iter() {
751        if attr.path().is_ident("shopify_function") {
752            attr.parse_nested_meta(|meta| {
753                if meta.path.is_ident("rename") {
754                    attributes.rename = Some(meta.value()?.parse::<syn::LitStr>()?.value());
755                    Ok(())
756                } else if meta.path.is_ident("default") {
757                    attributes.has_default = true;
758                    Ok(())
759                } else {
760                    Err(meta.error("unrecognized field attribute"))
761                }
762            })?;
763        }
764    }
765
766    Ok(attributes)
767}
768
769fn derive_deserialize_for_derive_input(input: &syn::DeriveInput) -> syn::Result<syn::ItemImpl> {
770    match &input.data {
771        syn::Data::Struct(data) => match &data.fields {
772            syn::Fields::Named(fields) => {
773                let name_ident = &input.ident;
774
775                let mut rename_all: Option<syn::LitStr> = None;
776
777                for attr in input.attrs.iter() {
778                    if attr.path().is_ident("shopify_function") {
779                        attr.parse_nested_meta(|meta| {
780                            if meta.path.is_ident("rename_all") {
781                                rename_all = Some(meta.value()?.parse()?);
782                                Ok(())
783                            } else {
784                                Err(meta.error("unrecognized repr"))
785                            }
786                        })?;
787                    }
788                }
789
790                let case_style = match rename_all {
791                    Some(rename_all) => match rename_all.value().as_str() {
792                        "camelCase" => Some(Case::Camel),
793                        "snake_case" => Some(Case::Snake),
794                        "kebab-case" => Some(Case::Kebab),
795                        _ => {
796                            return Err(syn::Error::new_spanned(
797                                rename_all,
798                                "unrecognized rename_all",
799                            ))
800                        }
801                    },
802                    None => None,
803                };
804
805                let field_values: Vec<syn::FieldValue> = fields
806                    .named
807                    .iter()
808                    .map(|field| {
809                        let field_name_ident = field.ident.as_ref().expect("Named fields must have identifiers");
810
811                        let field_attrs = parse_field_attributes(field)?;
812
813                        let field_name_str = match field_attrs.rename {
814                            Some(custom_name) => custom_name,
815                            None => {
816                                // Fall back to rename_all case transformation or original name
817                                case_style.map_or_else(
818                                    || field_name_ident.to_string(),
819                                    |case_style| field_name_ident.to_string().to_case(case_style)
820                                )
821                            }
822                        };
823
824                        let field_name_lit_str = syn::LitStr::new(field_name_str.as_str(), Span::mixed_site());
825
826                        if field_attrs.has_default {
827                            // For fields with default attribute, check if value is null or missing
828                            // This will use the Default implementation for the field type when either:
829                            // 1. The field is explicitly null in the JSON (we get NanBox::null())
830                            // 2. The field is missing in the JSON (get_obj_prop returns a null value)
831                            Ok(parse_quote! {
832                                #field_name_ident: {
833                                    let prop = value.get_obj_prop(#field_name_lit_str);
834                                    if prop.is_null() {
835                                        ::std::default::Default::default()
836                                    } else {
837                                        shopify_function::wasm_api::Deserialize::deserialize(&prop)?
838                                    }
839                                }
840                            })
841                        } else {
842                            // For fields without default, use normal deserialization
843                            Ok(parse_quote! {
844                                #field_name_ident: shopify_function::wasm_api::Deserialize::deserialize(&value.get_obj_prop(#field_name_lit_str))?
845                            })
846                        }
847                    })
848                    .collect::<syn::Result<Vec<_>>>()?;
849
850                let deserialize_impl = parse_quote! {
851                    impl shopify_function::wasm_api::Deserialize for #name_ident {
852                        fn deserialize(value: &shopify_function::wasm_api::Value) -> ::std::result::Result<Self, shopify_function::wasm_api::read::Error> {
853                            ::std::result::Result::Ok(Self {
854                                #(#field_values),*
855                            })
856                        }
857                    }
858                };
859
860                Ok(deserialize_impl)
861            }
862            syn::Fields::Unnamed(_) | syn::Fields::Unit => Err(syn::Error::new_spanned(
863                input,
864                "Structs must have named fields to derive `Deserialize`",
865            )),
866        },
867        syn::Data::Enum(_) => Err(syn::Error::new_spanned(
868            input,
869            "Enum types are not supported for deriving `Deserialize`",
870        )),
871        syn::Data::Union(_) => Err(syn::Error::new_spanned(
872            input,
873            "Union types are not supported for deriving `Deserialize`",
874        )),
875    }
876}