diff --git a/Cargo.lock b/Cargo.lock index b32584a..31c0abb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -53,6 +53,7 @@ name = "example" version = "1.0.0" dependencies = [ "graphql_client", + "graphql_client_codegen 0.13.0 (git+https://github.com/dphm/graphql-client.git?branch=query-string)", "serde", "serde_json", "shopify_function", @@ -63,6 +64,7 @@ name = "example_with_targets" version = "1.0.0" dependencies = [ "graphql_client", + "graphql_client_codegen 0.13.0 (git+https://github.com/dphm/graphql-client.git?branch=query-string)", "serde", "serde_json", "shopify_function", @@ -77,6 +79,14 @@ dependencies = [ "serde", ] +[[package]] +name = "graphql-introspection-query" +version = "0.2.0" +source = "git+https://github.com/dphm/graphql-client.git?branch=query-string#feed07f702d24f5a191c58687d20ed8fe07fe537" +dependencies = [ + "serde", +] + [[package]] name = "graphql-parser" version = "0.4.0" @@ -104,7 +114,23 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a40f793251171991c4eb75bd84bc640afa8b68ff6907bc89d3b712a22f700506" dependencies = [ - "graphql-introspection-query", + "graphql-introspection-query 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "graphql-parser", + "heck", + "lazy_static", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn", +] + +[[package]] +name = "graphql_client_codegen" +version = "0.13.0" +source = "git+https://github.com/dphm/graphql-client.git?branch=query-string#feed07f702d24f5a191c58687d20ed8fe07fe537" +dependencies = [ + "graphql-introspection-query 0.2.0 (git+https://github.com/dphm/graphql-client.git?branch=query-string)", "graphql-parser", "heck", "lazy_static", @@ -121,7 +147,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00bda454f3d313f909298f626115092d348bc231025699f557b27e248475f48c" dependencies = [ - "graphql_client_codegen", + "graphql_client_codegen 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)", "proc-macro2", "syn", ] @@ -211,6 +237,7 @@ version = "0.5.0" dependencies = [ "anyhow", "graphql_client", + "graphql_client_codegen 0.13.0 (git+https://github.com/dphm/graphql-client.git?branch=query-string)", "serde", "serde_json", "shopify_function_macro", @@ -221,6 +248,7 @@ name = "shopify_function_macro" version = "0.5.0" dependencies = [ "convert_case", + "graphql_client_codegen 0.13.0 (git+https://github.com/dphm/graphql-client.git?branch=query-string)", "proc-macro2", "quote", "syn", diff --git a/example/Cargo.toml b/example/Cargo.toml index 7cfe1d9..a2d3dac 100644 --- a/example/Cargo.toml +++ b/example/Cargo.toml @@ -9,3 +9,4 @@ shopify_function = { path = "../shopify_function", version = "0.5.0" } serde = { version = "1.0.13", features = ["derive"] } serde_json = "1.0" graphql_client = "0.13.0" +graphql_client_codegen = { git = "https://github.com/dphm/graphql-client.git", branch = "query-string" } 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 7aaf873..5e3fd55 100644 --- a/example_with_targets/Cargo.toml +++ b/example_with_targets/Cargo.toml @@ -9,3 +9,4 @@ shopify_function = { path = "../shopify_function", version = "0.5.0" } serde = { version = "1.0.13", features = ["derive"] } serde_json = "1.0" graphql_client = "0.13.0" +graphql_client_codegen = { git = "https://github.com/dphm/graphql-client.git", branch = "query-string" } 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 fc8b844..5472440 100644 --- a/shopify_function/Cargo.toml +++ b/shopify_function/Cargo.toml @@ -14,3 +14,4 @@ shopify_function_macro = { version = "0.5.0", path = "../shopify_function_macro" [dev-dependencies] graphql_client = "0.13.0" +graphql_client_codegen = { git = "https://github.com/dphm/graphql-client.git", branch = "query-string" } diff --git a/shopify_function/README.md b/shopify_function/README.md index cf86301..39b5a76 100644 --- a/shopify_function/README.md +++ b/shopify_function/README.md @@ -4,18 +4,17 @@ 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.13.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. @@ -25,36 +24,37 @@ See the [example] for details on usage, or use the following guide to convert an 1. `cargo add graphql_client@0.13.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 4b9f830..bdf89a5 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 = { git = "https://github.com/dphm/graphql-client.git", branch = "query-string" } diff --git a/shopify_function_macro/src/lib.rs b/shopify_function_macro/src/lib.rs index d2eb415..bc58553 100644 --- a/shopify_function_macro/src/lib.rs +++ b/shopify_function_macro/src/lib.rs @@ -1,5 +1,8 @@ use convert_case::{Case, Casing}; -use std::io::Write; +use graphql_client_codegen::{ + generate_module_token_stream, generate_module_token_stream_from_string, CodegenMode, + GraphQLClientCodegenOptions, +}; use std::path::Path; use proc_macro2::{Ident, Span, TokenStream, TokenTree}; @@ -237,23 +240,20 @@ 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 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 dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + let query_path = Path::new(&dir).join( + args.query_path + .expect("No value given for query_path") + .value() + .as_str(), ); - let output_struct = generate_struct( - "Output", - &output_query_file_name, - schema_path.value().as_str(), + let schema_path = Path::new(&dir).join( + args.schema_path + .expect("No value given for schema_path") + .value() + .as_str(), ); - if let Err(error) = extract_shopify_function_return_type(&ast) { - return error.to_compile_error().into(); - } + let output_result_type = extract_shopify_function_return_type(&ast) .unwrap() .to_token_stream() @@ -263,8 +263,12 @@ pub fn shopify_function_target( output_result_type, &target_handle_string.to_case(Case::Camel) ); + let input_struct = generate_input_struct(query_path.as_path(), &schema_path); + let output_struct = generate_output_struct(&output_query, &schema_path); - 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 @@ -318,8 +322,6 @@ fn extract_attr(attrs: &TokenStream, attr: &str) -> String { value.as_str()[1..value.len() - 1].to_string() } -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 @@ -340,15 +342,17 @@ const OUTPUT_QUERY_FILE_NAME: &str = ".output.graphql"; pub fn generate_types(attr: proc_macro::TokenStream) -> proc_macro::TokenStream { let params = TokenStream::from(attr); - let query_path = extract_attr(¶ms, "query_path"); - let schema_path = extract_attr(¶ms, "schema_path"); + let query_path_attr = extract_attr(¶ms, "query_path"); + let schema_path_attr = extract_attr(¶ms, "schema_path"); + + let schema_path = build_path(&schema_path_attr); + + let input_query_path = build_path(&query_path_attr); + let input_struct = generate_input_struct(input_query_path.as_path(), &schema_path); - let input_struct = generate_struct("Input", &query_path, &schema_path); - let output_struct = generate_struct("Output", OUTPUT_QUERY_FILE_NAME, &schema_path); 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); quote! { #input_struct @@ -357,29 +361,45 @@ pub fn generate_types(attr: proc_macro::TokenStream) -> proc_macro::TokenStream .into() } -fn generate_struct(name: &str, query_path: &str, schema_path: &str) -> TokenStream { - let name_ident = Ident::new(name, Span::mixed_site()); +fn build_path(path_attr: &str) -> std::path::PathBuf { + let cargo_manifest_dir = + std::env::var("CARGO_MANIFEST_DIR").expect("Error reading CARGO_MANIFEST_DIR from env"); + Path::new(&cargo_manifest_dir).join(path_attr) +} + +fn graphql_codegen_options(operation_name: 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); + + options +} + +fn generate_input_struct( + query_path: &std::path::Path, + schema_path: &Path, +) -> proc_macro2::TokenStream { + let options = graphql_codegen_options("Input".to_string()); + let token_stream = generate_module_token_stream(query_path.to_path_buf(), schema_path, options) + .expect("Error generating Input struct"); quote! { - #[derive(graphql_client::GraphQLQuery, Clone, Debug, serde::Deserialize, PartialEq)] - #[graphql( - query_path = #query_path, - schema_path = #schema_path, - response_derives = "Clone,Debug,PartialEq,Deserialize,Serialize", - variables_derives = "Clone,Debug,PartialEq,Deserialize", - skip_serializing_none - )] - pub struct #name_ident; + #token_stream + pub struct Input; } } -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: &Path) -> proc_macro2::TokenStream { + let options = graphql_codegen_options("Output".to_string()); + let token_stream = generate_module_token_stream_from_string(query, schema_path, options) + .expect("Error generating Output struct"); + + quote! { + #token_stream + pub struct Output; + } } #[cfg(test)]