From ebda74ba91ff739c0905d1a5f3c2a388c9029b97 Mon Sep 17 00:00:00 2001 From: Magnus Hallin Date: Fri, 23 Dec 2016 13:12:12 +0100 Subject: [PATCH] Refactor IntoResolvable (previously IntoFieldResult) It now supports a blanket implementation for all GraphQLTypes, and resolving into new context types. --- src/executor.rs | 84 ++++++++----- src/executor_tests/executor.rs | 219 ++++++++++++++++++++++++++++++++- src/lib.rs | 2 +- src/macros/enums.rs | 6 - src/macros/field.rs | 7 +- src/macros/interface.rs | 14 +-- src/macros/object.rs | 14 +-- src/macros/scalar.rs | 6 - src/macros/union.rs | 6 - src/types/containers.rs | 22 +--- src/types/pointers.rs | 14 +-- src/types/scalars.rs | 20 +-- 12 files changed, 286 insertions(+), 128 deletions(-) diff --git a/src/executor.rs b/src/executor.rs index 4dceee46..4f07d063 100644 --- a/src/executor.rs +++ b/src/executor.rs @@ -61,46 +61,79 @@ pub type FieldResult = Result; /// The result of resolving an unspecified field pub type ExecutionResult = Result; -/// Convert a value into a successful field result -/// -/// Used by the helper macros to support both returning a naked value -/// *and* a `FieldResult` from a field. -pub trait IntoFieldResult: Sized { - /// Wrap `self` in a `Result` - /// - /// The implementation of this should always be `Ok(self)`. - fn into(self) -> FieldResult; +#[doc(hidden)] +pub trait IntoResolvable<'a, T: GraphQLType, C>: Sized { + #[doc(hidden)] + fn into(self, ctx: &'a C) -> FieldResult>; } -impl IntoFieldResult for FieldResult { - fn into(self) -> FieldResult { +impl<'a, T: GraphQLType, C> IntoResolvable<'a, T, C> for T where T::Context: FromContext { + fn into(self, ctx: &'a C) -> FieldResult> { + Ok(Some((FromContext::from(ctx), self))) + } +} + +impl<'a, T: GraphQLType, C> IntoResolvable<'a, T, C> for FieldResult where T::Context: FromContext { + fn into(self, ctx: &'a C) -> FieldResult> { + self.map(|v| Some((FromContext::from(ctx), v))) + } +} + +impl<'a, T: GraphQLType, C> IntoResolvable<'a, T, C> for (&'a T::Context, T) { + fn into(self, _: &'a C) -> FieldResult> { + Ok(Some(self)) + } +} + +impl<'a, T: GraphQLType, C> IntoResolvable<'a, T, C> for Option<(&'a T::Context, T)> { + fn into(self, _: &'a C) -> FieldResult> { + Ok(self) + } +} + +impl<'a, T: GraphQLType, C> IntoResolvable<'a, T, C> for FieldResult<(&'a T::Context, T)> { + fn into(self, _: &'a C) -> FieldResult> { + self.map(|v| Some(v)) + } +} + +impl<'a, T: GraphQLType, C> IntoResolvable<'a, T, C> for FieldResult> { + fn into(self, _: &'a C) -> FieldResult> { self } } /// Conversion trait for context types /// -/// This is currently only used for converting arbitrary contexts into -/// the empty tuple, but will in the future be used to support general -/// context conversion for larger schemas. +/// Used to support different context types for different parts of an +/// application. By making each GraphQL type only aware of as much +/// context as it needs to, isolation and robustness can be +/// improved. Implement this trait if you have contexts that can +/// generally be converted between each other. +/// +/// The empty tuple `()` can be converted into from any context type, +/// making it suitable for GraphQL that don't need _any_ context to +/// work, e.g. scalars or enums. pub trait FromContext { /// Perform the conversion - fn from_context(value: &T) -> &Self; + fn from(value: &T) -> &Self; } /// Marker trait for types that can act as context objects for GraphQL types. pub trait Context { } +impl<'a, C: Context> Context for &'a C {} + static NULL_CONTEXT: () = (); impl FromContext for () { - fn from_context(_: &T) -> &Self { + fn from(_: &T) -> &Self { &NULL_CONTEXT } } impl FromContext for T where T: Context { - fn from_context(value: &T) -> &Self { + fn from(value: &T) -> &Self { value } } @@ -112,7 +145,7 @@ impl<'a, CtxT> Executor<'a, CtxT> { ) -> ExecutionResult where NewCtxT: FromContext, { - self.replaced_context(>::from_context(&self.context)) + self.replaced_context(>::from(&self.context)) .resolve(value) } @@ -363,7 +396,9 @@ impl Registry { } #[doc(hidden)] - pub fn field_convert, I>(&mut self, name: &str) -> Field where I: GraphQLType { + pub fn field_convert<'a, T: IntoResolvable<'a, I, C>, I, C>(&mut self, name: &str) -> Field + where I: GraphQLType + { Field { name: name.to_owned(), description: None, @@ -373,17 +408,6 @@ impl Registry { } } - #[doc(hidden)] - pub fn field_inside_result(&mut self, name: &str, _: FieldResult) -> Field where T: GraphQLType { - Field { - name: name.to_owned(), - description: None, - arguments: None, - field_type: self.get_type::(), - deprecation_reason: None, - } - } - /// Create an argument with the provided name pub fn arg(&mut self, name: &str) -> Argument where T: GraphQLType + FromInputValue { Argument::new(name, self.get_type::()) diff --git a/src/executor_tests/executor.rs b/src/executor_tests/executor.rs index b389dc92..ce565122 100644 --- a/src/executor_tests/executor.rs +++ b/src/executor_tests/executor.rs @@ -166,21 +166,29 @@ mod threads_context_correctly { use value::Value; use types::scalars::EmptyMutation; use schema::model::RootNode; + use executor::Context; struct Schema; - graphql_object!(Schema: String |&self| { - field a(&executor) -> String { executor.context().clone() } + struct TestContext { + value: String, + } + + impl Context for TestContext {} + + graphql_object!(Schema: TestContext |&self| { + field a(&executor) -> String { executor.context().value.clone() } }); #[test] fn test() { - let schema = RootNode::new(Schema, EmptyMutation::::new()); + let schema = RootNode::new(Schema, EmptyMutation::::new()); let doc = r"{ a }"; let vars = vec![].into_iter().collect(); - let (result, errs) = ::execute(doc, None, &schema, &vars, &"Context value".to_owned()) + let (result, errs) = ::execute(doc, None, &schema, &vars, + &TestContext { value: "Context value".to_owned() }) .expect("Execution failed"); assert_eq!(errs, []); @@ -195,6 +203,209 @@ mod threads_context_correctly { } } +mod dynamic_context_switching { + use std::collections::HashMap; + + use value::Value; + use types::scalars::EmptyMutation; + use schema::model::RootNode; + use parser::SourcePosition; + use executor::{FieldResult, Context, ExecutionError}; + + struct Schema; + + struct InnerContext { + value: String, + } + + struct OuterContext { + items: HashMap, + } + + impl Context for OuterContext {} + impl Context for InnerContext {} + + struct ItemRef; + + graphql_object!(Schema: OuterContext |&self| { + field item_opt(&executor, key: i64) -> Option<(&InnerContext, ItemRef)> { + executor.context().items.get(&key).map(|c| (c, ItemRef)) + } + + field item_res(&executor, key: i64) -> FieldResult<(&InnerContext, ItemRef)> { + executor.context().items.get(&key) + .ok_or(format!("Could not find key {}", key)) + .map(|c| (c, ItemRef)) + } + + field item_res_opt(&executor, key: i64) -> FieldResult> { + if key > 100 { + Err(format!("Key too large: {}", key)) + } else { + Ok(executor.context().items.get(&key) + .map(|c| (c, ItemRef))) + } + } + + field item_always(&executor, key: i64) -> (&InnerContext, ItemRef) { + executor.context().items.get(&key) + .map(|c| (c, ItemRef)) + .unwrap() + } + }); + + graphql_object!(ItemRef: InnerContext |&self| { + field value(&executor) -> String { executor.context().value.clone() } + }); + + #[test] + fn test_opt() { + let schema = RootNode::new(Schema, EmptyMutation::::new()); + let doc = r"{ first: itemOpt(key: 0) { value }, missing: itemOpt(key: 2) { value } }"; + + let vars = vec![].into_iter().collect(); + + let ctx = OuterContext { + items: vec![ + (0, InnerContext { value: "First value".to_owned() }), + (1, InnerContext { value: "Second value".to_owned() }), + ].into_iter().collect(), + }; + + let (result, errs) = ::execute(doc, None, &schema, &vars, &ctx) + .expect("Execution failed"); + + assert_eq!(errs, []); + + println!("Result: {:?}", result); + + assert_eq!( + result, + Value::object(vec![ + ("first", Value::object(vec![ + ("value", Value::string("First value")), + ].into_iter().collect())), + ("missing", Value::null()), + ].into_iter().collect())); + } + + #[test] + fn test_res() { + let schema = RootNode::new(Schema, EmptyMutation::::new()); + let doc = r" + { + first: itemRes(key: 0) { value } + missing: itemRes(key: 2) { value } + } + "; + + let vars = vec![].into_iter().collect(); + + let ctx = OuterContext { + items: vec![ + (0, InnerContext { value: "First value".to_owned() }), + (1, InnerContext { value: "Second value".to_owned() }), + ].into_iter().collect(), + }; + + let (result, errs) = ::execute(doc, None, &schema, &vars, &ctx) + .expect("Execution failed"); + + assert_eq!(errs, vec![ + ExecutionError::new( + SourcePosition::new(70, 3, 12), + &["missing"], + "Could not find key 2", + ), + ]); + + println!("Result: {:?}", result); + + assert_eq!( + result, + Value::object(vec![ + ("first", Value::object(vec![ + ("value", Value::string("First value")), + ].into_iter().collect())), + ("missing", Value::null()), + ].into_iter().collect())); + } + + #[test] + fn test_res_opt() { + let schema = RootNode::new(Schema, EmptyMutation::::new()); + let doc = r" + { + first: itemResOpt(key: 0) { value } + missing: itemResOpt(key: 2) { value } + tooLarge: itemResOpt(key: 200) { value } + } + "; + + let vars = vec![].into_iter().collect(); + + let ctx = OuterContext { + items: vec![ + (0, InnerContext { value: "First value".to_owned() }), + (1, InnerContext { value: "Second value".to_owned() }), + ].into_iter().collect(), + }; + + let (result, errs) = ::execute(doc, None, &schema, &vars, &ctx) + .expect("Execution failed"); + + assert_eq!(errs, [ + ExecutionError::new( + SourcePosition::new(123, 4, 12), + &["tooLarge"], + "Key too large: 200", + ), + ]); + + println!("Result: {:?}", result); + + assert_eq!( + result, + Value::object(vec![ + ("first", Value::object(vec![ + ("value", Value::string("First value")), + ].into_iter().collect())), + ("missing", Value::null()), + ("tooLarge", Value::null()), + ].into_iter().collect())); + } + + #[test] + fn test_always() { + let schema = RootNode::new(Schema, EmptyMutation::::new()); + let doc = r"{ first: itemAlways(key: 0) { value } }"; + + let vars = vec![].into_iter().collect(); + + let ctx = OuterContext { + items: vec![ + (0, InnerContext { value: "First value".to_owned() }), + (1, InnerContext { value: "Second value".to_owned() }), + ].into_iter().collect(), + }; + + let (result, errs) = ::execute(doc, None, &schema, &vars, &ctx) + .expect("Execution failed"); + + assert_eq!(errs, []); + + println!("Result: {:?}", result); + + assert_eq!( + result, + Value::object(vec![ + ("first", Value::object(vec![ + ("value", Value::string("First value")), + ].into_iter().collect())), + ].into_iter().collect())); + } +} + mod nulls_out_errors { use value::Value; use schema::model::RootNode; diff --git a/src/lib.rs b/src/lib.rs index 95df6f55..1cd439ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -221,7 +221,7 @@ pub use value::Value; pub use types::base::{Arguments, GraphQLType, TypeKind}; pub use executor::{ Context, FromContext, - Executor, Registry, ExecutionResult, ExecutionError, FieldResult, IntoFieldResult, + Executor, Registry, ExecutionResult, ExecutionError, FieldResult, IntoResolvable, }; pub use validation::RuleError; pub use types::scalars::{EmptyMutation, ID}; diff --git a/src/macros/enums.rs b/src/macros/enums.rs index 23424fdb..838079be 100644 --- a/src/macros/enums.rs +++ b/src/macros/enums.rs @@ -112,12 +112,6 @@ macro_rules! graphql_enum { } } } - - impl $crate::IntoFieldResult<$name> for $name { - fn into(self) -> $crate::FieldResult<$name> { - Ok(self) - } - } }; // No more items to parse diff --git a/src/macros/field.rs b/src/macros/field.rs index 64bff58d..8adb93c6 100644 --- a/src/macros/field.rs +++ b/src/macros/field.rs @@ -77,8 +77,11 @@ macro_rules! __graphql__build_field_matches { $body })(); - return ($crate::IntoFieldResult::into(result)).and_then( - |r| $executorvar.resolve_with_ctx(&r)) + return ($crate::IntoResolvable::into(result, $executorvar.context())).and_then( + |res| match res { + Some((ctx, r)) => $executorvar.replaced_context(ctx).resolve_with_ctx(&r), + None => Ok($crate::Value::null()), + }) } )* panic!("Field {} not found on type {}", $fieldvar, $outname); diff --git a/src/macros/interface.rs b/src/macros/interface.rs index 41cb96a4..b5bebabc 100644 --- a/src/macros/interface.rs +++ b/src/macros/interface.rs @@ -99,7 +99,7 @@ macro_rules! graphql_interface { $acc.push(__graphql__args!( @apply_args, $reg, - $reg.field_convert::<$t, _>( + $reg.field_convert::<$t, _, Self::Context>( &$crate::to_snake_case(stringify!($name))) .description($desc) .deprecated($reason), @@ -117,7 +117,7 @@ macro_rules! graphql_interface { $acc.push(__graphql__args!( @apply_args, $reg, - $reg.field_convert::<$t, _>( + $reg.field_convert::<$t, _, Self::Context>( &$crate::to_snake_case(stringify!($name))) .deprecated($reason), $args)); @@ -134,7 +134,7 @@ macro_rules! graphql_interface { $acc.push(__graphql__args!( @apply_args, $reg, - $reg.field_convert::<$t, _>( + $reg.field_convert::<$t, _, Self::Context>( &$crate::to_snake_case(stringify!($name))) .description($desc), $args)); @@ -151,7 +151,7 @@ macro_rules! graphql_interface { $acc.push(__graphql__args!( @apply_args, $reg, - $reg.field_convert::<$t, _>( + $reg.field_convert::<$t, _, Self::Context>( &$crate::to_snake_case(stringify!($name))), $args)); @@ -279,12 +279,6 @@ macro_rules! graphql_interface { $($items)*); } }); - - impl<$($lifetime)*> $crate::IntoFieldResult<$name> for $name { - fn into(self) -> $crate::FieldResult<$name> { - Ok(self) - } - } }; ( diff --git a/src/macros/object.rs b/src/macros/object.rs index fa440538..a6116c7d 100644 --- a/src/macros/object.rs +++ b/src/macros/object.rs @@ -237,7 +237,7 @@ macro_rules! graphql_object { $acc.push(__graphql__args!( @apply_args, $reg, - $reg.field_convert::<$t, _>( + $reg.field_convert::<$t, _, Self::Context>( &$crate::to_snake_case(stringify!($name))) .description($desc) .deprecated($reason), @@ -255,7 +255,7 @@ macro_rules! graphql_object { $acc.push(__graphql__args!( @apply_args, $reg, - $reg.field_convert::<$t, _>( + $reg.field_convert::<$t, _, Self::Context>( &$crate::to_snake_case(stringify!($name))) .deprecated($reason), $args)); @@ -272,7 +272,7 @@ macro_rules! graphql_object { $acc.push(__graphql__args!( @apply_args, $reg, - $reg.field_convert::<$t, _>( + $reg.field_convert::<$t, _, Self::Context>( &$crate::to_snake_case(stringify!($name))) .description($desc), $args)); @@ -289,7 +289,7 @@ macro_rules! graphql_object { $acc.push(__graphql__args!( @apply_args, $reg, - $reg.field_convert::<$t, _>( + $reg.field_convert::<$t, _, Self::Context>( &$crate::to_snake_case(stringify!($name))), $args)); @@ -394,12 +394,6 @@ macro_rules! graphql_object { $($items)*); } }); - - impl<$($lifetime)*> $crate::IntoFieldResult<$name> for $name { - fn into(self) -> $crate::FieldResult<$name> { - Ok(self) - } - } }; ( diff --git a/src/macros/scalar.rs b/src/macros/scalar.rs index 269745e4..d2045012 100644 --- a/src/macros/scalar.rs +++ b/src/macros/scalar.rs @@ -94,12 +94,6 @@ macro_rules! graphql_scalar { $fiv_body } } - - impl $crate::IntoFieldResult<$name> for $name { - fn into(self) -> $crate::FieldResult<$name> { - Ok(self) - } - } }; // No more items to parse diff --git a/src/macros/union.rs b/src/macros/union.rs index 0a78f26b..2bb2f79b 100644 --- a/src/macros/union.rs +++ b/src/macros/union.rs @@ -148,12 +148,6 @@ macro_rules! graphql_union { $($items)*); } }); - - impl<$($lifetime)*> $crate::IntoFieldResult<$name> for $name { - fn into(self) -> $crate::FieldResult<$name> { - Ok(self) - } - } }; ( diff --git a/src/types/containers.rs b/src/types/containers.rs index 86e8f14c..4a4a6710 100644 --- a/src/types/containers.rs +++ b/src/types/containers.rs @@ -2,7 +2,7 @@ use ast::{InputValue, ToInputValue, FromInputValue, Selection}; use value::Value; use schema::meta::MetaType; -use executor::{Executor, Registry, IntoFieldResult, FieldResult}; +use executor::{Executor, Registry}; use types::base::{GraphQLType}; impl GraphQLType for Option where T: GraphQLType { @@ -45,13 +45,6 @@ impl ToInputValue for Option where T: ToInputValue { } } -impl IntoFieldResult> for Option { - fn into(self) -> FieldResult> { - Ok(self) - } -} - - impl GraphQLType for Vec where T: GraphQLType { type Context = CtxT; @@ -99,13 +92,6 @@ impl ToInputValue for Vec where T: ToInputValue { } } -impl IntoFieldResult> for Vec { - fn into(self) -> FieldResult> { - Ok(self) - } -} - - impl<'a, T, CtxT> GraphQLType for &'a [T] where T: GraphQLType { type Context = CtxT; @@ -129,9 +115,3 @@ impl<'a, T> ToInputValue for &'a [T] where T: ToInputValue { InputValue::list(self.iter().map(|v| v.to()).collect()) } } - -impl<'a, T> IntoFieldResult<&'a [T]> for &'a [T] { - fn into(self) -> FieldResult<&'a [T]> { - Ok(self) - } -} diff --git a/src/types/pointers.rs b/src/types/pointers.rs index cf8d1b7d..595f9db6 100644 --- a/src/types/pointers.rs +++ b/src/types/pointers.rs @@ -2,7 +2,7 @@ use ast::{Selection, InputValue, ToInputValue, FromInputValue}; use value::Value; use schema::meta::MetaType; -use executor::{Executor, Registry, ExecutionResult, IntoFieldResult, FieldResult}; +use executor::{Executor, Registry, ExecutionResult}; use types::base::{Arguments, GraphQLType}; impl GraphQLType for Box where T: GraphQLType { @@ -45,12 +45,6 @@ impl ToInputValue for Box where T: ToInputValue { } } -impl IntoFieldResult> for Box { - fn into(self) -> FieldResult> { - Ok(self) - } -} - impl<'a, T, CtxT> GraphQLType for &'a T where T: GraphQLType { type Context = CtxT; @@ -81,9 +75,3 @@ impl<'a, T> ToInputValue for &'a T where T: ToInputValue { (**self).to() } } - -impl<'a, T> IntoFieldResult<&'a T> for &'a T { - fn into(self) -> FieldResult<&'a T> { - Ok(self) - } -} diff --git a/src/types/scalars.rs b/src/types/scalars.rs index 8d5f6499..f08b7adf 100644 --- a/src/types/scalars.rs +++ b/src/types/scalars.rs @@ -5,7 +5,7 @@ use value::Value; use schema::meta::MetaType; -use executor::{Executor, Registry, FieldResult, IntoFieldResult}; +use executor::{Executor, Registry}; use types::base::GraphQLType; /// An ID as defined by the GraphQL specification @@ -64,12 +64,6 @@ impl<'a> ToInputValue for &'a str { } } -impl<'a> IntoFieldResult<&'a str> for &'a str { - fn into(self) -> FieldResult<&'a str> { - Ok(self) - } -} - graphql_scalar!(bool as "Boolean" { @@ -133,12 +127,6 @@ impl FromInputValue for () { } } -impl IntoFieldResult<()> for () { - fn into(self) -> FieldResult<()> { - Ok(self) - } -} - /// Utility type to define read-only schemas /// @@ -168,9 +156,3 @@ impl GraphQLType for EmptyMutation { registry.build_object_type::()(&[]).into_meta() } } - -impl IntoFieldResult> for EmptyMutation { - fn into(self) -> FieldResult> { - Ok(self) - } -}