Refactor IntoResolvable (previously IntoFieldResult)

It now supports a blanket implementation for all GraphQLTypes, and
resolving into new context types.
This commit is contained in:
Magnus Hallin 2016-12-23 13:12:12 +01:00
parent 6b8f4c9562
commit ebda74ba91
12 changed files with 286 additions and 128 deletions

View file

@ -61,46 +61,79 @@ pub type FieldResult<T> = Result<T, String>;
/// The result of resolving an unspecified field
pub type ExecutionResult = Result<Value, String>;
/// 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<T>: Sized {
/// Wrap `self` in a `Result`
///
/// The implementation of this should always be `Ok(self)`.
fn into(self) -> FieldResult<T>;
#[doc(hidden)]
pub trait IntoResolvable<'a, T: GraphQLType, C>: Sized {
#[doc(hidden)]
fn into(self, ctx: &'a C) -> FieldResult<Option<(&'a T::Context, T)>>;
}
impl<T> IntoFieldResult<T> for FieldResult<T> {
fn into(self) -> FieldResult<T> {
impl<'a, T: GraphQLType, C> IntoResolvable<'a, T, C> for T where T::Context: FromContext<C> {
fn into(self, ctx: &'a C) -> FieldResult<Option<(&'a T::Context, T)>> {
Ok(Some((FromContext::from(ctx), self)))
}
}
impl<'a, T: GraphQLType, C> IntoResolvable<'a, T, C> for FieldResult<T> where T::Context: FromContext<C> {
fn into(self, ctx: &'a C) -> FieldResult<Option<(&'a T::Context, T)>> {
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<Option<(&'a T::Context, T)>> {
Ok(Some(self))
}
}
impl<'a, T: GraphQLType, C> IntoResolvable<'a, T, C> for Option<(&'a T::Context, T)> {
fn into(self, _: &'a C) -> FieldResult<Option<(&'a T::Context, T)>> {
Ok(self)
}
}
impl<'a, T: GraphQLType, C> IntoResolvable<'a, T, C> for FieldResult<(&'a T::Context, T)> {
fn into(self, _: &'a C) -> FieldResult<Option<(&'a T::Context, T)>> {
self.map(|v| Some(v))
}
}
impl<'a, T: GraphQLType, C> IntoResolvable<'a, T, C> for FieldResult<Option<(&'a T::Context, T)>> {
fn into(self, _: &'a C) -> FieldResult<Option<(&'a T::Context, T)>> {
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<T> {
/// 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<T> FromContext<T> for () {
fn from_context(_: &T) -> &Self {
fn from(_: &T) -> &Self {
&NULL_CONTEXT
}
}
impl<T> FromContext<T> 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<CtxT>,
{
self.replaced_context(<NewCtxT as FromContext<CtxT>>::from_context(&self.context))
self.replaced_context(<NewCtxT as FromContext<CtxT>>::from(&self.context))
.resolve(value)
}
@ -363,7 +396,9 @@ impl Registry {
}
#[doc(hidden)]
pub fn field_convert<T: IntoFieldResult<I>, 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<T>(&mut self, name: &str, _: FieldResult<T>) -> Field where T: GraphQLType {
Field {
name: name.to_owned(),
description: None,
arguments: None,
field_type: self.get_type::<T>(),
deprecation_reason: None,
}
}
/// Create an argument with the provided name
pub fn arg<T>(&mut self, name: &str) -> Argument where T: GraphQLType + FromInputValue {
Argument::new(name, self.get_type::<T>())

View file

@ -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::<String>::new());
let schema = RootNode::new(Schema, EmptyMutation::<TestContext>::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<i64, InnerContext>,
}
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<Option<(&InnerContext, ItemRef)>> {
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::<OuterContext>::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::<OuterContext>::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::<OuterContext>::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::<OuterContext>::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;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -148,12 +148,6 @@ macro_rules! graphql_union {
$($items)*);
}
});
impl<$($lifetime)*> $crate::IntoFieldResult<$name> for $name {
fn into(self) -> $crate::FieldResult<$name> {
Ok(self)
}
}
};
(

View file

@ -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<T, CtxT> GraphQLType for Option<T> where T: GraphQLType<Context=CtxT> {
@ -45,13 +45,6 @@ impl<T> ToInputValue for Option<T> where T: ToInputValue {
}
}
impl<T> IntoFieldResult<Option<T>> for Option<T> {
fn into(self) -> FieldResult<Option<T>> {
Ok(self)
}
}
impl<T, CtxT> GraphQLType for Vec<T> where T: GraphQLType<Context=CtxT> {
type Context = CtxT;
@ -99,13 +92,6 @@ impl<T> ToInputValue for Vec<T> where T: ToInputValue {
}
}
impl<T> IntoFieldResult<Vec<T>> for Vec<T> {
fn into(self) -> FieldResult<Vec<T>> {
Ok(self)
}
}
impl<'a, T, CtxT> GraphQLType for &'a [T] where T: GraphQLType<Context=CtxT> {
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)
}
}

View file

@ -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<T, CtxT> GraphQLType for Box<T> where T: GraphQLType<Context=CtxT> {
@ -45,12 +45,6 @@ impl<T> ToInputValue for Box<T> where T: ToInputValue {
}
}
impl<T> IntoFieldResult<Box<T>> for Box<T> {
fn into(self) -> FieldResult<Box<T>> {
Ok(self)
}
}
impl<'a, T, CtxT> GraphQLType for &'a T where T: GraphQLType<Context=CtxT> {
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)
}
}

View file

@ -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<T> GraphQLType for EmptyMutation<T> {
registry.build_object_type::<Self>()(&[]).into_meta()
}
}
impl<T> IntoFieldResult<EmptyMutation<T>> for EmptyMutation<T> {
fn into(self) -> FieldResult<EmptyMutation<T>> {
Ok(self)
}
}