Add ability to distinguish between implicit and explicit null (#795)

* add Nullable to distinguish between implicit and explicit null

* cargo fmt

* add page to book

* address comment
This commit is contained in:
Chris 2020-10-23 19:21:37 -04:00 committed by GitHub
parent 61d1365b15
commit cd66bdb450
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 576 additions and 33 deletions

View file

@ -29,6 +29,7 @@
- [Introspection](advanced/introspection.md)
- [Non-struct objects](advanced/non_struct_objects.md)
- [Implicit and explicit null](advanced/implicit_and_explicit_null.md)
- [Objects and generics](advanced/objects_and_generics.md)
- [Multiple operations per request](advanced/multiple_ops_per_request.md)
- [Dataloaders](advanced/dataloaders.md)

View file

@ -0,0 +1,116 @@
# Implicit and explicit null
There are two ways that a client can submit a null argument or field in a query.
They can use a null literal:
```graphql
{
field(arg: null)
}
```
Or they can simply omit the argument:
```graphql
{
field
}
```
The former is an explicit null and the latter is an implicit null.
There are some situations where it's useful to know which one the user provided.
For example, let's say your business logic has a function that allows users to
perform a "patch" operation on themselves. Let's say your users can optionally
have favorite and least favorite numbers, and the input for that might look
like this:
```rust
/// Updates user attributes. Fields that are `None` are left as-is.
pub struct UserPatch {
/// If `Some`, updates the user's favorite number.
pub favorite_number: Option<Option<i32>>,
/// If `Some`, updates the user's least favorite number.
pub least_favorite_number: Option<Option<i32>>,
}
# fn main() {}
```
To set a user's favorite number to 7, you would set `favorite_number` to
`Some(Some(7))`. In GraphQL, that might look like this:
```graphql
mutation { patchUser(patch: { favoriteNumber: 7 }) }
```
To unset the user's favorite number, you would set `favorite_number` to
`Some(None)`. In GraphQL, that might look like this:
```graphql
mutation { patchUser(patch: { favoriteNumber: null }) }
```
If you want to leave the user's favorite number alone, you would set it to
`None`. In GraphQL, that might look like this:
```graphql
mutation { patchUser(patch: {}) }
```
The last two cases rely on being able to distinguish between explicit and implicit null.
In Juniper, this can be done using the `Nullable` type:
```rust
# extern crate juniper;
use juniper::{FieldResult, Nullable};
#[derive(juniper::GraphQLInputObject)]
struct UserPatchInput {
pub favorite_number: Nullable<i32>,
pub least_favorite_number: Nullable<i32>,
}
impl Into<UserPatch> for UserPatchInput {
fn into(self) -> UserPatch {
UserPatch {
// The `explicit` function transforms the `Nullable` into an
// `Option<Option<T>>` as expected by the business logic layer.
favorite_number: self.favorite_number.explicit(),
least_favorite_number: self.least_favorite_number.explicit(),
}
}
}
# pub struct UserPatch {
# pub favorite_number: Option<Option<i32>>,
# pub least_favorite_number: Option<Option<i32>>,
# }
# struct Session;
# impl Session {
# fn patch_user(&self, _patch: UserPatch) -> FieldResult<()> { Ok(()) }
# }
struct Context {
session: Session,
}
struct Mutation;
#[juniper::graphql_object(Context=Context)]
impl Mutation {
fn patch_user(ctx: &Context, patch: UserPatchInput) -> FieldResult<bool> {
ctx.session.patch_user(patch.into())?;
Ok(true)
}
}
# fn main() {}
```
This type functions much like `Option`, but has two empty variants so you can
distinguish between implicit and explicit null.

View file

@ -4,7 +4,8 @@ The chapters below cover some more advanced scenarios.
- [Introspection](introspection.md)
- [Non-struct objects](non_struct_objects.md)
- [Implicit and explicit null](implicit_and_explicit_null.md)
- [Objects and generics](objects_and_generics.md)
- [Multiple operations per request](multiple_ops_per_request.md)
- [Dataloaders](dataloaders.md)
- [Subscriptions](subscriptions.md)
- [Subscriptions](subscriptions.md)

View file

@ -0,0 +1,81 @@
use juniper::*;
pub struct Context;
impl juniper::Context for Context {}
pub struct Query;
#[derive(juniper::GraphQLInputObject)]
struct ObjectInput {
field: Nullable<i32>,
}
#[graphql_object(Context=Context)]
impl Query {
fn is_explicit_null(arg: Nullable<i32>) -> bool {
arg.is_explicit_null()
}
fn object_field_is_explicit_null(obj: ObjectInput) -> bool {
obj.field.is_explicit_null()
}
}
type Schema = juniper::RootNode<'static, Query, EmptyMutation<Context>, EmptySubscription<Context>>;
#[tokio::test]
async fn explicit_null() {
let ctx = Context;
let query = r#"
query Foo($emptyObj: ObjectInput!, $literalNullObj: ObjectInput!) {
literalOneIsExplicitNull: isExplicitNull(arg: 1)
literalNullIsExplicitNull: isExplicitNull(arg: null)
noArgIsExplicitNull: isExplicitNull
literalOneFieldIsExplicitNull: objectFieldIsExplicitNull(obj: {field: 1})
literalNullFieldIsExplicitNull: objectFieldIsExplicitNull(obj: {field: null})
noFieldIsExplicitNull: objectFieldIsExplicitNull(obj: {})
emptyVariableObjectFieldIsExplicitNull: objectFieldIsExplicitNull(obj: $emptyObj)
literalNullVariableObjectFieldIsExplicitNull: objectFieldIsExplicitNull(obj: $literalNullObj)
}
"#;
let (data, errors) = juniper::execute(
query,
None,
&Schema::new(
Query,
EmptyMutation::<Context>::new(),
EmptySubscription::<Context>::new(),
),
&[
("emptyObj".to_string(), InputValue::Object(vec![])),
(
"literalNullObj".to_string(),
InputValue::object(vec![("field", InputValue::null())].into_iter().collect()),
),
]
.iter()
.cloned()
.collect(),
&ctx,
)
.await
.unwrap();
assert_eq!(errors.len(), 0);
assert_eq!(
data,
graphql_value!({
"literalOneIsExplicitNull": false,
"literalNullIsExplicitNull": true,
"noArgIsExplicitNull": false,
"literalOneFieldIsExplicitNull": false,
"literalNullFieldIsExplicitNull": true,
"noFieldIsExplicitNull": false,
"emptyVariableObjectFieldIsExplicitNull": false,
"literalNullVariableObjectFieldIsExplicitNull": true,
})
);
}

View file

@ -5,6 +5,8 @@ mod codegen;
#[cfg(test)]
mod custom_scalar;
#[cfg(test)]
mod explicit_null;
#[cfg(test)]
mod issue_371;
#[cfg(test)]
mod issue_398;

View file

@ -34,6 +34,8 @@
- `RuleError`
- Support `chrono-tz::Tz` scalar behind a `chrono-tz` feature flag. ([#519](https://github.com/graphql-rust/juniper/pull/519))
- Added support for distinguishing between between implicit and explicit null. ([#795](https://github.com/graphql-rust/juniper/pull/795))
## Fixes

View file

@ -153,6 +153,14 @@ pub type Document<'a, S> = Vec<Definition<'a, S>>;
pub trait FromInputValue<S = DefaultScalarValue>: Sized {
/// Performs the conversion.
fn from_input_value(v: &InputValue<S>) -> Option<Self>;
/// Performs the conversion from an absent value (e.g. to distinguish between implicit and
/// explicit null). The default implementation just uses `from_input_value` as if an explicit
/// null were provided. This conversion must not fail.
fn from_implicit_null() -> Self {
Self::from_input_value(&InputValue::<S>::Null)
.expect("input value conversion from null must not fail")
}
}
/// Losslessly clones a Rust data type into an InputValue.

View file

@ -169,6 +169,7 @@ pub use crate::{
async_await::{DynGraphQLValueAsync, GraphQLTypeAsync, GraphQLValueAsync},
base::{Arguments, DynGraphQLValue, GraphQLType, GraphQLValue, TypeKind},
marker::{self, GraphQLInterface, GraphQLUnion},
nullable::Nullable,
scalars::{EmptyMutation, EmptySubscription, ID},
subscriptions::{
ExecutionOutput, GraphQLSubscriptionType, GraphQLSubscriptionValue,

View file

@ -91,8 +91,6 @@ where
if !args.contains_key(arg.name.as_str()) || args[arg.name.as_str()].is_null() {
if let Some(ref default_value) = arg.default_value {
args.insert(arg.name.as_str(), default_value.clone());
} else {
args.insert(arg.name.as_str(), InputValue::null());
}
}
}

View file

@ -3,6 +3,7 @@ pub mod base;
pub mod containers;
pub mod marker;
pub mod name;
pub mod nullable;
pub mod pointers;
pub mod scalars;
pub mod subscriptions;

View file

@ -0,0 +1,328 @@
use crate::{
ast::{FromInputValue, InputValue, Selection, ToInputValue},
executor::{ExecutionResult, Executor, Registry},
schema::meta::MetaType,
types::{
async_await::GraphQLValueAsync,
base::{GraphQLType, GraphQLValue},
marker::IsInputType,
},
value::{ScalarValue, Value},
};
/// `Nullable` can be used in situations where you need to distinguish between an implicitly and
/// explicitly null input value.
///
/// The GraphQL spec states that these two field calls are similar, but are not identical:
///
/// ```graphql
/// {
/// field(arg: null)
/// field
/// }
/// ```
///
/// The first has explicitly provided null to the argument “arg”, while the second has implicitly
/// not provided a value to the argument “arg”. These two forms may be interpreted differently. For
/// example, a mutation representing deleting a field vs not altering a field, respectively.
///
/// In cases where you do not need to be able to distinguish between the two types of null, you
/// should simply use `Option<T>`.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum Nullable<T> {
/// No value
ImplicitNull,
/// No value, explicitly specified to be null
ExplicitNull,
/// Some value `T`
Some(T),
}
impl<T> Default for Nullable<T> {
fn default() -> Self {
Self::ImplicitNull
}
}
impl<T> Nullable<T> {
/// Returns `true` if the nullable is a `ExplicitNull` value.
#[inline]
pub fn is_explicit_null(&self) -> bool {
match self {
Self::ExplicitNull => true,
_ => false,
}
}
/// Returns `true` if the nullable is a `ImplicitNull` value.
#[inline]
pub fn is_implicit_null(&self) -> bool {
match self {
Self::ImplicitNull => true,
_ => false,
}
}
/// Returns `true` if the nullable is a `Some` value.
#[inline]
pub fn is_some(&self) -> bool {
match self {
Self::Some(_) => true,
_ => false,
}
}
/// Returns `true` if the nullable is not a `Some` value.
#[inline]
pub fn is_null(&self) -> bool {
match self {
Self::Some(_) => false,
_ => true,
}
}
/// Converts from `&mut Nullable<T>` to `Nullable<&mut T>`.
#[inline]
pub fn as_mut(&mut self) -> Nullable<&mut T> {
match *self {
Self::Some(ref mut x) => Nullable::Some(x),
Self::ImplicitNull => Nullable::ImplicitNull,
Self::ExplicitNull => Nullable::ExplicitNull,
}
}
/// Returns the contained `Some` value, consuming the `self` value.
///
/// # Panics
///
/// Panics if the value is not a `Some` with a custom panic message provided by `msg`.
#[inline]
#[track_caller]
pub fn expect(self, msg: &str) -> T {
self.some().expect(msg)
}
/// Returns the contained `Some` value or a provided default.
#[inline]
pub fn unwrap_or(self, default: T) -> T {
self.some().unwrap_or(default)
}
/// Returns the contained `Some` value or computes it from a closure.
#[inline]
pub fn unwrap_or_else<F: FnOnce() -> T>(self, f: F) -> T {
self.some().unwrap_or_else(f)
}
/// Maps a `Nullable<T>` to `Nullable<U>` by applying a function to a contained value.
#[inline]
pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Nullable<U> {
match self {
Self::Some(x) => Nullable::Some(f(x)),
Self::ImplicitNull => Nullable::ImplicitNull,
Self::ExplicitNull => Nullable::ExplicitNull,
}
}
/// Applies a function to the contained value (if any), or returns the provided default (if
/// not).
#[inline]
pub fn map_or<U, F: FnOnce(T) -> U>(self, default: U, f: F) -> U {
self.some().map_or(default, f)
}
/// Applies a function to the contained value (if any), or computes a default (if not).
#[inline]
pub fn map_or_else<U, D: FnOnce() -> U, F: FnOnce(T) -> U>(self, default: D, f: F) -> U {
self.some().map_or_else(default, f)
}
/// Transforms the `Nullable<T>` into a `Result<T, E>`, mapping `Some(v)` to `Ok(v)` and
/// `ImplicitNull` or `ExplicitNull` to `Err(err)`.
#[inline]
pub fn ok_or<E>(self, err: E) -> Result<T, E> {
self.some().ok_or(err)
}
/// Transforms the `Nullable<T>` into a `Result<T, E>`, mapping `Some(v)` to `Ok(v)` and
/// `ImplicitNull` or `ExplicitNull` to `Err(err())`.
#[inline]
pub fn ok_or_else<E, F: FnOnce() -> E>(self, err: F) -> Result<T, E> {
self.some().ok_or_else(err)
}
/// Returns the nullable if it contains a value, otherwise returns `b`.
#[inline]
pub fn or(self, b: Self) -> Self {
match self {
Self::Some(_) => self,
_ => b,
}
}
/// Returns the nullable if it contains a value, otherwise calls `f` and
/// returns the result.
#[inline]
pub fn or_else<F: FnOnce() -> Nullable<T>>(self, f: F) -> Nullable<T> {
match self {
Self::Some(_) => self,
_ => f(),
}
}
/// Replaces the actual value in the nullable by the value given in parameter, returning the
/// old value if present, leaving a `Some` in its place without deinitializing either one.
#[inline]
pub fn replace(&mut self, value: T) -> Self {
std::mem::replace(self, Self::Some(value))
}
/// Converts from `Nullable<T>` to `Option<T>`.
pub fn some(self) -> Option<T> {
match self {
Self::Some(v) => Some(v),
_ => None,
}
}
/// Converts from `Nullable<T>` to `Option<Option<T>>`, mapping `Some(v)` to `Some(Some(v))`,
/// `ExplicitNull` to `Some(None)`, and `ImplicitNull` to `None`.
pub fn explicit(self) -> Option<Option<T>> {
match self {
Self::Some(v) => Some(Some(v)),
Self::ExplicitNull => Some(None),
Self::ImplicitNull => None,
}
}
}
impl<T: Copy> Nullable<&T> {
/// Maps a `Nullable<&T>` to a `Nullable<T>` by copying the contents of the nullable.
pub fn copied(self) -> Nullable<T> {
self.map(|&t| t)
}
}
impl<T: Copy> Nullable<&mut T> {
/// Maps a `Nullable<&mut T>` to a `Nullable<T>` by copying the contents of the nullable.
pub fn copied(self) -> Nullable<T> {
self.map(|&mut t| t)
}
}
impl<T: Clone> Nullable<&T> {
/// Maps a `Nullable<&T>` to a `Nullable<T>` by cloning the contents of the nullable.
pub fn cloned(self) -> Nullable<T> {
self.map(|t| t.clone())
}
}
impl<T: Clone> Nullable<&mut T> {
/// Maps a `Nullable<&mut T>` to a `Nullable<T>` by cloning the contents of the nullable.
pub fn cloned(self) -> Nullable<T> {
self.map(|t| t.clone())
}
}
impl<S, T> GraphQLType<S> for Nullable<T>
where
T: GraphQLType<S>,
S: ScalarValue,
{
fn name(_: &Self::TypeInfo) -> Option<&'static str> {
None
}
fn meta<'r>(info: &Self::TypeInfo, registry: &mut Registry<'r, S>) -> MetaType<'r, S>
where
S: 'r,
{
registry.build_nullable_type::<T>(info).into_meta()
}
}
impl<S, T> GraphQLValue<S> for Nullable<T>
where
S: ScalarValue,
T: GraphQLValue<S>,
{
type Context = T::Context;
type TypeInfo = T::TypeInfo;
fn type_name(&self, _: &Self::TypeInfo) -> Option<&'static str> {
None
}
fn resolve(
&self,
info: &Self::TypeInfo,
_: Option<&[Selection<S>]>,
executor: &Executor<Self::Context, S>,
) -> ExecutionResult<S> {
match *self {
Self::Some(ref obj) => executor.resolve(info, obj),
_ => Ok(Value::null()),
}
}
}
impl<S, T> GraphQLValueAsync<S> for Nullable<T>
where
T: GraphQLValueAsync<S>,
T::TypeInfo: Sync,
T::Context: Sync,
S: ScalarValue + Send + Sync,
{
fn resolve_async<'a>(
&'a self,
info: &'a Self::TypeInfo,
_: Option<&'a [Selection<S>]>,
executor: &'a Executor<Self::Context, S>,
) -> crate::BoxFuture<'a, ExecutionResult<S>> {
let f = async move {
let value = match self {
Self::Some(obj) => executor.resolve_into_value_async(info, obj).await,
_ => Value::null(),
};
Ok(value)
};
Box::pin(f)
}
}
impl<S, T> FromInputValue<S> for Nullable<T>
where
T: FromInputValue<S>,
S: ScalarValue,
{
fn from_input_value(v: &InputValue<S>) -> Option<Nullable<T>> {
match v {
&InputValue::Null => Some(Self::ExplicitNull),
v => v.convert().map(Self::Some),
}
}
fn from_implicit_null() -> Self {
Self::ImplicitNull
}
}
impl<S, T> ToInputValue<S> for Nullable<T>
where
T: ToInputValue<S>,
S: ScalarValue,
{
fn to_input_value(&self) -> InputValue<S> {
match *self {
Self::Some(ref v) => v.to_input_value(),
_ => InputValue::null(),
}
}
}
impl<S, T> IsInputType<S> for Nullable<T>
where
T: IsInputType<S>,
S: ScalarValue,
{
}

View file

@ -46,6 +46,13 @@ fn create(
let top_attrs = &_impl.attrs;
let scalar = _impl
.attrs
.scalar
.as_ref()
.map(|s| quote!( #s ))
.unwrap_or_else(|| quote!(::juniper::DefaultScalarValue));
let fields = _impl
.methods
.iter()
@ -87,10 +94,6 @@ fn create(
.apply(&arg_name)
});
let expect_text = format!(
"Internal error: missing argument {} - validation must have failed",
&final_name
);
let mut_modifier = if is_mut { quote!(mut) } else { quote!() };
if final_name.starts_with("__") {
@ -109,7 +112,7 @@ fn create(
let resolver = quote!(
let #mut_modifier #arg_ident = args
.get::<#ty>(#final_name)
.expect(#expect_text);
.unwrap_or_else(::juniper::FromInputValue::<#scalar>::from_implicit_null);
);
let field_type = util::GraphQLTypeDefinitionFieldArg {

View file

@ -1701,34 +1701,35 @@ impl GraphQLTypeDefiniton {
})
.collect::<Vec<_>>();
let from_inputs = self.fields.iter().map(|field| {
let field_ident = &field.resolver_code;
let field_name = &field.name;
let from_inputs = self
.fields
.iter()
.map(|field| {
let field_ident = &field.resolver_code;
let field_name = &field.name;
// Build from_input clause.
let from_input_default = match field.default {
Some(ref def) => {
quote! {
Some(&&::juniper::InputValue::Null) | None if true => #def,
// Build from_input clause.
let from_input_default = match field.default {
Some(ref def) => {
quote! {
Some(&&::juniper::InputValue::Null) | None if true => #def,
}
}
}
None => quote! {},
};
None => quote! {},
};
quote!(
#field_ident: {
// TODO: investigate the unwraps here, they seem dangerous!
match obj.get(#field_name) {
#from_input_default
Some(ref v) => ::juniper::FromInputValue::from_input_value(v).unwrap(),
None => {
::juniper::FromInputValue::from_input_value(&::juniper::InputValue::<#scalar>::null())
.unwrap()
},
}
},
)
}).collect::<Vec<_>>();
quote!(
#field_ident: {
// TODO: investigate the unwraps here, they seem dangerous!
match obj.get(#field_name) {
#from_input_default
Some(ref v) => ::juniper::FromInputValue::from_input_value(v).unwrap(),
None => ::juniper::FromInputValue::<#scalar>::from_implicit_null(),
}
},
)
})
.collect::<Vec<_>>();
let to_inputs = self
.fields