Skip to content

Commit

Permalink
Allow using Rust arrays as GraphQL lists (#918) (#966)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
tyranron authored Jul 24, 2021
1 parent 8a90f86 commit 39d1e43
Show file tree
Hide file tree
Showing 25 changed files with 629 additions and 70 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use juniper::{graphql_interface, GraphQLObject};

#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue)]
pub struct ObjA {
test: String,
}

#[graphql_interface]
impl Character for ObjA {}

#[graphql_interface(for = ObjA)]
trait Character {
fn wrong(
&self,
#[graphql(default = [true, false, false])]
input: [bool; 2],
) -> bool {
input[0]
}
}

fn main() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
error[E0277]: the trait bound `[bool; 2]: From<[bool; 3]>` is not satisfied
--> $DIR/argument_wrong_default_array.rs:12:1
|
12 | #[graphql_interface(for = ObjA)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `From<[bool; 3]>` is not implemented for `[bool; 2]`
|
= help: the following implementations were found:
<&'a [ascii::ascii_char::AsciiChar] as From<&'a ascii::ascii_str::AsciiStr>>
<&'a [u8] as From<&'a ascii::ascii_str::AsciiStr>>
<&'a mut [ascii::ascii_char::AsciiChar] as From<&'a mut ascii::ascii_str::AsciiStr>>
= note: required because of the requirements on the impl of `Into<[bool; 2]>` for `[bool; 3]`
= note: this error originates in the attribute macro `graphql_interface` (in Nightly builds, run with -Z macro-backtrace for more info)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
struct Object;

#[juniper::graphql_object]
impl Object {
#[graphql(arguments(input(default = [true, false, false])))]
fn wrong(input: [bool; 2]) -> bool {
input[0]
}
}

fn main() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
error[E0308]: mismatched types
--> $DIR/impl_argument_wrong_default_array.rs:3:1
|
3 | #[juniper::graphql_object]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ expected an array with a fixed size of 2 elements, found one with 3 elements
|
= note: this error originates in the attribute macro `juniper::graphql_object` (in Nightly builds, run with -Z macro-backtrace for more info)
257 changes: 257 additions & 0 deletions integration_tests/juniper_tests/src/array.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
use juniper::{
graphql_object, graphql_value, EmptyMutation, EmptySubscription, GraphQLInputObject, RootNode,
Variables,
};

mod as_output_field {
use super::*;

struct Query;

#[graphql_object]
impl Query {
fn roll() -> [bool; 3] {
[true, false, true]
}
}

#[tokio::test]
async fn works() {
let query = r#"
query Query {
roll
}
"#;

let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new());
let (res, errors) = juniper::execute(query, None, &schema, &Variables::new(), &())
.await
.unwrap();

assert_eq!(errors.len(), 0);
assert_eq!(res, graphql_value!({"roll": [true, false, true]}));
}
}

mod as_input_field {
use super::*;

#[derive(GraphQLInputObject)]
struct Input {
two: [bool; 2],
}

#[derive(GraphQLInputObject)]
struct InputSingle {
one: [bool; 1],
}

struct Query;

#[graphql_object]
impl Query {
fn first(input: InputSingle) -> bool {
input.one[0]
}

fn second(input: Input) -> bool {
input.two[1]
}
}

#[tokio::test]
async fn works() {
let query = r#"
query Query {
second(input: { two: [true, false] })
}
"#;

let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new());
let (res, errors) = juniper::execute(query, None, &schema, &Variables::new(), &())
.await
.unwrap();

assert_eq!(errors.len(), 0);
assert_eq!(res, graphql_value!({"second": false}));
}

#[tokio::test]
async fn fails_on_incorrect_count() {
let query = r#"
query Query {
second(input: { two: [true, true, false] })
}
"#;

let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new());
let res = juniper::execute(query, None, &schema, &Variables::new(), &()).await;

assert!(res.is_err());
assert!(res
.unwrap_err()
.to_string()
.contains(r#"Invalid value for argument "input", expected type "Input!""#));
}

#[tokio::test]
async fn cannot_coerce_from_raw_value_if_multiple() {
let query = r#"
query Query {
second(input: { two: true })
}
"#;

let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new());
let res = juniper::execute(query, None, &schema, &Variables::new(), &()).await;

assert!(res.is_err());
assert!(res
.unwrap_err()
.to_string()
.contains(r#"Invalid value for argument "input", expected type "Input!""#));
}

#[tokio::test]
async fn can_coerce_from_raw_value_if_single() {
let query = r#"
query Query {
first(input: { one: true })
}
"#;

let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new());
let (res, errors) = juniper::execute(query, None, &schema, &Variables::new(), &())
.await
.unwrap();

assert_eq!(errors.len(), 0);
assert_eq!(res, graphql_value!({"first": true}));
}
}

mod as_input_argument {
use super::*;

struct Query;

#[graphql_object]
impl Query {
fn second(input: [bool; 2]) -> bool {
input[1]
}

fn first(input: [bool; 1]) -> bool {
input[0]
}

#[graphql(arguments(input(default = [true, false, false])))]
fn third(input: [bool; 3]) -> bool {
input[2]
}
}

#[tokio::test]
async fn works() {
let query = r#"
query Query {
second(input: [false, true])
}
"#;

let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new());
let (res, errors) = juniper::execute(query, None, &schema, &Variables::new(), &())
.await
.unwrap();

assert_eq!(errors.len(), 0);
assert_eq!(res, graphql_value!({"second": true}));
}

#[tokio::test]
async fn fails_on_incorrect_count() {
let query = r#"
query Query {
second(input: [true, true, false])
}
"#;

let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new());
let res = juniper::execute(query, None, &schema, &Variables::new(), &()).await;

assert!(res.is_err());
assert!(res
.unwrap_err()
.to_string()
.contains(r#"Invalid value for argument "input", expected type "[Boolean!]!""#));
}

#[tokio::test]
async fn cannot_coerce_from_raw_value_if_multiple() {
let query = r#"
query Query {
second(input: true)
}
"#;

let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new());
let res = juniper::execute(query, None, &schema, &Variables::new(), &()).await;

assert!(res.is_err());
assert!(res
.unwrap_err()
.to_string()
.contains(r#"Invalid value for argument "input", expected type "[Boolean!]!""#));
}

#[tokio::test]
async fn can_coerce_from_raw_value_if_single() {
let query = r#"
query Query {
first(input: true)
}
"#;

let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new());
let (res, errors) = juniper::execute(query, None, &schema, &Variables::new(), &())
.await
.unwrap();

assert_eq!(errors.len(), 0);
assert_eq!(res, graphql_value!({"first": true}));
}

#[tokio::test]
async fn picks_default() {
let query = r#"
query Query {
third
}
"#;

let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new());
let (res, errors) = juniper::execute(query, None, &schema, &Variables::new(), &())
.await
.unwrap();

assert_eq!(errors.len(), 0);
assert_eq!(res, graphql_value!({"third": false}));
}

#[tokio::test]
async fn picks_specified_over_default() {
let query = r#"
query Query {
third(input: [false, false, true])
}
"#;

let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new());
let (res, errors) = juniper::execute(query, None, &schema, &Variables::new(), &())
.await
.unwrap();

assert_eq!(errors.len(), 0);
assert_eq!(res, graphql_value!({"third": true}));
}
}
2 changes: 2 additions & 0 deletions integration_tests/juniper_tests/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#[cfg(test)]
mod arc_fields;
#[cfg(test)]
mod array;
#[cfg(test)]
mod codegen;
#[cfg(test)]
mod custom_scalar;
Expand Down
1 change: 1 addition & 0 deletions juniper/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

- 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))
- Expose `GraphQLRequest` fields ([#750](https://github.com/graphql-rust/juniper/issues/750))
- 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))

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

Expand Down
12 changes: 6 additions & 6 deletions juniper/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ pub enum Type<'a> {
/// A nullable list type, e.g. `[String]`
///
/// The list itself is what's nullable, the containing type might be non-null.
List(Box<Type<'a>>),
List(Box<Type<'a>>, Option<usize>),
/// A non-null named type, e.g. `String!`
NonNullNamed(Cow<'a, str>),
/// A non-null list type, e.g. `[String]!`.
///
/// The list itself is what's non-null, the containing type might be null.
NonNullList(Box<Type<'a>>),
NonNullList(Box<Type<'a>>, Option<usize>),
}

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

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

Expand All @@ -207,8 +207,8 @@ impl<'a> fmt::Display for Type<'a> {
match *self {
Type::Named(ref n) => write!(f, "{}", n),
Type::NonNullNamed(ref n) => write!(f, "{}!", n),
Type::List(ref t) => write!(f, "[{}]", t),
Type::NonNullList(ref t) => write!(f, "[{}]!", t),
Type::List(ref t, _) => write!(f, "[{}]", t),
Type::NonNullList(ref t, _) => write!(f, "[{}]!", t),
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion juniper/src/executor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1235,9 +1235,10 @@ where
pub fn build_list_type<T: GraphQLType<S> + ?Sized>(
&mut self,
info: &T::TypeInfo,
expected_size: Option<usize>,
) -> ListMeta<'r> {
let of_type = self.get_type::<T>(info);
ListMeta::new(of_type)
ListMeta::new(of_type, expected_size)
}

/// Create a nullable meta type
Expand Down
Loading

0 comments on commit 39d1e43

Please sign in to comment.