Skip to content

Commit 3bf99bb

Browse files
committed
Add extern_enums arg to macros
The `extern_enums` arg is forwarded to the `graphql-client` crate, which will use it as the type for the enum instead of generating code for it. By default, the 3 large enums from Function APIs are passed as `extern_enums`: LanguageCode, CountryCode, CurrencyCode. Users can override that by sending `extern_enums` explicitly.
1 parent f1514f4 commit 3bf99bb

File tree

9 files changed

+154
-22
lines changed

9 files changed

+154
-22
lines changed

example_with_targets/README.md

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -76,20 +76,27 @@ input_query = "b.graphql"
7676
export = "function_b"
7777
```
7878

79-
- `target`: the API-specific handle for the target implemented by the Wasm function
80-
- `input_query`: the path to the target-specific input query file
81-
- `export` (optional): the name of the Wasm function export to run
82-
- default: the target handle as `snake_case`
79+
- `target`: The API-specific handle for the target implemented by the Wasm function.
80+
- `input_query`: The path to the target-specific input query file.
81+
- `export` (optional): The name of the Wasm function export to run.
82+
- default: The target handle as `snake_case`.
8383

8484
## `shopify_function_target` usage
8585

8686
### Arguments
8787

88-
- `query_path`: the path to the input query file for the target
89-
- `schema_path`: the path to the API schema file for the target
90-
- `target` (optional): the API-specific handle for the target if the function name does not match the target handle as `snake_case`
91-
- `module_name` (optional): the name of the generated module
92-
- default: the target handle as `snake_case`
88+
- `query_path`: A path to a GraphQL query, whose result will be used
89+
as the input for the function invocation. The query MUST be named "Input".
90+
- `schema_path`: A path to Shopify's GraphQL schema definition. Use the CLI
91+
to download a fresh copy.
92+
- `target` (optional): The API-specific handle for the target if the function name does not match the target handle as `snake_case`.
93+
- `module_name` (optional): The name of the generated module.
94+
- default: The target handle as `snake_case`
95+
- `extern_enums` (optional): A list of Enums for which an external type should be used.
96+
For those, code generation will be skipped. This is useful for large enums
97+
which can increase binary size, or for enums shared between multiple targets.
98+
Example: `extern_enums = ["LanguageCode"]`
99+
- default: `["LanguageCode", "CountryCode", "CurrencyCode"]`
93100

94101
### `src/lib.rs`
95102

shopify_function/src/enums.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pub type CountryCode = String;
2+
pub type CurrencyCode = String;
3+
pub type LanguageCode = String;

shopify_function/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,14 @@
1818
1919
pub use shopify_function_macro::{generate_types, shopify_function, shopify_function_target};
2020

21+
#[doc(hidden)]
22+
pub mod enums;
2123
/// Only used for struct generation.
2224
#[doc(hidden)]
2325
pub mod scalars;
2426

2527
pub mod prelude {
28+
pub use crate::enums::*;
2629
pub use crate::scalars::*;
2730
pub use shopify_function_macro::{generate_types, shopify_function, shopify_function_target};
2831
}

shopify_function/tests/fixtures/input.graphql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ query Input {
22
id
33
num
44
name
5+
country
56
}

shopify_function/tests/fixtures/schema.graphql

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type Input {
2828
id: ID!
2929
num: Int
3030
name: String
31+
country: CountryCode
3132
}
3233

3334
"""
@@ -51,3 +52,8 @@ The result of the function.
5152
input FunctionResult {
5253
name: String
5354
}
55+
56+
enum CountryCode {
57+
AC
58+
CA
59+
}

shopify_function/tests/fixtures/schema_with_targets.graphql

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type Input {
2828
id: ID!
2929
num: Int
3030
name: String
31+
country: CountryCode,
3132
targetAResult: Int @restrictTarget(only: ["test.target-b"])
3233
}
3334

@@ -69,3 +70,8 @@ The result of API target B.
6970
input FunctionTargetBResult {
7071
name: String
7172
}
73+
74+
enum CountryCode {
75+
AC
76+
CA
77+
}

shopify_function/tests/shopify_function.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ use shopify_function::Result;
44
const FUNCTION_INPUT: &str = r#"{
55
"id": "gid://shopify/Order/1234567890",
66
"num": 123,
7-
"name": "test"
7+
"name": "test",
8+
"country": "CA"
89
}"#;
910
static mut FUNCTION_OUTPUT: Vec<u8> = vec![];
1011

shopify_function/tests/shopify_function_target.rs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ use shopify_function::Result;
44
const TARGET_A_INPUT: &str = r#"{
55
"id": "gid://shopify/Order/1234567890",
66
"num": 123,
7-
"name": "test"
7+
"name": "test",
8+
"country": "CA"
89
}"#;
910
static mut TARGET_A_OUTPUT: Vec<u8> = vec![];
1011

@@ -24,8 +25,11 @@ fn test_target_a_export() {
2425
output_stream = unsafe { &mut TARGET_A_OUTPUT }
2526
)]
2627
fn target_a(
27-
_input: target_a::input::ResponseData,
28+
input: target_a::input::ResponseData,
2829
) -> Result<target_a::output::FunctionTargetAResult> {
30+
if input.country != Some("CA".to_string()) {
31+
panic!("Expected CountryCode to be the CA String")
32+
}
2933
Ok(target_a::output::FunctionTargetAResult { status: Some(200) })
3034
}
3135

@@ -49,7 +53,7 @@ fn test_mod_b_export() {
4953
query_path = "./tests/fixtures/b.graphql",
5054
schema_path = "./tests/fixtures/schema_with_targets.graphql",
5155
input_stream = std::io::Cursor::new(TARGET_B_INPUT.as_bytes().to_vec()),
52-
output_stream = unsafe { &mut TARGET_B_OUTPUT }
56+
output_stream = unsafe { &mut TARGET_B_OUTPUT },
5357
)]
5458
fn some_function(
5559
input: mod_b::input::ResponseData,
@@ -58,3 +62,26 @@ fn some_function(
5862
name: Some(format!("new name: {}", input.id)),
5963
})
6064
}
65+
66+
// Verify that the CountryCode enum is generated when `extern_enums = []`
67+
#[shopify_function_target(
68+
target = "test.target-a",
69+
module_name = "country_enum",
70+
query_path = "./tests/fixtures/input.graphql",
71+
schema_path = "./tests/fixtures/schema_with_targets.graphql",
72+
extern_enums = []
73+
)]
74+
fn _with_generated_country_code(
75+
input: country_enum::input::ResponseData,
76+
) -> Result<country_enum::output::FunctionTargetAResult> {
77+
use country_enum::*;
78+
79+
let status = match input.country {
80+
Some(input::CountryCode::CA) => 200,
81+
_ => 201,
82+
};
83+
84+
Ok(output::FunctionTargetAResult {
85+
status: Some(status),
86+
})
87+
}

shopify_function_macro/src/lib.rs

Lines changed: 87 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ use std::path::Path;
44

55
use proc_macro2::{Ident, Span, TokenStream};
66
use quote::{quote, ToTokens};
7-
use syn::{self, parse::Parse, parse::ParseStream, parse_macro_input, Expr, FnArg, LitStr, Token};
7+
use syn::{
8+
self, parse::Parse, parse::ParseStream, parse_macro_input, Expr, ExprArray, FnArg, LitStr,
9+
Token,
10+
};
811

912
#[derive(Clone, Default)]
1013
struct ShopifyFunctionArgs {
@@ -123,6 +126,7 @@ struct ShopifyFunctionTargetArgs {
123126
schema_path: Option<LitStr>,
124127
input_stream: Option<Expr>,
125128
output_stream: Option<Expr>,
129+
extern_enums: Option<ExprArray>,
126130
}
127131

128132
impl ShopifyFunctionTargetArgs {
@@ -156,6 +160,8 @@ impl Parse for ShopifyFunctionTargetArgs {
156160
args.input_stream = Some(Self::parse::<kw::input_stream, Expr>(&input)?);
157161
} else if lookahead.peek(kw::output_stream) {
158162
args.output_stream = Some(Self::parse::<kw::output_stream, Expr>(&input)?);
163+
} else if lookahead.peek(kw::extern_enums) {
164+
args.extern_enums = Some(Self::parse::<kw::extern_enums, ExprArray>(&input)?);
159165
} else {
160166
return Err(lookahead.error());
161167
}
@@ -170,6 +176,7 @@ struct GenerateTypeArgs {
170176
schema_path: Option<LitStr>,
171177
input_stream: Option<Expr>,
172178
output_stream: Option<Expr>,
179+
extern_enums: Option<ExprArray>,
173180
}
174181

175182
impl GenerateTypeArgs {
@@ -199,6 +206,8 @@ impl Parse for GenerateTypeArgs {
199206
args.input_stream = Some(Self::parse::<kw::input_stream, Expr>(&input)?);
200207
} else if lookahead.peek(kw::output_stream) {
201208
args.output_stream = Some(Self::parse::<kw::output_stream, Expr>(&input)?);
209+
} else if lookahead.peek(kw::extern_enums) {
210+
args.extern_enums = Some(Self::parse::<kw::extern_enums, ExprArray>(&input)?);
202211
} else {
203212
return Err(lookahead.error());
204213
}
@@ -256,6 +265,26 @@ fn extract_shopify_function_return_type(ast: &syn::ItemFn) -> Result<&syn::Ident
256265
Ok(&path.path.segments.last().as_ref().unwrap().ident)
257266
}
258267

268+
/// Generates code for a Function using an explicitly-named target. This will:
269+
/// - Generate a module to host the generated types.
270+
/// - Generate types based on the GraphQL schema for the Function input and output.
271+
/// - Define a wrapper function that's exported to Wasm. The wrapper handles
272+
/// decoding the input from STDIN, and encoding the output to STDOUT.
273+
///
274+
///
275+
/// The macro takes the following parameters:
276+
/// - `query_path`: A path to a GraphQL query, whose result will be used
277+
/// as the input for the function invocation. The query MUST be named "Input".
278+
/// - `schema_path`: A path to Shopify's GraphQL schema definition. Use the CLI
279+
/// to download a fresh copy.
280+
/// - `target` (optional): The API-specific handle for the target if the function name does not match the target handle as `snake_case`
281+
/// - `module_name` (optional): The name of the generated module.
282+
/// - default: The target handle as `snake_case`
283+
/// - `extern_enums` (optional): A list of Enums for which an external type should be used.
284+
/// For those, code generation will be skipped. This is useful for large enums
285+
/// which can increase binary size, or for enums shared between multiple targets.
286+
/// Example: `extern_enums = ["LanguageCode"]`
287+
/// - default: `["LanguageCode", "CountryCode", "CurrencyCode"]`
259288
#[proc_macro_attribute]
260289
pub fn shopify_function_target(
261290
attr: proc_macro::TokenStream,
@@ -282,17 +311,21 @@ pub fn shopify_function_target(
282311

283312
let query_path = args.query_path.expect("No value given for query_path");
284313
let schema_path = args.schema_path.expect("No value given for schema_path");
314+
let extern_enums = args.extern_enums.as_ref().map(extract_extern_enums);
285315
let output_query_file_name = format!(".{}{}", &target_handle_string, OUTPUT_QUERY_FILE_NAME);
286316

287317
let input_struct = generate_struct(
288318
"Input",
289319
query_path.value().as_str(),
290320
schema_path.value().as_str(),
321+
extern_enums.as_deref(),
291322
);
323+
292324
let output_struct = generate_struct(
293325
"Output",
294326
&output_query_file_name,
295327
schema_path.value().as_str(),
328+
extern_enums.as_deref(),
296329
);
297330
if let Err(error) = extract_shopify_function_return_type(&ast) {
298331
return error.to_compile_error().into();
@@ -353,12 +386,16 @@ const OUTPUT_QUERY_FILE_NAME: &str = ".output.graphql";
353386
/// modules generate Rust types from the GraphQL schema file for the Function input
354387
/// and output respectively.
355388
///
356-
/// The macro takes two parameters:
389+
/// The macro takes the following parameters:
357390
/// - `query_path`: A path to a GraphQL query, whose result will be used
358391
/// as the input for the function invocation. The query MUST be named "Input".
359-
/// - `schema_path`: A path to Shopify's GraphQL schema definition. You
360-
/// can find it in the `example` folder of the repo, or use the CLI
361-
/// to download a fresh copy (not implemented yet).
392+
/// - `schema_path`: A path to Shopify's GraphQL schema definition. Use the CLI
393+
/// to download a fresh copy.
394+
/// - `extern_enums` (optional): A list of Enums for which an external type should be used.
395+
/// For those, code generation will be skipped. This is useful for large enums
396+
/// which can increase binary size, or for enums shared between multiple targets.
397+
/// Example: `extern_enums = ["LanguageCode"]`
398+
/// - default: `["LanguageCode", "CountryCode", "CurrencyCode"]`
362399
///
363400
/// Note: This macro creates a file called `.output.graphql` in the root
364401
/// directory of the project. It can be safely added to your `.gitignore`. We
@@ -375,9 +412,19 @@ pub fn generate_types(attr: proc_macro::TokenStream) -> proc_macro::TokenStream
375412
.schema_path
376413
.expect("No value given for schema_path")
377414
.value();
378-
379-
let input_struct = generate_struct("Input", query_path.as_str(), schema_path.as_str());
380-
let output_struct = generate_struct("Output", OUTPUT_QUERY_FILE_NAME, schema_path.as_str());
415+
let extern_enums = args.extern_enums.as_ref().map(extract_extern_enums);
416+
let input_struct = generate_struct(
417+
"Input",
418+
query_path.as_str(),
419+
schema_path.as_str(),
420+
extern_enums.as_deref(),
421+
);
422+
let output_struct = generate_struct(
423+
"Output",
424+
OUTPUT_QUERY_FILE_NAME,
425+
schema_path.as_str(),
426+
extern_enums.as_deref(),
427+
);
381428
let output_query =
382429
"mutation Output($result: FunctionResult!) {\n handleResult(result: $result)\n}\n";
383430

@@ -390,16 +437,28 @@ pub fn generate_types(attr: proc_macro::TokenStream) -> proc_macro::TokenStream
390437
.into()
391438
}
392439

393-
fn generate_struct(name: &str, query_path: &str, schema_path: &str) -> TokenStream {
440+
const DEFAULT_EXTERN_ENUMS: &[&str] = &["LanguageCode", "CountryCode", "CurrencyCode"];
441+
442+
fn generate_struct(
443+
name: &str,
444+
query_path: &str,
445+
schema_path: &str,
446+
extern_enums: Option<&[String]>,
447+
) -> TokenStream {
394448
let name_ident = Ident::new(name, Span::mixed_site());
395449

450+
let extern_enums = extern_enums
451+
.map(|e| e.to_owned())
452+
.unwrap_or_else(|| DEFAULT_EXTERN_ENUMS.iter().map(|e| e.to_string()).collect());
453+
396454
quote! {
397455
#[derive(graphql_client::GraphQLQuery, Clone, Debug, serde::Deserialize, PartialEq)]
398456
#[graphql(
399457
query_path = #query_path,
400458
schema_path = #schema_path,
401459
response_derives = "Clone,Debug,PartialEq,Deserialize,Serialize",
402460
variables_derives = "Clone,Debug,PartialEq,Deserialize",
461+
extern_enums(#(#extern_enums),*),
403462
skip_serializing_none
404463
)]
405464
pub struct #name_ident;
@@ -415,6 +474,24 @@ fn write_output_query_file(output_query_file_name: &str, contents: &str) {
415474
.unwrap_or_else(|_| panic!("Could not write to {}", output_query_file_name));
416475
}
417476

477+
fn extract_extern_enums(extern_enums: &ExprArray) -> Vec<String> {
478+
let extern_enum_error_msg = r#"The `extern_enums` attribute expects comma separated string literals\n\n= help: use `extern_enums = ["Enum1", "Enum2"]`"#;
479+
extern_enums
480+
.elems
481+
.iter()
482+
.map(|expr| {
483+
let value = match expr {
484+
Expr::Lit(lit) => lit.lit.clone(),
485+
_ => panic!("{}", extern_enum_error_msg),
486+
};
487+
match value {
488+
syn::Lit::Str(lit) => lit.value(),
489+
_ => panic!("{}", extern_enum_error_msg),
490+
}
491+
})
492+
.collect()
493+
}
494+
418495
#[cfg(test)]
419496
mod tests {}
420497

@@ -425,4 +502,5 @@ mod kw {
425502
syn::custom_keyword!(schema_path);
426503
syn::custom_keyword!(input_stream);
427504
syn::custom_keyword!(output_stream);
505+
syn::custom_keyword!(extern_enums);
428506
}

0 commit comments

Comments
 (0)