Make the executor and validation APIs public to enable split parsing and execution (#874)

* Make the executor and validation APIs public to enable splitting parsing and execution into two stages

Based on https://github.com/graphql-rust/juniper/pull/773#issuecomment-703783048 and https://github.com/graphql-rust/juniper/pull/773#issuecomment-704009918

* Fix fmt warning for visit_all_rules

* Add `Definition` to the public API so it's available for the result of compilation

* Make OperationType public so that user land code can tell the difference between query/mutation and subscription

* Add integrations tests for execute_validated_query_async, visit_all_rules, get_operation, and resolve_validated_subscription

Co-authored-by: Christian Legnitto <LegNeato@users.noreply.github.com>
This commit is contained in:
Jerel Unruh 2021-04-03 00:13:06 -05:00 committed by GitHub
parent e7f7e7bff3
commit c78045c167
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 127 additions and 6 deletions

View file

@ -8,6 +8,7 @@ publish = false
derive_more = "0.99"
futures = "0.3"
juniper = { path = "../../juniper" }
juniper_subscriptions = { path = "../../juniper_subscriptions" }
[dev-dependencies]
async-trait = "0.1.39"

View file

@ -18,3 +18,5 @@ mod issue_398;
mod issue_407;
#[cfg(test)]
mod issue_500;
#[cfg(test)]
mod pre_parse;

View file

@ -0,0 +1,102 @@
use futures::{Stream, StreamExt, TryFutureExt};
use juniper::{
executor::{execute_validated_query_async, get_operation, resolve_validated_subscription},
graphql_object, graphql_subscription,
parser::parse_document_source,
validation::{validate_input_values, visit_all_rules, ValidatorContext},
EmptyMutation, FieldError, OperationType, RootNode, Variables,
};
use std::pin::Pin;
pub struct Context;
impl juniper::Context for Context {}
pub type UserStream = Pin<Box<dyn Stream<Item = Result<User, FieldError>> + Send>>;
pub struct Query;
#[graphql_object(context = Context)]
impl Query {
fn users() -> Vec<User> {
vec![User]
}
}
pub struct Subscription;
#[graphql_subscription(context = Context)]
impl Subscription {
async fn users() -> UserStream {
Box::pin(futures::stream::iter(vec![Ok(User)]))
}
}
#[derive(Clone)]
pub struct User;
#[graphql_object(context = Context)]
impl User {
fn id() -> i32 {
1
}
}
type Schema = RootNode<'static, Query, EmptyMutation<Context>, Subscription>;
#[tokio::test]
async fn query_document_can_be_pre_parsed() {
let root_node = &Schema::new(Query, EmptyMutation::<Context>::new(), Subscription);
let document_source = r#"query { users { id } }"#;
let document = parse_document_source(document_source, &root_node.schema).unwrap();
{
let mut ctx = ValidatorContext::new(&root_node.schema, &document);
visit_all_rules(&mut ctx, &document);
let errors = ctx.into_errors();
assert!(errors.is_empty());
}
let operation = get_operation(&document, None).unwrap();
assert!(operation.item.operation_type == OperationType::Query);
let errors = validate_input_values(&juniper::Variables::new(), operation, &root_node.schema);
assert!(errors.is_empty());
let (_, errors) = execute_validated_query_async(
&document,
operation,
root_node,
&Variables::new(),
&Context {},
)
.await
.unwrap();
assert!(errors.len() == 0);
}
#[tokio::test]
async fn subscription_document_can_be_pre_parsed() {
let root_node = &Schema::new(Query, EmptyMutation::<Context>::new(), Subscription);
let document_source = r#"subscription { users { id } }"#;
let document = parse_document_source(document_source, &root_node.schema).unwrap();
let operation = get_operation(&document, None).unwrap();
assert!(operation.item.operation_type == OperationType::Subscription);
let mut stream = resolve_validated_subscription(
&document,
&operation,
&root_node,
&Variables::new(),
&Context {},
)
.map_ok(|(stream, errors)| juniper_subscriptions::Connection::from_stream(stream, errors))
.await
.unwrap();
let _ = stream.next().await.unwrap();
}

View file

@ -112,6 +112,7 @@ pub struct Directive<'a, S> {
pub arguments: Option<Spanning<Arguments<'a, S>>>,
}
#[allow(missing_docs)]
#[derive(Clone, PartialEq, Debug)]
pub enum OperationType {
Query,
@ -119,6 +120,7 @@ pub enum OperationType {
Subscription,
}
#[allow(missing_docs)]
#[derive(Clone, PartialEq, Debug)]
pub struct Operation<'a, S> {
pub operation_type: OperationType,
@ -136,12 +138,14 @@ pub struct Fragment<'a, S> {
pub selection_set: Vec<Selection<'a, S>>,
}
#[doc(hidden)]
#[derive(Clone, PartialEq, Debug)]
pub enum Definition<'a, S> {
Operation(Spanning<Operation<'a, S>>),
Fragment(Spanning<Fragment<'a, S>>),
}
#[doc(hidden)]
pub type Document<'a, S> = Vec<Definition<'a, S>>;
/// Parse an unstructured input value into a Rust data type.

View file

@ -96,6 +96,7 @@ where
}
}
#[doc(hidden)]
#[derive(Debug, Clone, PartialEq)]
pub struct ChildSelection<'a, S: 'a> {
pub(super) inner: LookAheadSelection<'a, S>,

View file

@ -1,3 +1,5 @@
//! Resolve the document to values
use std::{
borrow::Cow,
cmp::Ordering,
@ -54,6 +56,7 @@ pub struct Registry<'r, S = DefaultScalarValue> {
pub types: FnvHashMap<Name, MetaType<'r, S>>,
}
#[allow(missing_docs)]
#[derive(Clone)]
pub enum FieldPath<'a> {
Root(SourcePosition),
@ -979,6 +982,7 @@ where
Ok((value, errors))
}
#[doc(hidden)]
pub fn get_operation<'b, 'd, 'e, S>(
document: &'b Document<'d, S>,
operation_name: Option<&str>,

View file

@ -119,13 +119,13 @@ mod value;
#[macro_use]
mod macros;
mod ast;
mod executor;
pub mod executor;
mod introspection;
pub mod parser;
pub(crate) mod schema;
mod types;
mod util;
mod validation;
pub mod validation;
// This needs to be public until docs have support for private modules:
// https://github.com/rust-lang/cargo/issues/1520
pub mod http;
@ -145,12 +145,15 @@ pub use crate::util::to_camel_case;
use crate::{
executor::{execute_validated_query, get_operation},
introspection::{INTROSPECTION_QUERY, INTROSPECTION_QUERY_WITHOUT_DESCRIPTIONS},
parser::{parse_document_source, ParseError, Spanning},
parser::parse_document_source,
validation::{validate_input_values, visit_all_rules, ValidatorContext},
};
pub use crate::{
ast::{FromInputValue, InputValue, Selection, ToInputValue, Type},
ast::{
Definition, Document, FromInputValue, InputValue, Operation, OperationType, Selection,
ToInputValue, Type,
},
executor::{
Applies, Context, ExecutionError, ExecutionResult, Executor, FieldError, FieldResult,
FromContext, IntoFieldError, IntoResolvable, LookAheadArgument, LookAheadMethods,
@ -161,6 +164,7 @@ pub use crate::{
subscription::{ExtractTypeFromStream, IntoFieldResult},
AsDynGraphQLValue,
},
parser::{ParseError, Spanning},
schema::{
meta,
model::{RootNode, SchemaType},

View file

@ -19,6 +19,7 @@ enum Path<'a> {
ObjectField(&'a str, &'a Path<'a>),
}
#[doc(hidden)]
pub fn validate_input_values<S>(
values: &Variables<S>,
operation: &Spanning<Operation<S>>,

View file

@ -10,11 +10,11 @@ mod visitor;
#[cfg(test)]
pub(crate) mod test_harness;
pub(crate) use self::rules::visit_all_rules;
pub use self::{
context::{RuleError, ValidatorContext},
input_value::validate_input_values,
multi_visitor::MultiVisitorNil,
rules::visit_all_rules,
traits::Visitor,
visitor::visit,
};

View file

@ -11,6 +11,7 @@ use crate::{
#[doc(hidden)]
pub struct MultiVisitorNil;
#[doc(hidden)]
impl MultiVisitorNil {
pub fn with<V>(self, visitor: V) -> MultiVisitorCons<V, Self> {
MultiVisitorCons(visitor, self)

View file

@ -30,7 +30,8 @@ use crate::{
};
use std::fmt::Debug;
pub(crate) fn visit_all_rules<'a, S: Debug>(ctx: &mut ValidatorContext<'a, S>, doc: &'a Document<S>)
#[doc(hidden)]
pub fn visit_all_rules<'a, S: Debug>(ctx: &mut ValidatorContext<'a, S>, doc: &'a Document<S>)
where
S: ScalarValue,
{