diff --git a/src/context.rs b/src/context.rs index e2246a6e..31d7556d 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,8 +1,7 @@ use crate::extensions::BoxExtension; use crate::parser::ast::{Directive, Field, SelectionSet}; use crate::registry::Registry; -use crate::{InputValueType, QueryError, Result, Schema, Type}; -use crate::{Pos, Positioned, Value}; +use crate::{InputValueType, Lookahead, Pos, Positioned, QueryError, Result, Schema, Type, Value}; use async_graphql_parser::ast::Document; use async_graphql_parser::UploadValue; use fnv::FnvHashMap; @@ -521,4 +520,48 @@ impl<'a> ContextBase<'a, &'a Positioned> { pub fn position(&self) -> Pos { self.pos } + + /// Creates a uniform interface to inspect the forthcoming selections. + /// + /// # Examples + /// + /// ```no_run + /// use async_graphql::*; + /// + /// #[SimpleObject] + /// struct Detail { + /// c: i32, + /// d: i32, + /// } + /// + /// #[SimpleObject] + /// struct MyObj { + /// a: i32, + /// b: i32, + /// #[field(ref)] + /// detail: Detail, + /// } + /// + /// struct Query; + /// + /// #[Object] + /// impl Query { + /// async fn obj(&self, ctx: &Context<'_>) -> MyObj { + /// if ctx.look_ahead().field("a").exists() { + /// // This is a query like `obj { a }` + /// } else if ctx.look_ahead().field("detail").field("c").exists() { + /// // This is a query like `obj { detail { c } }` + /// } else { + /// // This query doesn't have `a` + /// } + /// unimplemented!() + /// } + /// } + /// ``` + pub fn look_ahead(&self) -> Lookahead { + Lookahead { + document: self.document, + field: Some(&self.item.node), + } + } } diff --git a/src/guard.rs b/src/guard.rs index 2ba97d08..e44cfed1 100644 --- a/src/guard.rs +++ b/src/guard.rs @@ -14,18 +14,18 @@ pub trait Guard { /// An extension trait for `Guard` pub trait GuardExt: Guard + Sized { /// Merge the two guards. - fn and(self, other: R) -> GuardAnd { - GuardAnd(self, other) + fn and(self, other: R) -> And { + And(self, other) } } impl GuardExt for T {} /// Guard for `GuardExt::and` -pub struct GuardAnd(A, B); +pub struct And(A, B); #[async_trait::async_trait] -impl Guard for GuardAnd { +impl Guard for And { async fn check(&self, ctx: &Context<'_>) -> FieldResult<()> { self.0.check(ctx).await?; self.1.check(ctx).await diff --git a/src/lib.rs b/src/lib.rs index bad63153..ca65fb2e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -84,6 +84,7 @@ extern crate serde_derive; mod base; mod context; mod error; +mod look_ahead; mod model; mod mutation_resolver; mod query; @@ -120,6 +121,7 @@ pub use error::{ Error, ErrorExtensions, FieldError, FieldResult, InputValueError, InputValueResult, ParseRequestError, QueryError, ResultExt, }; +pub use look_ahead::Lookahead; pub use parser::{Pos, Positioned, Value}; pub use query::{IntoQueryBuilder, IntoQueryBuilderOpts, QueryBuilder, QueryResponse}; pub use registry::CacheControl; diff --git a/src/look_ahead.rs b/src/look_ahead.rs new file mode 100644 index 00000000..934bb0d5 --- /dev/null +++ b/src/look_ahead.rs @@ -0,0 +1,208 @@ +use async_graphql_parser::ast::{Document, Field, Selection, SelectionSet}; + +/// A selection performed by a query +pub struct Lookahead<'a> { + pub(crate) document: &'a Document, + pub(crate) field: Option<&'a Field>, +} + +impl<'a> Lookahead<'a> { + /// Check if the specified field exists in the current selection. + pub fn field(&self, name: &str) -> Lookahead { + Lookahead { + document: self.document, + field: self + .field + .and_then(|field| find(self.document, &field.selection_set.node, name)), + } + } + + /// Returns true if field exists otherwise return false. + #[inline] + pub fn exists(&self) -> bool { + self.field.is_some() + } +} + +fn find<'a>( + document: &'a Document, + selection_set: &'a SelectionSet, + name: &str, +) -> Option<&'a Field> { + for item in &selection_set.items { + match &item.node { + Selection::Field(field) => { + if field.name.node == name { + return Some(&field.node); + } + } + Selection::InlineFragment(inline_fragment) => { + if let Some(field) = find(document, &inline_fragment.selection_set.node, name) { + return Some(field); + } + } + Selection::FragmentSpread(fragment_spread) => { + if let Some(fragment) = document.fragments().get(fragment_spread.fragment_name.node) + { + if let Some(field) = find(document, &fragment.selection_set.node, name) { + return Some(field); + } + } + } + } + } + None +} + +#[cfg(test)] +mod tests { + use crate::*; + + #[async_std::test] + async fn test_look_ahead() { + #[SimpleObject(internal)] + struct Detail { + c: i32, + d: i32, + } + + #[SimpleObject(internal)] + struct MyObj { + a: i32, + b: i32, + #[field(ref)] + detail: Detail, + } + + struct Query; + + #[Object(internal)] + impl Query { + async fn obj(&self, ctx: &Context<'_>, n: i32) -> MyObj { + if ctx.look_ahead().field("a").exists() { + // This is a query like `obj { a }` + assert_eq!(n, 1); + } else if ctx.look_ahead().field("detail").field("c").exists() { + // This is a query like `obj { detail { c } }` + assert_eq!(n, 2); + } else { + // This query doesn't have `a` + assert_eq!(n, 3); + } + MyObj { + a: 0, + b: 0, + detail: Detail { c: 0, d: 0 }, + } + } + } + + let schema = Schema::new(Query, EmptyMutation, EmptySubscription); + + schema + .execute( + r#"{ + obj(n: 1) { + a + } + }"#, + ) + .await + .unwrap(); + + schema + .execute( + r#"{ + obj(n: 1) { + k:a + } + }"#, + ) + .await + .unwrap(); + + schema + .execute( + r#"{ + obj(n: 2) { + detail { + c + } + } + }"#, + ) + .await + .unwrap(); + + schema + .execute( + r#"{ + obj(n: 3) { + b + } + }"#, + ) + .await + .unwrap(); + + schema + .execute( + r#"{ + obj(n: 1) { + ... { + a + } + } + }"#, + ) + .await + .unwrap(); + + schema + .execute( + r#"{ + obj(n: 2) { + ... { + detail { + c + } + } + } + }"#, + ) + .await + .unwrap(); + + schema + .execute( + r#"{ + obj(n: 1) { + ... A + } + } + + fragment A on MyObj { + a + }"#, + ) + .await + .unwrap(); + + schema + .execute( + r#"{ + obj(n: 2) { + ... A + } + } + + fragment A on MyObj { + detail { + c + } + }"#, + ) + .await + .unwrap(); + } +} diff --git a/src/validators/mod.rs b/src/validators/mod.rs index de09e79c..bd0bc00e 100644 --- a/src/validators/mod.rs +++ b/src/validators/mod.rs @@ -14,6 +14,8 @@ pub use string_validators::{Email, StringMaxLength, StringMinLength, MAC}; /// /// You can create your own input value validator by implementing this trait. /// +/// # Examples +/// /// ```no_run /// use async_graphql::*; /// use async_graphql::validators::{Email, MAC, IntRange};