Skip to content

Commit 96e2f98

Browse files
authored
Merge pull request #79 from tomhoule/customize-derives
Let users add arbitrary derives to ResponseData
2 parents 4a0eb43 + e91d117 commit 96e2f98

24 files changed

+336
-66
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1111

1212
- (breaking) Control over which types custom scalars deserialize to is given to the user: you now have to provide type aliases for the custom scalars in the scope of the struct under derive.
1313
- (breaking) Support for multi-operations documents. You can select a particular operation by naming the struct under derive after it. In case there is no match, we revert to the current behaviour: select the first operation.
14+
- Support arbitrary derives on the generated response types via the `response_derives` option on the `graphql` attribute.
1415

1516
## [0.3.0] - 2018-07-24
1617

README.md

+28
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ A typed GraphQL client library for Rust.
1313
- Works in the browser (WebAssembly)
1414
- Subscriptions support (serialization-deserialization only at the moment)
1515
- Copies documentation from the GraphQL schema to the generated Rust code
16+
- Arbitrary derives on the generated responses
17+
- Arbitrary custom scalars
18+
- Supports multiple operations per query document
1619

1720
## Getting started
1821

@@ -74,6 +77,31 @@ A typed GraphQL client library for Rust.
7477

7578
[A complete example using the GitHub GraphQL API is available](https://github.com/tomhoule/graphql-client/tree/master/examples/github), as well as sample [rustdoc output](https://www.tomhoule.com/docs/example_module/).
7679

80+
## Deriving specific traits on the response
81+
82+
The generated response types always derive `serde::Deserialize` but you may want to print them (`Debug`), compare them (`PartialEq`) or derive any other trait on it. You can achieve this with the `response_derives` option of the `graphql` attribute. Example:
83+
84+
```rust
85+
#[derive(GraphQLQuery)]
86+
#[graphql(
87+
schema_path = "src/search_schema.graphql",
88+
query_path = "src/search_query.graphql"
89+
query_path = "src/search_query.graphql",
90+
response_derives = "Serialize,PartialEq",
91+
)]
92+
struct SearchQuery;
93+
```
94+
95+
## Custom scalars
96+
97+
The generated code will reference the scalar types as defined in the server schema. This means you have to provide matching rust types in the scope of the struct under derive. It can be as simple as declarations like `type Email = String;`. This gives you complete freedom on how to treat custom scalars, as long as they can be deserialized.
98+
99+
## Query documents with multiple operations
100+
101+
You can write multiple operations in one query document (one `.graphql` file). You can then select one by naming the struct you `#[derive(GraphQLQuery)]` on with the same name as one of the operations. This is neat, as it allows sharing fragments between operations.
102+
103+
There is an example [in the tests](./tests/operation_selection).
104+
77105
## Examples
78106

79107
See the examples directory in this repository.

examples/example_module/src/lib.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use custom_scalars::*;
1111
#[derive(GraphQLQuery)]
1212
#[graphql(
1313
schema_path = "../github/src/schema.graphql",
14-
query_path = "../github/src/query_1.graphql"
14+
query_path = "../github/src/query_1.graphql",
15+
response_derives = "Debug",
1516
)]
1617
pub struct ExampleModule;

examples/github/src/main.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ type DateTime = String;
3232
#[derive(GraphQLQuery)]
3333
#[graphql(
3434
schema_path = "src/schema.graphql",
35-
query_path = "src/query_1.graphql"
35+
query_path = "src/query_1.graphql",
36+
response_derives = "Debug",
3637
)]
3738
struct Query1;
3839

graphql_client_cli/src/main.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ use structopt::StructOpt;
1515
#[derive(GraphQLQuery)]
1616
#[graphql(
1717
schema_path = "src/introspection_schema.graphql",
18-
query_path = "src/introspection_query.graphql"
18+
query_path = "src/introspection_query.graphql",
19+
response_derives = "Serialize",
1920
)]
2021
struct IntrospectionQuery;
2122

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
use failure;
2+
use syn;
3+
4+
pub(crate) fn extract_attr(ast: &syn::DeriveInput, attr: &str) -> Result<String, failure::Error> {
5+
let attributes = &ast.attrs;
6+
let attribute = attributes
7+
.iter()
8+
.find(|attr| {
9+
let path = &attr.path;
10+
quote!(#path).to_string() == "graphql"
11+
}).ok_or_else(|| format_err!("The graphql attribute is missing"))?;
12+
if let syn::Meta::List(items) = &attribute
13+
.interpret_meta()
14+
.expect("Attribute is well formatted")
15+
{
16+
for item in items.nested.iter() {
17+
if let syn::NestedMeta::Meta(syn::Meta::NameValue(name_value)) = item {
18+
let syn::MetaNameValue { ident, lit, .. } = name_value;
19+
if ident == attr {
20+
if let syn::Lit::Str(lit) = lit {
21+
return Ok(lit.value());
22+
}
23+
}
24+
}
25+
}
26+
}
27+
28+
Err(format_err!("attribute not found"))?
29+
}

graphql_query_derive/src/codegen.rs

+14-2
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,14 @@ pub(crate) fn response_for_query(
1111
schema: schema::Schema,
1212
query: query::Document,
1313
selected_operation: String,
14+
additional_derives: Option<String>,
1415
) -> Result<TokenStream, failure::Error> {
1516
let mut context = QueryContext::new(schema);
17+
18+
if let Some(derives) = additional_derives {
19+
context.ingest_additional_derives(&derives).unwrap();
20+
}
21+
1622
let mut definitions = Vec::new();
1723
let mut operations: Vec<Operation> = Vec::new();
1824

@@ -77,7 +83,11 @@ pub(crate) fn response_for_query(
7783
.unwrap()
7884
};
7985

80-
let enum_definitions = context.schema.enums.values().map(|enm| enm.to_rust());
86+
let enum_definitions = context
87+
.schema
88+
.enums
89+
.values()
90+
.map(|enm| enm.to_rust(&context));
8191
let fragment_definitions: Result<Vec<TokenStream>, _> = context
8292
.fragments
8393
.values()
@@ -101,6 +111,8 @@ pub(crate) fn response_for_query(
101111
.map(|s| s.to_rust())
102112
.collect();
103113

114+
let response_derives = context.response_derives();
115+
104116
Ok(quote! {
105117
type Boolean = bool;
106118
type Float = f64;
@@ -119,7 +131,7 @@ pub(crate) fn response_for_query(
119131

120132
#variables_struct
121133

122-
#[derive(Debug, Serialize, Deserialize)]
134+
#response_derives
123135
#[serde(rename_all = "camelCase")]
124136
pub struct ResponseData {
125137
#(#response_data_fields,)*

graphql_query_derive/src/enums.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ pub struct GqlEnum {
1616
}
1717

1818
impl GqlEnum {
19-
pub fn to_rust(&self) -> TokenStream {
19+
pub(crate) fn to_rust(&self, query_context: &::query::QueryContext) -> TokenStream {
20+
let derives = query_context.response_enum_derives();
2021
let variant_names: Vec<TokenStream> = self
2122
.variants
2223
.iter()
@@ -42,7 +43,7 @@ impl GqlEnum {
4243
let name = name_ident.clone();
4344

4445
quote! {
45-
#[derive(Debug)]
46+
#derives
4647
pub enum #name {
4748
#(#variant_names,)*
4849
Other(String),

graphql_query_derive/src/fragments.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@ pub struct GqlFragment {
1111

1212
impl GqlFragment {
1313
pub(crate) fn to_rust(&self, context: &QueryContext) -> Result<TokenStream, ::failure::Error> {
14+
let derives = context.response_derives();
1415
let name_ident = Ident::new(&self.name, Span::call_site());
1516
let object = context.schema.objects.get(&self.on).expect("oh, noes");
1617
let field_impls = object.field_impls_for_selection(context, &self.selection, &self.name)?;
1718
let fields = object.response_fields_for_selection(context, &self.selection, &self.name)?;
1819

1920
Ok(quote!{
20-
#[derive(Debug, Deserialize, Serialize)]
21+
#derives
2122
pub struct #name_ident {
2223
#(#fields,)*
2324
}

graphql_query_derive/src/interfaces.rs

+4-3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ impl GqlInterface {
3333
prefix: &str,
3434
) -> Result<TokenStream, failure::Error> {
3535
let name = Ident::new(&prefix, Span::call_site());
36+
let derives = query_context.response_derives();
3637

3738
selection
3839
.extract_typename()
@@ -78,13 +79,13 @@ impl GqlInterface {
7879
let attached_enum_name = Ident::new(&format!("{}On", name), Span::call_site());
7980
let (attached_enum, last_object_field) = if !union_variants.is_empty() {
8081
let attached_enum = quote! {
81-
#[derive(Deserialize, Debug, Serialize)]
82+
#derives
8283
#[serde(tag = "__typename")]
8384
pub enum #attached_enum_name {
8485
#(#union_variants,)*
8586
}
8687
};
87-
let last_object_field = quote!(#[serde(flatten)] on: #attached_enum_name,);
88+
let last_object_field = quote!(#[serde(flatten)] pub on: #attached_enum_name,);
8889
(attached_enum, last_object_field)
8990
} else {
9091
(quote!(), quote!())
@@ -98,7 +99,7 @@ impl GqlInterface {
9899

99100
#attached_enum
100101

101-
#[derive(Debug, Serialize, Deserialize)]
102+
#derives
102103
pub struct #name {
103104
#(#object_fields,)*
104105
#last_object_field

graphql_query_derive/src/lib.rs

+7-31
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#![recursion_limit = "128"]
1+
#![recursion_limit = "512"]
22

33
#[macro_use]
44
extern crate failure;
@@ -16,6 +16,7 @@ extern crate quote;
1616

1717
use proc_macro2::TokenStream;
1818

19+
mod attributes;
1920
mod codegen;
2021
mod constants;
2122
mod enums;
@@ -77,8 +78,9 @@ fn impl_gql_query(input: &syn::DeriveInput) -> Result<TokenStream, failure::Erro
7778
let cargo_manifest_dir =
7879
::std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR env variable is defined");
7980

80-
let query_path = extract_attr(input, "query_path")?;
81-
let schema_path = extract_attr(input, "schema_path")?;
81+
let query_path = attributes::extract_attr(input, "query_path")?;
82+
let schema_path = attributes::extract_attr(input, "schema_path")?;
83+
let response_derives = attributes::extract_attr(input, "response_derives").ok();
8284

8385
// We need to qualify the query with the path to the crate it is part of
8486
let query_path = format!("{}/{}", cargo_manifest_dir, query_path);
@@ -108,7 +110,8 @@ fn impl_gql_query(input: &syn::DeriveInput) -> Result<TokenStream, failure::Erro
108110

109111
let module_name = Ident::new(&input.ident.to_string().to_snake_case(), Span::call_site());
110112
let struct_name = &input.ident;
111-
let schema_output = codegen::response_for_query(schema, query, input.ident.to_string())?;
113+
let schema_output =
114+
codegen::response_for_query(schema, query, input.ident.to_string(), response_derives)?;
112115

113116
let result = quote!(
114117
pub mod #module_name {
@@ -139,30 +142,3 @@ fn impl_gql_query(input: &syn::DeriveInput) -> Result<TokenStream, failure::Erro
139142

140143
Ok(result)
141144
}
142-
143-
fn extract_attr(ast: &syn::DeriveInput, attr: &str) -> Result<String, failure::Error> {
144-
let attributes = &ast.attrs;
145-
let attribute = attributes
146-
.iter()
147-
.find(|attr| {
148-
let path = &attr.path;
149-
quote!(#path).to_string() == "graphql"
150-
}).ok_or_else(|| format_err!("The graphql attribute is missing"))?;
151-
if let syn::Meta::List(items) = &attribute
152-
.interpret_meta()
153-
.expect("Attribute is well formatted")
154-
{
155-
for item in items.nested.iter() {
156-
if let syn::NestedMeta::Meta(syn::Meta::NameValue(name_value)) = item {
157-
let syn::MetaNameValue { ident, lit, .. } = name_value;
158-
if ident == attr {
159-
if let syn::Lit::Str(lit) = lit {
160-
return Ok(lit.value());
161-
}
162-
}
163-
}
164-
}
165-
}
166-
167-
Err(format_err!("attribute not found"))?
168-
}

graphql_query_derive/src/objects.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,15 @@ impl GqlObject {
6767
selection: &Selection,
6868
prefix: &str,
6969
) -> Result<TokenStream, failure::Error> {
70+
let derives = query_context.response_derives();
7071
let name = Ident::new(prefix, Span::call_site());
7172
let fields = self.response_fields_for_selection(query_context, selection, prefix)?;
7273
let field_impls = self.field_impls_for_selection(query_context, selection, &prefix)?;
7374
let description = self.description.as_ref().map(|desc| quote!(#[doc = #desc]));
7475
Ok(quote! {
7576
#(#field_impls)*
7677

77-
#[derive(Debug, Serialize, Deserialize)]
78+
#derives
7879
#[serde(rename_all = "camelCase")]
7980
#description
8081
pub struct #name {

0 commit comments

Comments
 (0)