From 4f2312d027c613e1216e06b15b68a6c4d0cfbb28 Mon Sep 17 00:00:00 2001 From: David Graeff Date: Wed, 21 Aug 2019 18:48:43 +0200 Subject: [PATCH 1/7] Update all dependencies, especially syn and rustfmt to 1.0+ * Specify most dependencies with major.minor version, no patch * New syn versions syn::Visibility doesn't dervive from debug anymore. * Use the Derivative crate to skip the "module_visibility" field for debug out. Signed-off-by: David Graeff --- examples/github/Cargo.toml | 20 ++++++------- graphql_client/Cargo.toml | 28 +++++++++---------- graphql_client_cli/Cargo.toml | 18 ++++++------ graphql_client_cli/src/generate.rs | 1 - graphql_client_cli/src/introspect_schema.rs | 2 +- graphql_client_codegen/Cargo.toml | 19 +++++++------ graphql_client_codegen/src/codegen_options.rs | 5 +++- graphql_client_web/Cargo.toml | 4 +-- graphql_query_derive/Cargo.toml | 6 ++-- graphql_query_derive/src/attributes.rs | 15 +++++----- 10 files changed, 60 insertions(+), 58 deletions(-) diff --git a/examples/github/Cargo.toml b/examples/github/Cargo.toml index 6f27baddd..0423a0a2f 100644 --- a/examples/github/Cargo.toml +++ b/examples/github/Cargo.toml @@ -7,16 +7,16 @@ edition = "2018" [dependencies] failure = "*" graphql_client = { path = "../../graphql_client" } -serde = "1.0" -serde_derive = "1.0" -serde_json = "1.0" -reqwest = "^0.9.0" -prettytable-rs = "0.7.0" -structopt = "0.2.10" -dotenv = "0.13.0" -envy = "0.3.2" -log = "0.4.3" -env_logger = "0.5.10" +serde = "^1.0" +serde_derive = "^1.0" +serde_json = "^1.0" +reqwest = "^0.9" +prettytable-rs = "^0.7" +structopt = "^0.2" +dotenv = "^0.13" +envy = "^0.3" +log = "^0.4" +env_logger = "^0.5" [workspace] members = ["."] diff --git a/graphql_client/Cargo.toml b/graphql_client/Cargo.toml index 3ced39c07..7e1e75c53 100644 --- a/graphql_client/Cargo.toml +++ b/graphql_client/Cargo.toml @@ -10,26 +10,26 @@ categories = ["network-programming", "web-programming", "wasm"] edition = "2018" [dependencies] -failure = "0.1" +failure = "^0.1" graphql_query_derive = { path = "../graphql_query_derive", version = "0.8.0" } -serde = { version = "^1.0.78", features = ["derive"] } -serde_json = "1.0" -doc-comment = "0.3.1" +serde = { version = "^1.0", features = ["derive"] } +serde_json = "^1.0" +doc-comment = "^0.3" [dependencies.futures] -version = "0.1" +version = "^0.1" optional = true [dependencies.js-sys] -version = "0.3.5" +version = "^0.3" optional = true [dependencies.log] -version = "0.4.6" +version = "^0.4" optional = true [dependencies.web-sys] -version = "0.3.2" +version = "^0.3" optional = true features = [ "Headers", @@ -40,18 +40,18 @@ features = [ ] [dependencies.wasm-bindgen] -version = "0.2.43" +version = "^0.2" optional = true [dependencies.wasm-bindgen-futures] -version = "0.3.2" +version = "^0.3" optional = true -[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies.reqwest] -version = "0.9.16" +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +reqwest = "^0.9" -[dev-dependencies.wasm-bindgen-test] -version = "0.2.43" +[dev-dependencies] +wasm-bindgen-test = "^0.2" [features] web = [ diff --git a/graphql_client_cli/Cargo.toml b/graphql_client_cli/Cargo.toml index 2640fe536..8ffa2716e 100644 --- a/graphql_client_cli/Cargo.toml +++ b/graphql_client_cli/Cargo.toml @@ -12,18 +12,18 @@ name = "graphql-client" path = "src/main.rs" [dependencies] -failure = "0.1" -reqwest = "^0.9.0" +failure = "^0.1" +reqwest = "^0.9" graphql_client = { version = "0.8.0", path = "../graphql_client" } graphql_client_codegen = { path = "../graphql_client_codegen/", version = "0.8.0" } -structopt = "0.2" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -syn = "0.15" -log = "0.4.0" -env_logger = "0.6.0" +structopt = "0.2.18" +serde = { version = "^1.0", features = ["derive"] } +serde_json = "^1.0" +syn = "^1.0" +log = "^0.4" +env_logger = "^0.6" -rustfmt-nightly = { version = "0.99" , optional = true } +rustfmt-nightly = { version = "1.4.5", optional = true } [features] default = [] diff --git a/graphql_client_cli/src/generate.rs b/graphql_client_cli/src/generate.rs index 2d8c65fd4..dcd001df1 100644 --- a/graphql_client_cli/src/generate.rs +++ b/graphql_client_cli/src/generate.rs @@ -82,7 +82,6 @@ fn format(codes: &str) -> String { #[cfg(feature = "rustfmt")] { use rustfmt::{Config, Input, Session}; - use std::default::Default; let mut config = Config::default(); diff --git a/graphql_client_cli/src/introspect_schema.rs b/graphql_client_cli/src/introspect_schema.rs index f9045d5c5..c9e5d52ac 100644 --- a/graphql_client_cli/src/introspect_schema.rs +++ b/graphql_client_cli/src/introspect_schema.rs @@ -21,7 +21,7 @@ pub fn introspect_schema( ) -> Result<(), failure::Error> { use std::io::Write; - let out: Box = match output { + let out: Box = match output { Some(path) => Box::new(::std::fs::File::create(path)?), None => Box::new(::std::io::stdout()), }; diff --git a/graphql_client_codegen/Cargo.toml b/graphql_client_codegen/Cargo.toml index f0b2c71e5..4fb4836e6 100644 --- a/graphql_client_codegen/Cargo.toml +++ b/graphql_client_codegen/Cargo.toml @@ -8,12 +8,13 @@ repository = "https://github.com/graphql-rust/graphql-client" edition = "2018" [dependencies] -failure = "0.1" -lazy_static = "1.0" -quote = "0.6" -syn = "0.15.20" -proc-macro2 = { version = "0.4", features = [] } -serde = { version = "^1.0.78", features = ["derive"] } -serde_json = "1.0" -heck = "0.3" -graphql-parser = "0.2.2" +failure = "^0.1" +lazy_static = "^1.3" +quote = "^1.0" +syn = "^1.0" +proc-macro2 = { version = "^1.0", features = [] } +serde = { version = "^1.0", features = ["derive"] } +serde_json = "^1.0" +heck = "^0.3" +graphql-parser = "^0.2" +derivative = "1.0.2" diff --git a/graphql_client_codegen/src/codegen_options.rs b/graphql_client_codegen/src/codegen_options.rs index 5c31ca28a..ef555720e 100644 --- a/graphql_client_codegen/src/codegen_options.rs +++ b/graphql_client_codegen/src/codegen_options.rs @@ -1,4 +1,5 @@ use crate::deprecation::DeprecationStrategy; +use derivative::*; use proc_macro2::Ident; use std::path::{Path, PathBuf}; use syn::Visibility; @@ -13,7 +14,8 @@ pub enum CodegenMode { } /// Used to configure code generation. -#[derive(Debug)] +#[derive(Derivative)] +#[derivative(Debug)] pub struct GraphQLClientCodegenOptions { /// Which context is this code generation effort taking place. pub mode: CodegenMode, @@ -28,6 +30,7 @@ pub struct GraphQLClientCodegenOptions { /// The deprecation strategy to adopt. deprecation_strategy: Option, /// Target module visibility. + #[derivative(Debug = "ignore")] module_visibility: Option, /// A path to a file to include in the module to force Cargo to take into account changes in /// the query files when recompiling. diff --git a/graphql_client_web/Cargo.toml b/graphql_client_web/Cargo.toml index cbf8ea883..05a07ea91 100644 --- a/graphql_client_web/Cargo.toml +++ b/graphql_client_web/Cargo.toml @@ -15,5 +15,5 @@ path = "../graphql_client" features = ["web"] [dev-dependencies] -serde = { version = "1", features = ["derive"] } -wasm-bindgen-test = "0.2.25" +serde = { version = "^1.0", features = ["derive"] } +wasm-bindgen-test = "0.2.50" diff --git a/graphql_query_derive/Cargo.toml b/graphql_query_derive/Cargo.toml index 3afffb86e..5ad2b754f 100644 --- a/graphql_query_derive/Cargo.toml +++ b/graphql_query_derive/Cargo.toml @@ -11,7 +11,7 @@ edition = "2018" proc-macro = true [dependencies] -failure = "0.1" -syn = { version = "0.15.20", features = ["extra-traits"] } -proc-macro2 = { version = "0.4", features = [] } +failure = "^0.1" +syn = { version = "^1.0", features = ["extra-traits"] } +proc-macro2 = { version = "^1.0", features = [] } graphql_client_codegen = { path = "../graphql_client_codegen/", version = "0.8.0" } diff --git a/graphql_query_derive/src/attributes.rs b/graphql_query_derive/src/attributes.rs index 91a8b38ee..0bd4ade59 100644 --- a/graphql_query_derive/src/attributes.rs +++ b/graphql_query_derive/src/attributes.rs @@ -17,16 +17,15 @@ pub fn extract_attr(ast: &syn::DeriveInput, attr: &str) -> Result Date: Fri, 23 Aug 2019 11:45:32 +0200 Subject: [PATCH 2/7] Fix structopt usage in the CLI tool. Potential breaking change! structopt uses the first letter for a "short" option. "selected_operation" (with "o") as well as "output_directory" (with "out") claimed "-o" which resulted in: thread 'main' panicked at 'Argument short must be unique -o is already in use Fixed by removing the short option for "selected_operation" and keep "-o" for output directory. Signed-off-by: David Graeff --- graphql_client_cli/src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphql_client_cli/src/main.rs b/graphql_client_cli/src/main.rs index f6dba1f8e..a19cc8d4f 100644 --- a/graphql_client_cli/src/main.rs +++ b/graphql_client_cli/src/main.rs @@ -36,7 +36,7 @@ enum Cli { /// Path to the GraphQL query file. query_path: PathBuf, /// Name of target query. If you don't set this parameter, cli generate all queries in query file. - #[structopt(short = "o", long = "selected-operation")] + #[structopt(long = "selected-operation")] selected_operation: Option, /// Additional derives that will be added to the generated structs and enums for the response and the variables. /// --additional-derives='Serialize,PartialEq' @@ -59,7 +59,7 @@ enum Cli { /// /// If this option is omitted, the code will be generated next to the .graphql /// file, with the same name and the .rs extension. - #[structopt(short = "out", long = "output-directory")] + #[structopt(short = "o", long = "output-directory")] output_directory: Option, }, } From e8f8e478a713c8fd2392c3a5f15129ab3f58db5a Mon Sep 17 00:00:00 2001 From: David Graeff Date: Fri, 23 Aug 2019 14:15:23 +0200 Subject: [PATCH 3/7] Fix ambigous names of generated struct+module If a camel case query name like "ping_query" is used, the generated code fails. Fixed by using `to_camel_case` for the struct name, while the module name still uses `to_snake_case`. Even for a single word query name like "ping" that should work. Signed-off-by: David Graeff --- graphql_client_codegen/src/generated_module.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/graphql_client_codegen/src/generated_module.rs b/graphql_client_codegen/src/generated_module.rs index 5e6e9dce4..a1d5f83c2 100644 --- a/graphql_client_codegen/src/generated_module.rs +++ b/graphql_client_codegen/src/generated_module.rs @@ -28,7 +28,7 @@ impl<'a> GeneratedModule<'a> { let module_name = Ident::new(&self.operation.name.to_snake_case(), Span::call_site()); let module_visibility = &self.options.module_visibility(); let operation_name_literal = &self.operation.name; - let operation_name_ident = Ident::new(&self.operation.name, Span::call_site()); + let operation_name_ident = Ident::new(&self.operation.name.to_camel_case(), Span::call_site()); // Force cargo to refresh the generated code when the query file changes. let query_include = self @@ -52,6 +52,7 @@ impl<'a> GeneratedModule<'a> { }; Ok(quote!( + #[allow(dead_code)] #struct_declaration #module_visibility mod #module_name { From a159702eb75e136d5863cabb7fbd45b4bcf0774f Mon Sep 17 00:00:00 2001 From: David Graeff Date: Sun, 25 Aug 2019 15:42:28 +0200 Subject: [PATCH 4/7] Catch more cases where a rust keyword in schemas or queries would break code generation Add integration test (keywords_query.graphql+keywords_schema.graphql) Signed-off-by: David Graeff --- graphql_client_codegen/src/enums.rs | 13 ++- graphql_client_codegen/src/field_type.rs | 3 +- .../src/generated_module.rs | 3 +- graphql_client_codegen/src/inputs.rs | 12 +- graphql_client_codegen/src/operations.rs | 9 +- graphql_client_codegen/src/query.rs | 6 +- graphql_client_codegen/src/shared.rs | 107 ++++++++++++++---- .../src/tests/keywords_query.graphql | 7 ++ .../src/tests/keywords_schema.graphql | 52 +++++++++ graphql_client_codegen/src/tests/mod.rs | 42 +++++++ 10 files changed, 215 insertions(+), 39 deletions(-) create mode 100644 graphql_client_codegen/src/tests/keywords_query.graphql create mode 100644 graphql_client_codegen/src/tests/keywords_schema.graphql diff --git a/graphql_client_codegen/src/enums.rs b/graphql_client_codegen/src/enums.rs index d294cc8c0..ad2ecc046 100644 --- a/graphql_client_codegen/src/enums.rs +++ b/graphql_client_codegen/src/enums.rs @@ -19,6 +19,13 @@ pub struct GqlEnum<'schema> { } impl<'schema> GqlEnum<'schema> { + /** + * About rust keyword escaping: variant_names and constructors must be escaped, + * variant_str not. + * Example schema: enum AnEnum { where \n self } + * Generated "variant_names" enum: pub enum AnEnum { where_, self_, Other(String), } + * Generated serialize line: "AnEnum::where_ => "where"," + */ pub(crate) fn to_rust( &self, query_context: &crate::query::QueryContext<'_, '_>, @@ -28,7 +35,8 @@ impl<'schema> GqlEnum<'schema> { .variants .iter() .map(|v| { - let name = Ident::new(&v.name, Span::call_site()); + let rust_safe_field_name = crate::shared::keyword_replace(&v.name); + let name = Ident::new(&rust_safe_field_name, Span::call_site()); let description = &v.description; let description = description.as_ref().map(|d| quote!(#[doc = #d])); quote!(#description #name) @@ -40,7 +48,8 @@ impl<'schema> GqlEnum<'schema> { .variants .iter() .map(|v| { - let v = Ident::new(&v.name, Span::call_site()); + let rust_safe_field_name = crate::shared::keyword_replace(&v.name); + let v = Ident::new(&rust_safe_field_name, Span::call_site()); quote!(#name_ident::#v) }) .collect(); diff --git a/graphql_client_codegen/src/field_type.rs b/graphql_client_codegen/src/field_type.rs index 4d95f9486..97006fe03 100644 --- a/graphql_client_codegen/src/field_type.rs +++ b/graphql_client_codegen/src/field_type.rs @@ -46,8 +46,9 @@ impl<'a> FieldType<'a> { } prefix.to_string() }; - let full_name = Ident::new(&full_name, Span::call_site()); + let rust_safe_field_name = crate::shared::keyword_replace(&full_name); + let full_name = Ident::new(&rust_safe_field_name, Span::call_site()); quote!(#full_name) } FieldType::Optional(inner) => { diff --git a/graphql_client_codegen/src/generated_module.rs b/graphql_client_codegen/src/generated_module.rs index a1d5f83c2..0a585b81d 100644 --- a/graphql_client_codegen/src/generated_module.rs +++ b/graphql_client_codegen/src/generated_module.rs @@ -28,7 +28,8 @@ impl<'a> GeneratedModule<'a> { let module_name = Ident::new(&self.operation.name.to_snake_case(), Span::call_site()); let module_visibility = &self.options.module_visibility(); let operation_name_literal = &self.operation.name; - let operation_name_ident = Ident::new(&self.operation.name.to_camel_case(), Span::call_site()); + let operation_name_ident = + Ident::new(&self.operation.name.to_camel_case(), Span::call_site()); // Force cargo to refresh the generated code when the query file changes. let query_include = self diff --git a/graphql_client_codegen/src/inputs.rs b/graphql_client_codegen/src/inputs.rs index e0e109d19..1a29906d1 100644 --- a/graphql_client_codegen/src/inputs.rs +++ b/graphql_client_codegen/src/inputs.rs @@ -69,7 +69,6 @@ impl<'schema> GqlInput<'schema> { &self, context: &QueryContext<'_, '_>, ) -> Result { - let name = Ident::new(&self.name, Span::call_site()); let mut fields: Vec<&GqlObjectField<'_>> = self.fields.values().collect(); fields.sort_unstable_by(|a, b| a.name.cmp(&b.name)); let fields = fields.iter().map(|field| { @@ -87,15 +86,18 @@ impl<'schema> GqlInput<'schema> { }; context.schema.require(&field.type_.inner_name_str()); - let original_name = &field.name; - let snake_case_name = field.name.to_snake_case(); - let rename = crate::shared::field_rename_annotation(&original_name, &snake_case_name); - let name = Ident::new(&snake_case_name, Span::call_site()); + let rust_safe_field_name = crate::shared::keyword_replace(&field.name.to_snake_case()); + let rename = crate::shared::field_rename_annotation(&field.name, &rust_safe_field_name); + let name = Ident::new(&rust_safe_field_name, Span::call_site()); quote!(#rename pub #name: #ty) }); let variables_derives = context.variables_derives(); + // Prevent generated code like "pub struct crate" for a schema input like "input crate { ... }" + // This works in tandem with renamed struct Variables field types, eg: pub struct Variables { pub criteria : crate_ , } + let rust_safe_field_name = crate::shared::keyword_replace(&self.name); + let name = Ident::new(&rust_safe_field_name, Span::call_site()); Ok(quote! { #variables_derives pub struct #name { diff --git a/graphql_client_codegen/src/operations.rs b/graphql_client_codegen/src/operations.rs index 23dc477ec..96ceed92f 100644 --- a/graphql_client_codegen/src/operations.rs +++ b/graphql_client_codegen/src/operations.rs @@ -55,11 +55,12 @@ impl<'query> Operation<'query> { } let fields = variables.iter().map(|variable| { - let name = &variable.name; let ty = variable.ty.to_rust(context, ""); - let snake_case_name = name.to_snake_case(); - let rename = crate::shared::field_rename_annotation(&name, &snake_case_name); - let name = Ident::new(&snake_case_name, Span::call_site()); + let rust_safe_field_name = + crate::shared::keyword_replace(&variable.name.to_snake_case()); + let rename = + crate::shared::field_rename_annotation(&variable.name, &rust_safe_field_name); + let name = Ident::new(&rust_safe_field_name, Span::call_site()); quote!(#rename pub #name: #ty) }); diff --git a/graphql_client_codegen/src/query.rs b/graphql_client_codegen/src/query.rs index 341d41744..9ccfae919 100644 --- a/graphql_client_codegen/src/query.rs +++ b/graphql_client_codegen/src/query.rs @@ -115,7 +115,6 @@ impl<'query, 'schema> QueryContext<'query, 'schema> { pub(crate) fn response_derives(&self) -> TokenStream { let derives: BTreeSet<&Ident> = self.response_derives.iter().collect(); let derives = derives.iter(); - quote! { #[derive( #(#derives),* )] } @@ -130,8 +129,9 @@ impl<'query, 'schema> QueryContext<'query, 'schema> { .response_derives .iter() .filter(|derive| { - !derive.to_string().contains("erialize") - && !derive.to_string().contains("Deserialize") + // Do not apply the "Default" derive to enums. + let derive = derive.to_string(); + derive != "Serialize" && derive != "Deserialize" && derive != "Default" }) .collect(); enum_derives.extend(always_derives.iter()); diff --git a/graphql_client_codegen/src/shared.rs b/graphql_client_codegen/src/shared.rs index 6641c4728..6cfebbe57 100644 --- a/graphql_client_codegen/src/shared.rs +++ b/graphql_client_codegen/src/shared.rs @@ -7,6 +7,87 @@ use heck::{CamelCase, SnakeCase}; use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; +// List of keywords based on https://doc.rust-lang.org/grammar.html#keywords +const RUST_KEYWORDS: &'static [&'static str] = &[ + "abstract", + "alignof", + "as", + "async", + "await", + "become", + "box", + "break", + "const", + "continue", + "crate", + "do", + "else", + "enum", + "extern crate", + "extern", + "false", + "final", + "fn", + "for", + "for", + "if let", + "if", + "if", + "impl", + "impl", + "in", + "let", + "loop", + "macro", + "match", + "mod", + "move", + "mut", + "offsetof", + "override", + "priv", + "proc", + "pub", + "pure", + "ref", + "return", + "self", + "sizeof", + "static", + "struct", + "super", + "trait", + "true", + "type", + "typeof", + "unsafe", + "unsized", + "use", + "use", + "virtual", + "where", + "while", + "yield", +]; + +pub(crate) fn keyword_replace(needle: &str) -> String { + match RUST_KEYWORDS.binary_search(&needle) { + Ok(index) => [RUST_KEYWORDS[index], "_"].concat(), + Err(_) => needle.to_owned(), + } +} + +mod tests { + #[test] + fn keyword_replace() { + use super::keyword_replace; + assert_eq!("fora", keyword_replace("fora")); + assert_eq!("in_", keyword_replace("in")); + assert_eq!("fn_", keyword_replace("fn")); + assert_eq!("struct_", keyword_replace("struct")); + } +} + pub(crate) fn render_object_field( field_name: &str, field_type: &TokenStream, @@ -36,29 +117,9 @@ pub(crate) fn render_object_field( let description = description.map(|s| quote!(#[doc = #s])); - // List of keywords based on https://doc.rust-lang.org/grammar.html#keywords - let reserved = &[ - "abstract", "alignof", "as", "become", "box", "break", "const", "continue", "crate", "do", - "else", "enum", "extern", "false", "final", "fn", "for", "if", "impl", "in", "let", "loop", - "macro", "match", "mod", "move", "mut", "offsetof", "override", "priv", "proc", "pub", - "pure", "ref", "return", "Self", "self", "sizeof", "static", "struct", "super", "trait", - "true", "type", "typeof", "unsafe", "unsized", "use", "virtual", "where", "while", "yield", - ]; - - if reserved.contains(&field_name) { - let name_ident = Ident::new(&format!("{}_", field_name), Span::call_site()); - return quote! { - #description - #deprecation - #[serde(rename = #field_name)] - pub #name_ident: #field_type - }; - } - - let snake_case_name = field_name.to_snake_case(); - let rename = crate::shared::field_rename_annotation(&field_name, &snake_case_name); - let name_ident = Ident::new(&snake_case_name, Span::call_site()); - + let rust_safe_field_name = keyword_replace(&field_name.to_snake_case()); + let name_ident = Ident::new(&rust_safe_field_name, Span::call_site()); + let rename = crate::shared::field_rename_annotation(&field_name, &rust_safe_field_name); quote!(#description #deprecation #rename pub #name_ident: #field_type) } diff --git a/graphql_client_codegen/src/tests/keywords_query.graphql b/graphql_client_codegen/src/tests/keywords_query.graphql new file mode 100644 index 000000000..24f0bc997 --- /dev/null +++ b/graphql_client_codegen/src/tests/keywords_query.graphql @@ -0,0 +1,7 @@ +query searchQuery($criteria: extern!) { + search { + transactions(criteria:$searchID) { + for,status + } + } +} diff --git a/graphql_client_codegen/src/tests/keywords_schema.graphql b/graphql_client_codegen/src/tests/keywords_schema.graphql new file mode 100644 index 000000000..5f199c8d0 --- /dev/null +++ b/graphql_client_codegen/src/tests/keywords_schema.graphql @@ -0,0 +1,52 @@ +schema { + query: Query + mutation: Mutation +} + +"""This directive allows results to be deferred during execution""" +directive @defer on FIELD + +"""The top-level Query type.""" +type Query { + """Keyword type""" + search: Self +} + +"""Keyword type""" +type Self { + """ + A keyword variable name with a keyword-named input type + """ + transactions(struct: extern!): Result +} + +"""Keyword type""" +type Result { + """Keyword field.""" + for: String + """dummy field with enum""" + status: AnEnum +} + + +"""Keyword input""" +input extern { + """A field""" + id: crate +} + +"""Input fields for searching for specific values.""" +input crate { + """Keyword field.""" + enum: String + + """Keyword field.""" + in: [String!] +} + + +"""Enum with keywords""" +enum AnEnum { + where + self +} diff --git a/graphql_client_codegen/src/tests/mod.rs b/graphql_client_codegen/src/tests/mod.rs index 6a5a51cbc..3d7e29b7a 100644 --- a/graphql_client_codegen/src/tests/mod.rs +++ b/graphql_client_codegen/src/tests/mod.rs @@ -1 +1,43 @@ mod github; + +#[test] +fn schema_with_keywords_works() { + use crate::{ + codegen, generated_module, schema::Schema, CodegenMode, GraphQLClientCodegenOptions, + }; + use graphql_parser; + + let query_string = include_str!("keywords_query.graphql"); + let query = graphql_parser::parse_query(query_string).expect("Parse keywords query"); + let schema = graphql_parser::parse_schema(include_str!("keywords_schema.graphql")) + .expect("Parse keywords schema"); + let schema = Schema::from(&schema); + + let options = GraphQLClientCodegenOptions::new(CodegenMode::Cli); + let operations = codegen::all_operations(&query); + for operation in &operations { + let generated_tokens = generated_module::GeneratedModule { + query_string: query_string, + schema: &schema, + query_document: &query, + operation, + options: &options, + } + .to_token_stream() + .expect("Generate keywords module"); + let generated_code = generated_tokens.to_string(); + + // Parse generated code. All keywords should be correctly escaped. + let r: syn::parse::Result = syn::parse2(generated_tokens); + match r { + Ok(_) => { + // Rust keywords should be escaped / renamed now + assert!(generated_code.contains("pub in_")); + assert!(generated_code.contains("extern_")); + } + Err(e) => { + panic!("Error: {}\n Generated content: {}\n", e, &generated_code); + } + }; + } +} From db317b7fc28f244f2194f6406d1180a186199916 Mon Sep 17 00:00:00 2001 From: David Graeff Date: Tue, 27 Aug 2019 00:03:27 +0200 Subject: [PATCH 5/7] Add constructor method "new" for Input types. The constructor method provides a simpler way to initialize a type with many or only optional fields. Assuming an input type like this: input CustomerInput { company: String customFields: [CustomFieldInput!] email: String firstName: String lastName: String phoneNumber: String } The generated code looks like so: pub struct CustomerInput { pub company: Option, ... } impl CustomerInput { pub fn new() -> Self { Self { company: None, custom_fields: None, ... } } } ALSO: More idomatic rust is generated by generating Option> instead of Box>. Signed-off-by: David Graeff fixes --- .../tests/input_object_variables.rs | 12 +-- graphql_client_codegen/src/inputs.rs | 91 ++++++++++++++----- 2 files changed, 76 insertions(+), 27 deletions(-) diff --git a/graphql_client/tests/input_object_variables.rs b/graphql_client/tests/input_object_variables.rs index c31826ac6..15f16516a 100644 --- a/graphql_client/tests/input_object_variables.rs +++ b/graphql_client/tests/input_object_variables.rs @@ -58,14 +58,14 @@ fn recursive_input_objects_can_be_constructed() { RecursiveInput { head: "hello".to_string(), - tail: Box::new(None), + tail: None, }; RecursiveInput { head: "hi".to_string(), - tail: Box::new(Some(RecursiveInput { + tail: Some(Box::new(RecursiveInput { head: "this is crazy".to_string(), - tail: Box::new(None), + tail: None, })), }; } @@ -84,14 +84,14 @@ fn indirectly_recursive_input_objects_can_be_constructed() { IndirectlyRecursiveInput { head: "hello".to_string(), - tail: Box::new(None), + tail: None, }; IndirectlyRecursiveInput { head: "hi".to_string(), - tail: Box::new(Some(IndirectlyRecursiveInputTailPart { + tail: Some(Box::new(IndirectlyRecursiveInputTailPart { name: "this is crazy".to_string(), - recursed_field: Box::new(None), + recursed_field: None, })), }; } diff --git a/graphql_client_codegen/src/inputs.rs b/graphql_client_codegen/src/inputs.rs index 1a29906d1..1ea9209a0 100644 --- a/graphql_client_codegen/src/inputs.rs +++ b/graphql_client_codegen/src/inputs.rs @@ -65,33 +65,74 @@ impl<'schema> GqlInput<'schema> { self.contains_type_without_indirection(context, &self.name) } - pub(crate) fn to_rust( + fn map_field( &self, context: &QueryContext<'_, '_>, - ) -> Result { - let mut fields: Vec<&GqlObjectField<'_>> = self.fields.values().collect(); - fields.sort_unstable_by(|a, b| a.name.cmp(&b.name)); - let fields = fields.iter().map(|field| { - let ty = field.type_.to_rust(&context, ""); + field: &GqlObjectField<'_>, + required_fields: &mut Vec, + struct_field_assignments: &mut Vec, + ) -> TokenStream { + let is_recursive = + if let Some(input) = context.schema.inputs.get(field.type_.inner_name_str()) { + input.is_recursive_without_indirection(context) + } else { + false + }; - // If the type is recursive, we have to box it - let ty = if let Some(input) = context.schema.inputs.get(field.type_.inner_name_str()) { - if input.is_recursive_without_indirection(context) { + // If the type is recursive, we have to box it + let ty = if is_recursive { + match &field.type_ { + // If it's an optional field: Wrap the boxed inner type in an Option + crate::field_type::FieldType::Optional(inner) => { + let ty = inner.to_rust(&context, ""); + quote! { Option> } + } + _ => { + let ty = field.type_.to_rust(&context, ""); quote! { Box<#ty> } - } else { - quote!(#ty) } - } else { - quote!(#ty) - }; + } + } else { + let ty = field.type_.to_rust(&context, ""); + quote!(#ty) + }; - context.schema.require(&field.type_.inner_name_str()); - let rust_safe_field_name = crate::shared::keyword_replace(&field.name.to_snake_case()); - let rename = crate::shared::field_rename_annotation(&field.name, &rust_safe_field_name); - let name = Ident::new(&rust_safe_field_name, Span::call_site()); + context.schema.require(&field.type_.inner_name_str()); + let rust_safe_field_name = crate::shared::keyword_replace(&field.name.to_snake_case()); + let rename = crate::shared::field_rename_annotation(&field.name, &rust_safe_field_name); + let name = Ident::new(&rust_safe_field_name, Span::call_site()); - quote!(#rename pub #name: #ty) - }); + match &field.type_ { + crate::field_type::FieldType::Optional(_) => { + struct_field_assignments.push(quote!(#name: None)); + } + _ => { + required_fields.push(quote!(#name: #ty)); + struct_field_assignments.push(quote!(#name: #name)); + } + }; + quote!(#rename pub #name: #ty) + } + + pub(crate) fn to_rust( + &self, + context: &QueryContext<'_, '_>, + ) -> Result { + let mut obj_fields: Vec<&GqlObjectField<'_>> = self.fields.values().collect(); + obj_fields.sort_unstable_by(|a, b| a.name.cmp(&b.name)); + + let mut fields: Vec = vec![]; + let mut required_fields: Vec = vec![]; + let mut struct_field_assignments: Vec = vec![]; + + for field in obj_fields.iter() { + fields.push(self.map_field( + context, + field, + &mut required_fields, + &mut struct_field_assignments, + )); + } let variables_derives = context.variables_derives(); // Prevent generated code like "pub struct crate" for a schema input like "input crate { ... }" @@ -103,6 +144,13 @@ impl<'schema> GqlInput<'schema> { pub struct #name { #(#fields,)* } + impl #name { + pub fn new(#(#required_fields),*) -> Self { + Self { + #(#struct_field_assignments,)* + } + } + } }) } } @@ -228,7 +276,8 @@ mod tests { "# [ serde ( rename = \"pawsCount\" ) ] ", "pub paws_count : Float , ", "pub requirements : Option < CatRequirements > , ", - "}", + "} ", + "impl Cat { pub fn new ( offsprings : Vec < Cat > , paws_count : Float ) -> Self { Self { offsprings : offsprings , paws_count : paws_count , requirements : None , } } }" ] .into_iter() .collect(); From e554a849491b0e606b7f7d50b402058d2bf4ff01 Mon Sep 17 00:00:00 2001 From: David Graeff Date: Tue, 27 Aug 2019 10:01:48 +0200 Subject: [PATCH 6/7] Do not generate the Variables struct for queries without variables Signed-off-by: David Graeff --- graphql_client_codegen/src/generated_module.rs | 9 ++++++++- graphql_client_codegen/src/operations.rs | 5 +---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/graphql_client_codegen/src/generated_module.rs b/graphql_client_codegen/src/generated_module.rs index 0a585b81d..1a2583979 100644 --- a/graphql_client_codegen/src/generated_module.rs +++ b/graphql_client_codegen/src/generated_module.rs @@ -52,6 +52,13 @@ impl<'a> GeneratedModule<'a> { CodegenMode::Derive => quote!(), }; + let variables_type = match self.operation.variables.len() { + 0 => quote!(()), + _ => quote!( + #module_name::Variables + ), + }; + Ok(quote!( #[allow(dead_code)] #struct_declaration @@ -68,7 +75,7 @@ impl<'a> GeneratedModule<'a> { } impl graphql_client::GraphQLQuery for #operation_name_ident { - type Variables = #module_name::Variables; + type Variables = #variables_type; type ResponseData = #module_name::ResponseData; fn build_query(variables: Self::Variables) -> ::graphql_client::QueryBody { diff --git a/graphql_client_codegen/src/operations.rs b/graphql_client_codegen/src/operations.rs index 96ceed92f..12beeafb9 100644 --- a/graphql_client_codegen/src/operations.rs +++ b/graphql_client_codegen/src/operations.rs @@ -48,10 +48,7 @@ impl<'query> Operation<'query> { let variables_derives = context.variables_derives(); if variables.is_empty() { - return quote! { - #variables_derives - pub struct Variables; - }; + return quote! {}; } let fields = variables.iter().map(|variable| { From fb06f7dec54e385b32288534d2b70c089e93018a Mon Sep 17 00:00:00 2001 From: David Graeff Date: Tue, 27 Aug 2019 12:00:00 +0200 Subject: [PATCH 7/7] Don't serialize Option::None --- graphql_client_codegen/src/inputs.rs | 6 +++++- graphql_client_codegen/src/operations.rs | 9 ++++++++- graphql_client_codegen/src/shared.rs | 14 +++++++++++++- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/graphql_client_codegen/src/inputs.rs b/graphql_client_codegen/src/inputs.rs index 1ea9209a0..76b5b4025 100644 --- a/graphql_client_codegen/src/inputs.rs +++ b/graphql_client_codegen/src/inputs.rs @@ -99,12 +99,16 @@ impl<'schema> GqlInput<'schema> { context.schema.require(&field.type_.inner_name_str()); let rust_safe_field_name = crate::shared::keyword_replace(&field.name.to_snake_case()); - let rename = crate::shared::field_rename_annotation(&field.name, &rust_safe_field_name); + let mut rename = crate::shared::field_rename_annotation(&field.name, &rust_safe_field_name); let name = Ident::new(&rust_safe_field_name, Span::call_site()); match &field.type_ { crate::field_type::FieldType::Optional(_) => { struct_field_assignments.push(quote!(#name: None)); + rename = quote!( + #[serde(skip_serializing_if = "Option::is_none")] + #rename + ) } _ => { required_fields.push(quote!(#name: #ty)); diff --git a/graphql_client_codegen/src/operations.rs b/graphql_client_codegen/src/operations.rs index 12beeafb9..1684fc46e 100644 --- a/graphql_client_codegen/src/operations.rs +++ b/graphql_client_codegen/src/operations.rs @@ -55,10 +55,17 @@ impl<'query> Operation<'query> { let ty = variable.ty.to_rust(context, ""); let rust_safe_field_name = crate::shared::keyword_replace(&variable.name.to_snake_case()); - let rename = + let mut rename = crate::shared::field_rename_annotation(&variable.name, &rust_safe_field_name); let name = Ident::new(&rust_safe_field_name, Span::call_site()); + if let crate::field_type::FieldType::Optional(_) = &variable.ty { + rename = quote!( + #[serde(skip_serializing_if = "Option::is_none")] + #rename + ) + } + quote!(#rename pub #name: #ty) }); diff --git a/graphql_client_codegen/src/shared.rs b/graphql_client_codegen/src/shared.rs index 6cfebbe57..c4fe5625c 100644 --- a/graphql_client_codegen/src/shared.rs +++ b/graphql_client_codegen/src/shared.rs @@ -91,6 +91,7 @@ mod tests { pub(crate) fn render_object_field( field_name: &str, field_type: &TokenStream, + field_is_optional: bool, description: Option<&str>, status: &DeprecationStatus, strategy: &DeprecationStrategy, @@ -119,7 +120,14 @@ pub(crate) fn render_object_field( let rust_safe_field_name = keyword_replace(&field_name.to_snake_case()); let name_ident = Ident::new(&rust_safe_field_name, Span::call_site()); - let rename = crate::shared::field_rename_annotation(&field_name, &rust_safe_field_name); + let mut rename = crate::shared::field_rename_annotation(&field_name, &rust_safe_field_name); + + if field_is_optional { + rename = quote!( + #[serde(skip_serializing_if = "Option::is_none")] + #rename + ) + } quote!(#description #deprecation #rename pub #name_ident: #field_type) } @@ -192,6 +200,10 @@ pub(crate) fn response_fields_for_selection( Ok(render_object_field( alias, &ty, + match &schema_field.type_ { + crate::field_type::FieldType::Optional(_) => true, + _ => false, + }, schema_field.description.as_ref().cloned(), &schema_field.deprecation, &context.deprecation_strategy,