From 43c8daa1320e131869cc68ecdb4432d7f1f0b4bd Mon Sep 17 00:00:00 2001 From: Sunli Date: Thu, 28 May 2020 15:00:55 +0800 Subject: [PATCH] Add MaybeUndefined type (#123) * Add MaybeUndefined type --- async-graphql-derive/src/enum.rs | 4 +- async-graphql-derive/src/input_object.rs | 10 +-- async-graphql-derive/src/object.rs | 2 +- async-graphql-derive/src/scalar.rs | 4 +- async-graphql-parser/src/value.rs | 6 ++ src/base.rs | 4 +- src/context.rs | 35 +++++---- src/lib.rs | 4 +- src/types/list.rs | 8 +- src/types/maybe_undefined.rs | 97 ++++++++++++++++++++++++ src/types/mod.rs | 2 + src/types/optional.rs | 6 +- src/types/upload.rs | 3 +- tests/maybe_undefined.rs | 57 ++++++++++++++ 14 files changed, 206 insertions(+), 36 deletions(-) create mode 100644 src/types/maybe_undefined.rs create mode 100644 tests/maybe_undefined.rs diff --git a/async-graphql-derive/src/enum.rs b/async-graphql-derive/src/enum.rs index 17d4ed20..e2184890 100644 --- a/async-graphql-derive/src/enum.rs +++ b/async-graphql-derive/src/enum.rs @@ -156,8 +156,8 @@ pub fn generate(enum_args: &args::Enum, input: &DeriveInput) -> Result #crate_name::InputValueResult { - #crate_name::EnumType::parse_enum(value) + fn parse(value: Option<#crate_name::Value>) -> #crate_name::InputValueResult { + #crate_name::EnumType::parse_enum(value.unwrap_or_default()) } fn to_value(&self) -> #crate_name::Value { diff --git a/async-graphql-derive/src/input_object.rs b/async-graphql-derive/src/input_object.rs index aea8a995..20274081 100644 --- a/async-graphql-derive/src/input_object.rs +++ b/async-graphql-derive/src/input_object.rs @@ -80,14 +80,14 @@ pub fn generate(object_args: &args::InputObject, input: &DeriveInput) -> Result< get_fields.push(quote! { let #ident: #ty = { match obj.get(#name) { - Some(value) => #crate_name::InputValueType::parse(value.clone())?, + Some(value) => #crate_name::InputValueType::parse(Some(value.clone()))?, None => #default, } }; }); } else { get_fields.push(quote! { - let #ident:#ty = #crate_name::InputValueType::parse(obj.get(#name).cloned().unwrap_or(#crate_name::Value::Null))?; + let #ident:#ty = #crate_name::InputValueType::parse(obj.get(#name).cloned())?; }); } @@ -129,14 +129,14 @@ pub fn generate(object_args: &args::InputObject, input: &DeriveInput) -> Result< } impl #crate_name::InputValueType for #ident { - fn parse(value: #crate_name::Value) -> #crate_name::InputValueResult { + fn parse(value: Option<#crate_name::Value>) -> #crate_name::InputValueResult { use #crate_name::Type; - if let #crate_name::Value::Object(obj) = &value { + if let Some(#crate_name::Value::Object(obj)) = value { #(#get_fields)* Ok(Self { #(#fields),* }) } else { - Err(#crate_name::InputValueError::ExpectedType(value)) + Err(#crate_name::InputValueError::ExpectedType(value.unwrap_or_default())) } } diff --git a/async-graphql-derive/src/object.rs b/async-graphql-derive/src/object.rs index b5ccd312..81832c9f 100644 --- a/async-graphql-derive/src/object.rs +++ b/async-graphql-derive/src/object.rs @@ -126,7 +126,7 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< }); key_getter.push(quote! { params.get(#name).and_then(|value| { - let value: Option<#ty> = #crate_name::InputValueType::parse(value.clone()).ok(); + let value: Option<#ty> = #crate_name::InputValueType::parse(Some(value.clone())).ok(); value }) }); diff --git a/async-graphql-derive/src/scalar.rs b/async-graphql-derive/src/scalar.rs index 67d16e78..886c04a8 100644 --- a/async-graphql-derive/src/scalar.rs +++ b/async-graphql-derive/src/scalar.rs @@ -47,8 +47,8 @@ pub fn generate(scalar_args: &args::Scalar, item_impl: &mut ItemImpl) -> Result< } impl #generic #crate_name::InputValueType for #self_ty #where_clause { - fn parse(value: #crate_name::Value) -> #crate_name::InputValueResult { - <#self_ty as #crate_name::ScalarType>::parse(value) + fn parse(value: Option<#crate_name::Value>) -> #crate_name::InputValueResult { + <#self_ty as #crate_name::ScalarType>::parse(value.unwrap_or_default()) } fn to_value(&self) -> #crate_name::Value { diff --git a/async-graphql-parser/src/value.rs b/async-graphql-parser/src/value.rs index a08e74b3..560a868c 100644 --- a/async-graphql-parser/src/value.rs +++ b/async-graphql-parser/src/value.rs @@ -41,6 +41,12 @@ pub enum Value { Upload(UploadValue), } +impl Default for Value { + fn default() -> Self { + Value::Null + } +} + impl PartialEq for Value { fn eq(&self, other: &Self) -> bool { use Value::*; diff --git a/src/base.rs b/src/base.rs index dbedced8..e54dbe30 100644 --- a/src/base.rs +++ b/src/base.rs @@ -34,8 +34,8 @@ pub trait Type { /// Represents a GraphQL input value pub trait InputValueType: Type + Sized { - /// Parse from `Value` - fn parse(value: Value) -> InputValueResult; + /// Parse from `Value`,None represent undefined. + fn parse(value: Option) -> InputValueResult; /// Convert to `Value` for introspection fn to_value(&self) -> Value; diff --git a/src/context.rs b/src/context.rs index 2a44a729..c97aa8b7 100644 --- a/src/context.rs +++ b/src/context.rs @@ -459,9 +459,9 @@ impl<'a, T> ContextBase<'a, T> { for directive in directives { if directive.name.node == "skip" { if let Some(value) = directive.get_argument("if") { - match InputValueType::parse( + match InputValueType::parse(Some( self.resolve_input_value(value.clone_inner(), value.position())?, - ) { + )) { Ok(true) => return Ok(true), Ok(false) => {} Err(err) => { @@ -478,9 +478,9 @@ impl<'a, T> ContextBase<'a, T> { } } else if directive.name.node == "include" { if let Some(value) = directive.get_argument("if") { - match InputValueType::parse( + match InputValueType::parse(Some( self.resolve_input_value(value.clone_inner(), value.position())?, - ) { + )) { Ok(false) => return Ok(true), Ok(true) => {} Err(err) => { @@ -537,18 +537,23 @@ impl<'a> ContextBase<'a, &'a Positioned> { name: &str, default: Option T>, ) -> Result { - match (self.get_argument(name).cloned(), default) { - (Some(value), _) => { - let pos = value.position(); - let value = self.resolve_input_value(value.into_inner(), pos)?; - match InputValueType::parse(value) { - Ok(res) => Ok(res), - Err(err) => Err(err.into_error(pos, T::qualified_type_name())), - } + let value = self.get_argument(name).cloned(); + if let Some(default) = default { + if value.is_none() { + return Ok(default()); } - (None, Some(default)) => Ok(default()), - (None, None) => InputValueType::parse(Value::Null) - .map_err(|err| err.into_error(Pos::default(), T::qualified_type_name())), + } + let pos = value + .as_ref() + .map(|value| value.position()) + .unwrap_or_default(); + let resolved_value = match value { + Some(value) => Some(self.resolve_input_value(value.into_inner(), pos)?), + None => None, + }; + match InputValueType::parse(resolved_value) { + Ok(res) => Ok(res), + Err(err) => Err(err.into_error(pos, T::qualified_type_name())), } } diff --git a/src/lib.rs b/src/lib.rs index c47ed48c..5417366b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -153,7 +153,9 @@ pub use schema::{Schema, SchemaBuilder, SchemaEnv}; pub use subscription::{ SimpleBroker, SubscriptionStreams, SubscriptionTransport, WebSocketTransport, }; -pub use types::{connection, Deferred, EmptyMutation, EmptySubscription, Streamed, Upload}; +pub use types::{ + connection, Deferred, EmptyMutation, EmptySubscription, MaybeUndefined, Streamed, Upload, +}; pub use validation::ValidationMode; /// Result type diff --git a/src/types/list.rs b/src/types/list.rs index a04f7411..028baa38 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -21,16 +21,16 @@ impl Type for Vec { } impl InputValueType for Vec { - fn parse(value: Value) -> InputValueResult { - match value { + fn parse(value: Option) -> InputValueResult { + match value.unwrap_or_default() { Value::List(values) => { let mut result = Vec::new(); for elem_value in values { - result.push(InputValueType::parse(elem_value)?); + result.push(InputValueType::parse(Some(elem_value))?); } Ok(result) } - _ => Ok(vec![InputValueType::parse(value)?]), + value => Ok(vec![InputValueType::parse(Some(value))?]), } } diff --git a/src/types/maybe_undefined.rs b/src/types/maybe_undefined.rs new file mode 100644 index 00000000..61d8500a --- /dev/null +++ b/src/types/maybe_undefined.rs @@ -0,0 +1,97 @@ +use crate::{registry, InputValueResult, InputValueType, Type, Value}; +use std::borrow::Cow; + +/// Similar to `Option`, but it has three states, `undefined`, `null` and `x`. +/// +/// Spec: https://spec.graphql.org/June2018/#sec-Null-Value +#[allow(missing_docs)] +pub enum MaybeUndefined { + Undefined, + Null, + Value(T), +} + +impl MaybeUndefined { + /// Returns true if the MaybeUndefined is undefined. + #[inline] + pub fn is_undefined(&self) -> bool { + if let MaybeUndefined::Undefined = self { + true + } else { + false + } + } + + /// Returns true if the MaybeUndefined is null. + #[inline] + pub fn is_null(&self) -> bool { + if let MaybeUndefined::Null = self { + true + } else { + false + } + } + + /// Borrow the value, returns `None` if the value is `undefined` or `null`, otherwise returns `Some(T)`. + #[inline] + pub fn value(&self) -> Option<&T> { + match self { + MaybeUndefined::Value(value) => Some(value), + _ => None, + } + } + + /// Convert MaybeUndefined to Option. + #[inline] + pub fn take(self) -> Option { + match self { + MaybeUndefined::Value(value) => Some(value), + _ => None, + } + } +} + +impl Type for MaybeUndefined { + fn type_name() -> Cow<'static, str> { + T::type_name() + } + + fn qualified_type_name() -> String { + T::type_name().to_string() + } + + fn create_type_info(registry: &mut registry::Registry) -> String { + T::create_type_info(registry); + T::type_name().to_string() + } +} + +impl InputValueType for MaybeUndefined { + fn parse(value: Option) -> InputValueResult { + match value { + None => Ok(MaybeUndefined::Undefined), + Some(Value::Null) => Ok(MaybeUndefined::Null), + Some(value) => Ok(MaybeUndefined::Value(T::parse(Some(value))?)), + } + } + + fn to_value(&self) -> Value { + match self { + MaybeUndefined::Value(value) => value.to_value(), + _ => Value::Null, + } + } +} + +#[cfg(test)] +mod tests { + use crate::*; + + #[test] + fn test_optional_type() { + assert_eq!(MaybeUndefined::::type_name(), "Int"); + assert_eq!(MaybeUndefined::::qualified_type_name(), "Int"); + assert_eq!(&MaybeUndefined::::type_name(), "Int"); + assert_eq!(&MaybeUndefined::::qualified_type_name(), "Int"); + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs index 54dfc54e..0e8811db 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -5,6 +5,7 @@ mod empty_mutation; mod empty_subscription; mod r#enum; mod list; +mod maybe_undefined; mod optional; mod query_root; mod streamed; @@ -13,6 +14,7 @@ mod upload; pub use deferred::Deferred; pub use empty_mutation::EmptyMutation; pub use empty_subscription::EmptySubscription; +pub use maybe_undefined::MaybeUndefined; pub use query_root::QueryRoot; pub use r#enum::{EnumItem, EnumType}; pub use streamed::Streamed; diff --git a/src/types/optional.rs b/src/types/optional.rs index 2433744b..2416e349 100644 --- a/src/types/optional.rs +++ b/src/types/optional.rs @@ -21,10 +21,10 @@ impl Type for Option { } impl InputValueType for Option { - fn parse(value: Value) -> InputValueResult { - match value { + fn parse(value: Option) -> InputValueResult { + match value.unwrap_or_default() { Value::Null => Ok(None), - _ => Ok(Some(T::parse(value)?)), + value => Ok(Some(T::parse(Some(value))?)), } } diff --git a/src/types/upload.rs b/src/types/upload.rs index 5d5f272c..a57d306d 100644 --- a/src/types/upload.rs +++ b/src/types/upload.rs @@ -79,7 +79,8 @@ impl<'a> Type for Upload { } impl<'a> InputValueType for Upload { - fn parse(value: Value) -> InputValueResult { + fn parse(value: Option) -> InputValueResult { + let value = value.unwrap_or_default(); if let Value::Upload(upload) = value { Ok(Upload(upload)) } else { diff --git a/tests/maybe_undefined.rs b/tests/maybe_undefined.rs new file mode 100644 index 00000000..db160e17 --- /dev/null +++ b/tests/maybe_undefined.rs @@ -0,0 +1,57 @@ +use async_graphql::*; + +#[async_std::test] +pub async fn test_maybe_undefined_type() { + #[InputObject] + struct MyInput { + value: MaybeUndefined, + } + + struct Query; + + #[Object] + impl Query { + async fn value1(&self, input: MaybeUndefined) -> i32 { + if input.is_null() { + 1 + } else if input.is_undefined() { + 2 + } else { + input.take().unwrap() + } + } + + async fn value2(&self, input: MyInput) -> i32 { + if input.value.is_null() { + 1 + } else if input.value.is_undefined() { + 2 + } else { + input.value.take().unwrap() + } + } + } + + let schema = Schema::new(Query, EmptyMutation, EmptySubscription); + let query = r#" + { + v1:value1(input: 99) + v2:value1(input: null) + v3:value1() + v4:value2(input: { value: 99} ) + v5:value2(input: { value: null} ) + v6:value2(input: {} ) + } + "#; + assert_eq!( + schema.execute(&query).await.unwrap().data, + serde_json::json!({ + "v1": 99, + "v2": 1, + "v3": 2, + "v4": 99, + "v5": 1, + "v6": 2, + }) + ); +}