Skip to main content

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                        let value = self.#field_name_ident.get_or_init(|| {
353                            let value = self.__wasm_value.get_obj_prop(#field_name_lit_str);
354                            shopify_function::wasm_api::Deserialize::deserialize(&value).unwrap()
355                        });
356                        let value_ref = &value;
357                        #properly_referenced_value
358                    }
359                }
360            })
361            .collect();
362
363        let accessor_impl = parse_quote! {
364            impl #name_ident {
365                #(#accessors)*
366            }
367        };
368
369        vec![deserialize_impl, accessor_impl]
370    }
371
372    fn additional_impls_for_executable_enum(
373        &self,
374        executable_enum: &bluejay_typegen_codegen::ExecutableEnum,
375    ) -> Vec<syn::ItemImpl> {
376        let name_ident = names::type_ident(executable_enum.parent_name());
377
378        let match_arms: Vec<syn::Arm> = executable_enum
379            .variants()
380            .iter()
381            .map(|variant| {
382                let variant_name_ident = names::enum_variant_ident(variant.parent_name());
383                let variant_name_lit_str = syn::LitStr::new(variant.parent_name(), Span::mixed_site());
384
385                parse_quote! {
386                    #variant_name_lit_str => shopify_function::wasm_api::Deserialize::deserialize(value).map(Self::#variant_name_ident),
387                }
388            }).collect();
389
390        vec![parse_quote! {
391            impl shopify_function::wasm_api::Deserialize for #name_ident {
392                fn deserialize(value: &shopify_function::wasm_api::Value) -> ::std::result::Result<Self, shopify_function::wasm_api::read::Error> {
393                    let typename = value.get_obj_prop("__typename");
394                    let typename_str: ::std::string::String = shopify_function::wasm_api::Deserialize::deserialize(&typename)?;
395
396                    match typename_str.as_str() {
397                        #(#match_arms)*
398                        _ => ::std::result::Result::Ok(Self::Other),
399                    }
400                }
401            }
402        }]
403    }
404
405    fn additional_impls_for_enum(
406        &self,
407        enum_type_definition: &impl EnumTypeDefinition,
408    ) -> Vec<syn::ItemImpl> {
409        let name_ident = names::type_ident(enum_type_definition.name());
410
411        let from_str_match_arms: Vec<syn::Arm> = enum_type_definition
412            .enum_value_definitions()
413            .iter()
414            .map(|evd| {
415                let variant_name_ident = names::enum_variant_ident(evd.name());
416                let variant_name_lit_str = syn::LitStr::new(evd.name(), Span::mixed_site());
417
418                parse_quote! {
419                    #variant_name_lit_str => Self::#variant_name_ident,
420                }
421            })
422            .collect();
423
424        let as_str_match_arms: Vec<syn::Arm> = enum_type_definition
425            .enum_value_definitions()
426            .iter()
427            .map(|evd| {
428                let variant_name_ident = names::enum_variant_ident(evd.name());
429                let variant_name_lit_str = syn::LitStr::new(evd.name(), Span::mixed_site());
430
431                parse_quote! {
432                    Self::#variant_name_ident => #variant_name_lit_str,
433                }
434            })
435            .collect();
436
437        let non_trait_method_impls = parse_quote! {
438            impl #name_ident {
439                pub fn from_str(s: &str) -> Self {
440                    match s {
441                        #(#from_str_match_arms)*
442                        _ => Self::Other,
443                    }
444                }
445
446                fn as_str(&self) -> &::std::primitive::str {
447                    match self {
448                        #(#as_str_match_arms)*
449                        Self::Other => panic!("Cannot serialize `Other` variant"),
450                    }
451                }
452            }
453        };
454
455        let serialize_impl = parse_quote! {
456            impl shopify_function::wasm_api::Serialize for #name_ident {
457                fn serialize(&self, context: &mut shopify_function::wasm_api::Context) -> ::std::result::Result<(), shopify_function::wasm_api::write::Error> {
458                    let str_value = self.as_str();
459                    context.write_utf8_str(str_value)
460                }
461            }
462        };
463
464        let deserialize_impl = parse_quote! {
465            impl shopify_function::wasm_api::Deserialize for #name_ident {
466                fn deserialize(value: &shopify_function::wasm_api::Value) -> ::std::result::Result<Self, shopify_function::wasm_api::read::Error> {
467                    let str_value: ::std::string::String = shopify_function::wasm_api::Deserialize::deserialize(value)?;
468
469                    ::std::result::Result::Ok(Self::from_str(&str_value))
470                }
471            }
472        };
473
474        let display_impl = parse_quote! {
475            impl std::fmt::Display for #name_ident {
476                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
477                    write!(f, "{}", self.as_str())
478                }
479            }
480        };
481
482        vec![
483            non_trait_method_impls,
484            serialize_impl,
485            deserialize_impl,
486            display_impl,
487        ]
488    }
489
490    fn additional_impls_for_input_object(
491        &self,
492        #[allow(unused_variables)] input_object_type_definition: &impl InputObjectTypeDefinition,
493    ) -> Vec<syn::ItemImpl> {
494        let name_ident = names::type_ident(input_object_type_definition.name());
495
496        let field_statements: Vec<syn::Stmt> = input_object_type_definition
497            .input_field_definitions()
498            .iter()
499            .flat_map(|ivd| {
500                let field_name_ident = names::field_ident(ivd.name());
501                let field_name_lit_str = syn::LitStr::new(ivd.name(), Span::mixed_site());
502
503                vec![
504                    parse_quote! {
505                        context.write_utf8_str(#field_name_lit_str)?;
506                    },
507                    parse_quote! {
508                        self.#field_name_ident.serialize(context)?;
509                    },
510                ]
511            })
512            .collect();
513
514        let num_fields = input_object_type_definition.input_field_definitions().len();
515
516        let serialize_impl = parse_quote! {
517            impl shopify_function::wasm_api::Serialize for #name_ident {
518                fn serialize(&self, context: &mut shopify_function::wasm_api::Context) -> ::std::result::Result<(), shopify_function::wasm_api::write::Error> {
519                    context.write_object(
520                        |context| {
521                            #(#field_statements)*
522                            ::std::result::Result::Ok(())
523                        },
524                        #num_fields,
525                    )
526                }
527            }
528        };
529
530        let field_values: Vec<syn::FieldValue> = input_object_type_definition
531            .input_field_definitions()
532            .iter()
533            .map(|ivd| {
534                let field_name_ident = names::field_ident(ivd.name());
535                let field_name_lit_str = syn::LitStr::new(ivd.name(), Span::mixed_site());
536                parse_quote! { #field_name_ident: shopify_function::wasm_api::Deserialize::deserialize(&value.get_obj_prop(#field_name_lit_str))? }
537            })
538            .collect();
539
540        let deserialize_impl = parse_quote! {
541            impl shopify_function::wasm_api::Deserialize for #name_ident {
542                fn deserialize(value: &shopify_function::wasm_api::Value) -> ::std::result::Result<Self, shopify_function::wasm_api::read::Error> {
543                    ::std::result::Result::Ok(Self {
544                        #(#field_values),*
545                    })
546                }
547            }
548        };
549
550        vec![serialize_impl, deserialize_impl]
551    }
552
553    fn additional_impls_for_one_of_input_object(
554        &self,
555        input_object_type_definition: &impl InputObjectTypeDefinition,
556    ) -> Vec<syn::ItemImpl> {
557        let name_ident = names::type_ident(input_object_type_definition.name());
558
559        let match_arms: Vec<syn::Arm> = input_object_type_definition
560            .input_field_definitions()
561            .iter()
562            .map(|ivd| {
563                let variant_ident = names::enum_variant_ident(ivd.name());
564                let field_name_lit_str = syn::LitStr::new(ivd.name(), Span::mixed_site());
565
566                parse_quote! {
567                    Self::#variant_ident(value) => {
568                        context.write_utf8_str(#field_name_lit_str)?;
569                        shopify_function::wasm_api::Serialize::serialize(value, context)?;
570                    }
571                }
572            })
573            .collect();
574
575        let serialize_impl = parse_quote! {
576            impl shopify_function::wasm_api::Serialize for #name_ident {
577                fn serialize(&self, context: &mut shopify_function::wasm_api::Context) -> ::std::result::Result<(), shopify_function::wasm_api::write::Error> {
578                    context.write_object(|context| {
579                        match self {
580                            #(#match_arms)*
581                        }
582                        ::std::result::Result::Ok(())
583                    }, 1)
584                }
585            }
586        };
587
588        let deserialize_match_arms: Vec<syn::Arm> = input_object_type_definition
589            .input_field_definitions()
590            .iter()
591            .map(|ivd| {
592                let field_name_lit_str = syn::LitStr::new(ivd.name(), Span::mixed_site());
593                let variant_ident = names::enum_variant_ident(ivd.name());
594
595                parse_quote! {
596                    #field_name_lit_str => {
597                        let value = shopify_function::wasm_api::Deserialize::deserialize(&field_value)?;
598                        ::std::result::Result::Ok(Self::#variant_ident(value))
599                    }
600                }
601            })
602            .collect();
603
604        let deserialize_impl = parse_quote! {
605            impl shopify_function::wasm_api::Deserialize for #name_ident {
606                fn deserialize(value: &shopify_function::wasm_api::Value) -> ::std::result::Result<Self, shopify_function::wasm_api::read::Error> {
607                    let ::std::option::Option::Some(obj_len) = value.obj_len() else {
608                        return ::std::result::Result::Err(shopify_function::wasm_api::read::Error::InvalidType);
609                    };
610
611                    if obj_len != 1 {
612                        return ::std::result::Result::Err(shopify_function::wasm_api::read::Error::InvalidType);
613                    }
614
615                    let ::std::option::Option::Some(field_name) = value.get_obj_key_at_index(0) else {
616                        return ::std::result::Result::Err(shopify_function::wasm_api::read::Error::InvalidType);
617                    };
618                    let field_value = value.get_at_index(0);
619
620                    match field_name.as_str() {
621                        #(#deserialize_match_arms)*
622                        _ => ::std::result::Result::Err(shopify_function::wasm_api::read::Error::InvalidType),
623                    }
624                }
625            }
626        };
627
628        vec![serialize_impl, deserialize_impl]
629    }
630
631    fn attributes_for_enum(
632        &self,
633        _enum_type_definition: &impl EnumTypeDefinition,
634    ) -> Vec<syn::Attribute> {
635        vec![
636            parse_quote! { #[derive(::std::fmt::Debug, ::std::cmp::PartialEq, ::std::clone::Clone, ::std::marker::Copy)] },
637        ]
638    }
639
640    fn attributes_for_input_object(
641        &self,
642        _input_object_type_definition: &impl InputObjectTypeDefinition,
643    ) -> Vec<syn::Attribute> {
644        vec![
645            parse_quote! { #[derive(::std::fmt::Debug, ::std::cmp::PartialEq, ::std::clone::Clone)] },
646        ]
647    }
648
649    fn attributes_for_one_of_input_object(
650        &self,
651        _input_object_type_definition: &impl InputObjectTypeDefinition,
652    ) -> Vec<syn::Attribute> {
653        vec![
654            parse_quote! { #[derive(::std::fmt::Debug, ::std::cmp::PartialEq, ::std::clone::Clone)] },
655        ]
656    }
657}
658
659impl ShopifyFunctionCodeGenerator {
660    fn type_for_field(
661        executable_struct: &ExecutableStruct,
662        r#type: &WrappedExecutableType,
663        reference: bool,
664    ) -> syn::Type {
665        match r#type {
666            WrappedExecutableType::Base(base) => {
667                let base_type = executable_struct.compute_base_type(base);
668                if reference {
669                    parse_quote! { &#base_type }
670                } else {
671                    base_type
672                }
673            }
674            WrappedExecutableType::Optional(inner) => {
675                let inner_type = Self::type_for_field(executable_struct, inner, reference);
676                parse_quote! { ::std::option::Option<#inner_type> }
677            }
678            WrappedExecutableType::Vec(inner) => {
679                let inner_type = Self::type_for_field(executable_struct, inner, false);
680                if reference {
681                    parse_quote! { &[#inner_type] }
682                } else {
683                    parse_quote! { ::std::vec::Vec<#inner_type> }
684                }
685            }
686        }
687    }
688
689    fn reference_variable_for_type(
690        r#type: &WrappedExecutableType,
691        variable: &syn::Ident,
692    ) -> syn::Expr {
693        match r#type {
694            WrappedExecutableType::Base(_) => {
695                parse_quote! { #variable }
696            }
697            WrappedExecutableType::Vec(_) => {
698                parse_quote! { #variable.as_slice()}
699            }
700            WrappedExecutableType::Optional(inner) => {
701                let inner_variable = format_ident!("v_inner");
702                let inner_reference = Self::reference_variable_for_type(inner, &inner_variable);
703                parse_quote! { ::std::option::Option::as_ref(#variable).map(|#inner_variable| #inner_reference) }
704            }
705        }
706    }
707}
708
709/// Derives the `Deserialize` trait for structs to deserialize values from shopify_function_wasm_api::Value.
710///
711/// The derive macro supports the following attributes:
712///
713/// - `#[shopify_function(rename_all = "camelCase")]` - Converts field names from snake_case in Rust
714///   to the specified case style ("camelCase", "snake_case", or "kebab-case") when deserializing.
715///
716/// - `#[shopify_function(default)]` - When applied to a field, uses the `Default` implementation for
717///   that field's type if either:
718///   1. The field's value is explicitly `null` in the JSON
719///   2. The field is missing entirely from the JSON object
720///
721/// - `#[shopify_function(rename = "custom_name")]` - When applied to a field, uses the specified
722///   custom name for deserialization instead of the field's Rust name. This takes precedence over
723///   any struct-level `rename_all` attribute.
724///
725/// This is similar to serde's `#[serde(default)]` attribute, allowing structs to handle missing or null
726/// fields gracefully by using their default values instead of returning an error.
727///
728/// Note: Fields that use `#[shopify_function(default)]` must be a type that implements the `Default` trait.
729#[proc_macro_derive(Deserialize, attributes(shopify_function))]
730pub fn derive_deserialize(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
731    let input = syn::parse_macro_input!(input as syn::DeriveInput);
732
733    derive_deserialize_for_derive_input(&input)
734        .map(|impl_item| impl_item.to_token_stream().into())
735        .unwrap_or_else(|error| error.to_compile_error().into())
736}
737
738#[derive(Default)]
739struct FieldAttributes {
740    rename: Option<String>,
741    has_default: bool,
742}
743
744fn parse_field_attributes(field: &syn::Field) -> syn::Result<FieldAttributes> {
745    let mut attributes = FieldAttributes::default();
746
747    for attr in field.attrs.iter() {
748        if attr.path().is_ident("shopify_function") {
749            attr.parse_nested_meta(|meta| {
750                if meta.path.is_ident("rename") {
751                    attributes.rename = Some(meta.value()?.parse::<syn::LitStr>()?.value());
752                    Ok(())
753                } else if meta.path.is_ident("default") {
754                    attributes.has_default = true;
755                    Ok(())
756                } else {
757                    Err(meta.error("unrecognized field attribute"))
758                }
759            })?;
760        }
761    }
762
763    Ok(attributes)
764}
765
766fn derive_deserialize_for_derive_input(input: &syn::DeriveInput) -> syn::Result<syn::ItemImpl> {
767    match &input.data {
768        syn::Data::Struct(data) => match &data.fields {
769            syn::Fields::Named(fields) => {
770                let name_ident = &input.ident;
771
772                let mut rename_all: Option<syn::LitStr> = None;
773
774                for attr in input.attrs.iter() {
775                    if attr.path().is_ident("shopify_function") {
776                        attr.parse_nested_meta(|meta| {
777                            if meta.path.is_ident("rename_all") {
778                                rename_all = Some(meta.value()?.parse()?);
779                                Ok(())
780                            } else {
781                                Err(meta.error("unrecognized repr"))
782                            }
783                        })?;
784                    }
785                }
786
787                let case_style = match rename_all {
788                    Some(rename_all) => match rename_all.value().as_str() {
789                        "camelCase" => Some(Case::Camel),
790                        "snake_case" => Some(Case::Snake),
791                        "kebab-case" => Some(Case::Kebab),
792                        _ => {
793                            return Err(syn::Error::new_spanned(
794                                rename_all,
795                                "unrecognized rename_all",
796                            ))
797                        }
798                    },
799                    None => None,
800                };
801
802                let field_values: Vec<syn::FieldValue> = fields
803                    .named
804                    .iter()
805                    .map(|field| {
806                        let field_name_ident = field.ident.as_ref().expect("Named fields must have identifiers");
807
808                        let field_attrs = parse_field_attributes(field)?;
809
810                        let field_name_str = match field_attrs.rename {
811                            Some(custom_name) => custom_name,
812                            None => {
813                                // Fall back to rename_all case transformation or original name
814                                case_style.map_or_else(
815                                    || field_name_ident.to_string(),
816                                    |case_style| field_name_ident.to_string().to_case(case_style)
817                                )
818                            }
819                        };
820
821                        let field_name_lit_str = syn::LitStr::new(field_name_str.as_str(), Span::mixed_site());
822
823                        if field_attrs.has_default {
824                            // For fields with default attribute, check if value is null or missing
825                            // This will use the Default implementation for the field type when either:
826                            // 1. The field is explicitly null in the JSON (we get NanBox::null())
827                            // 2. The field is missing in the JSON (get_obj_prop returns a null value)
828                            Ok(parse_quote! {
829                                #field_name_ident: {
830                                    let prop = value.get_obj_prop(#field_name_lit_str);
831                                    if prop.is_null() {
832                                        ::std::default::Default::default()
833                                    } else {
834                                        shopify_function::wasm_api::Deserialize::deserialize(&prop)?
835                                    }
836                                }
837                            })
838                        } else {
839                            // For fields without default, use normal deserialization
840                            Ok(parse_quote! {
841                                #field_name_ident: shopify_function::wasm_api::Deserialize::deserialize(&value.get_obj_prop(#field_name_lit_str))?
842                            })
843                        }
844                    })
845                    .collect::<syn::Result<Vec<_>>>()?;
846
847                let deserialize_impl = parse_quote! {
848                    impl shopify_function::wasm_api::Deserialize for #name_ident {
849                        fn deserialize(value: &shopify_function::wasm_api::Value) -> ::std::result::Result<Self, shopify_function::wasm_api::read::Error> {
850                            ::std::result::Result::Ok(Self {
851                                #(#field_values),*
852                            })
853                        }
854                    }
855                };
856
857                Ok(deserialize_impl)
858            }
859            syn::Fields::Unnamed(_) | syn::Fields::Unit => Err(syn::Error::new_spanned(
860                input,
861                "Structs must have named fields to derive `Deserialize`",
862            )),
863        },
864        syn::Data::Enum(_) => Err(syn::Error::new_spanned(
865            input,
866            "Enum types are not supported for deriving `Deserialize`",
867        )),
868        syn::Data::Union(_) => Err(syn::Error::new_spanned(
869            input,
870            "Union types are not supported for deriving `Deserialize`",
871        )),
872    }
873}