diff --git a/Cargo.lock b/Cargo.lock index 415b659..984a2c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,6 +47,7 @@ name = "example" version = "1.0.0" dependencies = [ "graphql_client", + "graphql_client_codegen", "serde", "serde_json", "shopify_function", @@ -57,6 +58,7 @@ name = "example_with_targets" version = "1.0.0" dependencies = [ "graphql_client", + "graphql_client_codegen", "serde", "serde_json", "shopify_function", @@ -83,9 +85,9 @@ dependencies = [ [[package]] name = "graphql_client" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cdf7b487d864c2939b23902291a5041bc4a84418268f25fda1c8d4e15ad8fa" +checksum = "a50cfdc7f34b7f01909d55c2dcb71d4c13cbcbb4a1605d6c8bd760d654c1144b" dependencies = [ "graphql_query_derive", "serde", @@ -94,9 +96,9 @@ dependencies = [ [[package]] name = "graphql_client_codegen" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a40f793251171991c4eb75bd84bc640afa8b68ff6907bc89d3b712a22f700506" +checksum = "5e27ed0c2cf0c0cc52c6bcf3b45c907f433015e580879d14005386251842fb0a" dependencies = [ "graphql-introspection-query", "graphql-parser", @@ -111,9 +113,9 @@ dependencies = [ [[package]] name = "graphql_query_derive" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00bda454f3d313f909298f626115092d348bc231025699f557b27e248475f48c" +checksum = "83febfa838f898cfa73dfaa7a8eb69ff3409021ac06ee94cfb3d622f6eeb1a97" dependencies = [ "graphql_client_codegen", "proc-macro2", @@ -204,6 +206,7 @@ name = "shopify_function" version = "0.7.0" dependencies = [ "graphql_client", + "graphql_client_codegen", "ryu", "serde", "serde_json", @@ -215,6 +218,7 @@ name = "shopify_function_macro" version = "0.7.0" dependencies = [ "convert_case", + "graphql_client_codegen", "proc-macro2", "quote", "syn 1.0.109", diff --git a/example/Cargo.toml b/example/Cargo.toml index c77ae66..4c4cda8 100644 --- a/example/Cargo.toml +++ b/example/Cargo.toml @@ -8,4 +8,5 @@ license = "MIT" shopify_function = { path = "../shopify_function" } serde = { version = "1.0.13", features = ["derive"] } serde_json = "1.0" -graphql_client = "0.13.0" +graphql_client = "0.14.0" +graphql_client_codegen = "0.14.0" diff --git a/example_with_targets/.target_a.output.graphql b/example_with_targets/.target_a.output.graphql deleted file mode 100644 index 9e81f3f..0000000 --- a/example_with_targets/.target_a.output.graphql +++ /dev/null @@ -1,3 +0,0 @@ -mutation Output($result: FunctionTargetAResult!) { - targetA(result: $result) -} diff --git a/example_with_targets/.target_b.output.graphql b/example_with_targets/.target_b.output.graphql deleted file mode 100644 index 176316b..0000000 --- a/example_with_targets/.target_b.output.graphql +++ /dev/null @@ -1,3 +0,0 @@ -mutation Output($result: FunctionTargetBResult!) { - targetB(result: $result) -} diff --git a/example_with_targets/Cargo.toml b/example_with_targets/Cargo.toml index 6ad5248..71aab56 100644 --- a/example_with_targets/Cargo.toml +++ b/example_with_targets/Cargo.toml @@ -8,4 +8,5 @@ license = "MIT" shopify_function = { path = "../shopify_function" } serde = { version = "1.0.13", features = ["derive"] } serde_json = "1.0" -graphql_client = "0.13.0" +graphql_client = "0.14.0" +graphql_client_codegen = "0.14.0" diff --git a/example_with_targets/src/tests.rs b/example_with_targets/src/tests.rs index ae1772c..b3cd4a8 100644 --- a/example_with_targets/src/tests.rs +++ b/example_with_targets/src/tests.rs @@ -3,36 +3,39 @@ use shopify_function::{run_function_with_input, Result}; #[test] fn test_a() -> Result<()> { - let result = run_function_with_input( - target_a, - r#" + let result = serde_json::to_string( + &run_function_with_input( + target_a, + r#" { "id": "gid://shopify/Order/1234567890", "num": 123, "name": "test" } "#, + ) + .unwrap(), )?; - let expected = crate::target_a::output::FunctionTargetAResult { status: Some(200) }; + let expected = r#"{"status":200}"#; assert_eq!(result, expected); Ok(()) } #[test] fn test_function_b() -> Result<()> { - let result = run_function_with_input( - function_b, - r#" + let result = serde_json::to_string( + &run_function_with_input( + function_b, + r#" { "id": "gid://shopify/Order/1234567890", "aResult": 200 } "#, + ) + .unwrap(), )?; - let expected = crate::mod_b::output::FunctionTargetBResult { - name: Some("new name: \"gid://shopify/Order/1234567890\"".to_string()), - }; - + let expected = r#"{"name":"new name: \"gid://shopify/Order/1234567890\""}"#; assert_eq!(result, expected); Ok(()) } diff --git a/shopify_function/Cargo.toml b/shopify_function/Cargo.toml index 48edbb4..67f1e86 100644 --- a/shopify_function/Cargo.toml +++ b/shopify_function/Cargo.toml @@ -18,4 +18,5 @@ version = "1" features = ["small"] [dev-dependencies] -graphql_client = "0.13.0" +graphql_client = "0.14.0" +graphql_client_codegen = "0.14.0" diff --git a/shopify_function/README.md b/shopify_function/README.md index cf86301..dae4a44 100644 --- a/shopify_function/README.md +++ b/shopify_function/README.md @@ -4,57 +4,57 @@ A crate to help developers build [Shopify Functions]. ## Dependencies -* Make sure you have `graphql_client` in your dependencies +- Make sure you have `graphql_client` in your dependencies - ``` - cargo add graphql_client@0.13.0 - ``` + ``` + cargo add graphql_client@0.14.0 + ``` ## Usage -* The [`generate_types`] macro allows you to generate structs based on your [input query]. It will also generate output/response types for the current Function API, based on the provided schema. - * It will automatically generate an `.output.graphql` file for code generation purposes. This file can be added to your `.gitignore`. -* The [`shopify_function`] attribute macro marks the following function as the entry point for a Shopify Function. It manages the Functions `STDIN` input parsing and `STDOUT` output serialization for you. -* The [`run_function_with_input`] function is a utility for unit testing which allows you to quickly add new tests based on a given JSON input string. +- The [`generate_types`] macro allows you to generate structs based on your [input query]. It will also generate output/response types for the current Function API, based on the provided schema. +- The [`shopify_function`] attribute macro marks the following function as the entry point for a Shopify Function. It manages the Functions `STDIN` input parsing and `STDOUT` output serialization for you. +- The [`run_function_with_input`] function is a utility for unit testing which allows you to quickly add new tests based on a given JSON input string. See the [example] for details on usage, or use the following guide to convert an existing Rust-based function. ## Updating an existing function to use `shopify_function` 1. `cargo add shopify_function` -1. `cargo add graphql_client@0.13.0` +1. `cargo add graphql_client@0.14.0` 1. Delete `src/api.rs`. 1. In `main.rs`: - 1. Add imports for `shopify_function`. - ```rust - use shopify_function::prelude::*; - use shopify_function::Result; - ``` + 1. Add imports for `shopify_function`. - 1. Remove references to `mod api`. - 1. Add type generation, right under your imports. + ```rust + use shopify_function::prelude::*; + use shopify_function::Result; + ``` - ```rust - generate_types!(query_path = "./input.graphql", schema_path = "./schema.graphql"); - ``` + 1. Remove references to `mod api`. + 1. Add type generation, right under your imports. - 1. Remove the `main` function entirely. - 1. Attribute the `function` function with the `shopify_function` macro, and change its return type. + ```rust + generate_types!(query_path = "./input.graphql", schema_path = "./schema.graphql"); + ``` - ```rust - #[shopify_function] - fn function(input: input::ResponseData) -> Result { - ``` + 1. Remove the `main` function entirely. + 1. Attribute the `function` function with the `shopify_function` macro, and change its return type. - 1. Update the types and fields utilized in the function to the new, auto-generated structs. For example: - | Old | New | - | --- | --- | - | `input::Input` | `input::ResponseData` | - | `input::Metafield` | `input::InputDiscountNodeMetafield` | - | `input::DiscountNode` | `input::InputDiscountNode` | - | `FunctionResult` | `output::FunctionResult` | - | `DiscountApplicationStrategy::First` | `output::DiscountApplicationStrategy::FIRST` | + ```rust + #[shopify_function] + fn function(input: input::ResponseData) -> Result { + ``` + + 1. Update the types and fields utilized in the function to the new, auto-generated structs. For example: + | Old | New | + | --- | --- | + | `input::Input` | `input::ResponseData` | + | `input::Metafield` | `input::InputDiscountNodeMetafield` | + | `input::DiscountNode` | `input::InputDiscountNode` | + | `FunctionResult` | `output::FunctionResult` | + | `DiscountApplicationStrategy::First` | `output::DiscountApplicationStrategy::FIRST` | 1. Add `.output.graphql` to your `.gitignore`. @@ -69,6 +69,7 @@ cargo doc --open You can also use the [cargo-expand](https://github.com/dtolnay/cargo-expand) crate to view the generated source, or use the [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) VSCode extension to get [IntelliSense](https://code.visualstudio.com/docs/editor/intellisense) for Rust and the generated types. --- + License Apache-2.0 [Shopify Functions]: https://shopify.dev/api/functions diff --git a/shopify_function_macro/Cargo.toml b/shopify_function_macro/Cargo.toml index 6770174..49bd47f 100644 --- a/shopify_function_macro/Cargo.toml +++ b/shopify_function_macro/Cargo.toml @@ -14,3 +14,4 @@ syn = { version = "1.0", features = ["full"] } quote = "1.0" proc-macro2 = "1.0.43" convert_case = "0.6.0" +graphql_client_codegen = "0.14.0" diff --git a/shopify_function_macro/src/lib.rs b/shopify_function_macro/src/lib.rs index 96a2dcc..7d6d77f 100644 --- a/shopify_function_macro/src/lib.rs +++ b/shopify_function_macro/src/lib.rs @@ -1,5 +1,7 @@ use convert_case::{Case, Casing}; -use std::io::Write; +use graphql_client_codegen::{ + generate_module_token_stream_from_string, CodegenMode, GraphQLClientCodegenOptions, +}; use std::path::Path; use proc_macro2::{Ident, Span, TokenStream}; @@ -309,24 +311,22 @@ pub fn shopify_function_target( |module_name| Ident::new(module_name.value().as_str(), Span::mixed_site()), ); - let query_path = args.query_path.expect("No value given for query_path"); - let schema_path = args.schema_path.expect("No value given for schema_path"); + let query_path = args + .query_path + .expect("No value given for query_path") + .value(); + let schema_path = args + .schema_path + .expect("No value given for schema_path") + .value(); let extern_enums = args.extern_enums.as_ref().map(extract_extern_enums); - let output_query_file_name = format!(".{}{}", &target_handle_string, OUTPUT_QUERY_FILE_NAME); - let input_struct = generate_struct( - "Input", - query_path.value().as_str(), - schema_path.value().as_str(), + let input_struct = generate_input_struct( + query_path.as_str(), + schema_path.as_str(), extern_enums.as_deref(), ); - let output_struct = generate_struct( - "Output", - &output_query_file_name, - schema_path.value().as_str(), - extern_enums.as_deref(), - ); if let Err(error) = extract_shopify_function_return_type(&ast) { return error.to_compile_error().into(); } @@ -339,8 +339,12 @@ pub fn shopify_function_target( output_result_type, &target_handle_string.to_case(Case::Camel) ); + let output_struct = + generate_output_struct(&output_query, schema_path.as_str(), extern_enums.as_deref()); - write_output_query_file(&output_query_file_name, &output_query); + if let Err(error) = extract_shopify_function_return_type(&ast) { + return error.to_compile_error().into(); + } let input_stream = args .input_stream @@ -378,8 +382,6 @@ pub fn shopify_function_target( .into() } -const OUTPUT_QUERY_FILE_NAME: &str = ".output.graphql"; - /// Generate the types to interact with Shopify's API. /// /// The macro generates two inline modules: `input` and `output`. The @@ -396,10 +398,6 @@ const OUTPUT_QUERY_FILE_NAME: &str = ".output.graphql"; /// which can increase binary size, or for enums shared between multiple targets. /// Example: `extern_enums = ["LanguageCode"]` /// - default: `["LanguageCode", "CountryCode", "CurrencyCode"]` -/// -/// Note: This macro creates a file called `.output.graphql` in the root -/// directory of the project. It can be safely added to your `.gitignore`. We -/// hope we can avoid creating this file at some point in the future. #[proc_macro] pub fn generate_types(attr: proc_macro::TokenStream) -> proc_macro::TokenStream { let args = parse_macro_input!(attr as GenerateTypeArgs); @@ -413,22 +411,14 @@ pub fn generate_types(attr: proc_macro::TokenStream) -> proc_macro::TokenStream .expect("No value given for schema_path") .value(); let extern_enums = args.extern_enums.as_ref().map(extract_extern_enums); - let input_struct = generate_struct( - "Input", + let input_struct = generate_input_struct( query_path.as_str(), schema_path.as_str(), extern_enums.as_deref(), ); - let output_struct = generate_struct( - "Output", - OUTPUT_QUERY_FILE_NAME, - schema_path.as_str(), - extern_enums.as_deref(), - ); let output_query = "mutation Output($result: FunctionResult!) {\n handleResult(result: $result)\n}\n"; - - write_output_query_file(OUTPUT_QUERY_FILE_NAME, output_query); + let output_struct = generate_output_struct(output_query, &schema_path, extern_enums.as_deref()); quote! { #input_struct @@ -439,14 +429,11 @@ pub fn generate_types(attr: proc_macro::TokenStream) -> proc_macro::TokenStream const DEFAULT_EXTERN_ENUMS: &[&str] = &["LanguageCode", "CountryCode", "CurrencyCode"]; -fn generate_struct( - name: &str, +fn generate_input_struct( query_path: &str, schema_path: &str, extern_enums: Option<&[String]>, ) -> TokenStream { - let name_ident = Ident::new(name, Span::mixed_site()); - let extern_enums = extern_enums .map(|e| e.to_owned()) .unwrap_or_else(|| DEFAULT_EXTERN_ENUMS.iter().map(|e| e.to_string()).collect()); @@ -461,17 +448,42 @@ fn generate_struct( extern_enums(#(#extern_enums),*), skip_serializing_none )] - pub struct #name_ident; + pub struct Input; + } +} + +fn graphql_codegen_options( + operation_name: String, + extern_enums: Option<&[String]>, +) -> GraphQLClientCodegenOptions { + let mut options = GraphQLClientCodegenOptions::new(CodegenMode::Derive); + options.set_operation_name(operation_name); + options.set_response_derives("Clone,Debug,PartialEq,Deserialize,Serialize".to_string()); + options.set_variables_derives("Clone,Debug,PartialEq,Deserialize".to_string()); + options.set_skip_serializing_none(true); + if let Some(extern_enums) = extern_enums { + options.set_extern_enums(extern_enums.to_vec()); } + + options } -fn write_output_query_file(output_query_file_name: &str, contents: &str) { - let cargo_manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); - let output_query_path = Path::new(&cargo_manifest_dir).join(output_query_file_name); - std::fs::File::create(output_query_path) - .expect("Could not create output query file") - .write_all(contents.as_bytes()) - .unwrap_or_else(|_| panic!("Could not write to {}", output_query_file_name)); +fn generate_output_struct( + query: &str, + schema_path: &str, + extern_enums: Option<&[String]>, +) -> proc_macro2::TokenStream { + let options = graphql_codegen_options("Output".to_string(), extern_enums); + let cargo_manifest_dir = + std::env::var("CARGO_MANIFEST_DIR").expect("Error reading CARGO_MANIFEST_DIR from env"); + let schema_path = Path::new(&cargo_manifest_dir).join(schema_path); + let token_stream = generate_module_token_stream_from_string(query, &schema_path, options) + .expect("Error generating Output struct"); + + quote! { + #token_stream + pub struct Output; + } } fn extract_extern_enums(extern_enums: &ExprArray) -> Vec {