Add MaybeUndefined type (#123)

* Add MaybeUndefined type
This commit is contained in:
Sunli 2020-05-28 15:00:55 +08:00 committed by GitHub
parent 3cdf109bfd
commit 43c8daa132
14 changed files with 206 additions and 36 deletions

View File

@ -156,8 +156,8 @@ pub fn generate(enum_args: &args::Enum, input: &DeriveInput) -> Result<TokenStre
}
impl #crate_name::InputValueType for #ident {
fn parse(value: #crate_name::Value) -> #crate_name::InputValueResult<Self> {
#crate_name::EnumType::parse_enum(value)
fn parse(value: Option<#crate_name::Value>) -> #crate_name::InputValueResult<Self> {
#crate_name::EnumType::parse_enum(value.unwrap_or_default())
}
fn to_value(&self) -> #crate_name::Value {

View File

@ -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<Self> {
fn parse(value: Option<#crate_name::Value>) -> #crate_name::InputValueResult<Self> {
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()))
}
}

View File

@ -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
})
});

View File

@ -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> {
<#self_ty as #crate_name::ScalarType>::parse(value)
fn parse(value: Option<#crate_name::Value>) -> #crate_name::InputValueResult<Self> {
<#self_ty as #crate_name::ScalarType>::parse(value.unwrap_or_default())
}
fn to_value(&self) -> #crate_name::Value {

View File

@ -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::*;

View File

@ -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<Self>;
/// Parse from `Value`None represent undefined.
fn parse(value: Option<Value>) -> InputValueResult<Self>;
/// Convert to `Value` for introspection
fn to_value(&self) -> Value;

View File

@ -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<Field>> {
name: &str,
default: Option<fn() -> T>,
) -> Result<T> {
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())),
}
}

View File

@ -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

View File

@ -21,16 +21,16 @@ impl<T: Type> Type for Vec<T> {
}
impl<T: InputValueType> InputValueType for Vec<T> {
fn parse(value: Value) -> InputValueResult<Self> {
match value {
fn parse(value: Option<Value>) -> InputValueResult<Self> {
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))?]),
}
}

View File

@ -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<T> {
Undefined,
Null,
Value(T),
}
impl<T> MaybeUndefined<T> {
/// Returns true if the MaybeUndefined<T> is undefined.
#[inline]
pub fn is_undefined(&self) -> bool {
if let MaybeUndefined::Undefined = self {
true
} else {
false
}
}
/// Returns true if the MaybeUndefined<T> 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<T> to Option<T>.
#[inline]
pub fn take(self) -> Option<T> {
match self {
MaybeUndefined::Value(value) => Some(value),
_ => None,
}
}
}
impl<T: Type> Type for MaybeUndefined<T> {
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<T: InputValueType> InputValueType for MaybeUndefined<T> {
fn parse(value: Option<Value>) -> InputValueResult<Self> {
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::<i32>::type_name(), "Int");
assert_eq!(MaybeUndefined::<i32>::qualified_type_name(), "Int");
assert_eq!(&MaybeUndefined::<i32>::type_name(), "Int");
assert_eq!(&MaybeUndefined::<i32>::qualified_type_name(), "Int");
}
}

View File

@ -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;

View File

@ -21,10 +21,10 @@ impl<T: Type> Type for Option<T> {
}
impl<T: InputValueType> InputValueType for Option<T> {
fn parse(value: Value) -> InputValueResult<Self> {
match value {
fn parse(value: Option<Value>) -> InputValueResult<Self> {
match value.unwrap_or_default() {
Value::Null => Ok(None),
_ => Ok(Some(T::parse(value)?)),
value => Ok(Some(T::parse(Some(value))?)),
}
}

View File

@ -79,7 +79,8 @@ impl<'a> Type for Upload {
}
impl<'a> InputValueType for Upload {
fn parse(value: Value) -> InputValueResult<Self> {
fn parse(value: Option<Value>) -> InputValueResult<Self> {
let value = value.unwrap_or_default();
if let Value::Upload(upload) = value {
Ok(Upload(upload))
} else {

57
tests/maybe_undefined.rs Normal file
View File

@ -0,0 +1,57 @@
use async_graphql::*;
#[async_std::test]
pub async fn test_maybe_undefined_type() {
#[InputObject]
struct MyInput {
value: MaybeUndefined<i32>,
}
struct Query;
#[Object]
impl Query {
async fn value1(&self, input: MaybeUndefined<i32>) -> 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,
})
);
}