Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Let users add arbitrary derives to ResponseData #79

Merged
merged 4 commits into from
Aug 22, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

- (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.
- (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.
- Support arbitrary derives on the generated response types via the `response_derives` option on the `graphql` attribute.

## [0.3.0] - 2018-07-24

Expand Down
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ A typed GraphQL client library for Rust.
- Works in the browser (WebAssembly)
- Subscriptions support (serialization-deserialization only at the moment)
- Copies documentation from the GraphQL schema to the generated Rust code
- Arbitrary derives on the generated responses
- Arbitrary custom scalars
- Supports multiple operations per query document

## Getting started

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

[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/).

## Deriving specific traits on the response

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:

```rust
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "src/search_schema.graphql",
query_path = "src/search_query.graphql"
query_path = "src/search_query.graphql",
response_derives = "Serialize,PartialEq",
)]
struct SearchQuery;
```

## Custom scalars

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.

## Query documents with multiple operations

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.

There is an example [in the tests](./tests/operation_selection).

## Examples

See the examples directory in this repository.
Expand Down
3 changes: 2 additions & 1 deletion examples/example_module/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use custom_scalars::*;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "../github/src/schema.graphql",
query_path = "../github/src/query_1.graphql"
query_path = "../github/src/query_1.graphql",
response_derives = "Debug",
)]
pub struct ExampleModule;
3 changes: 2 additions & 1 deletion examples/github/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ type DateTime = String;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "src/schema.graphql",
query_path = "src/query_1.graphql"
query_path = "src/query_1.graphql",
response_derives = "Debug",
)]
struct Query1;

Expand Down
3 changes: 2 additions & 1 deletion graphql_client_cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ use structopt::StructOpt;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "src/introspection_schema.graphql",
query_path = "src/introspection_query.graphql"
query_path = "src/introspection_query.graphql",
response_derives = "Serialize",
)]
struct IntrospectionQuery;

Expand Down
29 changes: 29 additions & 0 deletions graphql_query_derive/src/attributes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use failure;
use syn;

pub(crate) fn extract_attr(ast: &syn::DeriveInput, attr: &str) -> Result<String, failure::Error> {
let attributes = &ast.attrs;
let attribute = attributes
.iter()
.find(|attr| {
let path = &attr.path;
quote!(#path).to_string() == "graphql"
}).ok_or_else(|| format_err!("The graphql attribute is missing"))?;
if let syn::Meta::List(items) = &attribute
.interpret_meta()
.expect("Attribute is well formatted")
{
for item in items.nested.iter() {
if let syn::NestedMeta::Meta(syn::Meta::NameValue(name_value)) = item {
let syn::MetaNameValue { ident, lit, .. } = name_value;
if ident == attr {
if let syn::Lit::Str(lit) = lit {
return Ok(lit.value());
}
}
}
}
}

Err(format_err!("attribute not found"))?
}
16 changes: 14 additions & 2 deletions graphql_query_derive/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,14 @@ pub(crate) fn response_for_query(
schema: schema::Schema,
query: query::Document,
selected_operation: String,
additional_derives: Option<String>,
) -> Result<TokenStream, failure::Error> {
let mut context = QueryContext::new(schema);

if let Some(derives) = additional_derives {
context.ingest_additional_derives(&derives).unwrap();
}

let mut definitions = Vec::new();
let mut operations: Vec<Operation> = Vec::new();

Expand Down Expand Up @@ -77,7 +83,11 @@ pub(crate) fn response_for_query(
.unwrap()
};

let enum_definitions = context.schema.enums.values().map(|enm| enm.to_rust());
let enum_definitions = context
.schema
.enums
.values()
.map(|enm| enm.to_rust(&context));
let fragment_definitions: Result<Vec<TokenStream>, _> = context
.fragments
.values()
Expand All @@ -101,6 +111,8 @@ pub(crate) fn response_for_query(
.map(|s| s.to_rust())
.collect();

let response_derives = context.response_derives();

Ok(quote! {
type Boolean = bool;
type Float = f64;
Expand All @@ -119,7 +131,7 @@ pub(crate) fn response_for_query(

#variables_struct

#[derive(Debug, Serialize, Deserialize)]
#response_derives
#[serde(rename_all = "camelCase")]
pub struct ResponseData {
#(#response_data_fields,)*
Expand Down
5 changes: 3 additions & 2 deletions graphql_query_derive/src/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ pub struct GqlEnum {
}

impl GqlEnum {
pub fn to_rust(&self) -> TokenStream {
pub(crate) fn to_rust(&self, query_context: &::query::QueryContext) -> TokenStream {
let derives = query_context.response_enum_derives();
let variant_names: Vec<TokenStream> = self
.variants
.iter()
Expand All @@ -42,7 +43,7 @@ impl GqlEnum {
let name = name_ident.clone();

quote! {
#[derive(Debug)]
#derives
pub enum #name {
#(#variant_names,)*
Other(String),
Expand Down
3 changes: 2 additions & 1 deletion graphql_query_derive/src/fragments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ pub struct GqlFragment {

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

Ok(quote!{
#[derive(Debug, Deserialize, Serialize)]
#derives
pub struct #name_ident {
#(#fields,)*
}
Expand Down
7 changes: 4 additions & 3 deletions graphql_query_derive/src/interfaces.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ impl GqlInterface {
prefix: &str,
) -> Result<TokenStream, failure::Error> {
let name = Ident::new(&prefix, Span::call_site());
let derives = query_context.response_derives();

selection
.extract_typename()
Expand Down Expand Up @@ -78,13 +79,13 @@ impl GqlInterface {
let attached_enum_name = Ident::new(&format!("{}On", name), Span::call_site());
let (attached_enum, last_object_field) = if !union_variants.is_empty() {
let attached_enum = quote! {
#[derive(Deserialize, Debug, Serialize)]
#derives
#[serde(tag = "__typename")]
pub enum #attached_enum_name {
#(#union_variants,)*
}
};
let last_object_field = quote!(#[serde(flatten)] on: #attached_enum_name,);
let last_object_field = quote!(#[serde(flatten)] pub on: #attached_enum_name,);
(attached_enum, last_object_field)
} else {
(quote!(), quote!())
Expand All @@ -98,7 +99,7 @@ impl GqlInterface {

#attached_enum

#[derive(Debug, Serialize, Deserialize)]
#derives
pub struct #name {
#(#object_fields,)*
#last_object_field
Expand Down
38 changes: 7 additions & 31 deletions graphql_query_derive/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#![recursion_limit = "128"]
#![recursion_limit = "512"]

#[macro_use]
extern crate failure;
Expand All @@ -16,6 +16,7 @@ extern crate quote;

use proc_macro2::TokenStream;

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

let query_path = extract_attr(input, "query_path")?;
let schema_path = extract_attr(input, "schema_path")?;
let query_path = attributes::extract_attr(input, "query_path")?;
let schema_path = attributes::extract_attr(input, "schema_path")?;
let response_derives = attributes::extract_attr(input, "response_derives").ok();

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

let module_name = Ident::new(&input.ident.to_string().to_snake_case(), Span::call_site());
let struct_name = &input.ident;
let schema_output = codegen::response_for_query(schema, query, input.ident.to_string())?;
let schema_output =
codegen::response_for_query(schema, query, input.ident.to_string(), response_derives)?;

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

Ok(result)
}

fn extract_attr(ast: &syn::DeriveInput, attr: &str) -> Result<String, failure::Error> {
let attributes = &ast.attrs;
let attribute = attributes
.iter()
.find(|attr| {
let path = &attr.path;
quote!(#path).to_string() == "graphql"
}).ok_or_else(|| format_err!("The graphql attribute is missing"))?;
if let syn::Meta::List(items) = &attribute
.interpret_meta()
.expect("Attribute is well formatted")
{
for item in items.nested.iter() {
if let syn::NestedMeta::Meta(syn::Meta::NameValue(name_value)) = item {
let syn::MetaNameValue { ident, lit, .. } = name_value;
if ident == attr {
if let syn::Lit::Str(lit) = lit {
return Ok(lit.value());
}
}
}
}
}

Err(format_err!("attribute not found"))?
}
3 changes: 2 additions & 1 deletion graphql_query_derive/src/objects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,15 @@ impl GqlObject {
selection: &Selection,
prefix: &str,
) -> Result<TokenStream, failure::Error> {
let derives = query_context.response_derives();
let name = Ident::new(prefix, Span::call_site());
let fields = self.response_fields_for_selection(query_context, selection, prefix)?;
let field_impls = self.field_impls_for_selection(query_context, selection, &prefix)?;
let description = self.description.as_ref().map(|desc| quote!(#[doc = #desc]));
Ok(quote! {
#(#field_impls)*

#[derive(Debug, Serialize, Deserialize)]
#derives
#[serde(rename_all = "camelCase")]
#description
pub struct #name {
Expand Down
Loading