diff --git a/README.md b/README.md index e6f91168..de667cae 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ - [X] Containers - [X] List - [X] Non-Null - - [ ] Object + - [X] Object - [X] Lifetime cycle - [X] Enum - [X] InputObject @@ -87,11 +87,11 @@ - [X] KnownArgumentNames - [ ] KnownDirectives - [X] KnownFragmentNames - - [ ] KnownTypeNames - - [ ] LoneAnonymousOperation + - [X] KnownTypeNames + - [X] LoneAnonymousOperation - [X] NoFragmentCycles - - [ ] NoUndefinedVariables - - [ ] NoUnusedFragments + - [X] NoUndefinedVariables + - [X] NoUnusedFragments - [ ] NoUnusedVariables - [ ] OverlappingFieldsCanBeMerged - [ ] PossibleFragmentSpreads diff --git a/async-graphql-derive/src/interface.rs b/async-graphql-derive/src/interface.rs index ff80e7a0..dc03b18c 100644 --- a/async-graphql-derive/src/interface.rs +++ b/async-graphql-derive/src/interface.rs @@ -117,7 +117,7 @@ pub fn generate(interface_args: &args::Interface, input: &DeriveInput) -> Result None => quote! { || #crate_name::Value::Null }, }; get_params.push(quote! { - let #ident: #ty = ctx_field.param_value(#name, #param_default)?; + let #ident: #ty = ctx.param_value(#name, #param_default)?; }); let desc = desc diff --git a/src/base.rs b/src/base.rs index d30f4485..006b16b1 100644 --- a/src/base.rs +++ b/src/base.rs @@ -2,33 +2,44 @@ use crate::{registry, Context, ContextSelectionSet, Result}; use graphql_parser::query::{Field, Value}; use std::borrow::Cow; +/// Represents a GraphQL type pub trait GQLType { + /// Type the name. fn type_name() -> Cow<'static, str>; + /// Qualified typename. fn qualified_type_name() -> String { format!("{}!", Self::type_name()) } + /// Create type information in the registry and return qualified typename. fn create_type_info(registry: &mut registry::Registry) -> String; } +/// Represents a GraphQL input value pub trait GQLInputValue: GQLType + Sized { fn parse(value: &Value) -> Option; } +/// Represents a GraphQL output value #[async_trait::async_trait] pub trait GQLOutputValue: GQLType { async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result; } +/// Represents a GraphQL object #[async_trait::async_trait] pub trait GQLObject: GQLOutputValue { + /// This function returns true of type `GQLEmptyMutation` only + #[doc(hidden)] fn is_empty() -> bool { return false; } + /// Resolves a field value and outputs it as a json value `serde_json::Value`. async fn resolve_field(&self, ctx: &Context<'_>, field: &Field) -> Result; + /// Resolve an inline fragment with the `name`. async fn resolve_inline_fragment( &self, name: &str, @@ -37,26 +48,66 @@ pub trait GQLObject: GQLOutputValue { ) -> Result<()>; } +/// Represents a GraphQL input object pub trait GQLInputObject: GQLInputValue {} +/// Represents a GraphQL scalar +/// +/// You can implement the trait to create a custom scalar. +/// +/// # Examples +/// +/// ```rust +/// use async_graphql::*; +/// +/// struct MyInt(i32); +/// +/// impl GQLScalar for MyInt { +/// fn type_name() -> &'static str { +/// "MyInt" +/// } +/// +/// fn parse(value: &Value) -> Option { +/// if let Value::Int(n) = value { +/// Some(MyInt(n.as_i64().unwrap() as i32)) +/// } else { +/// None +/// } +/// } +/// +/// fn to_json(&self) -> Result { +/// Ok(self.0.into()) +/// } +/// } +/// +/// impl_scalar!(MyInt); // // Don't forget this one +/// ``` pub trait GQLScalar: Sized + Send { + /// The type name of a scalar. fn type_name() -> &'static str; + /// The description of a scalar. fn description() -> Option<&'static str> { None } + /// Parse a scalar value, return `Some(Self)` if successful, otherwise return `None`. fn parse(value: &Value) -> Option; + /// Checks for a valid scalar value. + /// + /// The default implementation is to try to parse it, and in some cases you can implement this on your own to improve performance. fn is_valid(value: &Value) -> bool { Self::parse(value).is_some() } + /// Convert the scalar value to json value. fn to_json(&self) -> Result; } #[macro_export] -macro_rules! impl_scalar { +#[doc(hidden)] +macro_rules! impl_scalar_internal { ($ty:ty) => { impl crate::GQLType for $ty { fn type_name() -> std::borrow::Cow<'static, str> { @@ -114,6 +165,66 @@ macro_rules! impl_scalar { }; } +#[macro_export] +macro_rules! impl_scalar { + ($ty:ty) => { + impl async_graphql::GQLType for $ty { + fn type_name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed(<$ty as async_graphql::GQLScalar>::type_name()) + } + + fn create_type_info(registry: &mut async_graphql::registry::Registry) -> String { + registry.create_type::<$ty, _>(|_| async_graphql::registry::Type::Scalar { + name: <$ty as async_graphql::GQLScalar>::type_name().to_string(), + description: <$ty>::description(), + is_valid: |value| <$ty as async_graphql::GQLScalar>::is_valid(value), + }) + } + } + + impl async_graphql::GQLType for &$ty { + fn type_name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed(<$ty as async_graphql::GQLScalar>::type_name()) + } + + fn create_type_info(registry: &mut async_graphql::registry::Registry) -> String { + registry.create_type::<$ty, _>(|_| async_graphql::registry::Type::Scalar { + name: <$ty as async_graphql::GQLScalar>::type_name().to_string(), + description: <$ty>::description(), + is_valid: |value| <$ty as async_graphql::GQLScalar>::is_valid(value), + }) + } + } + + impl async_graphql::GQLInputValue for $ty { + fn parse(value: &async_graphql::Value) -> Option { + <$ty as async_graphql::GQLScalar>::parse(value) + } + } + + #[async_graphql::async_trait::async_trait] + impl async_graphql::GQLOutputValue for $ty { + async fn resolve( + value: &Self, + _: &async_graphql::ContextSelectionSet<'_>, + ) -> async_graphql::Result { + value.to_json() + } + } + + #[async_graphql::async_trait::async_trait] + impl async_graphql::GQLOutputValue for &$ty { + async fn resolve( + value: &Self, + _: &async_graphql::ContextSelectionSet<'_>, + ) -> async_graphql::Result { + value.to_json() + } + } + }; +} + +/// Represents a GraphQL output value #[async_trait::async_trait] impl GQLOutputValue for T { async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result { diff --git a/src/context.rs b/src/context.rs index 8948ce53..9454593b 100644 --- a/src/context.rs +++ b/src/context.rs @@ -9,6 +9,7 @@ use std::collections::{BTreeMap, HashMap}; use std::hash::BuildHasherDefault; use std::ops::{Deref, DerefMut}; +/// Variables of query #[derive(Default)] pub struct Variables(BTreeMap); @@ -67,6 +68,8 @@ impl Data { } pub type ContextSelectionSet<'a> = ContextBase<'a, &'a SelectionSet>; + +/// Context object for resolve field. pub type Context<'a> = ContextBase<'a, &'a Field>; pub struct ContextBase<'a, T> { @@ -99,6 +102,7 @@ impl<'a, T> ContextBase<'a, T> { } } + /// Gets the global data defined in the `Schema`. pub fn data(&self) -> &D { self.data .0 diff --git a/src/lib.rs b/src/lib.rs index ac88809f..307043e3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,15 +74,14 @@ pub use serde_json; pub mod http; -pub use async_graphql_derive::{Enum, InputObject, Interface, Object, Union}; -pub use base::{GQLInputObject, GQLInputValue, GQLObject, GQLOutputValue, GQLScalar, GQLType}; +pub use async_graphql_derive::Union; +pub use base::GQLScalar; pub use context::{Context, Variables}; pub use error::{ErrorWithPosition, PositionError, QueryError, QueryParseError}; pub use graphql_parser::query::Value; pub use scalars::ID; pub use schema::{QueryBuilder, Schema}; pub use types::GQLEmptyMutation; -pub use types::{GQLEnum, GQLEnumItem}; pub type Result = anyhow::Result; pub type Error = anyhow::Error; @@ -93,6 +92,332 @@ pub use context::ContextSelectionSet; #[doc(hidden)] pub mod registry; #[doc(hidden)] +pub use base::{GQLInputObject, GQLInputValue, GQLObject, GQLOutputValue, GQLType}; +#[doc(hidden)] pub use context::ContextBase; #[doc(hidden)] pub use resolver::do_resolve; +#[doc(hidden)] +pub use types::{GQLEnum, GQLEnumItem}; + +/// Define a GraphQL object +/// +/// # Macro parameters +/// +/// | Attribute | description | Type | Optional | +/// |-------------|---------------------------|----------|----------| +/// | name | Object name | string | Y | +/// | desc | Object description | string | Y | +/// +/// # Field parameters +/// +/// | Attribute | description | Type | Optional | +/// |-------------|---------------------------|----------|----------| +/// | name | Field name | string | Y | +/// | desc | Field description | string | Y | +/// | deprecation | Field deprecation reason | string | Y | +/// +/// # Field argument parameters +/// +/// | Attribute | description | Type | Optional | +/// |-------------|---------------------------|----------|----------| +/// | name | Argument name | string | Y | +/// | desc | Argument description | string | Y | +/// | default | Argument default value | string | Y | +/// +/// # The field returns the value type +/// +/// - A scalar value, such as `i32`, `bool` +/// - Borrowing of scalar values, such as `&i32`, `&bool` +/// - Vec, such as `Vec` +/// - Slice, such as `&[i32]` +/// - Option, such as `Option` +/// - GQLObject and `&GQLObject` +/// - GQLEnum +/// - Result, such as `Result` +/// +/// # Context +/// +/// You can define a context as an argument to a method, and the context should be the first argument to the method. +/// +/// ```ignore +/// #[Object] +/// impl MyObject { +/// async fn value(&self, ctx: &Context<'_>) -> { ... } +/// } +/// ``` +/// +/// # Examples +/// +/// ```rust +/// use async_graphql::*; +/// +/// struct MyObject { +/// value: i32, +/// } +/// +/// #[Object] +/// impl MyObject { +/// #[field(desc = "value")] +/// async fn value(&self) -> i32 { +/// self.value +/// } +/// +/// #[field(name = "valueRef", desc = "reference value")] +/// async fn value_ref(&self) -> &i32 { +/// &self.value +/// } +/// +/// #[field(name = "valueWithError", desc = "value with error")] +/// async fn value_with_error(&self) -> Result { +/// Ok(self.value) +/// } +/// +/// #[field(name = "valueWithArg")] +/// async fn value_with_arg(&self, #[arg(default = "1")] a: i32) -> i32 { +/// a +/// } +/// } +/// +/// #[async_std::main] +/// async fn main() { +/// let schema = Schema::new(MyObject{ value: 10 }, GQLEmptyMutation); +/// let res = schema.query(r#"{ +/// value +/// valueRef +/// valueWithError +/// valueWithArg1: valueWithArg +/// valueWithArg2: valueWithArg(a: 99) +/// }"#).execute().await.unwrap(); +/// assert_eq!(res, serde_json::json!({ +/// "value": 10, +/// "valueRef": 10, +/// "valueWithError": 10, +/// "valueWithArg1": 1, +/// "valueWithArg2": 99 +/// })); +/// } +/// ``` +pub use async_graphql_derive::Object; + +/// Define a GraphQL enum +/// +/// # Macro parameters +/// +/// | Attribute | description | Type | Optional | +/// |-------------|---------------------------|----------|----------| +/// | name | Enum name | string | Y | +/// | desc | Enum description | string | Y | +/// +/// # Item parameters +/// +/// | Attribute | description | Type | Optional | +/// |-------------|---------------------------|----------|----------| +/// | name | Item name | string | Y | +/// | desc | Item description | string | Y | +/// | deprecation | Item deprecation reason | string | Y | +/// +/// # Examples +/// +/// ```rust +/// use async_graphql::*; +/// +/// #[Enum] +/// enum MyEnum { +/// A, +/// #[item(name = "b")] B, +/// } +/// +/// struct MyObject { +/// value1: MyEnum, +/// value2: MyEnum, +/// } +/// +/// #[Object] +/// impl MyObject { +/// #[field(desc = "value")] +/// async fn value1(&self) -> MyEnum { +/// self.value1 +/// } +/// +/// #[field(desc = "value")] +/// async fn value2(&self) -> MyEnum { +/// self.value2 +/// } +/// } +/// +/// #[async_std::main] +/// async fn main() { +/// let schema = Schema::new(MyObject{ value1: MyEnum::A, value2: MyEnum::B }, GQLEmptyMutation); +/// let res = schema.query("{ value1 value2 }").execute().await.unwrap(); +/// assert_eq!(res, serde_json::json!({ "value1": "A", "value2": "b" })); +/// } +/// ``` +pub use async_graphql_derive::Enum; + +/// Define a GraphQL input object +/// +/// # Macro parameters +/// +/// | Attribute | description | Type | Optional | +/// |-------------|---------------------------|----------|----------| +/// | name | Object name | string | Y | +/// | desc | Object description | string | Y | +/// +/// # Field parameters +/// +/// | Attribute | description | Type | Optional | +/// |-------------|---------------------------|----------|----------| +/// | name | Field name | string | Y | +/// | desc | Field description | string | Y | +/// | default | Field default value | string | Y | +/// +/// # Examples +/// +/// ```rust +/// use async_graphql::*; +/// +/// #[InputObject] +/// struct MyInputObject { +/// a: i32, +/// #[field(default = "10")] +/// b: i32, +/// } +/// +/// struct MyObject; +/// +/// #[Object] +/// impl MyObject { +/// #[field(desc = "value")] +/// async fn value(&self, input: MyInputObject) -> i32 { +/// input.a * input.b +/// } +/// } +/// +/// #[async_std::main] +/// async fn main() { +/// let schema = Schema::new(MyObject, GQLEmptyMutation); +/// let res = schema.query(r#" +/// { +/// value1: value(input:{a:9, b:3}) +/// value2: value(input:{a:9}) +/// }"#).execute().await.unwrap(); +/// assert_eq!(res, serde_json::json!({ "value1": 27, "value2": 90 })); +/// } +/// ``` +pub use async_graphql_derive::InputObject; + +/// Define a GraphQL interface +/// +/// # Macro parameters +/// +/// | Attribute | description | Type | Optional | +/// |-------------|---------------------------|----------|----------| +/// | name | Object name | string | Y | +/// | desc | Object description | string | Y | +/// +/// # Field parameters +/// +/// | Attribute | description | Type | Optional | +/// |-------------|---------------------------|----------|----------| +/// | name | Field name | string | N | +/// | type | Field type | string | N | +/// | desc | Field description | string | Y | +/// | method | Field method name | string | Y | +/// | context | Method with the context | string | Y | +/// | deprecation | Field deprecation reason | string | Y | +/// | args | Field arguments | [Arg] | Y | +/// +/// # Field argument parameters +/// +/// | Attribute | description | Type | Optional | +/// |-------------|---------------------------|----------|----------| +/// | name | Argument name | string | N | +/// | type | Argument type | string | N | +/// | desc | Argument description | string | Y | +/// | default | Argument default value | string | Y | +/// +/// # Define an interface +/// +/// Define TypeA, TypeB, TypeC... Implement the MyInterface +/// +/// ```ignore +/// #[Interface] +/// struct MyInterface(TypeA, TypeB, TypeC, ...); +/// ``` +/// +/// # Fields +/// +/// The type, name, and parameters of the interface field must exactly match the type that implements the interface, +/// The internal implementation is a forward of the function call. +/// You can specify the field function name that implements the interface type through the 'method' property, +/// or you can specify that the field function has a context parameter through the 'context' attribute. +/// +/// ```rust +/// use async_graphql::*; +/// +/// struct TypeA { +/// value: i32, +/// } +/// +/// #[Object] +/// impl TypeA { +/// /// Returns data borrowed from the context +/// #[field] +/// async fn value_a<'a>(&self, ctx: &'a Context<'_>) -> &'a str { +/// ctx.data::().as_str() +/// } +/// +/// /// Returns data borrowed self +/// #[field] +/// async fn value_b(&self) -> &i32 { +/// &self.value +/// } +/// +/// /// With parameters +/// #[field] +/// async fn value_c(&self, a: i32, b: i32) -> i32 { +/// a + b +/// } +/// } +/// +/// #[Interface( +/// field(name = "value_a", type = "&'ctx str", context), +/// field(name = "value_b", type = "&i32"), +/// field(name = "value_c", type = "i32", +/// arg(name = "a", type = "i32"), +/// arg(name = "b", type = "i32")), +/// )] +/// struct MyInterface(TypeA); +/// +/// struct QueryRoot; +/// +/// #[Object] +/// impl QueryRoot { +/// #[field] +/// async fn type_a(&self) -> MyInterface { +/// TypeA { value: 10 }.into() +/// } +/// } +/// +/// #[async_std::main] +/// async fn main() { +/// let schema = Schema::new(QueryRoot, GQLEmptyMutation).data("hello".to_string()); +/// let res = schema.query(r#" +/// { +/// type_a { +/// value_a +/// value_b +/// value_c(a: 3, b: 2) +/// } +/// }"#).execute().await.unwrap(); +/// assert_eq!(res, serde_json::json!({ +/// "type_a": { +/// "value_a": "hello", +/// "value_b": 10, +/// "value_c": 5 +/// } +/// })); +/// } +/// ``` +pub use async_graphql_derive::Interface; diff --git a/src/registry.rs b/src/registry.rs index ddd2e04c..3a1139dc 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -33,6 +33,14 @@ impl<'a> TypeName<'a> { TypeName::Name(type_name) } } + + pub fn get_basic_typename(type_name: &str) -> &str { + match TypeName::create(type_name) { + TypeName::List(type_name) => Self::get_basic_typename(type_name), + TypeName::NonNull(type_name) => Self::get_basic_typename(type_name), + TypeName::Name(type_name) => type_name, + } + } } pub struct InputValue { @@ -196,10 +204,6 @@ impl Registry { } pub fn get_basic_type(&self, type_name: &str) -> Option<&Type> { - match TypeName::create(type_name) { - TypeName::Name(type_name) => self.types.get(type_name), - TypeName::List(type_name) => self.get_basic_type(type_name), - TypeName::NonNull(type_name) => self.get_basic_type(type_name), - } + self.types.get(TypeName::get_basic_typename(type_name)) } } diff --git a/src/scalars/bool.rs b/src/scalars/bool.rs index 7d3203ae..2a840643 100644 --- a/src/scalars/bool.rs +++ b/src/scalars/bool.rs @@ -1,4 +1,4 @@ -use crate::{impl_scalar, GQLScalar, Result, Value}; +use crate::{impl_scalar_internal, GQLScalar, Result, Value}; impl GQLScalar for bool { fn type_name() -> &'static str { @@ -21,4 +21,4 @@ impl GQLScalar for bool { } } -impl_scalar!(bool); +impl_scalar_internal!(bool); diff --git a/src/scalars/datetime.rs b/src/scalars/datetime.rs index 412a4bd7..457357de 100644 --- a/src/scalars/datetime.rs +++ b/src/scalars/datetime.rs @@ -1,4 +1,4 @@ -use crate::{impl_scalar, GQLScalar, Result, Value}; +use crate::{impl_scalar_internal, GQLScalar, Result, Value}; use chrono::{DateTime, TimeZone, Utc}; impl GQLScalar for DateTime { @@ -18,4 +18,4 @@ impl GQLScalar for DateTime { } } -impl_scalar!(DateTime); +impl_scalar_internal!(DateTime); diff --git a/src/scalars/floats.rs b/src/scalars/floats.rs index b7c3b341..8fced574 100644 --- a/src/scalars/floats.rs +++ b/src/scalars/floats.rs @@ -1,4 +1,4 @@ -use crate::{impl_scalar, GQLScalar, Result, Value}; +use crate::{impl_scalar_internal, GQLScalar, Result, Value}; macro_rules! impl_float_scalars { ($($ty:ty),*) => { @@ -25,7 +25,7 @@ macro_rules! impl_float_scalars { } } - impl_scalar!($ty); + impl_scalar_internal!($ty); )* }; } diff --git a/src/scalars/id.rs b/src/scalars/id.rs index c34982c4..24e6aa9b 100644 --- a/src/scalars/id.rs +++ b/src/scalars/id.rs @@ -1,6 +1,9 @@ -use crate::{impl_scalar, GQLScalar, Result, Value}; +use crate::{impl_scalar_internal, GQLScalar, Result, Value}; use std::ops::{Deref, DerefMut}; +/// ID scalar +/// +/// The input is a string or integer, and the output is a string. #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)] pub struct ID(String); @@ -36,4 +39,4 @@ impl GQLScalar for ID { } } -impl_scalar!(ID); +impl_scalar_internal!(ID); diff --git a/src/scalars/integers.rs b/src/scalars/integers.rs index 7cfaa8ec..003440d5 100644 --- a/src/scalars/integers.rs +++ b/src/scalars/integers.rs @@ -1,4 +1,4 @@ -use crate::{impl_scalar, GQLScalar, Result, Value}; +use crate::{impl_scalar_internal, GQLScalar, Result, Value}; macro_rules! impl_integer_scalars { ($($ty:ty),*) => { @@ -24,7 +24,7 @@ macro_rules! impl_integer_scalars { } } - impl_scalar!($ty); + impl_scalar_internal!($ty); )* }; } diff --git a/src/scalars/string.rs b/src/scalars/string.rs index ba7faa02..ee089d0d 100644 --- a/src/scalars/string.rs +++ b/src/scalars/string.rs @@ -1,5 +1,6 @@ use crate::{ - impl_scalar, registry, ContextSelectionSet, GQLOutputValue, GQLScalar, GQLType, Result, Value, + impl_scalar_internal, registry, ContextSelectionSet, GQLOutputValue, GQLScalar, GQLType, + Result, Value, }; use std::borrow::Cow; @@ -33,7 +34,7 @@ impl GQLScalar for String { } } -impl_scalar!(String); +impl_scalar_internal!(String); impl<'a> GQLType for &'a str { fn type_name() -> Cow<'static, str> { diff --git a/src/scalars/uuid.rs b/src/scalars/uuid.rs index a55e01a9..39f8c114 100644 --- a/src/scalars/uuid.rs +++ b/src/scalars/uuid.rs @@ -1,4 +1,4 @@ -use crate::{impl_scalar, GQLScalar, Result, Value}; +use crate::{impl_scalar_internal, GQLScalar, Result, Value}; use uuid::Uuid; impl GQLScalar for Uuid { @@ -18,4 +18,4 @@ impl GQLScalar for Uuid { } } -impl_scalar!(Uuid); +impl_scalar_internal!(Uuid); diff --git a/src/schema.rs b/src/schema.rs index 63c76c0b..0b53dd7f 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -11,6 +11,7 @@ use graphql_parser::query::{Definition, OperationDefinition}; use std::any::Any; use std::collections::HashMap; +/// GraphQL schema pub struct Schema { query: QueryRoot, mutation: Mutation, @@ -19,6 +20,10 @@ pub struct Schema { } impl Schema { + /// Create a schema. + /// + /// The root object for the query and Mutation needs to be specified. + /// If there is no mutation, you can use `GQLEmptyMutation`. pub fn new(query: Query, mutation: Mutation) -> Self { let mut registry = Registry { types: Default::default(), @@ -91,11 +96,13 @@ impl Schema { } } + /// Add a global data that can be accessed in the `Context`. pub fn data(mut self, data: D) -> Self { self.data.insert(data); self } + /// Start a query and return `QueryBuilder`. pub fn query<'a>(&'a self, query_source: &'a str) -> QueryBuilder<'a, Query, Mutation> { QueryBuilder { query: &self.query, @@ -109,6 +116,7 @@ impl Schema { } } +/// Query builder pub struct QueryBuilder<'a, Query, Mutation> { query: &'a QueryRoot, mutation: &'a Mutation, @@ -120,6 +128,7 @@ pub struct QueryBuilder<'a, Query, Mutation> { } impl<'a, Query, Mutation> QueryBuilder<'a, Query, Mutation> { + /// Specify the operation name. pub fn operator_name(self, name: &'a str) -> Self { QueryBuilder { operation_name: Some(name), @@ -127,6 +136,7 @@ impl<'a, Query, Mutation> QueryBuilder<'a, Query, Mutation> { } } + /// Specify the variables. pub fn variables(self, vars: &'a Variables) -> Self { QueryBuilder { variables: Some(vars), @@ -134,6 +144,7 @@ impl<'a, Query, Mutation> QueryBuilder<'a, Query, Mutation> { } } + /// Execute the query. pub async fn execute(self) -> Result where Query: GQLObject + Send + Sync, diff --git a/src/types/empty_mutation.rs b/src/types/empty_mutation.rs index 8c6fb4cd..660954cc 100644 --- a/src/types/empty_mutation.rs +++ b/src/types/empty_mutation.rs @@ -3,6 +3,24 @@ use graphql_parser::query::Field; use serde_json::{Map, Value}; use std::borrow::Cow; +/// Empty mutation +/// +/// Only the parameters used to construct the Schema, representing an unconfigured mutation. +/// +/// # Examples +/// +/// ```rust +/// use async_graphql::*; +/// +/// struct QueryRoot; +/// +/// #[Object] +/// impl QueryRoot {} +/// +/// fn main() { +/// let schema = Schema::new(QueryRoot, GQLEmptyMutation); +/// } +/// ``` pub struct GQLEmptyMutation; impl GQLType for GQLEmptyMutation { diff --git a/src/validation/mod.rs b/src/validation/mod.rs index f85066a8..4164d545 100644 --- a/src/validation/mod.rs +++ b/src/validation/mod.rs @@ -19,7 +19,11 @@ pub fn check_rules(registry: &Registry, doc: &Document) -> Result<()> { .with(rules::FragmentsOnCompositeTypes) .with(rules::KnownArgumentNames::default()) .with(rules::NoFragmentCycles::default()) - .with(rules::KnownFragmentNames); + .with(rules::KnownFragmentNames) + .with(rules::KnownTypeNames) + .with(rules::LoneAnonymousOperation::default()) + .with(rules::NoUndefinedVariables::default()) + .with(rules::NoUnusedFragments::default()); visit(&mut visitor, &mut ctx, doc); if !ctx.errors.is_empty() { diff --git a/src/validation/rules/known_type_names.rs b/src/validation/rules/known_type_names.rs new file mode 100644 index 00000000..05aecb6e --- /dev/null +++ b/src/validation/rules/known_type_names.rs @@ -0,0 +1,49 @@ +use crate::registry::TypeName; +use crate::validation::context::ValidatorContext; +use crate::validation::visitor::Visitor; +use graphql_parser::query::{ + FragmentDefinition, InlineFragment, TypeCondition, VariableDefinition, +}; +use graphql_parser::Pos; + +#[derive(Default)] +pub struct KnownTypeNames; + +impl<'a> Visitor<'a> for KnownTypeNames { + fn enter_fragment_definition( + &mut self, + ctx: &mut ValidatorContext<'a>, + fragment_definition: &'a FragmentDefinition, + ) { + let TypeCondition::On(name) = &fragment_definition.type_condition; + validate_type(ctx, &name, fragment_definition.position); + } + + fn enter_variable_definition( + &mut self, + ctx: &mut ValidatorContext<'a>, + variable_definition: &'a VariableDefinition, + ) { + validate_type( + ctx, + TypeName::get_basic_typename(&variable_definition.name), + variable_definition.position, + ); + } + + fn enter_inline_fragment( + &mut self, + ctx: &mut ValidatorContext<'a>, + inline_fragment: &'a InlineFragment, + ) { + if let Some(TypeCondition::On(name)) = &inline_fragment.type_condition { + validate_type(ctx, &name, inline_fragment.position); + } + } +} + +fn validate_type(ctx: &mut ValidatorContext<'_>, type_name: &str, pos: Pos) { + if ctx.registry.types.get(type_name).is_none() { + ctx.report_error(vec![pos], format!(r#"Unknown type "{}""#, type_name)); + } +} diff --git a/src/validation/rules/lone_anonymous_operation.rs b/src/validation/rules/lone_anonymous_operation.rs new file mode 100644 index 00000000..e42fc6d8 --- /dev/null +++ b/src/validation/rules/lone_anonymous_operation.rs @@ -0,0 +1,39 @@ +use crate::validation::context::ValidatorContext; +use crate::validation::visitor::Visitor; +use graphql_parser::query::{Definition, Document, OperationDefinition}; + +#[derive(Default)] +pub struct LoneAnonymousOperation { + operation_count: Option, +} + +impl<'a> Visitor<'a> for LoneAnonymousOperation { + fn enter_document(&mut self, _ctx: &mut ValidatorContext<'a>, doc: &'a Document) { + self.operation_count = Some( + doc.definitions + .iter() + .filter(|d| match d { + Definition::Operation(_) => true, + Definition::Fragment(_) => false, + }) + .count(), + ); + } + + fn enter_operation_definition( + &mut self, + ctx: &mut ValidatorContext<'a>, + operation_definition: &'a OperationDefinition, + ) { + if let Some(operation_count) = self.operation_count { + if let OperationDefinition::SelectionSet(s) = operation_definition { + if operation_count > 1 { + ctx.report_error( + vec![s.span.0, s.span.1], + "This anonymous operation must be the only defined operation", + ); + } + } + } + } +} diff --git a/src/validation/rules/mod.rs b/src/validation/rules/mod.rs index 7986a0cc..4c456caa 100644 --- a/src/validation/rules/mod.rs +++ b/src/validation/rules/mod.rs @@ -4,7 +4,11 @@ mod fields_on_correct_type; mod fragments_on_composite_types; mod known_argument_names; mod known_fragment_names; +mod known_type_names; +mod lone_anonymous_operation; mod no_fragment_cycles; +mod no_undefined_variables; +mod no_unused_fragments; pub use arguments_of_correct_type::ArgumentsOfCorrectType; pub use default_values_of_correct_type::DefaultValuesOfCorrectType; @@ -12,4 +16,8 @@ pub use fields_on_correct_type::FieldsOnCorrectType; pub use fragments_on_composite_types::FragmentsOnCompositeTypes; pub use known_argument_names::KnownArgumentNames; pub use known_fragment_names::KnownFragmentNames; +pub use known_type_names::KnownTypeNames; +pub use lone_anonymous_operation::LoneAnonymousOperation; pub use no_fragment_cycles::NoFragmentCycles; +pub use no_undefined_variables::NoUndefinedVariables; +pub use no_unused_fragments::NoUnusedFragments; diff --git a/src/validation/rules/no_undefined_variables.rs b/src/validation/rules/no_undefined_variables.rs new file mode 100644 index 00000000..50b3a7cb --- /dev/null +++ b/src/validation/rules/no_undefined_variables.rs @@ -0,0 +1,57 @@ +use crate::validation::context::ValidatorContext; +use crate::validation::visitor::Visitor; +use graphql_parser::query::{Field, OperationDefinition, VariableDefinition}; +use graphql_parser::schema::{Directive, Value}; +use graphql_parser::Pos; +use std::collections::HashSet; + +#[derive(Default)] +pub struct NoUndefinedVariables<'a> { + vars: HashSet<&'a str>, + pos_stack: Vec, +} + +impl<'a> Visitor<'a> for NoUndefinedVariables<'a> { + fn enter_operation_definition( + &mut self, + _ctx: &mut ValidatorContext<'a>, + _operation_definition: &'a OperationDefinition, + ) { + self.vars.clear(); + } + + fn enter_variable_definition( + &mut self, + _ctx: &mut ValidatorContext<'a>, + variable_definition: &'a VariableDefinition, + ) { + self.vars.insert(&variable_definition.name); + } + + fn enter_directive(&mut self, _ctx: &mut ValidatorContext<'a>, directive: &'a Directive) { + self.pos_stack.push(directive.position); + } + + fn exit_directive(&mut self, _ctx: &mut ValidatorContext<'a>, _directive: &'a Directive) { + self.pos_stack.pop(); + } + + fn enter_argument(&mut self, ctx: &mut ValidatorContext<'a>, _name: &str, value: &'a Value) { + if let Value::Variable(var_name) = value { + if !self.vars.contains(var_name.as_str()) { + ctx.report_error( + vec![self.pos_stack.last().cloned().unwrap()], + format!("Variable \"${}\" is not defined", var_name), + ); + } + } + } + + fn enter_field(&mut self, _ctx: &mut ValidatorContext<'a>, field: &'a Field) { + self.pos_stack.push(field.position); + } + + fn exit_field(&mut self, _ctx: &mut ValidatorContext<'a>, _field: &'a Field) { + self.pos_stack.pop(); + } +} diff --git a/src/validation/rules/no_unused_fragments.rs b/src/validation/rules/no_unused_fragments.rs new file mode 100644 index 00000000..d6115570 --- /dev/null +++ b/src/validation/rules/no_unused_fragments.rs @@ -0,0 +1,32 @@ +use crate::validation::context::ValidatorContext; +use crate::validation::visitor::Visitor; +use graphql_parser::query::{Definition, Document, FragmentSpread}; +use std::collections::HashSet; + +#[derive(Default)] +pub struct NoUnusedFragments<'a> { + spreads: HashSet<&'a str>, +} + +impl<'a> Visitor<'a> for NoUnusedFragments<'a> { + fn exit_document(&mut self, ctx: &mut ValidatorContext<'a>, doc: &'a Document) { + for d in &doc.definitions { + if let Definition::Fragment(fragment) = d { + if !self.spreads.contains(fragment.name.as_str()) { + ctx.report_error( + vec![fragment.position], + format!(r#"Fragment "{}" is never used"#, fragment.name), + ); + } + } + } + } + + fn enter_fragment_spread( + &mut self, + _ctx: &mut ValidatorContext<'a>, + fragment_spread: &'a FragmentSpread, + ) { + self.spreads.insert(&fragment_spread.fragment_name); + } +} diff --git a/src/validation/utils.rs b/src/validation/utils.rs index 504df46c..321c105c 100644 --- a/src/validation/utils.rs +++ b/src/validation/utils.rs @@ -2,6 +2,10 @@ use crate::registry::{Registry, Type, TypeName}; use crate::Value; pub fn is_valid_input_value(registry: &Registry, type_name: &str, value: &Value) -> bool { + if let Value::Variable(_) = value { + return true; + } + match TypeName::create(type_name) { TypeName::NonNull(type_name) => match value { Value::Null => false,