Skip to content

Commit c2309c2

Browse files
markrileybotMark Riley
and
Mark Riley
authored
Add support for custom variable and response types (#536)
* Custom variable and response types. This allows me to use my existing code in some or all of the request/response. * use typedefs instead so that we don't generate unused code * Use the custom response type as the first field of ResponseData --------- Co-authored-by: Mark Riley <[email protected]>
1 parent 7c67a82 commit c2309c2

File tree

10 files changed

+207
-13
lines changed

10 files changed

+207
-13
lines changed

graphql_client_cli/src/generate.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ pub(crate) struct CliCodegenParams {
2323
pub custom_scalars_module: Option<String>,
2424
pub fragments_other_variant: bool,
2525
pub external_enums: Option<Vec<String>>,
26+
pub custom_variable_types: Option<String>,
27+
pub custom_response_type: Option<String>,
2628
}
2729

2830
const WARNING_SUPPRESSION: &str = "#![allow(clippy::all, warnings)]";
@@ -41,6 +43,8 @@ pub(crate) fn generate_code(params: CliCodegenParams) -> CliResult<()> {
4143
custom_scalars_module,
4244
fragments_other_variant,
4345
external_enums,
46+
custom_variable_types,
47+
custom_response_type,
4448
} = params;
4549

4650
let deprecation_strategy = deprecation_strategy.as_ref().and_then(|s| s.parse().ok());
@@ -89,6 +93,14 @@ pub(crate) fn generate_code(params: CliCodegenParams) -> CliResult<()> {
8993

9094
options.set_custom_scalars_module(custom_scalars_module);
9195
}
96+
97+
if let Some(custom_variable_types) = custom_variable_types {
98+
options.set_custom_variable_types(custom_variable_types.split(",").map(String::from).collect());
99+
}
100+
101+
if let Some(custom_response_type) = custom_response_type {
102+
options.set_custom_response_type(custom_response_type);
103+
}
92104

93105
let gen = generate_module_token_stream(query_path.clone(), &schema_path, options)
94106
.map_err(|err| Error::message(format!("Error generating module code: {}", err)))?;

graphql_client_cli/src/main.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,14 @@ enum Cli {
9494
/// List of externally defined enum types. Type names must match those used in the schema exactly
9595
#[clap(long = "external-enums", num_args(0..), action(clap::ArgAction::Append))]
9696
external_enums: Option<Vec<String>>,
97+
/// Custom variable types to use
98+
/// --custom-variable-types='external_crate::MyStruct,external_crate::MyStruct2'
99+
#[clap(long = "custom-variable_types")]
100+
custom_variable_types: Option<String>,
101+
/// Custom response type to use
102+
/// --custom-response-type='external_crate::MyResponse'
103+
#[clap(long = "custom-response-type")]
104+
custom_response_type: Option<String>,
97105
},
98106
}
99107

@@ -131,7 +139,9 @@ fn main() -> CliResult<()> {
131139
selected_operation,
132140
custom_scalars_module,
133141
fragments_other_variant,
134-
external_enums,
142+
external_enums,
143+
custom_variable_types,
144+
custom_response_type,
135145
} => generate::generate_code(generate::CliCodegenParams {
136146
query_path,
137147
schema_path,
@@ -145,6 +155,8 @@ fn main() -> CliResult<()> {
145155
custom_scalars_module,
146156
fragments_other_variant,
147157
external_enums,
158+
custom_variable_types,
159+
custom_response_type,
148160
}),
149161
}
150162
}

graphql_client_codegen/src/codegen.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ pub(crate) fn response_for_query(
4242
generate_variables_struct(operation_id, &variable_derives, options, &query);
4343

4444
let definitions =
45-
render_response_data_fields(operation_id, options, &query).render(&response_derives);
45+
render_response_data_fields(operation_id, options, &query)?.render(&response_derives);
4646

4747
let q = quote! {
4848
use #serde::{Serialize, Deserialize};

graphql_client_codegen/src/codegen/inputs.rs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,18 @@ pub(super) fn generate_input_object_definitions(
1515
variable_derives: &impl quote::ToTokens,
1616
query: &BoundQuery<'_>,
1717
) -> Vec<TokenStream> {
18+
let custom_variable_types = options.custom_variable_types();
1819
all_used_types
1920
.inputs(query.schema)
20-
.map(|(_input_id, input)| {
21-
if input.is_one_of {
21+
.map(|(input_id, input)| {
22+
let custom_variable_type = query.query.variables.iter()
23+
.enumerate()
24+
.find(|(_, v) | v.r#type.id.as_input_id().is_some_and(|i| i == input_id))
25+
.map(|(index, _)| custom_variable_types.get(index))
26+
.flatten();
27+
if let Some(custom_type) = custom_variable_type {
28+
generate_type_def(input, options, custom_type)
29+
} else if input.is_one_of {
2230
generate_enum(input, options, variable_derives, query)
2331
} else {
2432
generate_struct(input, options, variable_derives, query)
@@ -27,6 +35,18 @@ pub(super) fn generate_input_object_definitions(
2735
.collect()
2836
}
2937

38+
fn generate_type_def(
39+
input: &StoredInputType,
40+
options: &GraphQLClientCodegenOptions,
41+
custom_type: &String,
42+
) -> TokenStream {
43+
let custom_type = syn::parse_str::<syn::Path>(custom_type).unwrap();
44+
let normalized_name = options.normalization().input_name(input.name.as_str());
45+
let safe_name = keyword_replace(normalized_name);
46+
let struct_name = Ident::new(safe_name.as_ref(), Span::call_site());
47+
quote!(pub type #struct_name = #custom_type;)
48+
}
49+
3050
fn generate_struct(
3151
input: &StoredInputType,
3252
options: &GraphQLClientCodegenOptions,

graphql_client_codegen/src/codegen/selection.rs

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,19 @@ use crate::{
1313
schema::{Schema, TypeId},
1414
type_qualifiers::GraphqlTypeQualifier,
1515
GraphQLClientCodegenOptions,
16+
GeneralError,
1617
};
1718
use heck::*;
1819
use proc_macro2::{Ident, Span, TokenStream};
1920
use quote::{quote, ToTokens};
2021
use std::borrow::Cow;
22+
use syn::Path;
2123

2224
pub(crate) fn render_response_data_fields<'a>(
2325
operation_id: OperationId,
2426
options: &'a GraphQLClientCodegenOptions,
2527
query: &'a BoundQuery<'a>,
26-
) -> ExpandedSelection<'a> {
28+
) -> Result<ExpandedSelection<'a>, GeneralError> {
2729
let operation = query.query.get_operation(operation_id);
2830
let mut expanded_selection = ExpandedSelection {
2931
query,
@@ -38,6 +40,18 @@ pub(crate) fn render_response_data_fields<'a>(
3840
name: Cow::Borrowed("ResponseData"),
3941
});
4042

43+
if let Some(custom_response_type) = options.custom_response_type() {
44+
if operation.selection_set.len() == 1 {
45+
let selection_id = operation.selection_set[0];
46+
let selection_field = query.query.get_selection(selection_id).as_selected_field()
47+
.ok_or_else(|| GeneralError(format!("Custom response type {custom_response_type} will only work on fields")))?;
48+
calculate_custom_response_type_selection(&mut expanded_selection, response_data_type_id, custom_response_type, selection_id, selection_field);
49+
return Ok(expanded_selection);
50+
} else {
51+
return Err(GeneralError(format!("Custom response type {custom_response_type} requires single selection field")));
52+
}
53+
}
54+
4155
calculate_selection(
4256
&mut expanded_selection,
4357
&operation.selection_set,
@@ -46,7 +60,38 @@ pub(crate) fn render_response_data_fields<'a>(
4660
options,
4761
);
4862

49-
expanded_selection
63+
Ok(expanded_selection)
64+
}
65+
66+
fn calculate_custom_response_type_selection<'a>(
67+
context: &mut ExpandedSelection<'a>,
68+
struct_id: ResponseTypeId,
69+
custom_response_type: &'a String,
70+
selection_id: SelectionId,
71+
field: &'a SelectedField)
72+
{
73+
let (graphql_name, rust_name) = context.field_name(field);
74+
let struct_name_string = full_path_prefix(selection_id, context.query);
75+
let field = context.query.schema.get_field(field.field_id);
76+
context.push_field(ExpandedField {
77+
struct_id,
78+
graphql_name: Some(graphql_name),
79+
rust_name,
80+
field_type_qualifiers: &field.r#type.qualifiers,
81+
field_type: struct_name_string.clone().into(),
82+
flatten: false,
83+
boxed: false,
84+
deprecation: field.deprecation(),
85+
});
86+
87+
let struct_id = context.push_type(ExpandedType {
88+
name: struct_name_string.into(),
89+
});
90+
context.push_type_alias(TypeAlias {
91+
name: custom_response_type.as_str(),
92+
struct_id,
93+
boxed: false,
94+
});
5095
}
5196

5297
pub(super) fn render_fragment<'a>(
@@ -557,14 +602,14 @@ impl<'a> ExpandedSelection<'a> {
557602

558603
// If the type is aliased, stop here.
559604
if let Some(alias) = self.aliases.iter().find(|alias| alias.struct_id == type_id) {
560-
let fragment_name = Ident::new(alias.name, Span::call_site());
561-
let fragment_name = if alias.boxed {
562-
quote!(Box<#fragment_name>)
605+
let type_name = syn::parse_str::<Path>(alias.name).unwrap();
606+
let type_name = if alias.boxed {
607+
quote!(Box<#type_name>)
563608
} else {
564-
quote!(#fragment_name)
609+
quote!(#type_name)
565610
};
566611
let item = quote! {
567-
pub type #struct_name = #fragment_name;
612+
pub type #struct_name = #type_name;
568613
};
569614
items.push(item);
570615
continue;

graphql_client_codegen/src/codegen_options.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ pub struct GraphQLClientCodegenOptions {
4949
skip_serializing_none: bool,
5050
/// Path to the serde crate.
5151
serde_path: syn::Path,
52+
/// list of custom type paths to use for input variables
53+
custom_variable_types: Option<Vec<String>>,
54+
/// Custom response type path
55+
custom_response_type: Option<String>,
5256
}
5357

5458
impl GraphQLClientCodegenOptions {
@@ -71,6 +75,8 @@ impl GraphQLClientCodegenOptions {
7175
fragments_other_variant: Default::default(),
7276
skip_serializing_none: Default::default(),
7377
serde_path: syn::parse_quote!(::serde),
78+
custom_variable_types: Default::default(),
79+
custom_response_type: Default::default(),
7480
}
7581
}
7682

@@ -138,6 +144,26 @@ impl GraphQLClientCodegenOptions {
138144
self.response_derives = Some(response_derives);
139145
}
140146

147+
/// Type use as the response type
148+
pub fn custom_response_type(&self) -> Option<&String> {
149+
self.custom_response_type.as_ref()
150+
}
151+
152+
/// Type use as the response type
153+
pub fn set_custom_response_type(&mut self, response_type: String) {
154+
self.custom_response_type = Some(response_type);
155+
}
156+
157+
/// list of custom type paths to use for input variables
158+
pub fn custom_variable_types(&self) -> Vec<String> {
159+
self.custom_variable_types.clone().unwrap_or_default()
160+
}
161+
162+
/// list of custom type paths to use for input variables
163+
pub fn set_custom_variable_types(&mut self, variables_types: Vec<String>) {
164+
self.custom_variable_types = Some(variables_types);
165+
}
166+
141167
/// The deprecation strategy to adopt.
142168
pub fn set_deprecation_strategy(&mut self, deprecation_strategy: DeprecationStrategy) {
143169
self.deprecation_strategy = Some(deprecation_strategy);

graphql_client_codegen/src/query.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ pub(crate) struct ResolvedFragmentId(u32);
5858

5959
#[allow(dead_code)]
6060
#[derive(Debug, Clone, Copy)]
61-
pub(crate) struct VariableId(u32);
61+
pub(crate) struct VariableId(pub u32);
6262

6363
pub(crate) fn resolve<'doc, T>(
6464
schema: &Schema,
@@ -512,7 +512,7 @@ pub(crate) struct Query {
512512
operations: Vec<ResolvedOperation>,
513513
selection_parent_idx: BTreeMap<SelectionId, SelectionParent>,
514514
selections: Vec<Selection>,
515-
variables: Vec<ResolvedVariable>,
515+
pub(crate) variables: Vec<ResolvedVariable>,
516516
}
517517

518518
impl Query {

graphql_client_codegen/src/tests/mod.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,35 @@ fn schema_with_keywords_works() {
4242
};
4343
}
4444

45+
#[test]
46+
fn blended_custom_types_works() {
47+
let query_string = KEYWORDS_QUERY;
48+
let schema_path = build_schema_path(KEYWORDS_SCHEMA_PATH);
49+
50+
let mut options = GraphQLClientCodegenOptions::new(CodegenMode::Cli);
51+
options.set_custom_response_type("external_crate::Transaction".to_string());
52+
options.set_custom_variable_types(vec!["external_crate::ID".to_string()]);
53+
54+
let generated_tokens =
55+
generate_module_token_stream_from_string(query_string, &schema_path, options)
56+
.expect("Generate keywords module");
57+
58+
let generated_code = generated_tokens.to_string();
59+
60+
// Parse generated code. Variables and returns should be replaced with custom types
61+
let r: syn::parse::Result<proc_macro2::TokenStream> = syn::parse2(generated_tokens);
62+
match r {
63+
Ok(_) => {
64+
// Variables and returns should be replaced with custom types
65+
assert!(generated_code.contains("pub type SearchQuerySearch = external_crate :: Transaction"));
66+
assert!(generated_code.contains("pub type extern_ = external_crate :: ID"));
67+
}
68+
Err(e) => {
69+
panic!("Error: {}\n Generated content: {}\n", e, &generated_code);
70+
}
71+
};
72+
}
73+
4574
#[test]
4675
fn fragments_other_variant_should_generate_unknown_other_variant() {
4776
let query_string = FOOBARS_QUERY;

graphql_query_derive/src/attributes.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,4 +295,44 @@ mod test {
295295
vec!["Direction", "DistanceUnit"],
296296
);
297297
}
298+
299+
#[test]
300+
fn test_custom_variable_types() {
301+
let input = r#"
302+
#[derive(Serialize, Deserialize, Debug)]
303+
#[derive(GraphQLQuery)]
304+
#[graphql(
305+
schema_path = "x",
306+
query_path = "x",
307+
variable_types("extern_crate::Var1", "extern_crate::Var2"),
308+
)]
309+
struct MyQuery;
310+
"#;
311+
let parsed: syn::DeriveInput = syn::parse_str(input).unwrap();
312+
313+
assert_eq!(
314+
extract_attr_list(&parsed, "variable_types").ok().unwrap(),
315+
vec!["extern_crate::Var1", "extern_crate::Var2"],
316+
);
317+
}
318+
319+
#[test]
320+
fn test_custom_response_type() {
321+
let input = r#"
322+
#[derive(Serialize, Deserialize, Debug)]
323+
#[derive(GraphQLQuery)]
324+
#[graphql(
325+
schema_path = "x",
326+
query_path = "x",
327+
response_type = "extern_crate::Resp",
328+
)]
329+
struct MyQuery;
330+
"#;
331+
let parsed: syn::DeriveInput = syn::parse_str(input).unwrap();
332+
333+
assert_eq!(
334+
extract_attr(&parsed, "response_type").ok().unwrap(),
335+
"extern_crate::Resp",
336+
);
337+
}
298338
}

graphql_query_derive/src/lib.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ fn build_graphql_client_derive_options(
6565
let extern_enums = attributes::extract_attr_list(input, "extern_enums").ok();
6666
let fragments_other_variant: bool = attributes::extract_fragments_other_variant(input);
6767
let skip_serializing_none: bool = attributes::extract_skip_serializing_none(input);
68+
let custom_variable_types = attributes::extract_attr_list(input, "variable_types").ok();
69+
let custom_response_type = attributes::extract_attr(input, "response_type").ok();
6870

6971
let mut options = GraphQLClientCodegenOptions::new(CodegenMode::Derive);
7072
options.set_query_file(query_path);
@@ -101,6 +103,14 @@ fn build_graphql_client_derive_options(
101103
options.set_extern_enums(extern_enums);
102104
}
103105

106+
if let Some(custom_variable_types) = custom_variable_types {
107+
options.set_custom_variable_types(custom_variable_types);
108+
}
109+
110+
if let Some(custom_response_type) = custom_response_type {
111+
options.set_custom_response_type(custom_response_type);
112+
}
113+
104114
options.set_struct_ident(input.ident.clone());
105115
options.set_module_visibility(input.vis.clone());
106116
options.set_operation_name(input.ident.to_string());

0 commit comments

Comments
 (0)