Skip to content

Commit 39d1e43

Browse files
authored
Allow using Rust arrays as GraphQL lists (#918) (#966)
* Provide impls for arrays * Remove redundant Default bound * Recheck other places of mem::transmute usage * Fix missing marker impls * Extend GraphQL list validation with optional expected size * Improve input object codegen * Cover arrays with tests * Add CHANGELOG entry * Consider panic safety in FromInputValue implementation for array * Tune up codegen failure tests
1 parent 8a90f86 commit 39d1e43

25 files changed

+629
-70
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
use juniper::{graphql_interface, GraphQLObject};
2+
3+
#[derive(GraphQLObject)]
4+
#[graphql(impl = CharacterValue)]
5+
pub struct ObjA {
6+
test: String,
7+
}
8+
9+
#[graphql_interface]
10+
impl Character for ObjA {}
11+
12+
#[graphql_interface(for = ObjA)]
13+
trait Character {
14+
fn wrong(
15+
&self,
16+
#[graphql(default = [true, false, false])]
17+
input: [bool; 2],
18+
) -> bool {
19+
input[0]
20+
}
21+
}
22+
23+
fn main() {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
error[E0277]: the trait bound `[bool; 2]: From<[bool; 3]>` is not satisfied
2+
--> $DIR/argument_wrong_default_array.rs:12:1
3+
|
4+
12 | #[graphql_interface(for = ObjA)]
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `From<[bool; 3]>` is not implemented for `[bool; 2]`
6+
|
7+
= help: the following implementations were found:
8+
<&'a [ascii::ascii_char::AsciiChar] as From<&'a ascii::ascii_str::AsciiStr>>
9+
<&'a [u8] as From<&'a ascii::ascii_str::AsciiStr>>
10+
<&'a mut [ascii::ascii_char::AsciiChar] as From<&'a mut ascii::ascii_str::AsciiStr>>
11+
= note: required because of the requirements on the impl of `Into<[bool; 2]>` for `[bool; 3]`
12+
= note: this error originates in the attribute macro `graphql_interface` (in Nightly builds, run with -Z macro-backtrace for more info)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
struct Object;
2+
3+
#[juniper::graphql_object]
4+
impl Object {
5+
#[graphql(arguments(input(default = [true, false, false])))]
6+
fn wrong(input: [bool; 2]) -> bool {
7+
input[0]
8+
}
9+
}
10+
11+
fn main() {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
error[E0308]: mismatched types
2+
--> $DIR/impl_argument_wrong_default_array.rs:3:1
3+
|
4+
3 | #[juniper::graphql_object]
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ expected an array with a fixed size of 2 elements, found one with 3 elements
6+
|
7+
= note: this error originates in the attribute macro `juniper::graphql_object` (in Nightly builds, run with -Z macro-backtrace for more info)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
use juniper::{
2+
graphql_object, graphql_value, EmptyMutation, EmptySubscription, GraphQLInputObject, RootNode,
3+
Variables,
4+
};
5+
6+
mod as_output_field {
7+
use super::*;
8+
9+
struct Query;
10+
11+
#[graphql_object]
12+
impl Query {
13+
fn roll() -> [bool; 3] {
14+
[true, false, true]
15+
}
16+
}
17+
18+
#[tokio::test]
19+
async fn works() {
20+
let query = r#"
21+
query Query {
22+
roll
23+
}
24+
"#;
25+
26+
let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new());
27+
let (res, errors) = juniper::execute(query, None, &schema, &Variables::new(), &())
28+
.await
29+
.unwrap();
30+
31+
assert_eq!(errors.len(), 0);
32+
assert_eq!(res, graphql_value!({"roll": [true, false, true]}));
33+
}
34+
}
35+
36+
mod as_input_field {
37+
use super::*;
38+
39+
#[derive(GraphQLInputObject)]
40+
struct Input {
41+
two: [bool; 2],
42+
}
43+
44+
#[derive(GraphQLInputObject)]
45+
struct InputSingle {
46+
one: [bool; 1],
47+
}
48+
49+
struct Query;
50+
51+
#[graphql_object]
52+
impl Query {
53+
fn first(input: InputSingle) -> bool {
54+
input.one[0]
55+
}
56+
57+
fn second(input: Input) -> bool {
58+
input.two[1]
59+
}
60+
}
61+
62+
#[tokio::test]
63+
async fn works() {
64+
let query = r#"
65+
query Query {
66+
second(input: { two: [true, false] })
67+
}
68+
"#;
69+
70+
let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new());
71+
let (res, errors) = juniper::execute(query, None, &schema, &Variables::new(), &())
72+
.await
73+
.unwrap();
74+
75+
assert_eq!(errors.len(), 0);
76+
assert_eq!(res, graphql_value!({"second": false}));
77+
}
78+
79+
#[tokio::test]
80+
async fn fails_on_incorrect_count() {
81+
let query = r#"
82+
query Query {
83+
second(input: { two: [true, true, false] })
84+
}
85+
"#;
86+
87+
let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new());
88+
let res = juniper::execute(query, None, &schema, &Variables::new(), &()).await;
89+
90+
assert!(res.is_err());
91+
assert!(res
92+
.unwrap_err()
93+
.to_string()
94+
.contains(r#"Invalid value for argument "input", expected type "Input!""#));
95+
}
96+
97+
#[tokio::test]
98+
async fn cannot_coerce_from_raw_value_if_multiple() {
99+
let query = r#"
100+
query Query {
101+
second(input: { two: true })
102+
}
103+
"#;
104+
105+
let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new());
106+
let res = juniper::execute(query, None, &schema, &Variables::new(), &()).await;
107+
108+
assert!(res.is_err());
109+
assert!(res
110+
.unwrap_err()
111+
.to_string()
112+
.contains(r#"Invalid value for argument "input", expected type "Input!""#));
113+
}
114+
115+
#[tokio::test]
116+
async fn can_coerce_from_raw_value_if_single() {
117+
let query = r#"
118+
query Query {
119+
first(input: { one: true })
120+
}
121+
"#;
122+
123+
let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new());
124+
let (res, errors) = juniper::execute(query, None, &schema, &Variables::new(), &())
125+
.await
126+
.unwrap();
127+
128+
assert_eq!(errors.len(), 0);
129+
assert_eq!(res, graphql_value!({"first": true}));
130+
}
131+
}
132+
133+
mod as_input_argument {
134+
use super::*;
135+
136+
struct Query;
137+
138+
#[graphql_object]
139+
impl Query {
140+
fn second(input: [bool; 2]) -> bool {
141+
input[1]
142+
}
143+
144+
fn first(input: [bool; 1]) -> bool {
145+
input[0]
146+
}
147+
148+
#[graphql(arguments(input(default = [true, false, false])))]
149+
fn third(input: [bool; 3]) -> bool {
150+
input[2]
151+
}
152+
}
153+
154+
#[tokio::test]
155+
async fn works() {
156+
let query = r#"
157+
query Query {
158+
second(input: [false, true])
159+
}
160+
"#;
161+
162+
let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new());
163+
let (res, errors) = juniper::execute(query, None, &schema, &Variables::new(), &())
164+
.await
165+
.unwrap();
166+
167+
assert_eq!(errors.len(), 0);
168+
assert_eq!(res, graphql_value!({"second": true}));
169+
}
170+
171+
#[tokio::test]
172+
async fn fails_on_incorrect_count() {
173+
let query = r#"
174+
query Query {
175+
second(input: [true, true, false])
176+
}
177+
"#;
178+
179+
let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new());
180+
let res = juniper::execute(query, None, &schema, &Variables::new(), &()).await;
181+
182+
assert!(res.is_err());
183+
assert!(res
184+
.unwrap_err()
185+
.to_string()
186+
.contains(r#"Invalid value for argument "input", expected type "[Boolean!]!""#));
187+
}
188+
189+
#[tokio::test]
190+
async fn cannot_coerce_from_raw_value_if_multiple() {
191+
let query = r#"
192+
query Query {
193+
second(input: true)
194+
}
195+
"#;
196+
197+
let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new());
198+
let res = juniper::execute(query, None, &schema, &Variables::new(), &()).await;
199+
200+
assert!(res.is_err());
201+
assert!(res
202+
.unwrap_err()
203+
.to_string()
204+
.contains(r#"Invalid value for argument "input", expected type "[Boolean!]!""#));
205+
}
206+
207+
#[tokio::test]
208+
async fn can_coerce_from_raw_value_if_single() {
209+
let query = r#"
210+
query Query {
211+
first(input: true)
212+
}
213+
"#;
214+
215+
let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new());
216+
let (res, errors) = juniper::execute(query, None, &schema, &Variables::new(), &())
217+
.await
218+
.unwrap();
219+
220+
assert_eq!(errors.len(), 0);
221+
assert_eq!(res, graphql_value!({"first": true}));
222+
}
223+
224+
#[tokio::test]
225+
async fn picks_default() {
226+
let query = r#"
227+
query Query {
228+
third
229+
}
230+
"#;
231+
232+
let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new());
233+
let (res, errors) = juniper::execute(query, None, &schema, &Variables::new(), &())
234+
.await
235+
.unwrap();
236+
237+
assert_eq!(errors.len(), 0);
238+
assert_eq!(res, graphql_value!({"third": false}));
239+
}
240+
241+
#[tokio::test]
242+
async fn picks_specified_over_default() {
243+
let query = r#"
244+
query Query {
245+
third(input: [false, false, true])
246+
}
247+
"#;
248+
249+
let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new());
250+
let (res, errors) = juniper::execute(query, None, &schema, &Variables::new(), &())
251+
.await
252+
.unwrap();
253+
254+
assert_eq!(errors.len(), 0);
255+
assert_eq!(res, graphql_value!({"third": true}));
256+
}
257+
}

integration_tests/juniper_tests/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#[cfg(test)]
22
mod arc_fields;
33
#[cfg(test)]
4+
mod array;
5+
#[cfg(test)]
46
mod codegen;
57
#[cfg(test)]
68
mod custom_scalar;

juniper/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
- Allow spreading interface fragments on unions and other interfaces ([#965](https://github.com/graphql-rust/juniper/pull/965), [#798](https://github.com/graphql-rust/juniper/issues/798))
44
- Expose `GraphQLRequest` fields ([#750](https://github.com/graphql-rust/juniper/issues/750))
5+
- Support using Rust array as GraphQL list ([#966](https://github.com/graphql-rust/juniper/pull/966), [#918](https://github.com/graphql-rust/juniper/issues/918))
56

67
# [[0.15.7] 2021-07-08](https://github.com/graphql-rust/juniper/releases/tag/juniper-v0.15.7)
78

juniper/src/ast.rs

+6-6
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ pub enum Type<'a> {
1919
/// A nullable list type, e.g. `[String]`
2020
///
2121
/// The list itself is what's nullable, the containing type might be non-null.
22-
List(Box<Type<'a>>),
22+
List(Box<Type<'a>>, Option<usize>),
2323
/// A non-null named type, e.g. `String!`
2424
NonNullNamed(Cow<'a, str>),
2525
/// A non-null list type, e.g. `[String]!`.
2626
///
2727
/// The list itself is what's non-null, the containing type might be null.
28-
NonNullList(Box<Type<'a>>),
28+
NonNullList(Box<Type<'a>>, Option<usize>),
2929
}
3030

3131
/// A JSON-like value that can be passed into the query execution, either
@@ -192,13 +192,13 @@ impl<'a> Type<'a> {
192192
pub fn innermost_name(&self) -> &str {
193193
match *self {
194194
Type::Named(ref n) | Type::NonNullNamed(ref n) => n,
195-
Type::List(ref l) | Type::NonNullList(ref l) => l.innermost_name(),
195+
Type::List(ref l, _) | Type::NonNullList(ref l, _) => l.innermost_name(),
196196
}
197197
}
198198

199199
/// Determines if a type only can represent non-null values.
200200
pub fn is_non_null(&self) -> bool {
201-
matches!(*self, Type::NonNullNamed(_) | Type::NonNullList(_))
201+
matches!(*self, Type::NonNullNamed(_) | Type::NonNullList(..))
202202
}
203203
}
204204

@@ -207,8 +207,8 @@ impl<'a> fmt::Display for Type<'a> {
207207
match *self {
208208
Type::Named(ref n) => write!(f, "{}", n),
209209
Type::NonNullNamed(ref n) => write!(f, "{}!", n),
210-
Type::List(ref t) => write!(f, "[{}]", t),
211-
Type::NonNullList(ref t) => write!(f, "[{}]!", t),
210+
Type::List(ref t, _) => write!(f, "[{}]", t),
211+
Type::NonNullList(ref t, _) => write!(f, "[{}]!", t),
212212
}
213213
}
214214
}

juniper/src/executor/mod.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1235,9 +1235,10 @@ where
12351235
pub fn build_list_type<T: GraphQLType<S> + ?Sized>(
12361236
&mut self,
12371237
info: &T::TypeInfo,
1238+
expected_size: Option<usize>,
12381239
) -> ListMeta<'r> {
12391240
let of_type = self.get_type::<T>(info);
1240-
ListMeta::new(of_type)
1241+
ListMeta::new(of_type, expected_size)
12411242
}
12421243

12431244
/// Create a nullable meta type

0 commit comments

Comments
 (0)