This commit is contained in:
sunli 2020-03-20 11:56:08 +08:00
parent ab0f0239d0
commit 62b4908ffe
26 changed files with 246 additions and 34 deletions

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql"
version = "1.3.1"
version = "1.3.2"
authors = ["sunli <scott_s829@163.com>"]
edition = "2018"
description = "The GraphQL server library implemented by rust"
@ -14,10 +14,10 @@ categories = ["network-programming", "asynchronous"]
readme = "README.md"
[features]
default = ["chrono", "uuid"]
default = ["chrono", "uuid", "url"]
[dependencies]
async-graphql-derive = { path = "async-graphql-derive", version = "1.3.1" }
async-graphql-derive = { path = "async-graphql-derive", version = "1.3.2" }
graphql-parser = "0.2.3"
anyhow = "1.0.26"
thiserror = "1.0.11"
@ -32,6 +32,7 @@ base64 = "0.12.0"
byteorder = "1.3.4"
chrono = { version = "0.4.10", optional = true }
uuid = { version = "0.8.1", optional = true }
url = { version = "2.1.1", optional = true }
[dev-dependencies]
async-std = { version = "1.5.0", features = ["attributes"] }

View File

@ -47,7 +47,8 @@ Open `http://localhost:8000` in browser
* Minimal overhead
* Easy integration (hyper, actix_web, tide ...)
* Upload files (Multipart request)
* Subscription (WebSocket transport
* Subscription (WebSocket transport)
## Integrations
* Actix-web [async-graphql-actix-web](https://crates.io/crates/async-graphql-actix-web)
@ -63,6 +64,7 @@ Open `http://localhost:8000` in browser
- [X] ID
- [X] DateTime
- [X] UUID
- [X] Url
- [X] Containers
- [X] List
- [X] Non-Null

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql-actix-web"
version = "0.4.1"
version = "0.4.2"
authors = ["sunli <scott_s829@163.com>"]
edition = "2018"
description = "async-graphql for actix-web"
@ -13,7 +13,7 @@ keywords = ["futures", "async", "graphql"]
categories = ["network-programming", "asynchronous"]
[dependencies]
async-graphql = { path = "..", version = "1.3.1" }
async-graphql = { path = "..", version = "1.3.2" }
actix-web = "2.0.0"
actix-multipart = "0.2.0"
actix-web-actors = "2.0.0"

View File

@ -1,3 +1,7 @@
//! Integrate `async-graphql` to `actix-web`
#![warn(missing_docs)]
#[macro_use]
extern crate serde_derive;
#[macro_use]

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql-derive"
version = "1.3.1"
version = "1.3.2"
authors = ["sunli <scott_s829@163.com>"]
edition = "2018"
description = "Macros for async-graphql"

8
scripts/publish-all.sh Executable file
View File

@ -0,0 +1,8 @@
#!/bin/sh
cd async-graphql-derive && cargo publish && cd ..
sleep 5
cd async-graphql-actix-web && cargo publish && cd ..
sleep 5
cargo publish
sleep 5

View File

@ -1,9 +1,11 @@
use crate::registry::Registry;
use crate::{registry, Context, ContextSelectionSet, Result};
use crate::{registry, Context, ContextSelectionSet, QueryError, Result, ID};
use graphql_parser::query::{Field, Value};
use std::borrow::Cow;
/// Represents a GraphQL type
///
/// All GraphQL types implement this trait, such as `Scalar`, `Object`, `Union` ...
pub trait Type {
/// Type the name.
fn type_name() -> Cow<'static, str>;
@ -15,16 +17,39 @@ pub trait Type {
/// Create type information in the registry and return qualified typename.
fn create_type_info(registry: &mut registry::Registry) -> String;
/// Returns a `GlobalID` that is unique among all types.
fn global_id(id: ID) -> ID {
base64::encode(format!("{}:{}", Self::type_name(), id)).into()
}
/// Parse `GlobalID`.
fn from_global_id(id: ID) -> Result<ID> {
let v: Vec<&str> = id.splitn(2, ":").collect();
if v.len() != 2 {
return Err(QueryError::InvalidGlobalID.into());
}
if v[0] != Self::type_name() {
return Err(QueryError::InvalidGlobalIDType {
expect: Self::type_name().to_string(),
actual: v[0].to_string(),
}
.into());
}
Ok(v[1].to_string().into())
}
}
/// Represents a GraphQL input value
pub trait InputValueType: Type + Sized {
/// Parse from `Value`
fn parse(value: &Value) -> Option<Self>;
}
/// Represents a GraphQL output value
#[async_trait::async_trait]
pub trait OutputValueType: Type {
/// Resolve an output value to `serde_json::Value`.
async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result<serde_json::Value>;
}
@ -142,6 +167,7 @@ macro_rules! impl_scalar_internal {
};
}
/// After implementing the `Scalar` trait, you must call this macro to implement some additional traits.
#[macro_export]
macro_rules! impl_scalar {
($ty:ty) => {

View File

@ -43,6 +43,7 @@ impl DerefMut for Variables {
}
impl Variables {
/// Parse variables from JSON object.
pub fn parse_from_json(value: serde_json::Value) -> Result<Self> {
let gql_value = json_value_to_gql_value(value);
if let Value::Object(_) = gql_value {
@ -140,11 +141,13 @@ impl Data {
}
}
/// Context for `SelectionSet`
pub type ContextSelectionSet<'a> = ContextBase<'a, &'a SelectionSet>;
/// Context object for resolve field.
/// Context object for resolve field
pub type Context<'a> = ContextBase<'a, &'a Field>;
/// Query context
pub struct ContextBase<'a, T> {
pub(crate) item: T,
pub(crate) variables: &'a Variables,

View File

@ -3,35 +3,59 @@ use graphql_parser::query::Value;
use graphql_parser::Pos;
use std::fmt::{Debug, Display, Formatter};
/// Error for query parser
#[derive(Debug, Error)]
#[error("{0}")]
pub struct QueryParseError(pub(crate) String);
/// Error for query
#[derive(Debug, Error)]
#[allow(missing_docs)]
pub enum QueryError {
#[error("Not supported.")]
NotSupported,
#[error("Expected type \"{expect}\", found {actual}.")]
ExpectedType { expect: String, actual: Value },
ExpectedType {
/// Expect input type
expect: String,
/// Actual input type
actual: Value,
},
#[error("Expected type \"{expect}\", found {actual}.")]
ExpectedJsonType {
/// Expect input JSON type
expect: String,
/// Actual input JSON type
actual: serde_json::Value,
},
#[error("Cannot query field \"{field_name}\" on type \"{object}\".")]
FieldNotFound { field_name: String, object: String },
FieldNotFound {
/// Field name
field_name: String,
/// Object name
object: String,
},
#[error("Missing operation")]
MissingOperation,
#[error("Unknown operation named \"{name}\"")]
UnknownOperationNamed { name: String },
UnknownOperationNamed {
/// Operation name for query
name: String,
},
#[error("Type \"{object}\" must have a selection of subfields.")]
MustHaveSubFields { object: String },
MustHaveSubFields {
/// Object name
object: String,
},
#[error("Schema is not configured for mutations.")]
NotConfiguredMutations,
@ -40,44 +64,98 @@ pub enum QueryError {
NotConfiguredSubscriptions,
#[error("Invalid value for enum \"{ty}\".")]
InvalidEnumValue { ty: String, value: String },
InvalidEnumValue {
/// Enum type name
ty: String,
/// Enum value
value: String,
},
#[error("Required field \"{field_name}\" for InputObject \"{object}\" does not exist.")]
RequiredField {
/// field name
field_name: String,
/// object name
object: &'static str,
},
#[error("Variable \"${var_name}\" is not defined")]
VarNotDefined { var_name: String },
VarNotDefined {
/// Variable name
var_name: String,
},
#[error(
"Directive \"{directive}\" argument \"{arg_name}\" of type \"{arg_type}\" is required, but it was not provided."
)]
RequiredDirectiveArgs {
/// Directive name
directive: &'static str,
/// Argument name
arg_name: &'static str,
/// Argument type
arg_type: &'static str,
},
#[error("Unknown directive \"{name}\".")]
UnknownDirective { name: String },
UnknownDirective {
/// Directive name
name: String,
},
#[error("Unknown fragment \"{name}\".")]
UnknownFragment { name: String },
UnknownFragment {
// Fragment name
name: String,
},
#[error("Object \"{object}\" does not implement interface \"{interface}\"")]
NotImplementedInterface { object: String, interface: String },
NotImplementedInterface {
/// Object name
object: String,
/// Interface name
interface: String,
},
#[error("Unrecognized inline fragment \"{name}\" on type \"{object}\"")]
UnrecognizedInlineFragment { object: String, name: String },
UnrecognizedInlineFragment {
/// Object name
object: String,
/// Inline fragment name
name: String,
},
#[error("Argument \"{field_name}\" must be a non-negative integer")]
ArgumentMustBeNonNegative { field_name: String },
ArgumentMustBeNonNegative {
/// Field name
field_name: String,
},
#[error("Invalid global id")]
InvalidGlobalID,
#[error("Invalid global id, expected type \"{expect}\", found {actual}.")]
InvalidGlobalIDType {
/// Expect type
expect: String,
/// Actual type
actual: String,
},
}
/// Creates a wrapper with an error location
#[allow(missing_docs)]
pub trait ErrorWithPosition {
type Result;
fn with_position(self, position: Pos) -> PositionError;
}
@ -92,6 +170,8 @@ impl<T: Into<Error>> ErrorWithPosition for T {
}
}
/// A wrapper with the wrong location
#[allow(missing_docs)]
#[derive(Debug, Error)]
pub struct PositionError {
pub position: Pos,
@ -99,10 +179,12 @@ pub struct PositionError {
}
impl PositionError {
#[allow(missing_docs)]
pub fn new(position: Pos, inner: Error) -> Self {
Self { position, inner }
}
#[allow(missing_docs)]
pub fn into_inner(self) -> Error {
self.inner
}

View File

@ -1,3 +1,4 @@
/// Generate the page for GraphIQL
pub fn graphiql_source(graphql_endpoint_url: &str) -> String {
let stylesheet_source = r#"
<style>

View File

@ -12,15 +12,22 @@ use serde::ser::{SerializeMap, SerializeSeq};
use serde::{Serialize, Serializer};
use std::ops::Deref;
/// GraphQL Request object
#[derive(Deserialize, Clone, PartialEq, Debug)]
pub struct GQLRequest {
/// Query source
pub query: String,
/// Operation name for this query
#[serde(rename = "operationName")]
pub operation_name: Option<String>,
/// Variables for this query
pub variables: Option<serde_json::Value>,
}
impl GQLRequest {
/// Execute the query and return the `GQLResponse`.
pub async fn execute<Query, Mutation, Subscription>(
mut self,
schema: &Schema<Query, Mutation, Subscription>,
@ -36,6 +43,7 @@ impl GQLRequest {
}
}
/// Prepare a query and return a `PreparedQuery` object that gets some information about the query.
pub fn prepare<'a, Query, Mutation, Subscription>(
&'a mut self,
schema: &'a Schema<Query, Mutation, Subscription>,
@ -65,6 +73,7 @@ impl GQLRequest {
}
}
/// Serializable query result type
pub struct GQLResponse(pub Result<serde_json::Value>);
impl Serialize for GQLResponse {
@ -86,6 +95,7 @@ impl Serialize for GQLResponse {
}
}
/// Serializable error type
pub struct GQLError<'a>(pub &'a anyhow::Error);
impl<'a> Deref for GQLError<'a> {

View File

@ -1,3 +1,4 @@
/// Generate the page for GraphQL Playground
pub fn playground_source(
graphql_endpoint_url: &str,
subscription_endpoint: Option<&str>,

View File

@ -53,6 +53,11 @@
//! ## References
//!
//! * [GraphQL](https://graphql.org)
//! * [GraphQL Multipart Request](https://github.com/jaydenseric/graphql-multipart-request-spec)
//! * [GraphQL Cursor Connections Specification](https://facebook.github.io/relay/graphql/connections.htm)
//! * [GraphQL over WebSocket Protocol](https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md)
#![warn(missing_docs)]
#[macro_use]
extern crate thiserror;
@ -80,9 +85,10 @@ pub use graphql_parser;
#[doc(hidden)]
pub use serde_json;
/// A helper module that supports HTTP
pub mod http;
pub use base::Scalar;
pub use base::{Scalar, Type};
pub use context::{Context, Variables};
pub use error::{ErrorWithPosition, PositionError, QueryError, QueryParseError};
pub use graphql_parser::query::Value;
@ -95,7 +101,10 @@ pub use types::{
Upload,
};
/// Result type, are actually `anyhow::Result<T>`
pub type Result<T> = anyhow::Result<T>;
/// Error type, are actually `anyhow::Error`
pub type Error = anyhow::Error;
// internal types
@ -104,7 +113,7 @@ pub use context::ContextSelectionSet;
#[doc(hidden)]
pub mod registry;
#[doc(hidden)]
pub use base::{InputObjectType, InputValueType, ObjectType, OutputValueType, Type};
pub use base::{InputObjectType, InputValueType, ObjectType, OutputValueType};
#[doc(hidden)]
pub use context::ContextBase;
#[doc(hidden)]

View File

@ -119,6 +119,7 @@ impl<'a, Query, Mutation> QueryBuilder<'a, Query, Mutation> {
}
}
/// Prepared query object
pub struct PreparedQuery<'a, Query, Mutation> {
root: Root<'a, Query, Mutation>,
registry: &'a Registry,

View File

@ -83,6 +83,7 @@ impl<'a, T: ObjectType + Send + Sync> Resolver<'a, T> {
}
}
#[allow(missing_docs)]
pub async fn do_resolve<'a, T: ObjectType + Send + Sync>(
ctx: &'a ContextSelectionSet<'a>,
root: &'a T,
@ -98,6 +99,7 @@ pub async fn do_resolve<'a, T: ObjectType + Send + Sync>(
Ok(result.into())
}
#[allow(missing_docs)]
pub async fn do_resolve_values<'a, T: ObjectType + Send + Sync>(
ctx: &'a ContextSelectionSet<'a>,
root: &'a T,

View File

@ -1,6 +1,9 @@
use crate::{impl_scalar_internal, Result, Scalar, Value};
use chrono::{DateTime, TimeZone, Utc};
/// Implement the DateTime<Utc> scalar
///
/// The input/output is a string in RFC3339 format.
impl Scalar for DateTime<Utc> {
fn type_name() -> &'static str {
"DateTime"

View File

@ -7,6 +7,12 @@ use std::ops::{Deref, DerefMut};
#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)]
pub struct ID(String);
impl std::fmt::Display for ID {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl Deref for ID {
type Target = String;

View File

@ -3,6 +3,7 @@ mod floats;
mod id;
mod integers;
mod string;
mod url;
#[cfg(feature = "chrono")]
mod datetime;

21
src/scalars/url.rs Normal file
View File

@ -0,0 +1,21 @@
use crate::{impl_scalar_internal, Result, Scalar, Value};
use url::Url;
impl Scalar for Url {
fn type_name() -> &'static str {
"Url"
}
fn parse(value: &Value) -> Option<Self> {
match value {
Value::String(s) => Some(Url::parse(s).ok()?),
_ => None,
}
}
fn to_json(&self) -> Result<serde_json::Value> {
Ok(self.to_string().into())
}
}
impl_scalar_internal!(Url);

View File

@ -19,7 +19,7 @@ pub struct Schema<Query, Mutation, Subscription> {
impl<Query: ObjectType, Mutation: ObjectType, Subscription: SubscriptionType>
Schema<Query, Mutation, Subscription>
{
/// Create a schema.
/// Create a schema
///
/// The root object for the query and Mutation needs to be specified.
/// If there is no mutation, you can use `EmptyMutation`.
@ -124,6 +124,7 @@ impl<Query: ObjectType, Mutation: ObjectType, Subscription: SubscriptionType>
}
}
/// Start a subscribe and return `SubscribeBuilder`.
pub fn subscribe<'a>(&'a self, source: &'a str) -> SubscribeBuilder<'a, Subscription> {
SubscribeBuilder {
subscription: &self.subscription,

View File

@ -11,6 +11,10 @@ use graphql_parser::query::{
use std::any::{Any, TypeId};
use std::collections::HashMap;
/// Subscribe stub
///
/// When a new push message is generated, a JSON object that needs to be pushed can be obtained by
/// `Subscribe::resolve`, and if None is returned, the Subscribe is not subscribed to a message of this type.
pub struct Subscribe {
types: HashMap<TypeId, Field>,
variables: Variables,
@ -19,6 +23,7 @@ pub struct Subscribe {
}
impl Subscribe {
#[allow(missing_docs)]
pub async fn resolve<Query, Mutation, Subscription>(
&self,
schema: &Schema<Query, Mutation, Subscription>,
@ -40,6 +45,7 @@ impl Subscribe {
}
/// Represents a GraphQL subscription object
#[allow(missing_docs)]
#[async_trait::async_trait]
pub trait SubscriptionType: Type {
/// This function returns true of type `EmptySubscription` only
@ -159,6 +165,7 @@ where
}
}
/// Perform a subscription operation and return `Subscribe`.
pub fn execute(self) -> Result<Subscribe> {
let document = parse_query(self.source).map_err(|err| QueryParseError(err.to_string()))?;
check_rules(self.registry, &document)?;

View File

@ -9,7 +9,7 @@ use inflector::Inflector;
use std::borrow::Cow;
use std::collections::HashMap;
/// Connection type.
/// Connection type
///
/// Connection is the result of a query for `DataSource`,
/// If the `T` type is `OutputValueType`, you can return the value as a field function directly,
@ -22,6 +22,7 @@ pub struct Connection<T, E: ObjectType + Sync + Send> {
}
impl<T, E: ObjectType + Sync + Send> Connection<T, E> {
/// Create a connection object.
pub fn new(
total_count: Option<usize>,
has_previous_page: bool,

View File

@ -7,25 +7,33 @@ use crate::{Context, ObjectType, QueryError, Result};
pub use connection::Connection;
/// Connection query operation.
/// Connection query operation
pub enum QueryOperation<'a> {
/// Forward query
Forward {
/// After this cursor
after: Option<&'a str>,
/// How many records did this query return
limit: usize,
},
/// Backward query
Backward {
/// Before this cursor
before: Option<&'a str>,
/// How many records did this query return
limit: usize,
},
}
/// Empty edge extension object.
/// Empty edge extension object
pub struct EmptyEdgeFields;
#[async_graphql_derive::Object(internal)]
impl EmptyEdgeFields {}
/// Data source of GraphQL Cursor Connections type.
/// Data source of GraphQL Cursor Connections type
///
/// `Edge` is an extension object type that extends the edge fields, If you don't need it, you can use `EmptyEdgeFields`.
///
@ -55,9 +63,9 @@ impl EmptyEdgeFields {}
/// #[async_trait::async_trait]
/// impl DataSource for Numbers {
/// type Element = i32;
/// type Edge = DiffFields;
/// type EdgeFieldsObj = DiffFields;
///
/// async fn query_operation(&self, operation: &QueryOperation<'_>) -> Result<Connection<Self::Element, Self::Edge>> {
/// async fn query_operation(&self, operation: &QueryOperation<'_>) -> Result<Connection<Self::Element, Self::EdgeFieldsObj>> {
/// let (start, end) = match operation {
/// QueryOperation::Forward {after, limit} => {
/// let start = after.and_then(|after| base64::decode(after).ok())
@ -119,9 +127,15 @@ impl EmptyEdgeFields {}
/// ```
#[async_trait::async_trait]
pub trait DataSource: Sync + Send {
/// Record type
type Element;
type Edge: ObjectType + Send + Sync;
/// Fields for Edge
///
/// Is a type that implements `ObjectType` and can be defined by the procedure macro `#[Object]`.
type EdgeFieldsObj: ObjectType + Send + Sync;
/// Execute the query.
async fn query(
&self,
ctx: &Context<'_>,
@ -129,7 +143,7 @@ pub trait DataSource: Sync + Send {
before: Option<String>,
first: Option<i32>,
last: Option<i32>,
) -> Result<Connection<Self::Element, Self::Edge>> {
) -> Result<Connection<Self::Element, Self::EdgeFieldsObj>> {
let operation = if let Some(after) = &after {
QueryOperation::Forward {
after: Some(after),
@ -198,8 +212,9 @@ pub trait DataSource: Sync + Send {
self.query_operation(&operation).await
}
/// Parses the parameters and executes the queryUsually you just need to implement this method.
async fn query_operation(
&self,
operation: &QueryOperation<'_>,
) -> Result<Connection<Self::Element, Self::Edge>>;
) -> Result<Connection<Self::Element, Self::EdgeFieldsObj>>;
}

View File

@ -5,12 +5,12 @@ use byteorder::{ReadBytesExt, BE};
#[async_trait::async_trait]
impl<'a, T: Sync> DataSource for &'a [T] {
type Element = &'a T;
type Edge = EmptyEdgeFields;
type EdgeFieldsObj = EmptyEdgeFields;
async fn query_operation(
&self,
operation: &QueryOperation<'_>,
) -> Result<Connection<Self::Element, Self::Edge>> {
) -> Result<Connection<Self::Element, Self::EdgeFieldsObj>> {
let (start, end) = match operation {
QueryOperation::Forward { after, limit } => {
let start = after

View File

@ -1,11 +1,13 @@
use crate::{Result, Type};
use graphql_parser::query::Value;
#[allow(missing_docs)]
pub struct EnumItem<T> {
pub name: &'static str,
pub value: T,
}
#[allow(missing_docs)]
#[async_trait::async_trait]
pub trait EnumType: Type + Sized + Eq + Send + Copy + Sized + 'static {
fn items() -> &'static [EnumItem<Self>];

View File

@ -5,8 +5,13 @@ use std::borrow::Cow;
///
/// Reference: https://github.com/jaydenseric/graphql-multipart-request-spec
pub struct Upload {
/// Filename
pub filename: String,
/// Content type, such as `application/json`, `image/jpg` ...
pub content_type: Option<String>,
/// File content
pub content: Vec<u8>,
}