Skip to content

Commit 269b04c

Browse files
committed
proto skip null fields
1 parent da26163 commit 269b04c

File tree

2 files changed

+93
-9
lines changed

2 files changed

+93
-9
lines changed

example_with_targets/src/tests.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,34 @@ fn test_target_b() -> Result<()> {
4242
assert_eq!(result, expected);
4343
Ok(())
4444
}
45+
46+
#[test]
47+
fn test_null_field_skipping() -> Result<()> {
48+
let test_function_none =
49+
|_input: crate::schema::target_a::Input| -> Result<crate::schema::FunctionTargetAResult> {
50+
Ok(crate::schema::FunctionTargetAResult {
51+
status: None, // This should not appear in serialized output
52+
})
53+
};
54+
55+
let test_function_some =
56+
|_input: crate::schema::target_a::Input| -> Result<crate::schema::FunctionTargetAResult> {
57+
Ok(crate::schema::FunctionTargetAResult {
58+
status: Some(200), // This should appear in serialized output
59+
})
60+
};
61+
62+
let test_input = r#"{
63+
"id": "gid://shopify/Order/1234567890",
64+
"num": 123,
65+
"name": "test"
66+
}"#;
67+
68+
let result_none = run_function_with_input(test_function_none, test_input)?;
69+
let result_some = run_function_with_input(test_function_some, test_input)?;
70+
71+
assert_eq!(result_none.status, None);
72+
assert_eq!(result_some.status, Some(200));
73+
74+
Ok(())
75+
}

shopify_function_macro/src/lib.rs

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,18 @@ pub fn typegen(
272272
module.to_token_stream().into()
273273
}
274274

275+
/// Helper function to determine if a GraphQL input field type is nullable
276+
/// Uses conservative detection to identify Optional fields from GraphQL schema
277+
fn is_input_field_nullable(ivd: &impl InputValueDefinition) -> bool {
278+
// Use std::any::type_name to get type information as a string
279+
let type_name = std::any::type_name_of_val(&ivd.r#type());
280+
281+
// only treat fields that are explicitly nullable as Option types
282+
// This prevents incorrectly wrapping required fields in Option<T>
283+
type_name.contains("Option")
284+
|| (type_name.contains("Nullable") && !type_name.contains("NonNull"))
285+
}
286+
275287
struct ShopifyFunctionCodeGenerator;
276288

277289
impl CodeGenerator for ShopifyFunctionCodeGenerator {
@@ -496,35 +508,76 @@ impl CodeGenerator for ShopifyFunctionCodeGenerator {
496508
) -> Vec<syn::ItemImpl> {
497509
let name_ident = names::type_ident(input_object_type_definition.name());
498510

511+
// Conditionally serialize fields based on GraphQL schema nullability
512+
// Nullable fields (Option<T>) are only serialized if Some(_)
513+
// Required fields are always serialized
514+
499515
let field_statements: Vec<syn::Stmt> = input_object_type_definition
500516
.input_field_definitions()
501517
.iter()
502518
.flat_map(|ivd| {
503519
let field_name_ident = names::field_ident(ivd.name());
504520
let field_name_lit_str = syn::LitStr::new(ivd.name(), Span::mixed_site());
505521

506-
vec![
522+
// Check if this field is nullable in the GraphQL schema
523+
if is_input_field_nullable(ivd) {
524+
// For nullable fields, only serialize if Some(_)
525+
vec![parse_quote! {
526+
if let ::std::option::Option::Some(ref value) = self.#field_name_ident {
527+
context.write_utf8_str(#field_name_lit_str)?;
528+
value.serialize(context)?;
529+
}
530+
}]
531+
} else {
532+
// For required fields, always serialize
533+
vec![
534+
parse_quote! {
535+
context.write_utf8_str(#field_name_lit_str)?;
536+
},
537+
parse_quote! {
538+
self.#field_name_ident.serialize(context)?;
539+
},
540+
]
541+
}
542+
})
543+
.collect();
544+
545+
// Generate field counting statements for dynamic field count calculation
546+
let field_count_statements: Vec<syn::Stmt> = input_object_type_definition
547+
.input_field_definitions()
548+
.iter()
549+
.map(|ivd| {
550+
let field_name_ident = names::field_ident(ivd.name());
551+
552+
if is_input_field_nullable(ivd) {
553+
// For nullable fields, count only if Some(_)
507554
parse_quote! {
508-
context.write_utf8_str(#field_name_lit_str)?;
509-
},
555+
if let ::std::option::Option::Some(_) = self.#field_name_ident {
556+
field_count += 1;
557+
}
558+
}
559+
} else {
560+
// For required fields, always count
510561
parse_quote! {
511-
self.#field_name_ident.serialize(context)?;
512-
},
513-
]
562+
field_count += 1;
563+
}
564+
}
514565
})
515566
.collect();
516567

517-
let num_fields = input_object_type_definition.input_field_definitions().len();
518-
519568
let serialize_impl = parse_quote! {
520569
impl shopify_function::wasm_api::Serialize for #name_ident {
521570
fn serialize(&self, context: &mut shopify_function::wasm_api::Context) -> ::std::result::Result<(), shopify_function::wasm_api::write::Error> {
571+
// Calculate dynamic field count based on non-null fields
572+
let mut field_count = 0usize;
573+
#(#field_count_statements)*
574+
522575
context.write_object(
523576
|context| {
524577
#(#field_statements)*
525578
::std::result::Result::Ok(())
526579
},
527-
#num_fields,
580+
field_count,
528581
)
529582
}
530583
}

0 commit comments

Comments
 (0)