From c2747d531b2aa45d635e7491892159be1ebcc7ae Mon Sep 17 00:00:00 2001 From: Delta Pham Date: Thu, 4 Jan 2024 17:06:03 -0500 Subject: [PATCH] Support query_string macro attribute Add test for query string attribute --- graphql_client/tests/query_string.rs | 18 +++ .../tests/query_string/schema.graphql | 7 + graphql_client_codegen/src/lib.rs | 138 +++++++++++------- 3 files changed, 108 insertions(+), 55 deletions(-) create mode 100644 graphql_client/tests/query_string.rs create mode 100644 graphql_client/tests/query_string/schema.graphql diff --git a/graphql_client/tests/query_string.rs b/graphql_client/tests/query_string.rs new file mode 100644 index 00000000..2d4428ae --- /dev/null +++ b/graphql_client/tests/query_string.rs @@ -0,0 +1,18 @@ +use graphql_client::*; + +#[derive(GraphQLQuery)] +#[graphql( + query_string = "query FromQueryString { s }", + schema_path = "tests/query_string/schema.graphql" +)] +struct FromQueryString; + +#[test] +fn response_serialization() { + const RESPONSE: &str = r#"{"s": "something"}"#; + + let response: from_query_string::ResponseData = serde_json::from_str(RESPONSE).unwrap(); + let s = response.s.unwrap(); + + assert_eq!("something", s); +} diff --git a/graphql_client/tests/query_string/schema.graphql b/graphql_client/tests/query_string/schema.graphql new file mode 100644 index 00000000..d765c1b7 --- /dev/null +++ b/graphql_client/tests/query_string/schema.graphql @@ -0,0 +1,7 @@ +schema { + query: QueryRoot +} + +type QueryRoot { + s: String +} diff --git a/graphql_client_codegen/src/lib.rs b/graphql_client_codegen/src/lib.rs index 542b0341..9713ecfb 100644 --- a/graphql_client_codegen/src/lib.rs +++ b/graphql_client_codegen/src/lib.rs @@ -9,6 +9,7 @@ use lazy_static::*; use proc_macro2::TokenStream; use quote::*; +use schema::Schema; mod codegen; mod codegen_options; @@ -44,82 +45,109 @@ 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, move || { + let schema_extension = schema_path + .extension() + .and_then(std::ffi::OsStr::to_str) + .unwrap_or("INVALID"); + let schema_string = read_file(schema_path)?; + 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 generated_query = crate::query::resolve(schema, query_document)?; // Determine which operation we are generating code for. This will be used in operationName. let operations = options .operation_name .as_ref() - .and_then(|operation_name| query.select_operation(operation_name, *options.normalization())) + .and_then(|operation_name| { + generated_query.select_operation(operation_name, *options.normalization()) + }) .map(|op| vec![op]); let operations = match (operations, &options.mode) { (Some(ops), _) => ops, - (None, &CodegenMode::Cli) => query.operations().collect(), + (None, &CodegenMode::Cli) => generated_query.operations().collect(), (None, &CodegenMode::Derive) => { return Err(GeneralError(derive_operation_not_found_error( options.struct_ident(), - &query, + &generated_query, )) .into()); } @@ -131,8 +159,8 @@ pub fn generate_module_token_stream( for operation in &operations { let generated = generated_module::GeneratedModule { query_string: query_string.as_str(), - schema: &schema, - resolved_query: &query, + schema, + resolved_query: &generated_query, operation: &operation.1.name, options: &options, }