Skip to content

Commit 9704efd

Browse files
authored
Merge pull request #19 from tomhoule/input-objects
Input objects support
2 parents a46a3a9 + 4d41bd0 commit 9704efd

13 files changed

+345
-19
lines changed

graphql_query_derive/src/constants.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,7 @@ pub const TYPENAME_FIELD: &'static str = "__typename";
55
pub fn string_type() -> Ident {
66
Ident::new("String", Span::call_site())
77
}
8+
9+
pub fn float_type() -> Ident {
10+
Ident::new("Float", Span::call_site())
11+
}

graphql_query_derive/src/field_type.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use proc_macro2::{Ident, Span, TokenStream};
55
use query::QueryContext;
66
use schema::DEFAULT_SCALARS;
77

8-
#[derive(Debug, PartialEq)]
8+
#[derive(Debug, PartialEq, Hash)]
99
pub enum FieldType {
1010
Named(Ident),
1111
Optional(Box<FieldType>),
@@ -15,6 +15,11 @@ pub enum FieldType {
1515
impl FieldType {
1616
/// Takes a field type with its name
1717
pub fn to_rust(&self, context: &QueryContext, prefix: &str) -> TokenStream {
18+
let prefix: String = if prefix.is_empty() {
19+
self.inner_name_string()
20+
} else {
21+
prefix.to_string()
22+
};
1823
match &self {
1924
FieldType::Named(name) => {
2025
let name_string = name.to_string();
@@ -32,17 +37,20 @@ impl FieldType {
3237
Span::call_site(),
3338
)
3439
} else {
35-
Ident::new(prefix, Span::call_site())
40+
if prefix.is_empty() {
41+
panic!("Empty prefix for {:?}", self);
42+
}
43+
Ident::new(&prefix, Span::call_site())
3644
};
3745

3846
quote!(#name)
3947
}
4048
FieldType::Optional(inner) => {
41-
let inner = inner.to_rust(context, prefix);
49+
let inner = inner.to_rust(context, &prefix);
4250
quote!( Option<#inner>)
4351
}
4452
FieldType::Vector(inner) => {
45-
let inner = inner.to_rust(context, prefix);
53+
let inner = inner.to_rust(context, &prefix);
4654
quote!( Vec<#inner>)
4755
}
4856
}
@@ -133,6 +141,12 @@ impl ::std::convert::From<introspection_response::FullTypeFieldsType> for FieldT
133141
}
134142
}
135143

144+
impl ::std::convert::From<introspection_response::InputValueType> for FieldType {
145+
fn from(schema_type: introspection_response::InputValueType) -> FieldType {
146+
from_json_type_inner(&schema_type.type_ref, false)
147+
}
148+
}
149+
136150
#[cfg(test)]
137151
mod tests {
138152
use super::*;

graphql_query_derive/src/inputs.rs

Lines changed: 151 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,152 @@
1+
use failure;
2+
use graphql_parser;
3+
use introspection_response;
4+
use objects::GqlObjectField;
5+
use proc_macro2::{Ident, Span, TokenStream};
6+
use query::QueryContext;
7+
use std::collections::HashMap;
8+
19
#[derive(Debug, PartialEq)]
2-
pub struct GqlInput;
10+
pub struct GqlInput {
11+
pub name: String,
12+
pub fields: HashMap<String, GqlObjectField>,
13+
}
14+
15+
impl GqlInput {
16+
pub fn to_rust(&self, context: &QueryContext) -> Result<TokenStream, failure::Error> {
17+
let name = Ident::new(&self.name, Span::call_site());
18+
let mut fields: Vec<&GqlObjectField> = self.fields.values().collect();
19+
fields.sort_unstable_by(|a, b| a.name.cmp(&b.name));
20+
let fields = fields.iter().map(|field| {
21+
let ty = field.type_.to_rust(&context, "");
22+
let name = Ident::new(&field.name, Span::call_site());
23+
quote!(pub #name: #ty)
24+
});
25+
26+
Ok(quote! {
27+
#[derive(Debug, Serialize)]
28+
#[serde(rename_all = "camelCase")]
29+
pub struct #name {
30+
#(#fields,)*
31+
}
32+
})
33+
}
34+
}
35+
36+
impl ::std::convert::From<graphql_parser::schema::InputObjectType> for GqlInput {
37+
fn from(schema_input: graphql_parser::schema::InputObjectType) -> GqlInput {
38+
GqlInput {
39+
name: schema_input.name,
40+
fields: schema_input
41+
.fields
42+
.into_iter()
43+
.map(|field| {
44+
let name = field.name.clone();
45+
let field = GqlObjectField {
46+
name: field.name,
47+
type_: field.value_type.into(),
48+
};
49+
(name, field)
50+
})
51+
.collect(),
52+
}
53+
}
54+
}
55+
56+
impl ::std::convert::From<introspection_response::FullType> for GqlInput {
57+
fn from(schema_input: introspection_response::FullType) -> GqlInput {
58+
GqlInput {
59+
name: schema_input.name.expect("unnamed input object"),
60+
fields: schema_input
61+
.input_fields
62+
.expect("fields on input object")
63+
.into_iter()
64+
.filter_map(|a| a)
65+
.map(|f| {
66+
let name = f.input_value.name.expect("unnamed input object field");
67+
let field = GqlObjectField {
68+
name: name.clone(),
69+
type_: f
70+
.input_value
71+
.type_
72+
.expect("type on input object field")
73+
.into(),
74+
};
75+
(name, field)
76+
})
77+
.collect(),
78+
}
79+
}
80+
}
81+
82+
#[cfg(test)]
83+
mod tests {
84+
use super::*;
85+
use constants::*;
86+
use field_type::FieldType;
87+
88+
#[test]
89+
fn gql_input_to_rust() {
90+
let cat = GqlInput {
91+
name: "Cat".to_string(),
92+
fields: vec![
93+
(
94+
"pawsCount".to_string(),
95+
GqlObjectField {
96+
name: "pawsCount".to_string(),
97+
type_: FieldType::Named(float_type()),
98+
},
99+
),
100+
(
101+
"offsprings".to_string(),
102+
GqlObjectField {
103+
name: "offsprings".to_string(),
104+
type_: FieldType::Vector(Box::new(FieldType::Named(Ident::new(
105+
"Cat",
106+
Span::call_site(),
107+
)))),
108+
},
109+
),
110+
(
111+
"requirements".to_string(),
112+
GqlObjectField {
113+
name: "requirements".to_string(),
114+
type_: FieldType::Optional(Box::new(FieldType::Named(Ident::new(
115+
"CatRequirements",
116+
Span::call_site(),
117+
)))),
118+
},
119+
),
120+
].into_iter()
121+
.collect(),
122+
};
123+
124+
let expected: String = vec![
125+
"# [ derive ( Debug , Serialize ) ] ",
126+
"# [ serde ( rename_all = \"camelCase\" ) ] ",
127+
"pub struct Cat { ",
128+
"pub offsprings : Vec < Cat > , ",
129+
"pub pawsCount : Float , ",
130+
"pub requirements : Option < CatRequirements > , ",
131+
"}",
132+
].into_iter()
133+
.collect();
134+
135+
let mut context = QueryContext::new_empty();
136+
context.schema.inputs.insert(cat.name.clone(), cat);
137+
138+
assert_eq!(
139+
format!(
140+
"{}",
141+
context
142+
.schema
143+
.inputs
144+
.get("Cat")
145+
.unwrap()
146+
.to_rust(&context)
147+
.unwrap()
148+
),
149+
expected
150+
);
151+
}
152+
}

graphql_query_derive/src/introspection_response.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ pub struct FullTypeFields {
169169
#[serde(rename_all = "camelCase")]
170170
pub struct FullTypeInputFields {
171171
#[serde(flatten)]
172-
input_value: InputValue,
172+
pub input_value: InputValue,
173173
}
174174

175175
#[derive(Clone, Debug, Deserialize)]
@@ -198,11 +198,11 @@ pub struct FullTypePossibleTypes {
198198
#[derive(Clone, Debug, Deserialize)]
199199
#[serde(rename_all = "camelCase")]
200200
pub struct InputValue {
201-
name: Option<String>,
202-
description: Option<String>,
201+
pub name: Option<String>,
202+
pub description: Option<String>,
203203
#[serde(rename = "type")]
204-
type_: Option<InputValueType>,
205-
default_value: Option<String>,
204+
pub type_: Option<InputValueType>,
205+
pub default_value: Option<String>,
206206
}
207207

208208
#[derive(Clone, Debug, Deserialize)]

graphql_query_derive/src/objects.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ pub struct GqlObject {
1414
pub fields: Vec<GqlObjectField>,
1515
}
1616

17-
#[derive(Debug, PartialEq)]
17+
#[derive(Debug, PartialEq, Hash)]
1818
pub struct GqlObjectField {
1919
pub name: String,
2020
pub type_: FieldType,

graphql_query_derive/src/query.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use selection::Selection;
77
use std::collections::BTreeMap;
88
use variables::Variable;
99

10+
/// This holds all the information we need during the code generation phase.
1011
pub struct QueryContext {
1112
pub _subscription_root: Option<Vec<TokenStream>>,
1213
pub fragments: BTreeMap<String, GqlFragment>,
@@ -17,6 +18,7 @@ pub struct QueryContext {
1718
}
1819

1920
impl QueryContext {
21+
/// Create a QueryContext with the given Schema.
2022
pub fn new(schema: Schema) -> QueryContext {
2123
QueryContext {
2224
_subscription_root: None,
@@ -28,12 +30,14 @@ impl QueryContext {
2830
}
2931
}
3032

33+
/// Ingest the variable definitions for the query we are generating code for.
3134
pub fn register_variables(&mut self, variables: &[graphql_parser::query::VariableDefinition]) {
3235
variables.iter().for_each(|variable| {
3336
self.variables.push(variable.clone().into());
3437
});
3538
}
3639

40+
/// Generate the Variables struct and all the necessary supporting code.
3741
pub fn expand_variables(&self) -> TokenStream {
3842
if self.variables.is_empty() {
3943
return quote!(#[derive(Serialize)]

graphql_query_derive/src/schema.rs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,20 +146,31 @@ impl Schema {
146146
.or(context._subscription_root.as_ref())
147147
.expect("no selection defined");
148148

149-
use proc_macro2::{Ident, Span};
150-
149+
// TODO: do something smarter here
151150
let scalar_definitions = context.schema.scalars.iter().map(|scalar_name| {
151+
use proc_macro2::{Ident, Span};
152152
let ident = Ident::new(scalar_name, Span::call_site());
153153
quote!(type #ident = String;)
154154
});
155155

156+
let input_object_definitions: Result<Vec<TokenStream>, _> = context
157+
.schema
158+
.inputs
159+
.values()
160+
.map(|i| i.to_rust(&context))
161+
.collect();
162+
let input_object_definitions = input_object_definitions?;
163+
156164
Ok(quote! {
157165
type Boolean = bool;
158166
type Float = f64;
159167
type Int = i64;
168+
type ID = String;
160169

161170
#(#scalar_definitions)*
162171

172+
#(#input_object_definitions)*
173+
163174
#(#enum_definitions)*
164175

165176
#(#fragment_definitions)*
@@ -235,7 +246,9 @@ impl ::std::convert::From<graphql_parser::schema::Document> for Schema {
235246
);
236247
}
237248
schema::TypeDefinition::InputObject(input) => {
238-
schema.inputs.insert(input.name, GqlInput);
249+
schema
250+
.inputs
251+
.insert(input.name.clone(), GqlInput::from(input));
239252
}
240253
},
241254
schema::Definition::DirectiveDefinition(_) => (),
@@ -342,7 +355,7 @@ impl ::std::convert::From<::introspection_response::IntrospectionResponse> for S
342355
schema.interfaces.insert(name, iface);
343356
}
344357
Some(__TypeKind::INPUT_OBJECT) => {
345-
schema.inputs.insert(name, GqlInput);
358+
schema.inputs.insert(name, GqlInput::from(ty.clone()));
346359
}
347360
_ => unimplemented!("unimplemented definition"),
348361
}

graphql_query_derive/src/tests/github.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ fn ast_from_graphql_and_json_produce_the_same_schema() {
2525
assert_eq!(json.query_type, gql.query_type);
2626
assert_eq!(json.mutation_type, gql.mutation_type);
2727
assert_eq!(json.subscription_type, gql.subscription_type);
28-
assert_eq!(json.inputs, gql.inputs);
28+
for (json, gql) in json.inputs.iter().zip(gql.inputs.iter()) {
29+
assert_eq!(json, gql);
30+
}
31+
assert_eq!(json.inputs, gql.inputs, "inputs differ");
2932
for ((json_name, json_value), (gql_name, gql_value)) in json.enums.iter().zip(gql.enums.iter())
3033
{
3134
assert_eq!(json_name, gql_name);

0 commit comments

Comments
 (0)