Skip to content

Commit d38f9c6

Browse files
authored
feat: field type overrides in generator (#1184)
#### Why are we making this change? I'm using `cynic-querygen` for a work project, and it's been a joy to simply copy queries from Graphiql to our rust codebase, so thank you for that! However, we've ended up needing to make quite a few find-and-replace changes to the generated code due to the crate's limited code generation options. We'd like to start contributing some of the features we've found we needed as new options to `cynic-querygen`. This first one is the one that's the hardest to do via regex, because it should incorporate both the type name and the field name, and also seemed like one of the simplest to implement. The main use case for this feature for us is specifying different deserialization targets for different `Json` fields. For instance, we have one JSON field that should deserialize to `HashMap<String, String>`, and another that should deserialize directly to a complex enum structure. #### What effects does this change have? This adds a new `field_overrides` field to `QueryGenOptions`, which allows overriding the type of individual fields on specific types using a `.` separator. I have no strong opinion on the shape of the option, this just seemed the most natural. This field is passed down into `make_query_fragment`, where each field checks it to potentially set its `type_name_override`. For example: ```rust let result = cynic_querygen::document_to_fragment_structs( &query_content, &schema_content, &cynic_querygen::QueryGenOptions { schema_name: Some("my_schema".to_string()), field_overrides: maplit::hashmap!( "MyType.complexEnum" => "crate::complex_enum::ComplexEnum", "PresignedUrl.fields" => "std::collections::HashMap<String, String>", ), ..Default::default() }, ); ``` Test and documentation are included for the new field, and I also added a doc comment for `schema_name`, since I was surprised when I originally looked into `cynic-querygen` to find it basically lacking any documentation. If you like the idea of making the querygen more customizable, we have a couple other ideas I'd like to follow up with, which would fully supplant all our find-and-replaces: - overriding the default type for scalars like `DateTime`, `Json`, and `Uuid` (e.g. to use `chrono`, `serde_json` and `uuid` crate types) - specifying additional traits to derive for `cynic::QueryFragment`s and `cynic::Enum`s
1 parent 54b8b4b commit d38f9c6

File tree

7 files changed

+115
-19
lines changed

7 files changed

+115
-19
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html
1111
### New features
1212

1313
- The `QueryFragment` derive now supports the `rename_all` attribute
14+
- `cynic-querygen` now has a `field_overrides` option that can be used to
15+
customize the type of individual scalar fields in generated `QueryFragment`s.
1416

1517
### Bug Fixes
1618

cynic-querygen-web/src/graphiql_page.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
use seed::{prelude::*, *};
22

3-
pub struct Model {
3+
pub struct Model<'a> {
44
schema_url: Option<String>,
55
schema_data: Option<String>,
66
query: String,
7-
opts: cynic_querygen::QueryGenOptions,
7+
opts: cynic_querygen::QueryGenOptions<'a>,
88
generated_code: Result<String, cynic_querygen::Error>,
99
}
1010

11-
impl Model {
11+
impl<'a> Model<'a> {
1212
pub fn new_from_url(url: String) -> Self {
1313
Model {
1414
schema_url: Some(url),
@@ -34,8 +34,8 @@ impl Model {
3434
}
3535
}
3636

37-
impl Default for Model {
38-
fn default() -> Model {
37+
impl Default for Model<'_> {
38+
fn default() -> Model<'static> {
3939
Model {
4040
schema_url: None,
4141
schema_data: None,

cynic-querygen-web/src/lib.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ mod view;
1414
// ------ ------
1515

1616
// `init` describes what should happen when your app started.
17-
fn init(_: Url, _: &mut impl Orders<Msg>) -> Model {
17+
fn init(_: Url, _: &mut impl Orders<Msg>) -> Model<'static> {
1818
Model::default()
1919
}
2020

@@ -23,13 +23,13 @@ fn init(_: Url, _: &mut impl Orders<Msg>) -> Model {
2323
// ------ ------
2424

2525
// `Model` describes our app state.
26-
enum Model {
26+
enum Model<'a> {
2727
EntryPage(entry_page::Model),
28-
GraphiqlPage(graphiql_page::Model),
28+
GraphiqlPage(graphiql_page::Model<'a>),
2929
}
3030

31-
impl Default for Model {
32-
fn default() -> Model {
31+
impl Default for Model<'_> {
32+
fn default() -> Model<'static> {
3333
Model::EntryPage(entry_page::Model::default())
3434
}
3535
}

cynic-querygen/src/lib.rs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::rc::Rc;
1+
use std::{collections::HashMap, rc::Rc};
22

33
mod casings;
44
mod naming;
@@ -94,16 +94,27 @@ pub enum Error {
9494
}
9595

9696
#[derive(Debug)]
97-
pub struct QueryGenOptions {
97+
pub struct QueryGenOptions<'a> {
9898
pub schema_module_name: String,
99+
/// The name of a registered schema to use inside generated `#[cynic(schema = "schema_name")]` attributes.
99100
pub schema_name: Option<String>,
101+
/// Mapping of `("TypeName.fieldName", "fully::qualified::type::Path")` overrides to customize the scalar type used for specific fields.
102+
///
103+
/// The field name should have the same casing as the field name in the GraphQL schema
104+
/// (i.e. before conversion to snake_case). Note that the provided type override will still
105+
/// be wrapped in an [`Option`] if the field is nullable in the schema.
106+
///
107+
/// The override type must be a registered [custom scalar](https://cynic-rs.dev/derives/scalars#custom-scalars) for the schema scalar type
108+
/// of the overridden field.
109+
pub field_overrides: HashMap<&'a str, &'a str>,
100110
}
101111

102-
impl Default for QueryGenOptions {
103-
fn default() -> QueryGenOptions {
112+
impl Default for QueryGenOptions<'_> {
113+
fn default() -> QueryGenOptions<'static> {
104114
QueryGenOptions {
105115
schema_module_name: "schema".into(),
106116
schema_name: None,
117+
field_overrides: HashMap::default(),
107118
}
108119
}
109120
}
@@ -124,7 +135,8 @@ pub fn document_to_fragment_structs(
124135
let (schema, typename_id) = add_builtins(schema);
125136

126137
let type_index = Rc::new(TypeIndex::from_schema(&schema, typename_id));
127-
let mut parsed_output = query_parsing::parse_query_document(&query, &type_index)?;
138+
let mut parsed_output =
139+
query_parsing::parse_query_document(&query, &type_index, &options.field_overrides)?;
128140

129141
add_schema_name(&mut parsed_output, options.schema_name.as_deref());
130142

cynic-querygen/src/query_parsing/mod.rs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::rc::Rc;
1+
use std::{collections::HashMap, rc::Rc};
22

33
mod inputs;
44
mod leaf_types;
@@ -18,13 +18,14 @@ use cynic_parser::{ExecutableDocument, executable as parser};
1818
use crate::{
1919
Error, TypeIndex,
2020
casings::CasingExt,
21-
naming::Namer,
21+
naming::{Nameable, Namer},
2222
output::{self, Output},
2323
};
2424

2525
pub fn parse_query_document<'a>(
2626
doc: &'a ExecutableDocument,
2727
type_index: &Rc<TypeIndex<'a>>,
28+
field_overrides: &HashMap<&str, &str>,
2829
) -> Result<Output<'a, 'a>, Error> {
2930
let normalised = normalisation::normalise(doc, type_index)?;
3031
let input_objects = InputObjects::new(&normalised);
@@ -51,7 +52,14 @@ pub fn parse_query_document<'a>(
5152

5253
let query_fragments = sorting::topological_sort(normalised.selection_sets.iter().cloned())
5354
.into_iter()
54-
.map(|selection| make_query_fragment(selection, &mut namers, &variable_struct_details))
55+
.map(|selection| {
56+
make_query_fragment(
57+
selection,
58+
&mut namers,
59+
&variable_struct_details,
60+
field_overrides,
61+
)
62+
})
5563
.collect::<Vec<_>>();
5664

5765
let inline_fragments = normalised
@@ -84,12 +92,15 @@ fn make_query_fragment<'text>(
8492
selection: Rc<normalisation::SelectionSet<'text, 'text>>,
8593
namers: &mut Namers<'text>,
8694
variable_struct_details: &VariableStructDetails<'text, 'text>,
95+
field_overrides: &HashMap<&str, &str>,
8796
) -> crate::output::QueryFragment<'text, 'text> {
8897
use crate::output::query_fragment::{
8998
FieldArgument, OutputField, QueryFragment, RustOutputFieldType,
9099
};
91100
use normalisation::{Field, Selection};
92101

102+
let requested_fragment_name = selection.requested_name();
103+
93104
QueryFragment {
94105
fields: selection
95106
.selections
@@ -99,7 +110,11 @@ fn make_query_fragment<'text>(
99110
let schema_field = &field.schema_field;
100111

101112
let type_name_override = match &field.field {
102-
Field::Leaf => None,
113+
Field::Leaf => field_overrides
114+
// Check for field-level type overrides using the requested fragment name (before incrementing suffix),
115+
// and un-aliased field name in the schema.
116+
.get(format!("{}.{}", requested_fragment_name, schema_field.name).as_str())
117+
.map(|o| o.to_string()),
103118
Field::Composite(ss) => Some(namers.selection_sets.name_subject(ss)),
104119
Field::InlineFragments(fragments) => {
105120
Some(namers.inline_fragments.name_subject(fragments))
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
use std::collections::HashMap;
2+
3+
use insta::assert_snapshot;
4+
5+
use cynic_querygen::{QueryGenOptions, document_to_fragment_structs};
6+
7+
#[test]
8+
fn test_field_overrides() {
9+
let schema = include_str!("../../schemas/test_cases.graphql");
10+
let query = r#"
11+
query MyQuery($input: OneOfObject!) {
12+
clashes {
13+
str
14+
bool
15+
i32
16+
u32
17+
}
18+
}
19+
"#;
20+
21+
let mut field_overrides = HashMap::new();
22+
field_overrides.insert(
23+
"FieldNameClashes.str",
24+
"std::collections::HashMap<String, String>",
25+
);
26+
let options = QueryGenOptions {
27+
field_overrides,
28+
..Default::default()
29+
};
30+
31+
assert_snapshot!(
32+
document_to_fragment_structs(query, schema, &options).expect("QueryGen Failed")
33+
)
34+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
source: cynic-querygen/tests/option-tests.rs
3+
expression: "document_to_fragment_structs(query, schema,\n&options).expect(\"QueryGen Failed\")"
4+
---
5+
#[derive(cynic::QueryFragment, Debug)]
6+
#[cynic(graphql_type = "Foo")]
7+
pub struct MyQuery {
8+
pub clashes: Option<FieldNameClashes>,
9+
}
10+
11+
#[derive(cynic::QueryFragment, Debug)]
12+
pub struct FieldNameClashes {
13+
pub str: Option<std::collections::HashMap<String, String>>,
14+
pub bool: Option<bool>,
15+
pub i32: Option<i32>,
16+
pub u32: Option<i32>,
17+
}
18+
19+
#[derive(cynic::InputObject, Debug)]
20+
pub enum OneOfObject<'a> {
21+
String(&'a str),
22+
Int(i32),
23+
Nested(Baz<'a>),
24+
}
25+
26+
#[derive(cynic::InputObject, Debug)]
27+
pub struct Baz<'a> {
28+
pub id: &'a cynic::Id,
29+
pub a_string: &'a str,
30+
pub an_optional_string: Option<&'a str>,
31+
}
32+
33+

0 commit comments

Comments
 (0)