From c3dab2b0da87f6c1e25dd6f7bc69c29d65ed733f Mon Sep 17 00:00:00 2001 From: Delta Pham Date: Thu, 4 Jan 2024 17:06:03 -0500 Subject: [PATCH 1/2] Support query_string macro attribute Add test for query string attribute Handle non-utf8 paths and files without extensions Co-authored-by: Surma Update crate doc comment Use options: GraphQLClientCodegenOptions --- graphql_client_codegen/src/lib.rs | 132 ++++++++------ graphql_client_codegen/src/tests/mod.rs | 218 ++++++++++-------------- 2 files changed, 170 insertions(+), 180 deletions(-) diff --git a/graphql_client_codegen/src/lib.rs b/graphql_client_codegen/src/lib.rs index 542b0341..5bba0410 100644 --- a/graphql_client_codegen/src/lib.rs +++ b/graphql_client_codegen/src/lib.rs @@ -2,13 +2,12 @@ #![warn(rust_2018_idioms)] #![allow(clippy::option_option)] -//! Crate for internal use by other graphql-client crates, for code generation. -//! -//! It is not meant to be used directly by users of the library. +//! Crate for Rust code generation from a GraphQL query, schema, and options. use lazy_static::*; use proc_macro2::TokenStream; use quote::*; +use schema::Schema; mod codegen; mod codegen_options; @@ -44,67 +43,92 @@ impl std::error::Error for GeneralError {} type BoxError = Box; type CacheMap = std::sync::Mutex>; +type QueryDocument = graphql_parser::query::Document<'static, String>; lazy_static! { - static ref SCHEMA_CACHE: CacheMap = CacheMap::default(); - static ref QUERY_CACHE: CacheMap<(String, graphql_parser::query::Document<'static, String>)> = - CacheMap::default(); + static ref SCHEMA_CACHE: CacheMap = CacheMap::default(); + static ref QUERY_CACHE: CacheMap<(String, QueryDocument)> = CacheMap::default(); } -/// Generates Rust code given a query document, a schema and options. +fn get_set_cached( + cache: &CacheMap, + key: &std::path::Path, + value_func: impl FnOnce() -> T, +) -> T { + let mut lock = cache.lock().expect("cache is poisoned"); + lock.entry(key.into()).or_insert_with(value_func).clone() +} + +fn query_document(query_string: &str) -> Result { + let document = graphql_parser::parse_query(query_string) + .map_err(|err| GeneralError(format!("Query parser error: {}", err)))? + .into_static(); + Ok(document) +} + +fn get_set_query_from_file(query_path: &std::path::Path) -> (String, QueryDocument) { + get_set_cached(&QUERY_CACHE, query_path, || { + let query_string = read_file(query_path).unwrap(); + let query_document = query_document(&query_string).unwrap(); + (query_string, query_document) + }) +} + +fn get_set_schema_from_file(schema_path: &std::path::Path) -> Schema { + get_set_cached(&SCHEMA_CACHE, schema_path, || { + let schema_extension = schema_path + .extension() + .map(|ext| ext.to_str().expect("Path must be valid UTF-8")) + .unwrap_or(""); + let schema_string = read_file(schema_path).unwrap(); + match schema_extension { + "graphql" | "gql" => { + let s = graphql_parser::schema::parse_schema::<&str>(&schema_string).map_err(|parser_error| GeneralError(format!("Parser error: {}", parser_error))).unwrap(); + Schema::from(s) + } + "json" => { + let parsed: graphql_introspection_query::introspection_response::IntrospectionResponse = serde_json::from_str(&schema_string).unwrap(); + Schema::from(parsed) + } + extension => panic!("Unsupported extension for the GraphQL schema: {} (only .json and .graphql are supported)", extension) + } + }) +} + +/// Generates Rust code given a path to a query file, a path to a schema file, and options. pub fn generate_module_token_stream( query_path: std::path::PathBuf, schema_path: &std::path::Path, options: GraphQLClientCodegenOptions, ) -> Result { - use std::collections::btree_map; - - let schema_extension = schema_path - .extension() - .and_then(std::ffi::OsStr::to_str) - .unwrap_or("INVALID"); - let schema_string; - - // Check the schema cache. - let schema: schema::Schema = { - let mut lock = SCHEMA_CACHE.lock().expect("schema cache is poisoned"); - match lock.entry(schema_path.to_path_buf()) { - btree_map::Entry::Occupied(o) => o.get().clone(), - btree_map::Entry::Vacant(v) => { - schema_string = read_file(v.key())?; - let schema = match schema_extension { - "graphql" | "gql" => { - let s = graphql_parser::schema::parse_schema::<&str>(&schema_string).map_err(|parser_error| GeneralError(format!("Parser error: {}", parser_error)))?; - schema::Schema::from(s) - } - "json" => { - let parsed: graphql_introspection_query::introspection_response::IntrospectionResponse = serde_json::from_str(&schema_string)?; - schema::Schema::from(parsed) - } - extension => return Err(GeneralError(format!("Unsupported extension for the GraphQL schema: {} (only .json and .graphql are supported)", extension)).into()) - }; - - v.insert(schema).clone() - } - } - }; + let query = get_set_query_from_file(query_path.as_path()); + let schema = get_set_schema_from_file(schema_path); - // We need to qualify the query with the path to the crate it is part of - let (query_string, query) = { - let mut lock = QUERY_CACHE.lock().expect("query cache is poisoned"); - match lock.entry(query_path) { - btree_map::Entry::Occupied(o) => o.get().clone(), - btree_map::Entry::Vacant(v) => { - let query_string = read_file(v.key())?; - let query = graphql_parser::parse_query(&query_string) - .map_err(|err| GeneralError(format!("Query parser error: {}", err)))? - .into_static(); - v.insert((query_string, query)).clone() - } - } - }; + generate_module_token_stream_inner(&query, &schema, options) +} + +/// Generates Rust code given a query string, a path to a schema file, and options. +pub fn generate_module_token_stream_from_string( + query_string: &str, + schema_path: &std::path::Path, + options: GraphQLClientCodegenOptions, +) -> Result { + let query = (query_string.to_string(), query_document(query_string)?); + let schema = get_set_schema_from_file(schema_path); + + generate_module_token_stream_inner(&query, &schema, options) +} - let query = crate::query::resolve(&schema, &query)?; +/// Generates Rust code given a query string and query document, a schema, and options. +fn generate_module_token_stream_inner( + query: &(String, QueryDocument), + schema: &Schema, + options: GraphQLClientCodegenOptions, +) -> Result { + let (query_string, query_document) = query; + + // We need to qualify the query with the path to the crate it is part of + let query = crate::query::resolve(schema, query_document)?; // Determine which operation we are generating code for. This will be used in operationName. let operations = options @@ -131,7 +155,7 @@ pub fn generate_module_token_stream( for operation in &operations { let generated = generated_module::GeneratedModule { query_string: query_string.as_str(), - schema: &schema, + schema, resolved_query: &query, operation: &operation.1.name, options: &options, diff --git a/graphql_client_codegen/src/tests/mod.rs b/graphql_client_codegen/src/tests/mod.rs index b80ba5d2..263001c6 100644 --- a/graphql_client_codegen/src/tests/mod.rs +++ b/graphql_client_codegen/src/tests/mod.rs @@ -1,161 +1,127 @@ -use crate::{generated_module, schema::Schema, CodegenMode, GraphQLClientCodegenOptions}; +use std::path::PathBuf; + +use crate::{generate_module_token_stream_from_string, CodegenMode, GraphQLClientCodegenOptions}; + +const KEYWORDS_QUERY: &str = include_str!("keywords_query.graphql"); +const KEYWORDS_SCHEMA_PATH: &str = "keywords_schema.graphql"; + +const FOOBARS_QUERY: &str = include_str!("foobars_query.graphql"); +const FOOBARS_SCHEMA_PATH: &str = "foobars_schema.graphql"; + +fn build_schema_path(path: &str) -> PathBuf { + std::env::current_dir() + .unwrap() + .join("src/tests") + .join(path) +} #[test] fn schema_with_keywords_works() { - let query_string = include_str!("keywords_query.graphql"); - let query = graphql_parser::parse_query::<&str>(query_string).expect("Parse keywords query"); - let schema = graphql_parser::parse_schema(include_str!("keywords_schema.graphql")) - .expect("Parse keywords schema") - .into_static(); - let schema = Schema::from(schema); + let query_string = KEYWORDS_QUERY; + let schema_path = build_schema_path(KEYWORDS_SCHEMA_PATH); let options = GraphQLClientCodegenOptions::new(CodegenMode::Cli); - let query = crate::query::resolve(&schema, &query).unwrap(); - - for (_id, operation) in query.operations() { - let generated_tokens = generated_module::GeneratedModule { - query_string, - schema: &schema, - operation: &operation.name, - resolved_query: &query, - options: &options, + + let generated_tokens = + generate_module_token_stream_from_string(query_string, &schema_path, options) + .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); } - .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); - } - }; - } + }; } #[test] fn fragments_other_variant_should_generate_unknown_other_variant() { - let query_string = include_str!("foobars_query.graphql"); - let query = graphql_parser::parse_query::<&str>(query_string).expect("Parse foobars query"); - let schema = graphql_parser::parse_schema(include_str!("foobars_schema.graphql")) - .expect("Parse foobars schema") - .into_static(); - let schema = Schema::from(schema); + let query_string = FOOBARS_QUERY; + let schema_path = build_schema_path(FOOBARS_SCHEMA_PATH); let mut options = GraphQLClientCodegenOptions::new(CodegenMode::Cli); options.set_fragments_other_variant(true); - let query = crate::query::resolve(&schema, &query).unwrap(); - - for (_id, operation) in query.operations() { - let generated_tokens = generated_module::GeneratedModule { - query_string, - schema: &schema, - operation: &operation.name, - resolved_query: &query, - options: &options, + + let generated_tokens = + generate_module_token_stream_from_string(query_string, &schema_path, options) + .expect("Generate foobars module"); + + let generated_code = generated_tokens.to_string(); + + let r: syn::parse::Result = syn::parse2(generated_tokens); + match r { + Ok(_) => { + // Rust keywords should be escaped / renamed now + assert!(generated_code.contains("# [serde (other)] Unknown")); + assert!(generated_code.contains("Unknown")); + } + Err(e) => { + panic!("Error: {}\n Generated content: {}\n", e, &generated_code); } - .to_token_stream() - .expect("Generate foobars module"); - let generated_code = generated_tokens.to_string(); - - let r: syn::parse::Result = syn::parse2(generated_tokens); - match r { - Ok(_) => { - // Rust keywords should be escaped / renamed now - assert!(generated_code.contains("# [serde (other)] Unknown")); - assert!(generated_code.contains("Unknown")); - } - Err(e) => { - panic!("Error: {}\n Generated content: {}\n", e, &generated_code); - } - }; - } + }; } #[test] fn fragments_other_variant_false_should_not_generate_unknown_other_variant() { - let query_string = include_str!("foobars_query.graphql"); - let query = graphql_parser::parse_query::<&str>(query_string).expect("Parse foobars query"); - let schema = graphql_parser::parse_schema(include_str!("foobars_schema.graphql")) - .expect("Parse foobars schema") - .into_static(); - let schema = Schema::from(schema); + let query_string = FOOBARS_QUERY; + let schema_path = build_schema_path(FOOBARS_SCHEMA_PATH); - let options = GraphQLClientCodegenOptions::new(CodegenMode::Cli); + let mut options = GraphQLClientCodegenOptions::new(CodegenMode::Cli); + + options.set_fragments_other_variant(false); - let query = crate::query::resolve(&schema, &query).unwrap(); + let generated_tokens = + generate_module_token_stream_from_string(query_string, &schema_path, options) + .expect("Generate foobars module token stream"); - for (_id, operation) in query.operations() { - let generated_tokens = generated_module::GeneratedModule { - query_string, - schema: &schema, - operation: &operation.name, - resolved_query: &query, - options: &options, + let generated_code = generated_tokens.to_string(); + + let r: syn::parse::Result = syn::parse2(generated_tokens); + match r { + Ok(_) => { + // Rust keywords should be escaped / renamed now + assert!(!generated_code.contains("# [serde (other)] Unknown")); + assert!(!generated_code.contains("Unknown")); + } + Err(e) => { + panic!("Error: {}\n Generated content: {}\n", e, &generated_code); } - .to_token_stream() - .expect("Generate foobars module"); - let generated_code = generated_tokens.to_string(); - - let r: syn::parse::Result = syn::parse2(generated_tokens); - match r { - Ok(_) => { - // Rust keywords should be escaped / renamed now - assert!(!generated_code.contains("# [serde (other)] Unknown")); - assert!(!generated_code.contains("Unknown")); - } - Err(e) => { - panic!("Error: {}\n Generated content: {}\n", e, &generated_code); - } - }; - } + }; } #[test] fn skip_serializing_none_should_generate_serde_skip_serializing() { - let query_string = include_str!("keywords_query.graphql"); - let query = graphql_parser::parse_query::<&str>(query_string).expect("Parse keywords query"); - let schema = graphql_parser::parse_schema(include_str!("keywords_schema.graphql")) - .expect("Parse keywords schema") - .into_static(); - let schema = Schema::from(schema); + let query_string = KEYWORDS_QUERY; + let schema_path = build_schema_path(KEYWORDS_SCHEMA_PATH); let mut options = GraphQLClientCodegenOptions::new(CodegenMode::Cli); options.set_skip_serializing_none(true); - let query = crate::query::resolve(&schema, &query).unwrap(); + let generated_tokens = + generate_module_token_stream_from_string(query_string, &schema_path, options) + .expect("Generate foobars module"); + + let generated_code = generated_tokens.to_string(); - for (_id, operation) in query.operations() { - let generated_tokens = generated_module::GeneratedModule { - query_string, - schema: &schema, - operation: &operation.name, - resolved_query: &query, - options: &options, + let r: syn::parse::Result = syn::parse2(generated_tokens); + + match r { + Ok(_) => { + println!("{}", generated_code); + assert!(generated_code.contains("skip_serializing_if")); + } + Err(e) => { + panic!("Error: {}\n Generated content: {}\n", e, &generated_code); } - .to_token_stream() - .expect("Generate keywords module"); - - let generated_code = generated_tokens.to_string(); - - let r: syn::parse::Result = syn::parse2(generated_tokens); - - match r { - Ok(_) => { - println!("{}", generated_code); - assert!(generated_code.contains("skip_serializing_if")); - } - Err(e) => { - panic!("Error: {}\n Generated content: {}\n", e, &generated_code); - } - }; - } + }; } From fb7ee0f7033386c238a34c4782d1a3362b26d3bf Mon Sep 17 00:00:00 2001 From: Delta Pham Date: Wed, 6 Mar 2024 13:24:37 -0500 Subject: [PATCH 2/2] Prettier fixes --- examples/github/examples/schema.graphql | 6 +++--- .../src/schema/tests/github_schema.graphql | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/github/examples/schema.graphql b/examples/github/examples/schema.graphql index c13a47a4..92ee5b0a 100644 --- a/examples/github/examples/schema.graphql +++ b/examples/github/examples/schema.graphql @@ -2410,7 +2410,7 @@ type IssueTimelineConnection { "An item in an issue timeline" union IssueTimelineItem = - AssignedEvent + | AssignedEvent | ClosedEvent | Commit | CrossReferencedEvent @@ -5009,7 +5009,7 @@ type PullRequestTimelineConnection { "An item in an pull request timeline" union PullRequestTimelineItem = - AssignedEvent + | AssignedEvent | BaseRefForcePushedEvent | ClosedEvent | Commit @@ -6940,7 +6940,7 @@ type ReviewRequestedEvent implements Node { "The results of a search." union SearchResultItem = - Issue + | Issue | MarketplaceListing | Organization | PullRequest diff --git a/graphql_client_codegen/src/schema/tests/github_schema.graphql b/graphql_client_codegen/src/schema/tests/github_schema.graphql index 98a6b0dd..a7372456 100644 --- a/graphql_client_codegen/src/schema/tests/github_schema.graphql +++ b/graphql_client_codegen/src/schema/tests/github_schema.graphql @@ -2409,7 +2409,7 @@ type IssueTimelineConnection { "An item in an issue timeline" union IssueTimelineItem = - AssignedEvent + | AssignedEvent | ClosedEvent | Commit | CrossReferencedEvent @@ -5008,7 +5008,7 @@ type PullRequestTimelineConnection { "An item in an pull request timeline" union PullRequestTimelineItem = - AssignedEvent + | AssignedEvent | BaseRefForcePushedEvent | ClosedEvent | Commit @@ -6939,7 +6939,7 @@ type ReviewRequestedEvent implements Node { "The results of a search." union SearchResultItem = - Issue + | Issue | MarketplaceListing | Organization | PullRequest