Skip to content

Commit a9792ac

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 52d4856 commit a9792ac

File tree

9 files changed

+155
-20
lines changed

9 files changed

+155
-20
lines changed

example_with_targets/README.md

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -76,20 +76,28 @@ 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. You
91+
can find it in the `example` folder of the repo, or use the CLI
92+
to download a fresh copy (not implemented yet).
93+
- `target` (optional): The API-specific handle for the target if the function name does not match the target handle as `snake_case`.
94+
- `module_name` (optional): The name of the generated module.
95+
- default: The target handle as `snake_case`
96+
- `extern_enums` (optional): A list of Enums for which an external type should be used.
97+
For those, code generation will be skipped. This is useful for large enums
98+
which can increase binary size, or to enums between multiple targets.
99+
Example: `extern_enums = ["LanguageCode"]`
100+
- default: `["LanguageCode", "CountryCode", "CurrencyCode"]`
93101

94102
### `src/lib.rs`
95103

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 a 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 & 7 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,27 @@ 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. You
279+
/// can find it in the `example` folder of the repo, or use the CLI
280+
/// to download a fresh copy.
281+
/// - `target` (optional): The API-specific handle for the target if the function name does not match the target handle as `snake_case`
282+
/// - `module_name` (optional): The name of the generated module.
283+
/// - default: The target handle as `snake_case`
284+
/// - `extern_enums` (optional): A list of enums for which an external type should be used.
285+
/// For those, code generation will be skipped. This is useful to skip
286+
/// codegen for large enums, or share enums between multiple functions.
287+
/// Example: `extern_enums = ["LanguageCode"]`
288+
/// - default: `["LanguageCode", "CountryCode", "CurrencyCode"]`
259289
#[proc_macro_attribute]
260290
pub fn shopify_function_target(
261291
attr: proc_macro::TokenStream,
@@ -282,17 +312,21 @@ pub fn shopify_function_target(
282312

283313
let query_path = args.query_path.expect("No value given for query_path");
284314
let schema_path = args.schema_path.expect("No value given for schema_path");
315+
let extern_enums = args.extern_enums.as_ref().map(extract_extern_enums);
285316
let output_query_file_name = format!(".{}{}", &target_handle_string, OUTPUT_QUERY_FILE_NAME);
286317

287318
let input_struct = generate_struct(
288319
"Input",
289320
query_path.value().as_str(),
290321
schema_path.value().as_str(),
322+
extern_enums.as_deref(),
291323
);
324+
292325
let output_struct = generate_struct(
293326
"Output",
294327
&output_query_file_name,
295328
schema_path.value().as_str(),
329+
extern_enums.as_deref(),
296330
);
297331
if let Err(error) = extract_shopify_function_return_type(&ast) {
298332
return error.to_compile_error().into();
@@ -353,12 +387,17 @@ const OUTPUT_QUERY_FILE_NAME: &str = ".output.graphql";
353387
/// modules generate Rust types from the GraphQL schema file for the Function input
354388
/// and output respectively.
355389
///
356-
/// The macro takes two parameters:
390+
/// The macro takes the following parameters:
357391
/// - `query_path`: A path to a GraphQL query, whose result will be used
358392
/// as the input for the function invocation. The query MUST be named "Input".
359393
/// - `schema_path`: A path to Shopify's GraphQL schema definition. You
360394
/// can find it in the `example` folder of the repo, or use the CLI
361-
/// to download a fresh copy (not implemented yet).
395+
/// to download a fresh copy.
396+
/// - `extern_enums`: An list of enums for which an external type should be used.
397+
/// For those, code generation will be skipped. This is useful to skip
398+
/// codegen for large enums, or share enums between multiple functions.
399+
/// Example: `extern_enums = ["LanguageCode"]`
400+
/// Defaults to: `["LanguageCode", "CountryCode", "CurrencyCode"]`
362401
///
363402
/// Note: This macro creates a file called `.output.graphql` in the root
364403
/// directory of the project. It can be safely added to your `.gitignore`. We
@@ -375,9 +414,19 @@ pub fn generate_types(attr: proc_macro::TokenStream) -> proc_macro::TokenStream
375414
.schema_path
376415
.expect("No value given for schema_path")
377416
.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());
417+
let extern_enums = args.extern_enums.as_ref().map(extract_extern_enums);
418+
let input_struct = generate_struct(
419+
"Input",
420+
query_path.as_str(),
421+
schema_path.as_str(),
422+
extern_enums.as_deref(),
423+
);
424+
let output_struct = generate_struct(
425+
"Output",
426+
OUTPUT_QUERY_FILE_NAME,
427+
schema_path.as_str(),
428+
extern_enums.as_deref(),
429+
);
381430
let output_query =
382431
"mutation Output($result: FunctionResult!) {\n handleResult(result: $result)\n}\n";
383432

@@ -390,16 +439,28 @@ pub fn generate_types(attr: proc_macro::TokenStream) -> proc_macro::TokenStream
390439
.into()
391440
}
392441

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

452+
let extern_enums = extern_enums
453+
.map(|e| e.to_owned())
454+
.unwrap_or_else(|| DEFAULT_EXTERN_ENUMS.iter().map(|e| e.to_string()).collect());
455+
396456
quote! {
397457
#[derive(graphql_client::GraphQLQuery, Clone, Debug, serde::Deserialize, PartialEq)]
398458
#[graphql(
399459
query_path = #query_path,
400460
schema_path = #schema_path,
401461
response_derives = "Clone,Debug,PartialEq,Deserialize,Serialize",
402462
variables_derives = "Clone,Debug,PartialEq,Deserialize",
463+
extern_enums(#(#extern_enums),*),
403464
skip_serializing_none
404465
)]
405466
pub struct #name_ident;
@@ -415,6 +476,24 @@ fn write_output_query_file(output_query_file_name: &str, contents: &str) {
415476
.unwrap_or_else(|_| panic!("Could not write to {}", output_query_file_name));
416477
}
417478

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

@@ -425,4 +504,5 @@ mod kw {
425504
syn::custom_keyword!(schema_path);
426505
syn::custom_keyword!(input_stream);
427506
syn::custom_keyword!(output_stream);
507+
syn::custom_keyword!(extern_enums);
428508
}

0 commit comments

Comments
 (0)