diff --git a/graphql_client/tests/introspection.rs b/graphql_client/tests/introspection.rs index acb5e9dd..c701be64 100644 --- a/graphql_client/tests/introspection.rs +++ b/graphql_client/tests/introspection.rs @@ -18,5 +18,5 @@ fn leading_underscores_are_preserved() { let deserialized: graphql_client::Response = serde_json::from_str(INTROSPECTION_RESPONSE).unwrap(); assert!(deserialized.data.is_some()); - assert!(deserialized.data.unwrap().schema.is_some()); + assert!(deserialized.data.unwrap().__schema.is_some()); } diff --git a/graphql_client_cli/src/generate.rs b/graphql_client_cli/src/generate.rs index 1a36d0cf..f6fe05fe 100644 --- a/graphql_client_cli/src/generate.rs +++ b/graphql_client_cli/src/generate.rs @@ -93,11 +93,13 @@ pub(crate) fn generate_code(params: CliCodegenParams) -> CliResult<()> { options.set_custom_scalars_module(custom_scalars_module); } - + if let Some(custom_variable_types) = custom_variable_types { - options.set_custom_variable_types(custom_variable_types.split(",").map(String::from).collect()); + options.set_custom_variable_types( + custom_variable_types.split(",").map(String::from).collect(), + ); } - + if let Some(custom_response_type) = custom_response_type { options.set_custom_response_type(custom_response_type); } diff --git a/graphql_client_cli/src/main.rs b/graphql_client_cli/src/main.rs index 6721953b..a7476477 100644 --- a/graphql_client_cli/src/main.rs +++ b/graphql_client_cli/src/main.rs @@ -139,8 +139,8 @@ fn main() -> CliResult<()> { selected_operation, custom_scalars_module, fragments_other_variant, - external_enums, - custom_variable_types, + external_enums, + custom_variable_types, custom_response_type, } => generate::generate_code(generate::CliCodegenParams { query_path, diff --git a/graphql_client_codegen/src/codegen.rs b/graphql_client_codegen/src/codegen.rs index e33bb1b1..ce55394b 100644 --- a/graphql_client_codegen/src/codegen.rs +++ b/graphql_client_codegen/src/codegen.rs @@ -1,7 +1,7 @@ mod enums; mod inputs; mod selection; -mod shared; +pub(crate) mod shared; use crate::{ query::*, @@ -9,10 +9,10 @@ use crate::{ type_qualifiers::GraphqlTypeQualifier, GeneralError, GraphQLClientCodegenOptions, }; -use heck::ToSnakeCase; use proc_macro2::{Ident, Span, TokenStream}; use quote::{quote, ToTokens}; use selection::*; +use shared::to_snake_case_preserve_leading_underscores; use std::collections::BTreeMap; /// The main code generation function. @@ -139,7 +139,7 @@ fn generate_variable_struct_field( options: &GraphQLClientCodegenOptions, query: &BoundQuery<'_>, ) -> TokenStream { - let snake_case_name = variable.name.to_snake_case(); + let snake_case_name = to_snake_case_preserve_leading_underscores(&variable.name); let safe_name = shared::keyword_replace(&snake_case_name); let ident = Ident::new(&safe_name, Span::call_site()); let rename_annotation = shared::field_rename_annotation(&variable.name, &safe_name); diff --git a/graphql_client_codegen/src/codegen/inputs.rs b/graphql_client_codegen/src/codegen/inputs.rs index d8cc1080..ce4f1d43 100644 --- a/graphql_client_codegen/src/codegen/inputs.rs +++ b/graphql_client_codegen/src/codegen/inputs.rs @@ -1,11 +1,13 @@ -use super::shared::{field_rename_annotation, keyword_replace}; +use super::shared::{ + field_rename_annotation, keyword_replace, to_snake_case_preserve_leading_underscores, +}; use crate::{ codegen_options::GraphQLClientCodegenOptions, query::{BoundQuery, UsedTypes}, schema::{input_is_recursive_without_indirection, StoredInputType}, type_qualifiers::GraphqlTypeQualifier, }; -use heck::{ToSnakeCase, ToUpperCamelCase}; +use heck::ToUpperCamelCase; use proc_macro2::{Ident, Span, TokenStream}; use quote::{quote, ToTokens}; @@ -19,9 +21,12 @@ pub(super) fn generate_input_object_definitions( all_used_types .inputs(query.schema) .map(|(input_id, input)| { - let custom_variable_type = query.query.variables.iter() + let custom_variable_type = query + .query + .variables + .iter() .enumerate() - .find(|(_, v) | v.r#type.id.as_input_id().is_some_and(|i| i == input_id)) + .find(|(_, v)| v.r#type.id.as_input_id().is_some_and(|i| i == input_id)) .map(|(index, _)| custom_variable_types.get(index)) .flatten(); if let Some(custom_type) = custom_variable_type { @@ -61,7 +66,8 @@ fn generate_struct( let struct_name = Ident::new(safe_name.as_ref(), Span::call_site()); let fields = input.fields.iter().map(|(field_name, field_type)| { - let safe_field_name = keyword_replace(field_name.to_snake_case()); + let safe_field_name = + keyword_replace(to_snake_case_preserve_leading_underscores(field_name)); let annotation = field_rename_annotation(field_name, safe_field_name.as_ref()); let name_ident = Ident::new(safe_field_name.as_ref(), Span::call_site()); let normalized_field_type_name = options diff --git a/graphql_client_codegen/src/codegen/selection.rs b/graphql_client_codegen/src/codegen/selection.rs index ec1703b8..4c473021 100644 --- a/graphql_client_codegen/src/codegen/selection.rs +++ b/graphql_client_codegen/src/codegen/selection.rs @@ -3,7 +3,9 @@ use crate::{ codegen::{ decorate_type, - shared::{field_rename_annotation, keyword_replace}, + shared::{ + field_rename_annotation, keyword_replace, to_snake_case_preserve_leading_underscores, + }, }, deprecation::DeprecationStrategy, query::{ @@ -12,10 +14,8 @@ use crate::{ }, schema::{Schema, TypeId}, type_qualifiers::GraphqlTypeQualifier, - GraphQLClientCodegenOptions, - GeneralError, + GeneralError, GraphQLClientCodegenOptions, }; -use heck::*; use proc_macro2::{Ident, Span, TokenStream}; use quote::{quote, ToTokens}; use std::borrow::Cow; @@ -43,12 +43,27 @@ pub(crate) fn render_response_data_fields<'a>( if let Some(custom_response_type) = options.custom_response_type() { if operation.selection_set.len() == 1 { let selection_id = operation.selection_set[0]; - let selection_field = query.query.get_selection(selection_id).as_selected_field() - .ok_or_else(|| GeneralError(format!("Custom response type {custom_response_type} will only work on fields")))?; - calculate_custom_response_type_selection(&mut expanded_selection, response_data_type_id, custom_response_type, selection_id, selection_field); + let selection_field = query + .query + .get_selection(selection_id) + .as_selected_field() + .ok_or_else(|| { + GeneralError(format!( + "Custom response type {custom_response_type} will only work on fields" + )) + })?; + calculate_custom_response_type_selection( + &mut expanded_selection, + response_data_type_id, + custom_response_type, + selection_id, + selection_field, + ); return Ok(expanded_selection); } else { - return Err(GeneralError(format!("Custom response type {custom_response_type} requires single selection field"))); + return Err(GeneralError(format!( + "Custom response type {custom_response_type} requires single selection field" + ))); } } @@ -68,8 +83,8 @@ fn calculate_custom_response_type_selection<'a>( struct_id: ResponseTypeId, custom_response_type: &'a String, selection_id: SelectionId, - field: &'a SelectedField) -{ + field: &'a SelectedField, +) { let (graphql_name, rust_name) = context.field_name(field); let struct_name_string = full_path_prefix(selection_id, context.query); let field = context.query.schema.get_field(field.field_id); @@ -281,7 +296,10 @@ fn calculate_selection<'a>( field_type_qualifiers: &[GraphqlTypeQualifier::Required], flatten: true, graphql_name: None, - rust_name: fragment.name.to_snake_case().into(), + rust_name: to_snake_case_preserve_leading_underscores( + &fragment.name, + ) + .into(), struct_id, deprecation: None, boxed: fragment_is_recursive(*fragment_id, context.query.query), @@ -395,7 +413,8 @@ fn calculate_selection<'a>( continue; } - let original_field_name = fragment.name.to_snake_case(); + let original_field_name = + to_snake_case_preserve_leading_underscores(&fragment.name); let final_field_name = keyword_replace(original_field_name); context.push_field(ExpandedField { @@ -578,7 +597,7 @@ impl<'a> ExpandedSelection<'a> { let name = field .alias() .unwrap_or_else(|| &field.schema_field(self.query.schema).name); - let snake_case_name = name.to_snake_case(); + let snake_case_name = to_snake_case_preserve_leading_underscores(name); let final_name = keyword_replace(snake_case_name); (name, final_name) diff --git a/graphql_client_codegen/src/codegen/shared.rs b/graphql_client_codegen/src/codegen/shared.rs index 9bffe3bf..5ea085bd 100644 --- a/graphql_client_codegen/src/codegen/shared.rs +++ b/graphql_client_codegen/src/codegen/shared.rs @@ -1,7 +1,21 @@ +use heck::ToSnakeCase; use proc_macro2::TokenStream; use quote::quote; use std::borrow::Cow; +/// Convert to snake_case while preserving leading underscores. +/// This is important for GraphQL fields like `_id` which should become `_id` not `id`. +pub(crate) fn to_snake_case_preserve_leading_underscores(s: &str) -> String { + let leading_underscores = s.chars().take_while(|&c| c == '_').count(); + if leading_underscores == 0 { + s.to_snake_case() + } else { + let prefix = "_".repeat(leading_underscores); + let rest = &s[leading_underscores..]; + format!("{}{}", prefix, rest.to_snake_case()) + } +} + // List of keywords based on https://doc.rust-lang.org/reference/keywords.html // code snippet: `[...new Set($$("code.hljs").map(x => x.textContent).filter(x => x.match(/^[_a-z0-9]+$/i)))].sort()` const RUST_KEYWORDS: &[&str] = &[ diff --git a/graphql_client_codegen/src/generated_module.rs b/graphql_client_codegen/src/generated_module.rs index b225d001..9bc03883 100644 --- a/graphql_client_codegen/src/generated_module.rs +++ b/graphql_client_codegen/src/generated_module.rs @@ -1,9 +1,9 @@ use crate::{ + codegen::shared::to_snake_case_preserve_leading_underscores, codegen_options::*, query::{BoundQuery, OperationId}, BoxError, }; -use heck::*; use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; use std::{error::Error, fmt::Display}; @@ -57,7 +57,10 @@ impl GeneratedModule<'_> { /// Generate the module and all the code inside. pub(crate) fn to_token_stream(&self) -> Result { - let module_name = Ident::new(&self.operation.to_snake_case(), Span::call_site()); + let module_name = Ident::new( + &to_snake_case_preserve_leading_underscores(self.operation), + Span::call_site(), + ); let module_visibility = &self.options.module_visibility(); let operation_name = self.operation; let operation_name_ident = self.options.normalization().operation(self.operation); diff --git a/graphql_client_codegen/src/tests/mod.rs b/graphql_client_codegen/src/tests/mod.rs index aaed3e5d..5bd158f9 100644 --- a/graphql_client_codegen/src/tests/mod.rs +++ b/graphql_client_codegen/src/tests/mod.rs @@ -62,7 +62,8 @@ fn blended_custom_types_works() { match r { Ok(_) => { // Variables and returns should be replaced with custom types - assert!(generated_code.contains("pub type SearchQuerySearch = external_crate :: Transaction")); + assert!(generated_code + .contains("pub type SearchQuerySearch = external_crate :: Transaction")); assert!(generated_code.contains("pub type extern_ = external_crate :: ID")); } Err(e) => { diff --git a/graphql_query_derive/src/lib.rs b/graphql_query_derive/src/lib.rs index c6a7eca3..e1314f16 100644 --- a/graphql_query_derive/src/lib.rs +++ b/graphql_query_derive/src/lib.rs @@ -106,7 +106,7 @@ fn build_graphql_client_derive_options( if let Some(custom_variable_types) = custom_variable_types { options.set_custom_variable_types(custom_variable_types); } - + if let Some(custom_response_type) = custom_response_type { options.set_custom_response_type(custom_response_type); }