Skip to content

Commit 8cf0694

Browse files
authored
Merge pull request #61 from tomhoule/subscriptions-support
Subscriptions support
2 parents 6a425cb + 96048df commit 8cf0694

11 files changed

+148
-18
lines changed

CHANGELOG.md

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

1212
- Copy documentation from the GraphQL schema to the generated types (including their fields) as normal Rust documentation. Documentation will show up in the generated docs as well as IDEs that support expanding derive macros (which does not include the RLS yet).
13+
- Implement and test deserializing subscription responses. We also try to provide helpful error messages when a subscription query is not valid (i.e. when it has more than one top-level field).
14+
15+
### Fixed
16+
17+
- The generated `ResponseData` structs did not convert between snake and camel case.
1318

1419
## [0.1.0] - 2018-07-04
1520

graphql_query_derive/src/constants.rs

+6
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,9 @@ pub fn typename_field() -> GqlObjectField {
2222
type_: FieldType::Named(string_type()),
2323
}
2424
}
25+
26+
pub const MULTIPLE_SUBSCRIPTION_FIELDS_ERROR: &str = r##"
27+
Multiple-field queries on the root subscription field are forbidden by the spec.
28+
29+
See: https://github.com/facebook/graphql/blob/master/spec/Section%205%20--%20Validation.md#subscription-operation-definitions
30+
"##;

graphql_query_derive/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ mod inputs;
2424
mod interfaces;
2525
mod introspection_response;
2626
mod objects;
27+
mod operations;
2728
mod query;
2829
mod scalars;
2930
mod schema;
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
use selection::Selection;
2+
3+
pub enum OperationType {
4+
Query,
5+
Mutation,
6+
Subscription,
7+
}
8+
9+
#[derive(Debug)]
10+
pub struct Operation {
11+
pub name: String,
12+
pub selection: Selection,
13+
}
14+
15+
pub struct Operations(Vec<Operation>);
16+
17+
impl Operations {
18+
fn from_document(doc: ::graphql_parser::query::Document) -> Result<Self, ::failure::Error> {
19+
unimplemented!()
20+
}
21+
}

graphql_query_derive/src/query.rs

+3-9
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,8 @@ use variables::Variable;
99

1010
/// This holds all the information we need during the code generation phase.
1111
pub struct QueryContext {
12-
pub _subscription_root: Option<Vec<TokenStream>>,
12+
pub root: Option<Vec<TokenStream>>,
1313
pub fragments: BTreeMap<String, GqlFragment>,
14-
pub mutation_root: Option<Vec<TokenStream>>,
15-
pub query_root: Option<Vec<TokenStream>>,
1614
pub schema: Schema,
1715
pub variables: Vec<Variable>,
1816
}
@@ -21,10 +19,8 @@ impl QueryContext {
2119
/// Create a QueryContext with the given Schema.
2220
pub fn new(schema: Schema) -> QueryContext {
2321
QueryContext {
24-
_subscription_root: None,
22+
root: None,
2523
fragments: BTreeMap::new(),
26-
mutation_root: None,
27-
query_root: None,
2824
schema,
2925
variables: Vec::new(),
3026
}
@@ -72,10 +68,8 @@ impl QueryContext {
7268
#[cfg(test)]
7369
pub fn new_empty() -> QueryContext {
7470
QueryContext {
75-
_subscription_root: None,
7671
fragments: BTreeMap::new(),
77-
mutation_root: None,
78-
query_root: None,
72+
root: None,
7973
schema: Schema::new(),
8074
variables: Vec::new(),
8175
}

graphql_query_derive/src/schema.rs

+12-9
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ impl Schema {
7878
for definition in query.definitions {
7979
match definition {
8080
query::Definition::Operation(query::OperationDefinition::Query(q)) => {
81-
context.query_root = {
81+
context.root = {
8282
let definition = context
8383
.schema
8484
.query_type
@@ -101,7 +101,7 @@ impl Schema {
101101
context.register_variables(&q.variable_definitions);
102102
}
103103
query::Definition::Operation(query::OperationDefinition::Mutation(q)) => {
104-
context.mutation_root = {
104+
context.root = {
105105
let definition = context
106106
.schema
107107
.mutation_type
@@ -124,7 +124,7 @@ impl Schema {
124124
context.register_variables(&q.variable_definitions);
125125
}
126126
query::Definition::Operation(query::OperationDefinition::Subscription(q)) => {
127-
context._subscription_root = {
127+
context.root = {
128128
let definition = context
129129
.schema
130130
.subscription_type
@@ -137,6 +137,13 @@ impl Schema {
137137
let prefix = format!("RUST_{}", prefix);
138138
let selection = Selection::from(&q.selection_set);
139139

140+
if selection.0.len() > 1 {
141+
Err(format_err!(
142+
"{}",
143+
::constants::MULTIPLE_SUBSCRIPTION_FIELDS_ERROR
144+
))?
145+
}
146+
140147
definitions.extend(
141148
definition.field_impls_for_selection(&context, &selection, &prefix)?,
142149
);
@@ -173,12 +180,7 @@ impl Schema {
173180
.collect();
174181
let fragment_definitions = fragment_definitions?;
175182
let variables_struct = context.expand_variables();
176-
let response_data_fields = context
177-
.query_root
178-
.as_ref()
179-
.or_else(|| context.mutation_root.as_ref())
180-
.or_else(|| context._subscription_root.as_ref())
181-
.expect("no selection defined");
183+
let response_data_fields = context.root.as_ref().expect("no selection defined");
182184

183185
let input_object_definitions: Result<Vec<TokenStream>, _> = context
184186
.schema
@@ -214,6 +216,7 @@ impl Schema {
214216
#variables_struct
215217

216218
#[derive(Debug, Serialize, Deserialize)]
219+
#[serde(rename_all = "camelCase")]
217220
pub struct ResponseData {
218221
#(#response_data_fields)*,
219222
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
subscription InvalidSubscription($filter: String) {
2+
newDogs {
3+
name
4+
}
5+
dogBirthdays(filter: $filter) {
6+
name
7+
}
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
subscription Birthdays($filter: String) {
2+
dogBirthdays(filter: $filter) {
3+
name
4+
}
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"dogBirthdays": [
3+
{
4+
"name": "Maya"
5+
},
6+
{
7+
"name": "Norbert"
8+
},
9+
{
10+
"name": "Strelka"
11+
},
12+
{
13+
"name": "Belka"
14+
}
15+
]
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
schema {
2+
query: SimpleQuery
3+
mutation: SimpleMutation
4+
subscription: SimpleSubscription
5+
}
6+
7+
type SimpleQuery {
8+
dogByName(name: String): Dog
9+
}
10+
11+
type SimpleMutation {
12+
petDog(dogName: String): Dog
13+
}
14+
15+
type SimpleSubscription {
16+
newDogs: [Dog]
17+
dogBirthdays(filter: String): [DogBirthday!]
18+
}
19+
20+
type DogBirthday {
21+
name: String
22+
date: String
23+
age: Int
24+
treats: [String]
25+
}
26+
27+
type Dog {
28+
name: String!
29+
"""
30+
Always returns true
31+
"""
32+
isGoodDog: Boolean!
33+
}

tests/subscriptions.rs

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#[macro_use]
2+
extern crate graphql_client;
3+
#[macro_use]
4+
extern crate serde_derive;
5+
extern crate serde;
6+
extern crate serde_json;
7+
8+
const RESPONSE: &str = include_str!("subscription/subscription_query_response.json");
9+
10+
// If you uncomment this, it will not compile because the query is not valid. We need to investigate how we can make this a real test.
11+
//
12+
// #[derive(GraphQLQuery)]
13+
// #[graphql(
14+
// schema_path = "tests/subscription/subscription_schema.graphql",
15+
// query_path = "tests/subscription/subscription_invalid_query.graphql"
16+
// )]
17+
// struct SubscriptionInvalidQuery;
18+
19+
#[derive(GraphQLQuery)]
20+
#[graphql(
21+
schema_path = "tests/subscription/subscription_schema.graphql",
22+
query_path = "tests/subscription/subscription_query.graphql",
23+
)]
24+
struct SubscriptionQuery;
25+
26+
#[test]
27+
fn subscriptions_work() {
28+
let response_data: subscription_query::ResponseData = serde_json::from_str(RESPONSE).unwrap();
29+
30+
let expected = r##"ResponseData { dog_birthdays: Some([RustBirthdaysDogBirthdays { name: Some("Maya") }, RustBirthdaysDogBirthdays { name: Some("Norbert") }, RustBirthdaysDogBirthdays { name: Some("Strelka") }, RustBirthdaysDogBirthdays { name: Some("Belka") }]) }"##;
31+
32+
assert_eq!(format!("{:?}", response_data), expected);
33+
34+
assert_eq!(
35+
response_data.dog_birthdays.map(|birthdays| birthdays.len()),
36+
Some(4)
37+
);
38+
}

0 commit comments

Comments
 (0)