Merge pull request #80 from graphql-rust/field-error-refactor

Field error refactor
This commit is contained in:
Magnus Hallin 2017-09-02 10:44:50 +02:00 committed by GitHub
commit b3ea59cd3b
7 changed files with 300 additions and 35 deletions

View file

@ -1,3 +1,5 @@
use std::cmp::Ordering;
use std::fmt::Display;
use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::RwLock;
@ -52,18 +54,116 @@ where
///
/// All execution errors contain the source position in the query of the field
/// that failed to resolve. It also contains the field stack.
#[derive(Debug, PartialOrd, Ord, PartialEq, Eq)]
#[derive(Debug, PartialEq)]
pub struct ExecutionError {
location: SourcePosition,
path: Vec<String>,
error: FieldError,
}
impl Eq for ExecutionError {}
impl PartialOrd for ExecutionError {
fn partial_cmp(&self, other: &ExecutionError) -> Option<Ordering> {
(&self.location, &self.path, &self.error.message)
.partial_cmp(&(&other.location, &other.path, &other.error.message))
}
}
impl Ord for ExecutionError {
fn cmp(&self, other: &ExecutionError) -> Ordering {
(&self.location, &self.path, &self.error.message)
.cmp(&(&other.location, &other.path, &other.error.message))
}
}
/// Error type for errors that occur during field resolution
///
/// Field errors are represented by a human-readable error message and an
/// optional `Value` structure containing additional information.
///
/// They can be converted to from any type that implements `std::fmt::Display`,
/// which makes error chaining with the `?` operator a breeze:
///
/// ```rust
/// # use juniper::FieldError;
/// fn get_string(data: Vec<u8>) -> Result<String, FieldError> {
/// let s = String::from_utf8(data)?;
/// Ok(s)
/// }
/// ```
#[derive(Debug, PartialEq)]
pub struct FieldError {
message: String,
data: Value,
}
impl<T: Display> From<T> for FieldError {
fn from(e: T) -> FieldError {
FieldError {
message: format!("{}", e),
data: Value::null(),
}
}
}
impl FieldError {
/// Construct a new error with additional data
///
/// You can use the `graphql_value!` macro to construct an error:
///
/// ```rust
/// # #[macro_use] extern crate juniper;
/// use juniper::FieldError;
///
/// # fn sample() {
/// FieldError::new(
/// "Could not open connection to the database",
/// graphql_value!({ "internal_error": "Connection refused" })
/// );
/// # }
/// # fn main() { }
/// ```
///
/// The `data` parameter will be added to the `"data"` field of the error
/// object in the JSON response:
///
/// ```json
/// {
/// "errors": [
/// "message": "Could not open connection to the database",
/// "locations": [{"line": 2, "column": 4}],
/// "data": {
/// "internal_error": "Connection refused"
/// }
/// ]
/// }
/// ```
///
/// If the argument is `Value::null()`, no extra data will be included.
pub fn new<T: Display>(e: T, data: Value) -> FieldError {
FieldError {
message: format!("{}", e),
data: data,
}
}
#[doc(hidden)]
pub fn message(&self) -> &str {
&self.message
}
#[doc(hidden)]
pub fn data(&self) -> &Value {
&self.data
}
}
/// The result of resolving the value of a field of type `T`
pub type FieldResult<T> = Result<T, String>;
pub type FieldResult<T> = Result<T, FieldError>;
/// The result of resolving an unspecified field
pub type ExecutionResult = Result<Value, String>;
pub type ExecutionResult = Result<Value, FieldError>;
/// The map of variables used for substitution during query execution
pub type Variables = HashMap<String, InputValue>;
@ -256,7 +356,7 @@ impl<'a, CtxT> Executor<'a, CtxT> {
}
/// Add an error to the execution engine
pub fn push_error(&self, error: String, location: SourcePosition) {
pub fn push_error(&self, error: FieldError, location: SourcePosition) {
let mut path = Vec::new();
self.field_path.construct_path(&mut path);
@ -265,7 +365,7 @@ impl<'a, CtxT> Executor<'a, CtxT> {
errors.push(ExecutionError {
location: location,
path: path,
message: error,
error: error,
});
}
}
@ -290,17 +390,17 @@ impl<'a> FieldPath<'a> {
impl ExecutionError {
#[doc(hidden)]
pub fn new(location: SourcePosition, path: &[&str], message: &str) -> ExecutionError {
pub fn new(location: SourcePosition, path: &[&str], error: FieldError) -> ExecutionError {
ExecutionError {
location: location,
path: path.iter().map(|s| (*s).to_owned()).collect(),
message: message.to_owned(),
error: error,
}
}
/// The error message
pub fn message(&self) -> &str {
&self.message
pub fn error(&self) -> &FieldError {
&self.error
}
/// The source location _in the query_ of the field that failed to resolve

View file

@ -215,7 +215,8 @@ mod dynamic_context_switching {
use types::scalars::EmptyMutation;
use schema::model::RootNode;
use parser::SourcePosition;
use executor::{Context, ExecutionError, FieldResult};
use executor::{Context, ExecutionError, FieldError, FieldResult};
use result_ext::ResultExt;
struct Schema;
@ -241,11 +242,12 @@ mod dynamic_context_switching {
executor.context().items.get(&key)
.ok_or(format!("Could not find key {}", key))
.map(|c| (c, ItemRef))
.to_field_result()
}
field item_res_opt(&executor, key: i32) -> FieldResult<Option<(&InnerContext, ItemRef)>> {
if key > 100 {
Err(format!("Key too large: {}", key))
Err(format!("Key too large: {}", key)).to_field_result()
} else {
Ok(executor.context().items.get(&key)
.map(|c| (c, ItemRef)))
@ -320,7 +322,7 @@ mod dynamic_context_switching {
ExecutionError::new(
SourcePosition::new(70, 3, 12),
&["missing"],
"Could not find key 2",
FieldError::new("Could not find key 2", Value::null()),
),
]);
@ -363,7 +365,7 @@ mod dynamic_context_switching {
ExecutionError::new(
SourcePosition::new(123, 4, 12),
&["tooLarge"],
"Key too large: 200",
FieldError::new("Key too large: 200", Value::null()),
),
]);
@ -414,7 +416,7 @@ mod dynamic_context_switching {
mod nulls_out_errors {
use value::Value;
use schema::model::RootNode;
use executor::{ExecutionError, FieldResult};
use executor::{ExecutionError, FieldError, FieldResult};
use parser::SourcePosition;
use types::scalars::EmptyMutation;
@ -422,7 +424,7 @@ mod nulls_out_errors {
graphql_object!(Schema: () |&self| {
field sync() -> FieldResult<&str> { Ok("sync") }
field sync_error() -> FieldResult<&str> { Err("Error for syncError".to_owned()) }
field sync_error() -> FieldResult<&str> { Err("Error for syncError")? }
});
#[test]
@ -449,7 +451,7 @@ mod nulls_out_errors {
ExecutionError::new(
SourcePosition::new(8, 0, 8),
&["syncError"],
"Error for syncError",
FieldError::new("Error for syncError", Value::null()),
),
]);
}

View file

@ -15,10 +15,10 @@ impl ser::Serialize for ExecutionError {
where
S: ser::Serializer,
{
let mut map = try!(serializer.serialize_map(Some(3)));
let mut map = try!(serializer.serialize_map(Some(4)));
try!(map.serialize_key("message"));
try!(map.serialize_value(self.message()));
try!(map.serialize_value(self.error().message()));
let locations = vec![self.location()];
try!(map.serialize_key("locations"));
@ -27,6 +27,11 @@ impl ser::Serialize for ExecutionError {
try!(map.serialize_key("path"));
try!(map.serialize_value(self.path()));
if !self.error().data().is_null() {
try!(map.serialize_key("data"));
try!(map.serialize_value(self.error().data()));
}
map.end()
}
}

View file

@ -57,11 +57,12 @@ graphql_object!(User: Database |&self| {
&self.name
}
// FieldResult<T> is an alias for Result<T, String> - simply return
// a string from this method and it will be correctly inserted into
// the execution response.
// FieldResult<T> is an alias for Result<T, FieldError>, which can be
// converted to from anything that implements std::fmt::Display - simply
// return an error with a string using the ? operator from this method and
// it will be correctly inserted into the execution response.
field secret() -> FieldResult<&String> {
Err("Can't touch this".to_owned())
Err("Can't touch this".to_owned())?
}
// Field accessors can optionally take an "executor" as their first
@ -158,8 +159,8 @@ use executor::execute_validated_query;
pub use ast::{FromInputValue, InputValue, Selection, ToInputValue, Type};
pub use value::Value;
pub use types::base::{Arguments, GraphQLType, TypeKind};
pub use executor::{Context, ExecutionError, ExecutionResult, Executor, FieldResult, FromContext,
IntoResolvable, Registry, Variables};
pub use executor::{Context, ExecutionError, ExecutionResult, Executor, FieldError, FieldResult,
FromContext, IntoResolvable, Registry, Variables};
pub use validation::RuleError;
pub use types::scalars::{EmptyMutation, ID};
pub use schema::model::RootNode;

View file

@ -120,10 +120,16 @@ even have to be backed by a trait!
## Emitting errors
`FieldResult<T>` is a simple type alias for `Result<T, String>`. In the end,
errors that fields emit are serialized into strings in the response. However,
the execution system will keep track of the source of all errors, and will
continue executing despite some fields failing.
`FieldResult<T>` is a type alias for `Result<T, FieldError>`, where
`FieldResult` is a tuple that contains an error message and optionally a
JSON-like data structure. In the end, errors that fields emit are serialized
into strings in the response. However, the execution system will keep track of
the source of all errors, and will continue executing despite some fields
failing.
Anything that implements `std::fmt::Display` can be converted to a `FieldError`
automatically via the `?` operator, or you can construct them yourself using
`FieldError::new`.
```
# #[macro_use] extern crate juniper;
@ -136,7 +142,7 @@ graphql_object!(User: () |&self| {
}
field name() -> FieldResult<&String> {
Err("Does not have a name".to_owned())
Err("Does not have a name".to_owned())?
}
});

View file

@ -1,20 +1,22 @@
use std::fmt;
use std::result::Result;
use FieldError;
/**
Helper trait to produce `FieldResult`s
`FieldResult` only have strings as errors as that's what's going out
in the GraphQL response. As such, all errors must be manually
converted to strings. Importing the `ResultExt` macro and using its
only method `to_field_err` can help with that:
only method `to_field_result` can help with that:
```rust
use std::str::FromStr;
use juniper::{FieldResult, ResultExt};
fn sample_fn(s: &str) -> FieldResult<i32> {
i32::from_str(s).to_field_err()
i32::from_str(s).to_field_result()
}
# fn main() { assert_eq!(sample_fn("12"), Ok(12)); }
@ -42,12 +44,12 @@ fn sample_fn(s: &str) -> FieldResult<i32> {
*/
pub trait ResultExt<T, E: fmt::Display> {
/// Convert the error to a string by using it's `Display` implementation
fn to_field_err(self) -> Result<T, String>;
fn to_field_result(self) -> Result<T, FieldError>;
}
impl<T, E: fmt::Display> ResultExt<T, E> for Result<T, E> {
fn to_field_err(self) -> Result<T, String> {
self.map_err(|e| format!("{}", e))
fn to_field_result(self) -> Result<T, FieldError> {
self.map_err(|e| FieldError::from(e))
}
}
@ -59,5 +61,5 @@ trait.
*/
#[macro_export]
macro_rules! jtry {
( $e:expr ) => { try!($crate::ResultExt::to_field_err($e)) }
( $e:expr ) => { try!($crate::ResultExt::to_field_result($e)) }
}

View file

@ -130,3 +130,152 @@ impl ToInputValue for Value {
}
}
}
impl<'a> From<&'a str> for Value {
fn from(s: &'a str) -> Value {
Value::string(s)
}
}
impl From<String> for Value {
fn from(s: String) -> Value {
Value::string(s)
}
}
impl From<bool> for Value {
fn from(b: bool) -> Value {
Value::boolean(b)
}
}
impl From<i32> for Value {
fn from(i: i32) -> Value {
Value::int(i)
}
}
impl From<f64> for Value {
fn from(f: f64) -> Value {
Value::float(f)
}
}
impl<T> From<Option<T>> for Value where Value: From<T> {
fn from(v: Option<T>) -> Value {
match v {
Some(v) => Value::from(v),
None => Value::null()
}
}
}
/// Construct JSON-like values by using JSON syntax
///
/// This macro can be used to create `Value` instances using a JSON syntax.
/// Value objects are used mostly when creating custom errors from fields.
///
/// Here are some examples; the resulting JSON will look just like what you
/// passed in.
/// ```rust
/// #[macro_use] extern crate juniper;
///
/// graphql_value!(1234);
/// graphql_value!("test");
/// graphql_value!([ 1234, "test", true ]);
/// graphql_value!({ "key": "value", "foo": 1234 });
/// ```
#[macro_export]
macro_rules! graphql_value {
([ $($arg:tt),* $(,)* ]) => {
$crate::Value::list(vec![
$( graphql_value!($arg), )*
])
};
({ $($key:tt : $val:tt ),* $(,)* }) => {
$crate::Value::object(vec![
$( ($key, graphql_value!($val)), )*
].into_iter().collect())
};
($e:expr) => ($crate::Value::from($e))
}
#[cfg(test)]
mod tests {
use super::Value;
#[test]
fn value_macro_string() {
assert_eq!(
graphql_value!("test"),
Value::string("test")
);
}
#[test]
fn value_macro_int() {
assert_eq!(
graphql_value!(123),
Value::int(123)
);
}
#[test]
fn value_macro_float() {
assert_eq!(
graphql_value!(123.5),
Value::float(123.5)
);
}
#[test]
fn value_macro_boolean() {
assert_eq!(
graphql_value!(false),
Value::boolean(false)
);
}
#[test]
fn value_macro_option() {
assert_eq!(
graphql_value!(Some("test")),
Value::string("test")
);
assert_eq!(
graphql_value!((None as Option<String>)),
Value::null()
);
}
#[test]
fn value_macro_list() {
assert_eq!(
graphql_value!([ 123, "Test", false ]),
Value::list(vec![
Value::int(123),
Value::string("Test"),
Value::boolean(false),
])
);
assert_eq!(
graphql_value!([ 123, [ 456 ], 789 ]),
Value::list(vec![
Value::int(123),
Value::list(vec![ Value::int(456) ]),
Value::int(789),
])
);
}
#[test]
fn value_macro_object() {
assert_eq!(
graphql_value!({ "key": 123, "next": true }),
Value::object(vec![
("key", Value::int(123)),
("next", Value::boolean(true)),
].into_iter().collect())
);
}
}