From 3e169d61a04bae997911b17e0d586fd9cd5acabd Mon Sep 17 00:00:00 2001 From: Delta Pham Date: Thu, 4 Jan 2024 17:06:03 -0500 Subject: [PATCH] WIP Support query_string macro attribute --- graphql_client_codegen/src/lib.rs | 146 +++++++++++++++++++----------- graphql_query_derive/src/lib.rs | 59 +++++++----- 2 files changed, 130 insertions(+), 75 deletions(-) diff --git a/graphql_client_codegen/src/lib.rs b/graphql_client_codegen/src/lib.rs index 542b0341..7716e65a 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; @@ -43,83 +44,122 @@ impl Display for GeneralError { impl std::error::Error for GeneralError {} type BoxError = Box; -type CacheMap = std::sync::Mutex>; +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>)> = + static ref SCHEMA_CACHE: CacheMap = CacheMap::default(); + static ref QUERY_CACHE: CacheMap = CacheMap::default(); } -/// Generates Rust code given a query document, a schema and options. -pub fn generate_module_token_stream( - query_path: std::path::PathBuf, - schema_path: &std::path::Path, - options: GraphQLClientCodegenOptions, -) -> Result { +fn get_set_cached( + cache: &CacheMap, + key: K, + value: V, +) -> 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()) { + let cached: V = { + let mut lock = cache.lock().expect("cache is poisoned"); + match lock.entry(key) { 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() - } + btree_map::Entry::Vacant(v) => v.insert(value).clone(), } }; - // 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() + Ok(cached) +} + +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, +) -> Result<(String, QueryDocument), BoxError> { + get_set_cached(&QUERY_CACHE, query_path.to_path_buf(), { + let query_string = read_file(query_path)?; + let query_document = query_document(&query_string)?; + (query_string, query_document) + }) +} + +fn get_set_schema_from_file(schema_path: &std::path::Path) -> Result { + get_set_cached(&SCHEMA_CACHE, schema_path.to_path_buf(), { + 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)))?; + Schema::from(s) + } + "json" => { + let parsed: graphql_introspection_query::introspection_response::IntrospectionResponse = serde_json::from_str(&schema_string)?; + Schema::from(parsed) } + extension => return Err(GeneralError(format!("Unsupported extension for the GraphQL schema: {} (only .json and .graphql are supported)", extension)).into()) } - }; + }) +} - let query = crate::query::resolve(&schema, &query)?; +/// 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 { + let query = get_set_query_from_file(query_path.as_path())?; + let schema = get_set_schema_from_file(schema_path)?; + + 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) +} + +/// 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 +171,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, } diff --git a/graphql_query_derive/src/lib.rs b/graphql_query_derive/src/lib.rs index f335aa79..260859cd 100644 --- a/graphql_query_derive/src/lib.rs +++ b/graphql_query_derive/src/lib.rs @@ -4,7 +4,8 @@ extern crate proc_macro; mod attributes; use graphql_client_codegen::{ - generate_module_token_stream, CodegenMode, GraphQLClientCodegenOptions, + generate_module_token_stream, generate_module_token_stream_from_string, CodegenMode, + GraphQLClientCodegenOptions, }; use std::{ env, @@ -26,38 +27,48 @@ fn graphql_query_derive_inner( ) -> Result { let input = TokenStream::from(input); let ast = syn::parse2(input)?; - let (query_path, schema_path) = build_query_and_schema_path(&ast)?; - let options = build_graphql_client_derive_options(&ast, query_path.clone())?; - - generate_module_token_stream(query_path, &schema_path, options) - .map(Into::into) - .map_err(|err| { - syn::Error::new_spanned( - ast, - format!("Failed to generate GraphQLQuery impl: {}", err), - ) - }) -} -fn build_query_and_schema_path(input: &syn::DeriveInput) -> Result<(PathBuf, PathBuf), syn::Error> { let cargo_manifest_dir = env::var("CARGO_MANIFEST_DIR").map_err(|_err| { syn::Error::new_spanned( - input, + &ast, "Error checking that the CARGO_MANIFEST_DIR env variable is defined.", ) })?; - let query_path = attributes::extract_attr(input, "query_path")?; - let query_path = format!("{}/{}", cargo_manifest_dir, query_path); - let query_path = Path::new(&query_path).to_path_buf(); - let schema_path = attributes::extract_attr(input, "schema_path")?; + let schema_path = attributes::extract_attr(&ast, "schema_path")?; let schema_path = Path::new(&cargo_manifest_dir).join(schema_path); - Ok((query_path, schema_path)) + + let query_string = attributes::extract_attr(&ast, "query_string").ok(); + let module_token_stream = match query_string { + Some(query_string) => { + let options = build_graphql_client_derive_options(&ast, None)?; + generate_module_token_stream_from_string( + query_string.as_str(), + schema_path.as_path(), + options, + ) + } + None => { + let query_path = attributes::extract_attr(&ast, "query_path")?; + let query_path = format!("{}/{}", cargo_manifest_dir, query_path); + let query_path = Path::new(&query_path).to_path_buf(); + let options = build_graphql_client_derive_options(&ast, Some(query_path.clone()))?; + + generate_module_token_stream(query_path, &schema_path, options) + } + }; + + module_token_stream.map(Into::into).map_err(|err| { + syn::Error::new_spanned( + ast, + format!("Failed to generate GraphQLQuery impl: {}", err), + ) + }) } fn build_graphql_client_derive_options( input: &syn::DeriveInput, - query_path: PathBuf, + query_path: Option, ) -> Result { let variables_derives = attributes::extract_attr(input, "variables_derives").ok(); let response_derives = attributes::extract_attr(input, "response_derives").ok(); @@ -67,7 +78,11 @@ fn build_graphql_client_derive_options( let skip_serializing_none: bool = attributes::extract_skip_serializing_none(input); let mut options = GraphQLClientCodegenOptions::new(CodegenMode::Derive); - options.set_query_file(query_path); + + if let Some(query_path) = query_path { + options.set_query_file(query_path); + } + options.set_fragments_other_variant(fragments_other_variant); options.set_skip_serializing_none(skip_serializing_none);