//! Provides the [`Error`] type for Warpgrapher
#[cfg(feature = "gremlin")]
use gremlin_client::GremlinError;
use http::header::{InvalidHeaderName, InvalidHeaderValue};
use std::fmt::{Display, Formatter};
use std::num::ParseIntError;
use std::str::ParseBoolError;
/// Error type for Warpgrapher
///
/// # Examples
///
/// ```rust
/// use serde_json::json;
/// use warpgrapher::Error;
///
/// let e = Error::PayloadNotFound { response: json!{"surprise"} };
/// ```
#[derive(Debug)]
pub enum Error {
/// Returned to wrap an error from the bolt client. Most likely indicates something like
/// a network connection failure
#[cfg(feature = "cypher")]
BoltClientFailed {
source: bolt_client::error::CommunicationError,
},
/// Returned if a [`Client`] is unable to submit a request to the server, such as due to a
/// network or server error, or the response cannot be parsed as valid JSON. Inspect the
/// [`reqwest::Error`] included as a source error for additional detail.
///
/// [`Client`]: ./client/enum.Client.html
ClientRequestFailed {
source: reqwest::Error,
},
/// Returned if two Warpgrapher endpoints or two Warpgrapher types are defined with the same
/// name. The `type_name` field contains the name of the duplicated type.
ConfigItemDuplicated {
type_name: String,
},
/// Returned if a Warpgrapher endpoint or type is defined with a name that is a reserved
/// word, such as "ID" or a GraphQL scalar. The field `type_name` is the name that triggered the
/// error.
ConfigItemReserved {
type_name: String,
},
/// Returned if a `Config` file cannot be opened, typically because the configuration file
/// cannot be found on disk
ConfigOpenFailed {
source: std::io::Error,
},
/// Returned if attempting to compose configs with different versions. The field `expected`
/// contains the version of the base `Config`, and `found` contains the version of the `Config`
/// being merged in.
///
/// [`Config`]: ../engine/config/struct.Config.html
ConfigVersionMismatched {
expected: i32,
found: i32,
},
/// Returned if the engine is configured to operate without a database. Typically this would
/// never be done in production
DatabaseNotFound,
/// Returned if a `serde_json::Value` struct fails to deserialize into a struct
JsonDeserializationFailed {
source: serde_json::Error,
},
/// Returned if an environment variable cannot be found. The `name` field contains the name of
/// the environment variable that could not be found.
EnvironmentVariableNotFound {
name: String,
},
/// Returned if an environemtn variable for a boolean flag cannot be parsed from the
/// environment variable string into a bool
EnvironmentVariableBoolNotParsed {
source: ParseBoolError,
},
/// Returned if an environment variable for a port number cannot be parsed from the
/// environment variable string into a number
EnvironmentVariableIntNotParsed {
source: ParseIntError,
},
/// Returned if a registered extension function returns an error
EventError {
source: Box<dyn std::error::Error + Sync + Send>,
},
/// Returned if a client for a Cosmos or Gremlin database pool cannot be built or a query fails.
#[cfg(feature = "gremlin")]
GremlinActionFailed {
source: Box<gremlin_client::GremlinError>,
},
/// Returned if a GraphQL query is missing an expected argument. For example, if a create
/// mutation call were missing its input argument. Also returned if an input argument is
/// missing an expected field.
InputItemNotFound {
name: String,
},
/// Returned if an invalid header name is passed to the constructor for creating an http client.
InvalidHeaderName {
source: InvalidHeaderName,
},
// Returne if an invalid header value is passed to the constructor for creating an HTTP client.
InvalidHeaderValue {
source: InvalidHeaderValue,
},
/// Returned if an internal CRUD handler tries to retrieve the label for a node or relationship
/// from an internal temporary bookkeeping structure and is unable to do so. This almost
/// certainly indicates an internal bug. Thus, if you happen to see it, please open an issue at
/// the Warpgrapher project.
LabelNotFound,
/// Returned if the Bolt database driver suffers an internal value type conversation failure.
/// The source error contains additional information.
#[cfg(feature = "cypher")]
CypherConversionFailed {
source: bolt_proto::error::ConversionError,
},
/// Returned if a bolt pool cannot be built or cannot return a client transaction. The source
/// error contains additional information.
#[cfg(feature = "cypher")]
CypherPoolFailed {
source: mobc::Error<<mobc_bolt::Manager as mobc::Manager>::Error>,
},
/// Returned if a cypher query fails to execute correctly
#[cfg(feature = "cypher")]
CypherQueryFailed {
message: bolt_proto::message::Message,
},
/// Returned if a [`Client`] receives a valid JSON response that does not contain the
/// expected 'data' or 'errors' objects.
///
/// The [`serde_json::Value`] tuple value contains the deserialized JSON response.
///
/// [`Client`]: ./client/enum.Client.html
PayloadNotFound {
response: serde_json::Value,
},
/// Return if a query tries to read and return a relationship defined in the GraphQL schema as
/// being a single relationship (one-to-one), for which the back-end database has multiple
/// outgoing relationship edges (one-to-many or many-to-many). The `rel_name` field holds the
/// name of the relationship, and the `ids` field holds a list of ids of the relationships
/// found.
RelDuplicated {
rel_name: String,
ids: String,
},
/// Returned if a custom endpoint is defined or a resolver is defined for a field, but the
/// corresponding resolver is not provided. The `name` field contains the name of the resolver
/// that could not be found.
ResolverNotFound {
name: String,
},
/// Returned if a database query is missing a set of results altogether, where one is expected.
/// This likely indicates an internal bug. Thus, if you happen to see it, please open an issue
/// at the Warpgrapher project.
ResponseSetNotFound,
/// Returned if a database query result is missing an expected property. For example, if a
/// Cosmos DB query were to be missing the value for a property, or if the query fails to
/// to return an expected node or relationship. This could occur if a custom resolver creates a
/// node or rel witout adding mandatory properties, such as an ID.
ResponseItemNotFound {
name: String,
},
/// Returned if a GraphQL response or a database query parameter cannot be converted to a
/// serde_json::Value, or if a query
SerializationFailed {
source: serde_json::Error,
},
/// Returned if Warpgrapher fails to find an element within a schema, such as a type or
/// property. This is very unlikely to be returned as a result of problems with inputs to the
/// engine and most likely indicates an internal bug. Thus, if you happen to see it, please
/// open an issue at the Warpgrapher project. The field is the name of the schema element that
/// could not be fiound.
SchemaItemNotFound {
name: String,
},
/// When the Warpgrapher client sends queries to a local instance of a Warpgrapher engine,
/// it runs the engine in a separate thread, where it can have its own tokio execution context.
/// This error indicates an error in receiving the query answer from the engine thread.
ThreadCommunicationFailed {
source: std::sync::mpsc::RecvError,
},
/// Returned if a transaction is used after it is committed or rolled back.
TransactionFinished,
/// Warpgrapher transforms data between different serialization formats in the course of
/// relaying data between GraphQL and database back-ends. If data fails to convert successfully,
/// this error is thrown. The `src` field contains the source type name or value that could not
/// be converted.
TypeConversionFailed {
src: String,
dst: String,
},
/// Returned in multiple circumstances if the type information associated with a [`Value`] is
/// inconsistent with the type required. Examples include:
///
/// * a Warpgrapher [`Value`] enum doesn't match the variant expected for a given property, such
/// as an ID represented by something other than a string value
/// * a configuration schema is parsed incorrectly, resulting in an unexpected GraphQL endpoint
/// input argument
///
/// This error could be returned if a custom resolver creates a node or relationship with a
/// property of a type that doesn't match the type in the schema, such a creating an integer
/// property where the schema configures Warpgrapher to expect a string. However, other than
/// that case, this error most likely indicates an internal bug for which an issue should be
/// opened at the Warpgrapher project.
///
/// [`Value`]: ./engine/value/enum.Value.html
TypeNotExpected {
details: Option<String>,
},
/// Returned when encapsulating an error thrown in event handlers provided by users of
/// Warpgrapher
UserDefinedError {
source: Box<dyn std::error::Error + Sync + Send>,
},
/// Returned if the String argument for an id cannot be parsed into a UUID
UuidNotParsed {
source: uuid::Error,
},
/// This error is returned by a custom input validator when the validation fails. The message
/// String describes the reason the field failed validation.
ValidationFailed {
message: String,
},
/// Returned if a custom input validator is defined, but the corresponding validator is not
/// provided. The `name` field contains the name of the validator that wasn't found.
ValidatorNotFound {
name: String,
},
/// Returned if a `serde_yaml::Value` struct fails to deserialize into a given struct
YamlDeserializationFailed {
source: serde_yaml::Error,
},
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
match self {
#[cfg(feature = "cypher")]
Error::BoltClientFailed { source } => {
write!(f, "Bolt client failed. Source error: {}.", source)
}
Error::ClientRequestFailed { source } => {
write!(f, "Client request failed. Source error: {}", source)
}
Error::ConfigItemDuplicated { type_name } => {
write!(f, "Config model contains duplicate item: {}", type_name)
}
Error::ConfigItemReserved { type_name } => {
write!(
f,
"Config item cannot use a reserved word as a name: {}",
type_name
)
}
Error::ConfigOpenFailed { source } => {
write!(
f,
"Config file could not be opened. Source error: {}",
source
)
}
Error::ConfigVersionMismatched { expected, found } => {
write!(
f,
"Configs must be the same version: expected {} but found {}",
expected, found
)
}
Error::DatabaseNotFound => {
write!(f, "Use of resolvers required a database back-end. Please select either cypher or gremlin.")
}
Error::EnvironmentVariableNotFound { name } => {
write!(f, "Could not find environment variable: {}", name)
}
Error::EnvironmentVariableBoolNotParsed { source } => {
write!(
f,
"Failed to parse environment variable to boolean flag. Source error: {}",
source
)
}
Error::EnvironmentVariableIntNotParsed { source } => {
write!(
f,
"Failed to parse environment variable to integer port number. Source error: {}",
source
)
}
Error::EventError { source } => {
write!(f, "Event handler returned an error: {}", source)
}
#[cfg(feature = "gremlin")]
Error::GremlinActionFailed { source } => {
write!(
f,
"Either building a database connection pool or query failed. Source error: {}",
source
)
}
Error::InputItemNotFound { name } => {
write!(
f,
"Could not find an expected argument, {}, in the GraphQL query.",
name
)
}
Error::InvalidHeaderName { source } => {
write!(f, "Invalid HTTP header given to Client: {}", source)
}
Error::InvalidHeaderValue { source } => {
write!(f, "Invalid HTTP header given to Client: {}", source)
}
Error::LabelNotFound => {
write!(f, "Could not find a label for a destination node.")
}
Error::JsonDeserializationFailed { source } => {
write!(f, "Failed to deserialize JSON into struct: {}", source)
}
#[cfg(feature = "cypher")]
Error::CypherConversionFailed { source } => {
write!(f, "cypher value conversion failure: {}", source)
}
#[cfg(feature = "cypher")]
Error::CypherPoolFailed { source } => {
write!(f, "Could not get cypher transaction from pool: {}", source)
}
#[cfg(feature = "cypher")]
Error::CypherQueryFailed { message } => {
write!(
f,
"Cypher query execution failed. Error message: {:#?}.",
message
)
}
Error::PayloadNotFound { response } => {
write!(
f,
"Required data and/or error fields are missing from the response: {}",
response
)
}
Error::RelDuplicated { rel_name, ids } => {
write!(f, "Tried to read the single-node (i.e. one-to-one) relationship named {}, but found multipled ids: {}", rel_name, ids)
}
Error::ResolverNotFound { name } => {
write!(f, "Could not find a custom resolver named {}", name)
}
Error::ResponseItemNotFound { name } => {
write!(
f,
"Could not find an expected response item, {}, in the database results.",
name
)
}
Error::ResponseSetNotFound => {
write!(f, "Could not find an expected database set of results.")
}
Error::SerializationFailed { source } => {
write!(
f,
"Serialization of the GraphQL response failed. Source error: {}",
source
)
}
Error::SchemaItemNotFound { name } => {
write!(
f,
"The following item could not be found in the schema: {}",
name
)
}
Error::ThreadCommunicationFailed { source } => {
write!(
f,
"Communication from the engine thread failed. Source error: {}",
source
)
}
Error::TransactionFinished => {
write!(
f,
"Cannot use a database transaction already committed or rolled back."
)
}
Error::TypeConversionFailed { src, dst } => {
write!(
f,
"The type or value {} could not be converted to type {}",
src, dst
)
}
Error::TypeNotExpected { details } => {
write!(
f,
"Warpgrapher encountered a type that was not expected {}",
if let Some(s) = details {
format!("({:#?}", s)
} else {
"".to_string()
}
)
}
Error::UserDefinedError { source } => {
write!(f, "User defined error. Source error: {:#?}", source)
}
Error::UuidNotParsed { source } => {
write!(
f,
"Failed to parse id attribute value. Source error: {}",
source
)
}
Error::ValidationFailed { message } => {
write!(f, "{}", message)
}
Error::ValidatorNotFound { name } => {
write!(f, "A validator function named {} could not be found", name)
}
Error::YamlDeserializationFailed { source } => {
write!(
f,
"Failed to deserialize yaml struct. Source error: {}",
source
)
}
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
#[cfg(feature = "cypher")]
Error::BoltClientFailed { source } => Some(source),
Error::ClientRequestFailed { source } => Some(source),
Error::ConfigItemDuplicated { type_name: _ } => None,
Error::ConfigItemReserved { type_name: _ } => None,
Error::ConfigOpenFailed { source } => Some(source),
Error::ConfigVersionMismatched {
expected: _,
found: _,
} => None,
Error::DatabaseNotFound => None,
Error::EnvironmentVariableNotFound { name: _ } => None,
Error::EnvironmentVariableBoolNotParsed { source } => Some(source),
Error::EnvironmentVariableIntNotParsed { source } => Some(source),
Error::EventError { source } => Some(source.as_ref()),
#[cfg(feature = "gremlin")]
Error::GremlinActionFailed { source } => Some(source),
Error::InputItemNotFound { name: _ } => None,
Error::InvalidHeaderName { source } => Some(source),
Error::InvalidHeaderValue { source } => Some(source),
Error::JsonDeserializationFailed { source } => Some(source),
Error::LabelNotFound => None,
#[cfg(feature = "cypher")]
Error::CypherConversionFailed { source } => Some(source),
#[cfg(feature = "cypher")]
Error::CypherPoolFailed { source } => Some(source),
#[cfg(feature = "cypher")]
Error::CypherQueryFailed { message: _ } => None,
Error::PayloadNotFound { response: _ } => None,
Error::RelDuplicated {
rel_name: _,
ids: _,
} => None,
Error::ResolverNotFound { name: _ } => None,
Error::ResponseItemNotFound { name: _ } => None,
Error::ResponseSetNotFound => None,
Error::SerializationFailed { source } => Some(source),
Error::SchemaItemNotFound { name: _ } => None,
Error::ThreadCommunicationFailed { source } => Some(source),
Error::TransactionFinished => None,
Error::TypeConversionFailed { src: _, dst: _ } => None,
Error::TypeNotExpected { details: _ } => None,
Error::UserDefinedError { source: _ } => None,
Error::UuidNotParsed { source } => Some(source),
Error::ValidationFailed { message: _ } => None,
Error::ValidatorNotFound { name: _ } => None,
Error::YamlDeserializationFailed { source } => Some(source),
}
}
}
impl From<Box<dyn std::error::Error + Sync + Send>> for Error {
fn from(e: Box<dyn std::error::Error + Sync + Send>) -> Self {
Error::EventError { source: e }
}
}
#[cfg(feature = "cypher")]
impl From<bolt_client::error::CommunicationError> for Error {
fn from(e: bolt_client::error::CommunicationError) -> Self {
Error::BoltClientFailed { source: e }
}
}
#[cfg(feature = "cypher")]
impl From<bolt_proto::error::Error> for Error {
fn from(_e: bolt_proto::error::Error) -> Self {
Error::TypeConversionFailed {
src: "bolt_proto::value::Value".to_string(),
dst: "Value".to_string(),
}
}
}
#[cfg(feature = "gremlin")]
impl From<GremlinError> for Error {
fn from(e: GremlinError) -> Self {
Error::GremlinActionFailed {
source: Box::new(e),
}
}
}
#[cfg(feature = "cypher")]
impl From<bolt_proto::error::ConversionError> for Error {
fn from(e: bolt_proto::error::ConversionError) -> Self {
Error::CypherConversionFailed { source: e }
}
}
#[cfg(feature = "cypher")]
impl From<mobc::Error<<mobc_bolt::Manager as mobc::Manager>::Error>> for Error {
fn from(e: mobc::Error<<mobc_bolt::Manager as mobc::Manager>::Error>) -> Self {
Error::CypherPoolFailed { source: e }
}
}
impl From<reqwest::Error> for Error {
fn from(e: reqwest::Error) -> Self {
Error::ClientRequestFailed { source: e }
}
}
impl From<serde_json::Error> for Error {
fn from(e: serde_json::Error) -> Self {
Error::SerializationFailed { source: e }
}
}
impl From<serde_yaml::Error> for Error {
fn from(e: serde_yaml::Error) -> Self {
Error::YamlDeserializationFailed { source: e }
}
}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Error::ConfigOpenFailed { source: e }
}
}
impl From<std::str::ParseBoolError> for Error {
fn from(e: std::str::ParseBoolError) -> Self {
Error::EnvironmentVariableBoolNotParsed { source: e }
}
}
impl From<std::num::ParseIntError> for Error {
fn from(e: std::num::ParseIntError) -> Self {
Error::EnvironmentVariableIntNotParsed { source: e }
}
}
impl From<std::num::TryFromIntError> for Error {
fn from(_e: std::num::TryFromIntError) -> Self {
Error::TypeConversionFailed {
src: "i64 or uint64".to_string(),
dst: "i32".to_string(),
}
}
}
impl From<std::sync::mpsc::RecvError> for Error {
fn from(e: std::sync::mpsc::RecvError) -> Self {
Error::ThreadCommunicationFailed { source: e }
}
}
impl From<uuid::Error> for Error {
fn from(e: uuid::Error) -> Self {
Error::UuidNotParsed { source: e }
}
}
#[cfg(test)]
mod tests {
use super::Error;
/// Passes if a new error with no wrapped source error is created
#[test]
fn new_error() {
let e = Error::DatabaseNotFound;
assert!(std::error::Error::source(&e).is_none());
}
/// Passes if an error prints a display string correctly
#[test]
fn display_fmt() {
let s = std::io::Error::new(std::io::ErrorKind::Other, "oh no!");
let e = Error::ConfigOpenFailed { source: s };
assert_eq!(
"Config file could not be opened. Source error: oh no!",
&format!("{}", e)
);
}
/// Passes if Error implements the Send trait
#[test]
fn test_send() {
fn assert_send<T: Send>() {}
assert_send::<Error>();
}
/// Passes if Client implements the Sync trait
#[test]
fn test_sync() {
fn assert_sync<T: Sync>() {}
assert_sync::<Error>();
}
}