diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d67685595..82c0f4def 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -103,8 +103,8 @@ jobs: - { feature: chrono-tz, crate: juniper } - { feature: expose-test-schema, crate: juniper } - { feature: graphql-parser, crate: juniper } + - { feature: json, crate: juniper } - { feature: schema-language, crate: juniper } - - { feature: serde_json, crate: juniper } - { feature: time, crate: juniper } - { feature: url, crate: juniper } - { feature: uuid, crate: juniper } diff --git a/README.md b/README.md index 3bd500489..59169b0a6 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ your Schemas automatically. - [chrono-tz][chrono-tz] - [time][time] - [bson][bson] +- [serde_json][serde_json] ### Web Frameworks @@ -122,3 +123,4 @@ Juniper has not reached 1.0 yet, thus some API instability should be expected. [time]: https://crates.io/crates/time [bson]: https://crates.io/crates/bson [juniper-from-schema]: https://github.com/davidpdrsn/juniper-from-schema +[serde_json]: https://crates.io/crates/serde_json \ No newline at end of file diff --git a/book/src/advanced/index.md b/book/src/advanced/index.md index 8db84fc64..c1ace946d 100644 --- a/book/src/advanced/index.md +++ b/book/src/advanced/index.md @@ -9,3 +9,4 @@ The chapters below cover some more advanced scenarios. - [Multiple operations per request](multiple_ops_per_request.md) - [Dataloaders](dataloaders.md) - [Subscriptions](subscriptions.md) +- [Dynamic JSON value](serde_json.md) diff --git a/book/src/advanced/serde_json.md b/book/src/advanced/serde_json.md new file mode 100644 index 000000000..61f1d0bb4 --- /dev/null +++ b/book/src/advanced/serde_json.md @@ -0,0 +1,151 @@ +Dynamic JSON value +================== + +The following example shows you how to run a GraphQL query against JSON data held in a `serde_json::Value`. To make this work you have to construct the `RootNode` using the `new_with_info` method so that you can describe the GraphQL schema of the JSON data. + +```rust +use serde_json::json; +use juniper::{ + integrations::serde_json::TypeInfo, + graphql_value, RootNode, EmptyMutation, EmptySubscription, Variables, +}; + +fn main() { + // Use SDL to define the structure of the JSON data. + let type_info = TypeInfo { + name: "Query".to_string(), + schema: Some(r#" + type Query { + bar: Bar + } + type Bar { + name: String + capacity: Int + open: Boolean! + } + "#.to_string()), + }; + + let data = json!({ + "bar": { + "name": "Cheers", + "capacity": 80, + "open": true, + }, + }); + + + let schema = RootNode::new_with_info( + data, + EmptyMutation::new(), + EmptySubscription::new(), + type_info, + (), + (), + ); + + let (res, _errors) = juniper::execute_sync( + "query { bar { name} }", + None, + &schema, + &Variables::new(), + &(), + ).unwrap(); + + // Ensure the value matches. + assert_eq!( + res, + graphql_value!({ + "bar": { "name": "Cheers"}, + }) + ); +} +``` + +## Using Json Fields in a GraphQL Object + +If you know the schema definition at compile time for a value that your want to hold as a json +field of normal juniper graphql object, you can use the `TypedJson` wrapper struct to provide the +type information of the wrapped `serde_json::Value`. + +```rust +use serde_json::json; +use juniper::{ + graphql_object, EmptyMutation, EmptySubscription, FieldResult, + Variables, graphql_value, + integrations::serde_json::{TypedJsonInfo,TypedJson}, +}; + +// Defines schema for the Person Graphql Type +struct Person; +impl TypedJsonInfo for Person { + fn type_name() -> &'static str { + "Person" + } + fn schema() -> &'static str { + r#" + type Person { + name: String + age: Int + } + "# + } +} + +// You can also define graphql input types this way +struct DetailInput; +impl TypedJsonInfo for DetailInput { + fn type_name() -> &'static str { + "DetailInput" + } + fn schema() -> &'static str { + r#" + input DetailInput { + ever: Boolean! + } + "# + } +} + +struct Query; + +#[graphql_object] +impl Query { + // define a field that uses both Json input type and output type. + pub fn favorite_person(details: TypedJson) -> FieldResult> { + let ever = details.json.get("ever").unwrap().as_bool().unwrap(); + let data = if ever { + json!({"name": "Destiny", "age":29}) + } else { + json!({"name": "David", "age":45}) + }; + Ok(TypedJson::new(data)) + } +} + +fn main() { + + let root_node = &juniper::RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new()); + + // Run the executor. + let (res, _errors) = juniper::execute_sync(r#" + query { + favoritePerson( details: { ever: true }) { + name, age + } + }"#, + None, + root_node, + &Variables::new(), + &(), + ).unwrap(); + + // Ensure the value matches. + assert_eq!( + res, + graphql_value!({ + "favoritePerson": {"name": "Destiny", "age":29}, + }), + ); +} +``` diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index 149b7e151..53f2332b1 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -46,6 +46,8 @@ All user visible changes to `juniper` crate will be documented in this file. Thi - Reworked [`chrono` crate] integration GraphQL scalars according to [graphql-scalars.dev] specs: ([#1010]) - Disabled `chrono` [Cargo feature] by default. - Removed `scalar-naivetime` [Cargo feature]. +- Renamed `graphql-parser-integration` [Cargo feature] as `graphql-parser`. ([#1043]) +- Renamed `serde_json` [Cargo feature] as `json`. ([#975]) ### Added @@ -74,6 +76,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi [#965]: /../../pull/965 [#966]: /../../pull/966 [#971]: /../../pull/971 +[#975]: /../../pull/975 [#979]: /../../pull/979 [#985]: /../../pull/985 [#987]: /../../pull/987 @@ -90,6 +93,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi [#1017]: /../../pull/1017 [#1025]: /../../pull/1025 [#1026]: /../../pull/1026 +[#1043]: /../../pull/1043 [#1051]: /../../issues/1051 [#1054]: /../../pull/1054 diff --git a/juniper/Cargo.toml b/juniper/Cargo.toml index 8fde83175..c4040bad5 100644 --- a/juniper/Cargo.toml +++ b/juniper/Cargo.toml @@ -32,6 +32,7 @@ default = [ ] chrono-clock = ["chrono", "chrono/clock"] expose-test-schema = ["anyhow", "serde_json"] +json = ["ref-cast", "serde_json", "smartstring/serde"] schema-language = ["graphql-parser"] [dependencies] @@ -46,6 +47,7 @@ futures-enum = { version = "0.1.12", default-features = false } graphql-parser = { version = "0.4", optional = true } indexmap = { version = "1.0", features = ["serde-1"] } juniper_codegen = { version = "0.16.0-dev", path = "../juniper_codegen" } +ref-cast = { version = "1.0.6", optional = true } serde = { version = "1.0.8", features = ["derive"], default-features = false } serde_json = { version = "1.0.2", default-features = false, optional = true } smartstring = "1.0" diff --git a/juniper/README.md b/juniper/README.md index 03b1ed8cf..bc408c0f9 100644 --- a/juniper/README.md +++ b/juniper/README.md @@ -47,6 +47,7 @@ As an exception to other [GraphQL] libraries for other languages, [Juniper] buil - [`bson`] - [`chrono`] (feature gated) - [`chrono-tz`] (feature gated) +- [`serde_json`] (`json` feature gated) - [`time`] (feature gated) - [`url`] - [`uuid`] diff --git a/juniper/src/executor/look_ahead.rs b/juniper/src/executor/look_ahead.rs index 41bae7eea..3d43845eb 100644 --- a/juniper/src/executor/look_ahead.rs +++ b/juniper/src/executor/look_ahead.rs @@ -38,26 +38,21 @@ where { fn from_input_value(input_value: &'a InputValue, vars: &'a Variables) -> Self { match *input_value { - InputValue::Null => LookAheadValue::Null, - InputValue::Scalar(ref s) => LookAheadValue::Scalar(s), - InputValue::Enum(ref e) => LookAheadValue::Enum(e), + InputValue::Null => Self::Null, + InputValue::Scalar(ref s) => Self::Scalar(s), + InputValue::Enum(ref e) => Self::Enum(e), InputValue::Variable(ref name) => vars .get(name) .map(|v| Self::from_input_value(v, vars)) - .unwrap_or(LookAheadValue::Null), - InputValue::List(ref l) => LookAheadValue::List( + .unwrap_or(Self::Null), + InputValue::List(ref l) => Self::List( l.iter() - .map(|i| LookAheadValue::from_input_value(&i.item, vars)) + .map(|i| Self::from_input_value(&i.item, vars)) .collect(), ), - InputValue::Object(ref o) => LookAheadValue::Object( + InputValue::Object(ref o) => Self::Object( o.iter() - .map(|&(ref n, ref i)| { - ( - &n.item as &str, - LookAheadValue::from_input_value(&i.item, vars), - ) - }) + .map(|&(ref n, ref i)| (&n.item as &str, Self::from_input_value(&i.item, vars))) .collect(), ), } diff --git a/juniper/src/executor/mod.rs b/juniper/src/executor/mod.rs index 47d5fead2..0aae3ad2f 100644 --- a/juniper/src/executor/mod.rs +++ b/juniper/src/executor/mod.rs @@ -600,6 +600,34 @@ where } } + // This hack is required as Juniper doesn't allow at the + // moment for custom defined types to tweak into executor. + // TODO: Redesign executor layer to allow such things. + #[cfg(feature = "json")] + #[doc(hidden)] + pub(crate) fn field_with_parent_type_sub_executor<'s>( + &'s self, + field_alias: &'a str, + location: SourcePosition, + selection_set: Option<&'s [Selection<'a, S>]>, + ) -> Executor<'s, 'a, CtxT, S> { + Executor { + fragments: self.fragments, + variables: self.variables, + current_selection_set: selection_set, + parent_selection_set: self.current_selection_set, + current_type: self.current_type.clone(), + schema: self.schema, + context: self.context, + errors: self.errors, + field_path: Arc::new(FieldPath::Field( + field_alias, + location, + Arc::clone(&self.field_path), + )), + } + } + #[doc(hidden)] pub fn type_sub_executor<'s>( &'s self, @@ -826,6 +854,9 @@ where QueryT: GraphQLType, MutationT: GraphQLType, SubscriptionT: GraphQLType, + QueryT::TypeInfo: Sized, + MutationT::TypeInfo: Sized, + SubscriptionT::TypeInfo: Sized, { if operation.item.operation_type == OperationType::Subscription { return Err(GraphQLError::IsSubscription); @@ -917,12 +948,12 @@ pub async fn execute_validated_query_async<'a, 'b, QueryT, MutationT, Subscripti ) -> Result<(Value, Vec>), GraphQLError<'a>> where QueryT: GraphQLTypeAsync, - QueryT::TypeInfo: Sync, + QueryT::TypeInfo: Sync + Sized, QueryT::Context: Sync, MutationT: GraphQLTypeAsync, - MutationT::TypeInfo: Sync, + MutationT::TypeInfo: Sync + Sized, SubscriptionT: GraphQLType + Sync, - SubscriptionT::TypeInfo: Sync, + SubscriptionT::TypeInfo: Sync + Sized, S: ScalarValue + Send + Sync, { if operation.item.operation_type == OperationType::Subscription { @@ -1064,12 +1095,12 @@ where 'd: 'r, 'op: 'd, QueryT: GraphQLTypeAsync, - QueryT::TypeInfo: Sync, + QueryT::TypeInfo: Sync + Sized, QueryT::Context: Sync + 'r, MutationT: GraphQLTypeAsync, - MutationT::TypeInfo: Sync, + MutationT::TypeInfo: Sync + Sized, SubscriptionT: GraphQLSubscriptionType, - SubscriptionT::TypeInfo: Sync, + SubscriptionT::TypeInfo: Sync + Sized, S: ScalarValue + Send + Sync, { if operation.item.operation_type != OperationType::Subscription { diff --git a/juniper/src/http/mod.rs b/juniper/src/http/mod.rs index 17420de3a..de5499250 100644 --- a/juniper/src/http/mod.rs +++ b/juniper/src/http/mod.rs @@ -96,6 +96,9 @@ where QueryT: GraphQLType, MutationT: GraphQLType, SubscriptionT: GraphQLType, + QueryT::TypeInfo: Sized, + MutationT::TypeInfo: Sized, + SubscriptionT::TypeInfo: Sized, { GraphQLResponse(crate::execute_sync( &self.query, @@ -117,12 +120,12 @@ where ) -> GraphQLResponse<'a, S> where QueryT: GraphQLTypeAsync, - QueryT::TypeInfo: Sync, + QueryT::TypeInfo: Sync + Sized, QueryT::Context: Sync, MutationT: GraphQLTypeAsync, - MutationT::TypeInfo: Sync, + MutationT::TypeInfo: Sync + Sized, SubscriptionT: GraphQLType + Sync, - SubscriptionT::TypeInfo: Sync, + SubscriptionT::TypeInfo: Sync + Sized, S: ScalarValue + Send + Sync, { let op = self.operation_name.as_deref(); @@ -146,12 +149,12 @@ where 'rn: 'a, 'ctx: 'a, QueryT: GraphQLTypeAsync, - QueryT::TypeInfo: Sync, + QueryT::TypeInfo: Sync + Sized, QueryT::Context: Sync, MutationT: GraphQLTypeAsync, - MutationT::TypeInfo: Sync, + MutationT::TypeInfo: Sync + Sized, SubscriptionT: GraphQLSubscriptionType, - SubscriptionT::TypeInfo: Sync, + SubscriptionT::TypeInfo: Sync + Sized, S: ScalarValue + Send + Sync, { let op = req.operation_name.as_deref(); @@ -277,6 +280,9 @@ where QueryT: GraphQLType, MutationT: GraphQLType, SubscriptionT: GraphQLType, + QueryT::TypeInfo: Sized, + MutationT::TypeInfo: Sized, + SubscriptionT::TypeInfo: Sized, { match *self { Self::Single(ref req) => { @@ -301,12 +307,12 @@ where ) -> GraphQLBatchResponse<'a, S> where QueryT: GraphQLTypeAsync, - QueryT::TypeInfo: Sync, + QueryT::TypeInfo: Sync + Sized, QueryT::Context: Sync, MutationT: GraphQLTypeAsync, - MutationT::TypeInfo: Sync, + MutationT::TypeInfo: Sync + Sized, SubscriptionT: GraphQLSubscriptionType, - SubscriptionT::TypeInfo: Sync, + SubscriptionT::TypeInfo: Sync + Sized, S: Send + Sync, { match self { diff --git a/juniper/src/integrations/json.rs b/juniper/src/integrations/json.rs new file mode 100644 index 000000000..34865cf3f --- /dev/null +++ b/juniper/src/integrations/json.rs @@ -0,0 +1,2974 @@ +//! GraphQL support for [`serde_json::Value`]. + +use std::{ + convert::{TryFrom as _, TryInto as _}, + marker::PhantomData, + ops::{Deref, DerefMut}, + sync::atomic::AtomicPtr, +}; + +use futures::future; +use graphql_parser::{ + query::Type as SchemaType, + schema::{Document as Schema, ParseError}, +}; +use ref_cast::RefCast; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; + +use crate::{ + ast, + macros::reflect, + marker::{IsInputType, IsOutputType}, + meta::{self, MetaType}, + parser::ScalarToken, + Arguments, BoxFuture, ExecutionResult, Executor, FieldError, FromInputValue, GraphQLType, + GraphQLValue, GraphQLValueAsync, InputValue, IntoFieldError, ParseScalarResult, + ParseScalarValue, Registry, ScalarValue, Selection, Spanning, ToInputValue, ID, +}; + +pub use serde_json::{json, Error, Value}; + +impl IntoFieldError for Error { + fn into_field_error(self) -> FieldError { + self.into() + } +} + +impl IsInputType for Value {} + +impl IsOutputType for Value {} + +impl GraphQLType for Value { + fn name(info: &Self::TypeInfo) -> Option<&str> { + >::name(info) + } + + fn meta<'r>(info: &Self::TypeInfo, registry: &mut Registry<'r, S>) -> MetaType<'r, S> + where + S: 'r, + { + >::meta(info, registry) + } +} + +impl GraphQLValue for Value { + type Context = (); + type TypeInfo = (); + + fn type_name<'i>(&self, info: &'i Self::TypeInfo) -> Option<&'i str> { + >::name(info) + } + + fn resolve( + &self, + info: &Self::TypeInfo, + selection: Option<&[Selection]>, + executor: &Executor, + ) -> ExecutionResult { + use serde::ser::Error as _; + + if selection.is_some() && matches!(self, Self::Bool(_) | Self::Number(_) | Self::String(_)) + { + return Err(FieldError::new( + "cannot select fields on a leaf opaque JSON value", + crate::Value::null(), + )); + } + + match self { + Self::Null => Ok(crate::Value::null()), + Self::Bool(b) => executor.resolve(&(), &b), + Self::Number(n) => { + if let Some(n) = n.as_u64() { + executor.resolve::(&(), &n.try_into().map_err(Error::custom)?) + } else if let Some(n) = n.as_i64() { + executor.resolve::(&(), &n.try_into().map_err(Error::custom)?) + } else if let Some(n) = n.as_f64() { + executor.resolve(&(), &n) + } else { + return Err( + Error::custom("`serde_json::Number` has only 3 number variants").into(), + ); + } + } + Self::String(s) => executor.resolve(&(), &s), + Self::Array(arr) => Ok(crate::Value::list( + arr.iter() + .map(|v| executor.resolve(info, v)) + .collect::>()?, + )), + Self::Object(obj) => { + // If selection set is none we should still output all the + // object fields. + let full_selection = selection + .is_none() + .then(|| { + obj.keys() + .map(|k| { + Selection::Field(Spanning::unlocated(ast::Field { + alias: None, + name: Spanning::unlocated(&*k), + arguments: None, + directives: None, + selection_set: None, + })) + }) + .collect::>() + }) + .unwrap_or_default(); + let selection = selection.unwrap_or(&full_selection); + + let mut out = crate::Object::with_capacity(selection.len()); + for sel in selection { + match sel { + Selection::Field(Spanning { + item: f, + start: start_pos, + .. + }) => { + let resp_name = f.alias.as_ref().unwrap_or(&f.name).item; + let sub_exec = executor.field_with_parent_type_sub_executor( + resp_name, + *start_pos, + f.selection_set.as_ref().map(|v| &v[..]), + ); + let _ = out.add_field( + resp_name, + self.resolve_field( + info, + f.name.item, + &Arguments::new(None, &None), + &sub_exec, + )?, + ); + } + _ => { + return Err(FieldError::new( + "spreading fragments on opaque JSON value is \ + not supported", + crate::Value::null(), + )) + } + } + } + Ok(crate::Value::Object(out)) + } + } + } + + fn resolve_field( + &self, + info: &Self::TypeInfo, + field_name: &str, + _: &Arguments, + executor: &Executor, + ) -> ExecutionResult { + match self { + Self::Object(obj) => match obj.get(field_name) { + None => Ok(crate::Value::null()), + Some(field) => executor.resolve(info, field), + }, + _ => Err(FieldError::new("not an object value", crate::Value::null())), + } + } +} + +impl GraphQLValueAsync for Value { + fn resolve_async<'a>( + &'a self, + info: &'a Self::TypeInfo, + selection: Option<&'a [Selection]>, + executor: &'a Executor, + ) -> BoxFuture<'a, ExecutionResult> { + Box::pin(future::ready(self.resolve(info, selection, executor))) + } + + fn resolve_field_async<'a>( + &'a self, + info: &'a Self::TypeInfo, + field_name: &'a str, + arguments: &'a Arguments, + executor: &'a Executor, + ) -> BoxFuture<'a, ExecutionResult> { + Box::pin(future::ready( + self.resolve_field(info, field_name, arguments, executor), + )) + } +} + +impl ToInputValue for Value { + fn to_input_value(&self) -> InputValue { + match self { + Self::Null => InputValue::null(), + Self::Bool(b) => InputValue::scalar(*b), + Self::Number(n) => { + if let Some(n) = n.as_u64() { + InputValue::scalar(i32::try_from(n).expect("`i32` number")) + } else if let Some(n) = n.as_i64() { + InputValue::scalar(i32::try_from(n).expect("`i32` number")) + } else if let Some(n) = n.as_f64() { + InputValue::scalar(n) + } else { + unreachable!("`serde_json::Number` has only 3 number variants") + } + } + Self::String(s) => InputValue::scalar(s.clone()), + Self::Array(arr) => InputValue::list(arr.iter().map(Self::to_input_value).collect()), + Self::Object(obj) => { + InputValue::object(obj.iter().map(|(k, v)| (k, v.to_input_value())).collect()) + } + } + } +} + +impl FromInputValue for Value { + type Error = FieldError; + + fn from_input_value(val: &InputValue) -> Result { + match val { + InputValue::Null => Ok(Self::Null), + InputValue::Scalar(x) => Ok(if let Some(i) = x.as_int() { + Self::Number(serde_json::Number::from(i)) + } else if let Some(f) = x.as_float() { + Self::Number(serde_json::Number::from_f64(f).ok_or_else(|| { + format!( + "`serde_json::Number` cannot be created from invalid \ + `f64` value: {f}", + ) + })?) + } else if let Some(b) = x.as_bool() { + Self::Bool(b) + } else if let Some(s) = x.as_str() { + Self::String(s.into()) + } else { + return Err("`ScalarValue` must represent at least one of the \ + GraphQL spec types" + .into()); + }), + InputValue::Enum(x) => Ok(Self::String(x.clone())), + InputValue::List(ls) => Ok(Self::Array( + ls.iter() + .map(|i| i.item.convert()) + .collect::>()?, + )), + InputValue::Object(fs) => Ok(Self::Object( + fs.iter() + .map(|(n, v)| v.item.convert().map(|v| (n.item.clone(), v))) + .collect::>()?, + )), + InputValue::Variable(_) => { + Err("`serde_json::Value` cannot be created from GraphQL variable".into()) + } + } + } +} + +impl ParseScalarValue for Value { + fn from_str(val: ScalarToken<'_>) -> ParseScalarResult<'_, S> { + match val { + ScalarToken::String(_) => >::from_str(val), + ScalarToken::Float(_) => >::from_str(val), + ScalarToken::Int(_) => >::from_str(val), + } + } +} + +impl ScalarValue for Value { + fn as_int(&self) -> Option { + match self { + Self::Number(n) => (n.as_u64().map(i32::try_from)) + .or_else(|| n.as_i64().map(i32::try_from)) + .transpose() + .expect("`i32` number"), + _ => None, + } + } + + fn as_float(&self) -> Option { + match self { + Self::Number(n) => (n.as_u64().map(|u| u as f64)) + .or_else(|| n.as_i64().map(|i| i as f64)) + .or_else(|| n.as_f64()), + _ => None, + } + } + + fn as_str(&self) -> Option<&str> { + match self { + Self::String(s) => Some(s.as_str()), + _ => None, + } + } + + fn as_string(&self) -> Option { + match self { + Self::String(s) => Some(s.clone()), + _ => None, + } + } + + fn into_string(self) -> Option { + match self { + Self::String(s) => Some(s), + _ => None, + } + } + + fn as_bool(&self) -> Option { + match self { + Self::Bool(b) => Some(*b), + _ => None, + } + } + + fn into_another(self) -> S { + match self { + Self::Bool(b) => S::from(b), + Self::Number(n) => { + if let Some(n) = n.as_u64() { + S::from(i32::try_from(n).expect("`i32` number")) + } else if let Some(n) = n.as_i64() { + S::from(i32::try_from(n).expect("`i32` number")) + } else if let Some(n) = n.as_f64() { + S::from(n) + } else { + unreachable!("`serde_json::Number` has only 3 number variants") + } + } + Self::String(s) => S::from(s), + _ => unreachable!("not a leaf `serde_json::Value`"), + } + } +} + +impl From> for Value { + fn from(val: crate::Value) -> Self { + match val { + crate::Value::Null => Self::Null, + crate::Value::Scalar(s) => s.into_another(), + crate::Value::List(l) => Self::Array(l.into_iter().map(Self::from).collect()), + crate::Value::Object(o) => { + Self::Object(o.into_iter().map(|(k, v)| (k, Self::from(v))).collect()) + } + } + } +} + +impl From for crate::Value { + fn from(val: Value) -> Self { + match val { + Value::Null => Self::Null, + Value::Array(a) => Self::List(a.into_iter().map(Self::from).collect()), + Value::Object(o) => { + Self::Object(o.into_iter().map(|(k, v)| (k, Self::from(v))).collect()) + } + s @ (Value::Bool(_) | Value::Number(_) | Value::String(_)) => { + Self::Scalar(s.into_another()) + } + } + } +} + +impl reflect::BaseType for Value { + const NAME: reflect::Type = "Json"; +} + +impl reflect::BaseSubTypes for Value { + const NAMES: reflect::Types = &[>::NAME]; +} + +impl reflect::WrappedType for Value { + const VALUE: reflect::WrappedValue = 1; +} + +#[derive(Clone, Deserialize, Copy, Debug, RefCast, Serialize)] +#[repr(transparent)] +pub struct Json { + _type: PhantomData>>, + val: T, +} + +impl From for Json { + fn from(val: T) -> Self { + Self { + _type: PhantomData, + val, + } + } +} + +impl Json { + /// Wraps the given `value` into a [`Json`] wrapper. + #[must_use] + pub fn wrap(value: T) -> Self { + value.into() + } + + /// Unwraps into the underlying value of this [`Json`] wrapper. + #[must_use] + pub fn into_inner(self) -> T { + self.val + } + + /// Maps the inner value of this [`Json`] wrapper with the given function. + #[must_use] + pub fn map(self, f: impl FnOnce(T) -> To) -> Json { + Json { + _type: PhantomData, + val: f(self.val), + } + } +} + +impl Deref for Json { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.val + } +} + +impl DerefMut for Json { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.val + } +} + +impl IsInputType for Json +where + T: DeserializeOwned + Serialize + ?Sized, + I: TypeInfo + ?Sized, + S: ScalarValue, +{ +} + +impl IsOutputType for Json +where + T: DeserializeOwned + Serialize + ?Sized, + I: TypeInfo + ?Sized, + S: ScalarValue, +{ +} + +impl GraphQLType for Json +where + T: DeserializeOwned + Serialize + ?Sized, + I: TypeInfo + ?Sized, + S: ScalarValue, +{ + fn name(info: &Self::TypeInfo) -> Option<&str> { + Some(info.name()) + } + + fn meta<'r>(info: &Self::TypeInfo, registry: &mut Registry<'r, S>) -> MetaType<'r, S> + where + S: 'r, + { + info.meta::(registry) + } +} + +impl GraphQLValue for Json +where + T: DeserializeOwned + Serialize + ?Sized, + I: TypeInfo + ?Sized, + S: ScalarValue, +{ + type Context = (); + type TypeInfo = I; + + fn type_name<'i>(&self, info: &'i Self::TypeInfo) -> Option<&'i str> { + >::name(info) + } + + fn resolve( + &self, + _: &Self::TypeInfo, + selection: Option<&[Selection]>, + executor: &Executor, + ) -> ExecutionResult { + serde_json::to_value(&self.val)?.resolve(&(), selection, executor) + } + + fn resolve_field( + &self, + _: &Self::TypeInfo, + field_name: &str, + args: &Arguments, + executor: &Executor, + ) -> ExecutionResult { + serde_json::to_value(&self.val)?.resolve_field(&(), field_name, args, executor) + } +} + +impl GraphQLValueAsync for Json +where + T: DeserializeOwned + Serialize + Sync + ?Sized, + I: TypeInfo + Sync + ?Sized, + S: ScalarValue + Send + Sync, +{ + fn resolve_async<'a>( + &'a self, + info: &'a Self::TypeInfo, + selection: Option<&'a [Selection]>, + executor: &'a Executor, + ) -> BoxFuture<'a, ExecutionResult> { + Box::pin(future::ready(self.resolve(info, selection, executor))) + } + + fn resolve_field_async<'a>( + &'a self, + info: &'a Self::TypeInfo, + field_name: &'a str, + arguments: &'a Arguments, + executor: &'a Executor, + ) -> BoxFuture<'a, ExecutionResult> { + Box::pin(future::ready( + self.resolve_field(info, field_name, arguments, executor), + )) + } +} + +impl ToInputValue for Json +where + T: Serialize, // TODO: + ?Sized + I: TypeInfo + ?Sized, + S: ScalarValue, +{ + fn to_input_value(&self) -> InputValue { + serde_json::to_value(&self.val) + .expect("Failed to serialize") + .to_input_value() + } +} + +impl FromInputValue for Json +where + T: DeserializeOwned, + I: TypeInfo + ?Sized, + S: ScalarValue, +{ + type Error = FieldError; + + fn from_input_value(val: &InputValue) -> Result { + Ok(Self::wrap(serde_json::from_value( + >::from_input_value(val)?, + )?)) + } +} + +impl ParseScalarValue for Json +where + T: ?Sized, + I: TypeInfo + ?Sized, + S: ScalarValue, +{ + fn from_str(val: ScalarToken<'_>) -> ParseScalarResult<'_, S> { + >::from_str(val) + } +} + +impl reflect::BaseType for Json { + const NAME: reflect::Type = "Json"; // TODO: json? +} + +impl reflect::BaseSubTypes for Json { + const NAMES: reflect::Types = &[>::NAME]; +} + +impl reflect::WrappedType for Json { + const VALUE: reflect::WrappedValue = 1; +} + +pub trait TypeInfo { + fn name(&self) -> &str; + + fn meta<'r, T, S>(&self, registry: &mut Registry<'r, S>) -> MetaType<'r, S> + where + T: FromInputValue + GraphQLType + ?Sized, + T::Error: IntoFieldError, + S: ScalarValue + 'r; +} + +impl TypeInfo for () { + fn name(&self) -> &str { + >::NAME + } + + fn meta<'r, T, S>(&self, registry: &mut Registry<'r, S>) -> MetaType<'r, S> + where + T: FromInputValue + GraphQLType + ?Sized, + T::Error: IntoFieldError, + S: ScalarValue + 'r, + { + registry + .build_scalar_type::(self) + .description("Opaque JSON value.") + .into_meta() + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Info { + /// Parsed [`Schema`] containing a definition of the GraphQL type. + schema: Schema<'static, String>, + + /// Type name of a [`GraphQLValue`] using this [`Info`]. + name: String, +} + +impl Info { + pub fn try_new>( + name: N, + schema: Schema<'_, String>, + ) -> Result { + let schema = schema.into_static(); + let name = name.into(); + + // TODO: validate `name` is contained in `schema`. + + Ok(Self { schema, name }) + } + + pub fn parse, S: AsRef>(name: N, sdl: S) -> Result { + Self::try_new(name, graphql_parser::parse_schema::(sdl.as_ref())?) + } + + /// Returns type name of a [`GraphQLValue`] using this [`Info`]. + #[must_use] + pub fn name(&self) -> &str { + &self.name + } + + /// Returns parsed [`Schema`] defining this [`Info`]. + #[must_use] + pub fn schema(&self) -> &Schema<'static, String> { + &self.schema + } + + fn build_field<'r, 't, S>( + &self, + registry: &mut Registry<'r, S>, + field_name: &str, + ty: &SchemaType<'t, String>, + nullable: bool, + ) -> meta::Field<'r, S> + where + S: 'r + ScalarValue, + { + match ty { + SchemaType::NamedType(n) => match n.as_ref() { + "Boolean" => { + if nullable { + registry.field::>(field_name, &()) + } else { + registry.field::(field_name, &()) + } + } + "Int" => { + if nullable { + registry.field::>(field_name, &()) + } else { + registry.field::(field_name, &()) + } + } + "Float" => { + if nullable { + registry.field::>(field_name, &()) + } else { + registry.field::(field_name, &()) + } + } + "String" => { + if nullable { + registry.field::>(field_name, &()) + } else { + registry.field::(field_name, &()) + } + } + "ID" => { + if nullable { + registry.field::>(field_name, &()) + } else { + registry.field::(field_name, &()) + } + } + _ => { + let field_type_info = Info { + schema: self.schema.clone(), + name: n.clone(), + }; + if nullable { + registry.field::>>(field_name, &field_type_info) + } else { + registry.field::>(field_name, &field_type_info) + } + } + }, + SchemaType::ListType(ty) => { + let mut item = self.build_field(registry, field_name, &**ty, true); + if nullable { + item.field_type = crate::Type::List(Box::new(item.field_type), None); + } else { + item.field_type = crate::Type::NonNullList(Box::new(item.field_type), None); + } + item + } + SchemaType::NonNullType(ty) => self.build_field(registry, field_name, &**ty, false), + } + } +} + +impl TypeInfo for Info { + fn name(&self) -> &str { + self.name.as_str() + } + + fn meta<'r, T, S>(&self, registry: &mut Registry<'r, S>) -> MetaType<'r, S> + where + T: FromInputValue + GraphQLType + ?Sized, + T::Error: IntoFieldError, + S: ScalarValue + 'r, + { + use graphql_parser::schema::{Definition, TypeDefinition}; + + let mut fields = Vec::new(); + let mut input_fields = Vec::new(); + let mut is_input_object = false; + + for d in &self.schema.definitions { + match &d { + Definition::TypeDefinition(d) => match d { + TypeDefinition::Object(o) => { + if o.name == self.name { + for f in &o.fields { + fields.push(self.build_field( + registry, + &f.name, + &f.field_type, + true, + )); + } + break; + } + } + TypeDefinition::InputObject(o) => { + if o.name == self.name { + is_input_object = true; + for f in &o.fields { + let f = self.build_field(registry, &f.name, &f.value_type, true); + input_fields.push(meta::Argument { + name: f.name.to_string(), + description: f.description.clone(), + arg_type: f.field_type, + default_value: None, + }); + } + break; + } + } + // We do just nothing in other cases, as at this point the + // `self.schema` has been validated already in + // `Info::parse()` to contain the necessary types. + _ => {} + }, + _ => {} + } + } + + if is_input_object { + registry + .build_input_object_type::(self, &input_fields) + .into_meta() + } else { + registry.build_object_type::(self, &fields).into_meta() + } + } +} + +/// Dynamic [`Json`] value typed by an [`Info`]. +pub type Typed = Json; + +#[cfg(test)] +mod value_test { + mod as_output { + use futures::FutureExt as _; + use serde_json::{json, Value}; + + use crate::{ + execute, execute_sync, graphql_object, graphql_subscription, graphql_vars, + resolve_into_stream, + tests::util::{extract_next, stream, Stream}, + EmptyMutation, FieldResult, RootNode, + }; + + struct Query; + + #[graphql_object] + impl Query { + fn null() -> Value { + Value::Null + } + + fn bool() -> Value { + json!(true) + } + + fn int() -> Value { + json!(42) + } + + fn float() -> Value { + json!(3.14) + } + + fn string() -> Value { + json!("Galadriel") + } + + fn array() -> Value { + json!(["Ai", "Ambarendya!"]) + } + + fn object() -> Value { + json!({"message": ["Ai", "Ambarendya!"]}) + } + + fn nullable() -> Option { + Some(json!({"message": ["Ai", "Ambarendya!"]})) + } + + fn fallible() -> FieldResult { + Ok(json!({"message": ["Ai", "Ambarendya!"]})) + } + + fn nested() -> Value { + json!({"message": { + "header": "Ai", + "body": "Ambarendya!", + }}) + } + } + + struct Subscription; + + #[graphql_subscription] + impl Subscription { + async fn null() -> Stream { + stream(Value::Null) + } + + async fn bool() -> Stream { + stream(json!(true)) + } + + async fn int() -> Stream { + stream(json!(42)) + } + + async fn float() -> Stream { + stream(json!(3.14)) + } + + async fn string() -> Stream { + stream(json!("Galadriel")) + } + + async fn array() -> Stream { + stream(json!(["Ai", "Ambarendya!"])) + } + + async fn object() -> Stream { + stream(json!({"message": ["Ai", "Ambarendya!"]})) + } + + async fn nullable() -> Stream> { + stream(Some(json!({"message": ["Ai", "Ambarendya!"]}))) + } + + async fn fallible() -> FieldResult>> { + Ok(stream(Ok(json!({"message": ["Ai", "Ambarendya!"]})))) + } + + async fn nested() -> Stream { + stream(json!({"message": { + "header": "Ai", + "body": "Ambarendya!", + }})) + } + } + + #[tokio::test] + async fn resolves_null() { + const QRY: &str = "{ null }"; + const SUB: &str = "subscription { null }"; + + let schema = RootNode::new(Query, EmptyMutation::new(), Subscription); + + let expected = Ok((graphql_value!({ "null": null }), vec![])); + + assert_eq!( + execute(QRY, None, &schema, &graphql_vars! {}, &()).await, + expected, + ); + assert_eq!( + execute_sync(QRY, None, &schema, &graphql_vars! {}, &()), + expected, + ); + assert_eq!( + resolve_into_stream(SUB, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + expected, + ); + } + + #[tokio::test] + async fn resolves_bool() { + const QRY: &str = "{ bool }"; + const SUB: &str = "subscription { bool }"; + + let schema = RootNode::new(Query, EmptyMutation::new(), Subscription); + + let expected = Ok((graphql_value!({"bool": true}), vec![])); + + assert_eq!( + execute(QRY, None, &schema, &graphql_vars! {}, &()).await, + expected, + ); + assert_eq!( + execute_sync(QRY, None, &schema, &graphql_vars! {}, &()), + expected, + ); + assert_eq!( + resolve_into_stream(SUB, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + expected, + ); + } + + #[tokio::test] + async fn resolves_int() { + const QRY: &str = "{ int }"; + const SUB: &str = "subscription { int }"; + + let schema = RootNode::new(Query, EmptyMutation::new(), Subscription); + + let expected = Ok((graphql_value!({"int": 42}), vec![])); + + assert_eq!( + execute(QRY, None, &schema, &graphql_vars! {}, &()).await, + expected, + ); + assert_eq!( + execute_sync(QRY, None, &schema, &graphql_vars! {}, &()), + expected, + ); + assert_eq!( + resolve_into_stream(SUB, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + expected, + ); + } + + #[tokio::test] + async fn resolves_float() { + const QRY: &str = "{ float }"; + const SUB: &str = "subscription { float }"; + + let schema = RootNode::new(Query, EmptyMutation::new(), Subscription); + + let expected = Ok((graphql_value!({"float": 3.14}), vec![])); + + assert_eq!( + execute(QRY, None, &schema, &graphql_vars! {}, &()).await, + expected, + ); + assert_eq!( + execute_sync(QRY, None, &schema, &graphql_vars! {}, &()), + expected, + ); + assert_eq!( + resolve_into_stream(SUB, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + expected, + ); + } + + #[tokio::test] + async fn resolves_string() { + const QRY: &str = "{ string }"; + const SUB: &str = "subscription { string }"; + + let schema = RootNode::new(Query, EmptyMutation::new(), Subscription); + + let expected = Ok((graphql_value!({"string": "Galadriel"}), vec![])); + + assert_eq!( + execute(QRY, None, &schema, &graphql_vars! {}, &()).await, + expected, + ); + assert_eq!( + execute_sync(QRY, None, &schema, &graphql_vars! {}, &()), + expected, + ); + assert_eq!( + resolve_into_stream(SUB, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + expected, + ); + } + + #[tokio::test] + async fn resolves_array() { + const QRY: &str = "{ array }"; + const SUB: &str = "subscription { array }"; + + let schema = RootNode::new(Query, EmptyMutation::new(), Subscription); + + let expected = Ok((graphql_value!({"array": ["Ai", "Ambarendya!"]}), vec![])); + + assert_eq!( + execute(QRY, None, &schema, &graphql_vars! {}, &()).await, + expected, + ); + assert_eq!( + execute_sync(QRY, None, &schema, &graphql_vars! {}, &()), + expected, + ); + assert_eq!( + resolve_into_stream(SUB, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + expected, + ); + } + + #[tokio::test] + async fn resolves_object() { + const QRY: &str = "{ object }"; + const SUB: &str = "subscription { object }"; + + let schema = RootNode::new(Query, EmptyMutation::new(), Subscription); + + let expected = Ok(( + graphql_value!({ + "object": {"message": ["Ai", "Ambarendya!"]}, + }), + vec![], + )); + + assert_eq!( + execute(QRY, None, &schema, &graphql_vars! {}, &()).await, + expected, + ); + assert_eq!( + execute_sync(QRY, None, &schema, &graphql_vars! {}, &()), + expected, + ); + assert_eq!( + resolve_into_stream(SUB, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + expected, + ); + } + + #[tokio::test] + async fn resolves_nullable() { + const QRY: &str = "{ nullable }"; + const SUB: &str = "subscription { nullable }"; + + let schema = RootNode::new(Query, EmptyMutation::new(), Subscription); + + let expected = Ok(( + graphql_value!({ + "nullable": {"message": ["Ai", "Ambarendya!"]}, + }), + vec![], + )); + + assert_eq!( + execute(QRY, None, &schema, &graphql_vars! {}, &()).await, + expected, + ); + assert_eq!( + execute_sync(QRY, None, &schema, &graphql_vars! {}, &()), + expected, + ); + assert_eq!( + resolve_into_stream(SUB, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + expected, + ); + } + + #[tokio::test] + async fn resolves_fallible() { + const QRY: &str = "{ fallible }"; + const SUB: &str = "subscription { fallible }"; + + let schema = RootNode::new(Query, EmptyMutation::new(), Subscription); + + let expected = Ok(( + graphql_value!({ + "fallible": {"message": ["Ai", "Ambarendya!"]}, + }), + vec![], + )); + + assert_eq!( + execute(QRY, None, &schema, &graphql_vars! {}, &()).await, + expected, + ); + assert_eq!( + execute_sync(QRY, None, &schema, &graphql_vars! {}, &()), + expected, + ); + assert_eq!( + resolve_into_stream(SUB, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + expected, + ); + } + + #[tokio::test] + async fn resolves_fields() { + const QRY: &str = r#"{ + object { message } + nullable { message } + fallible { message } + }"#; + const SUB1: &str = r#"subscription { + object { message } + }"#; + const SUB2: &str = r#"subscription { + nullable { message } + }"#; + const SUB3: &str = r#"subscription { + fallible { message } + }"#; + + let schema = RootNode::new(Query, EmptyMutation::new(), Subscription); + + let expected = Ok(( + graphql_value!({ + "object": {"message": ["Ai", "Ambarendya!"]}, + "nullable": {"message": ["Ai", "Ambarendya!"]}, + "fallible": {"message": ["Ai", "Ambarendya!"]}, + }), + vec![], + )); + + assert_eq!( + execute(QRY, None, &schema, &graphql_vars! {}, &()).await, + expected, + ); + assert_eq!( + execute_sync(QRY, None, &schema, &graphql_vars! {}, &()), + expected, + ); + assert_eq!( + resolve_into_stream(SUB1, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + Ok(( + graphql_value!({ + "object": {"message": ["Ai", "Ambarendya!"]}, + }), + vec![], + )), + ); + assert_eq!( + resolve_into_stream(SUB2, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + Ok(( + graphql_value!({ + "nullable": {"message": ["Ai", "Ambarendya!"]}, + }), + vec![], + )), + ); + assert_eq!( + resolve_into_stream(SUB3, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + Ok(( + graphql_value!({ + "fallible": {"message": ["Ai", "Ambarendya!"]}, + }), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_unknown_fields_as_null() { + const QRY: &str = r#"{ + object { message, friend } + nullable { message, mellon } + fallible { message, freund } + }"#; + const SUB1: &str = r#"subscription { + object { message, friend } + }"#; + const SUB2: &str = r#"subscription { + nullable { message, mellon } + }"#; + const SUB3: &str = r#"subscription { + fallible { message, freund } + }"#; + + let schema = RootNode::new(Query, EmptyMutation::new(), Subscription); + + let expected = Ok(( + graphql_value!({ + "object": { + "message": ["Ai", "Ambarendya!"], + "friend": null, + }, + "nullable": { + "message": ["Ai", "Ambarendya!"], + "mellon": null, + }, + "fallible": { + "message": ["Ai", "Ambarendya!"], + "freund": null, + }, + }), + vec![], + )); + + assert_eq!( + execute(QRY, None, &schema, &graphql_vars! {}, &()).await, + expected, + ); + assert_eq!( + execute_sync(QRY, None, &schema, &graphql_vars! {}, &()), + expected, + ); + assert_eq!( + resolve_into_stream(SUB1, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + Ok(( + graphql_value!({ + "object": { + "message": ["Ai", "Ambarendya!"], + "friend": null, + }, + }), + vec![], + )), + ); + assert_eq!( + resolve_into_stream(SUB2, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + Ok(( + graphql_value!({ + "nullable": { + "message": ["Ai", "Ambarendya!"], + "mellon": null, + }, + }), + vec![], + )), + ); + assert_eq!( + resolve_into_stream(SUB3, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + Ok(( + graphql_value!({ + "fallible": { + "message": ["Ai", "Ambarendya!"], + "freund": null, + }, + }), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_nested_object_fields() { + const QRY: &str = "{ nested { message { body } } }"; + const SUB: &str = "subscription { nested { message { body } } }"; + + let schema = RootNode::new(Query, EmptyMutation::new(), Subscription); + + let expected = Ok(( + graphql_value!({ + "nested": {"message": {"body": "Ambarendya!"}}, + }), + vec![], + )); + + assert_eq!( + execute(QRY, None, &schema, &graphql_vars! {}, &()).await, + expected, + ); + assert_eq!( + execute_sync(QRY, None, &schema, &graphql_vars! {}, &()), + expected, + ); + assert_eq!( + resolve_into_stream(SUB, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + expected, + ); + } + + #[tokio::test] + async fn resolves_nested_unknown_object_fields() { + const QRY: &str = "{ nested { message { body, foo } } }"; + const SUB: &str = "subscription { nested { message { body, foo } } }"; + + let schema = RootNode::new(Query, EmptyMutation::new(), Subscription); + + let expected = Ok(( + graphql_value!({ + "nested": {"message": { + "body": "Ambarendya!", + "foo": null, + }}, + }), + vec![], + )); + + assert_eq!( + execute(QRY, None, &schema, &graphql_vars! {}, &()).await, + expected, + ); + assert_eq!( + execute_sync(QRY, None, &schema, &graphql_vars! {}, &()), + expected, + ); + assert_eq!( + resolve_into_stream(SUB, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + expected, + ); + } + + #[tokio::test] + async fn resolves_nested_aliased_object_fields() { + const QRY: &str = "{ nested { m: message { b: body } } }"; + const SUB: &str = "subscription { nested { m: message { b: body } } }"; + + let schema = RootNode::new(Query, EmptyMutation::new(), Subscription); + + let expected = Ok(( + graphql_value!({ + "nested": {"m": {"b": "Ambarendya!"}}, + }), + vec![], + )); + + assert_eq!( + execute(QRY, None, &schema, &graphql_vars! {}, &()).await, + expected, + ); + assert_eq!( + execute_sync(QRY, None, &schema, &graphql_vars! {}, &()), + expected, + ); + assert_eq!( + resolve_into_stream(SUB, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + expected, + ); + } + + #[tokio::test] + async fn allows_fields_on_null() { + const QRY: &str = "{ null { message } }"; + const SUB: &str = "subscription { null { message } }"; + + let schema = RootNode::new(Query, EmptyMutation::new(), Subscription); + + let expected = Ok((graphql_value!({ "null": null }), vec![])); + + assert_eq!( + execute(QRY, None, &schema, &graphql_vars! {}, &()).await, + expected, + ); + assert_eq!( + execute_sync(QRY, None, &schema, &graphql_vars! {}, &()), + expected, + ); + assert_eq!( + resolve_into_stream(SUB, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + expected, + ); + } + + #[tokio::test] + async fn errors_selecting_fields_on_leaf_value() { + let schema = RootNode::new(Query, EmptyMutation::new(), Subscription); + + let expected = Ok(Some("cannot select fields on a leaf opaque JSON value")); + + for qry in [ + "{ bool { message } }", + "{ int { message } }", + "{ float { message } }", + "{ string { message } }", + "{ array { message } }", + "{ object { message { body } } }", + "{ nested { message { body { theme } } } }", + ] { + let res = execute(qry, None, &schema, &graphql_vars! {}, &()).await; + assert_eq!( + res.as_ref() + .map(|(_, errs)| errs.first().map(|e| e.error().message())), + expected, + "query: {}\nactual result: {:?}", + qry, + res, + ); + + let res = execute_sync(qry, None, &schema, &graphql_vars! {}, &()); + assert_eq!( + res.as_ref() + .map(|(_, errs)| errs.first().map(|e| e.error().message())), + expected, + "query: {}\nactual result: {:?}", + qry, + res, + ); + + let sub = format!("subscription {}", qry); + let res = resolve_into_stream(&sub, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await; + assert_eq!( + res.as_ref() + .map(|(_, errs)| errs.first().map(|e| e.error().message())), + expected, + "query: {}\nactual result: {:?}", + qry, + res, + ); + } + } + + #[tokio::test] + async fn represents_scalar() { + const QRY: &str = r#"{ __type(name: "Json") { kind } }"#; + + let schema = RootNode::new(Query, EmptyMutation::new(), Subscription); + + let expected = Ok((graphql_value!({"__type": {"kind": "SCALAR"}}), vec![])); + + assert_eq!( + execute(QRY, None, &schema, &graphql_vars! {}, &()).await, + expected, + ); + assert_eq!( + execute_sync(QRY, None, &schema, &graphql_vars! {}, &()), + expected, + ); + } + } + + mod as_input { + use serde_json::Value; + + use crate::{ + execute, graphql_object, graphql_vars, EmptyMutation, EmptySubscription, RootNode, + }; + + struct Query; + + #[graphql_object] + impl Query { + fn input(arg: Value) -> Value { + arg + } + } + + #[tokio::test] + async fn accepts_null() { + const DOC: &str = r#"{ + null: input(arg: null) + }"#; + + let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new()); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({ "null": None }), vec![])), + ); + } + + #[tokio::test] + async fn accepts_bool() { + const DOC: &str = r#"{ + bool: input(arg: true) + }"#; + + let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new()); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"bool": true}), vec![])), + ); + } + + #[tokio::test] + async fn accepts_int() { + const DOC: &str = r#"{ + int: input(arg: 42) + }"#; + + let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new()); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"int": 42}), vec![])), + ); + } + + #[tokio::test] + async fn accepts_float() { + const DOC: &str = r#"{ + float: input(arg: 3.14) + }"#; + + let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new()); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"float": 3.14}), vec![])), + ); + } + + #[tokio::test] + async fn accepts_string() { + const DOC: &str = r#"{ + string: input(arg: "Galadriel") + }"#; + + let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new()); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"string": "Galadriel"}), vec![])), + ); + } + + #[tokio::test] + async fn accepts_array() { + const DOC: &str = r#"{ + array: input(arg: ["Ai", "Ambarendya!"]) + }"#; + + let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new()); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"array": ["Ai", "Ambarendya!"]}), vec![])), + ); + } + + #[tokio::test] + async fn accepts_object() { + const DOC: &str = r#"{ + object: input(arg: {message: ["Ai", "Ambarendya!"]}) + }"#; + + let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new()); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({ + "object": {"message": ["Ai", "Ambarendya!"]}, + }), + vec![], + )), + ); + } + } +} + +#[cfg(test)] +mod json_test { + mod as_output { + use futures::FutureExt as _; + use serde::{Deserialize, Serialize}; + use serde_json::Value; + + use crate::{ + execute, execute_sync, graphql_object, graphql_subscription, graphql_vars, + resolve_into_stream, + tests::util::{extract_next, stream, Stream}, + EmptyMutation, FieldResult, RootNode, + }; + + use super::super::Json; + + #[derive(Debug, Deserialize, Serialize)] + struct Message { + message: Vec, + } + + #[derive(Debug, Deserialize, Serialize)] + struct Envelope { + envelope: Message, + } + + struct Query; + + #[graphql_object] + impl Query { + fn null() -> Json { + Value::Null.into() + } + + fn bool() -> Json { + true.into() + } + + fn int() -> Json { + 42.into() + } + + fn float() -> Json { + 3.14.into() + } + + fn string() -> Json { + Json::wrap("Galadriel".into()) + } + + fn array() -> Json> { + vec!["Ai".into(), "Ambarendya!".into()].into() + } + + fn object() -> Json { + Json::wrap(Message { + message: vec!["Ai".into(), "Ambarendya!".into()], + }) + } + + fn nullable() -> Option> { + Some(Json::wrap(Message { + message: vec!["Ai".into(), "Ambarendya!".into()], + })) + } + + fn fallible() -> FieldResult> { + Ok(Json::wrap(Message { + message: vec!["Ai".into(), "Ambarendya!".into()], + })) + } + + fn nested() -> Json { + Json::wrap(Envelope { + envelope: Message { + message: vec!["Ai".into(), "Ambarendya!".into()], + }, + }) + } + } + + struct Subscription; + + #[graphql_subscription] + impl Subscription { + async fn null() -> Stream { + stream(Value::Null.into()) + } + + async fn bool() -> Stream> { + stream(true.into()) + } + + async fn int() -> Stream> { + stream(42.into()) + } + + async fn float() -> Stream> { + stream(3.14.into()) + } + + async fn string() -> Stream> { + stream(Json::wrap("Galadriel".into())) + } + + async fn array() -> Stream>> { + stream(Json::wrap(vec!["Ai".into(), "Ambarendya!".into()])) + } + + async fn object() -> Stream> { + stream(Json::wrap(Message { + message: vec!["Ai".into(), "Ambarendya!".into()], + })) + } + + async fn nullable() -> Stream>> { + stream(Some(Json::wrap(Message { + message: vec!["Ai".into(), "Ambarendya!".into()], + }))) + } + + async fn fallible() -> FieldResult>>> { + Ok(stream(Ok(Json::wrap(Message { + message: vec!["Ai".into(), "Ambarendya!".into()], + })))) + } + + async fn nested() -> Stream> { + stream(Json::wrap(Envelope { + envelope: Message { + message: vec!["Ai".into(), "Ambarendya!".into()], + }, + })) + } + } + + #[tokio::test] + async fn resolves_null() { + const QRY: &str = "{ null }"; + const SUB: &str = "subscription { null }"; + + let schema = RootNode::new(Query, EmptyMutation::new(), Subscription); + + let expected = Ok((graphql_value!({ "null": null }), vec![])); + + assert_eq!( + execute(QRY, None, &schema, &graphql_vars! {}, &()).await, + expected, + ); + assert_eq!( + execute_sync(QRY, None, &schema, &graphql_vars! {}, &()), + expected, + ); + assert_eq!( + resolve_into_stream(SUB, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + expected, + ); + } + + #[tokio::test] + async fn resolves_bool() { + const QRY: &str = "{ bool }"; + const SUB: &str = "subscription { bool }"; + + let schema = RootNode::new(Query, EmptyMutation::new(), Subscription); + + let expected = Ok((graphql_value!({"bool": true}), vec![])); + + assert_eq!( + execute(QRY, None, &schema, &graphql_vars! {}, &()).await, + expected, + ); + assert_eq!( + execute_sync(QRY, None, &schema, &graphql_vars! {}, &()), + expected, + ); + assert_eq!( + resolve_into_stream(SUB, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + expected, + ); + } + + #[tokio::test] + async fn resolves_int() { + const QRY: &str = "{ int }"; + const SUB: &str = "subscription { int }"; + + let schema = RootNode::new(Query, EmptyMutation::new(), Subscription); + + let expected = Ok((graphql_value!({"int": 42}), vec![])); + + assert_eq!( + execute(QRY, None, &schema, &graphql_vars! {}, &()).await, + expected, + ); + assert_eq!( + execute_sync(QRY, None, &schema, &graphql_vars! {}, &()), + expected, + ); + assert_eq!( + resolve_into_stream(SUB, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + expected, + ); + } + + #[tokio::test] + async fn resolves_float() { + const QRY: &str = "{ float }"; + const SUB: &str = "subscription { float }"; + + let schema = RootNode::new(Query, EmptyMutation::new(), Subscription); + + let expected = Ok((graphql_value!({"float": 3.14}), vec![])); + + assert_eq!( + execute(QRY, None, &schema, &graphql_vars! {}, &()).await, + expected, + ); + assert_eq!( + execute_sync(QRY, None, &schema, &graphql_vars! {}, &()), + expected, + ); + assert_eq!( + resolve_into_stream(SUB, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + expected, + ); + } + + #[tokio::test] + async fn resolves_string() { + const QRY: &str = "{ string }"; + const SUB: &str = "subscription { string }"; + + let schema = RootNode::new(Query, EmptyMutation::new(), Subscription); + + let expected = Ok((graphql_value!({"string": "Galadriel"}), vec![])); + + assert_eq!( + execute(QRY, None, &schema, &graphql_vars! {}, &()).await, + expected, + ); + assert_eq!( + execute_sync(QRY, None, &schema, &graphql_vars! {}, &()), + expected, + ); + assert_eq!( + resolve_into_stream(SUB, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + expected, + ); + } + + #[tokio::test] + async fn resolves_array() { + const QRY: &str = "{ array }"; + const SUB: &str = "subscription { array }"; + + let schema = RootNode::new(Query, EmptyMutation::new(), Subscription); + + let expected = Ok((graphql_value!({"array": ["Ai", "Ambarendya!"]}), vec![])); + + assert_eq!( + execute(QRY, None, &schema, &graphql_vars! {}, &()).await, + expected, + ); + assert_eq!( + execute_sync(QRY, None, &schema, &graphql_vars! {}, &()), + expected, + ); + assert_eq!( + resolve_into_stream(SUB, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + expected, + ); + } + + #[tokio::test] + async fn resolves_object() { + const QRY: &str = "{ object }"; + const SUB: &str = "subscription { object }"; + + let schema = RootNode::new(Query, EmptyMutation::new(), Subscription); + + let expected = Ok(( + graphql_value!({ + "object": {"message": ["Ai", "Ambarendya!"]}, + }), + vec![], + )); + + assert_eq!( + execute(QRY, None, &schema, &graphql_vars! {}, &()).await, + expected, + ); + assert_eq!( + execute_sync(QRY, None, &schema, &graphql_vars! {}, &()), + expected, + ); + assert_eq!( + resolve_into_stream(SUB, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + expected, + ); + } + + #[tokio::test] + async fn resolves_nullable() { + const QRY: &str = "{ nullable }"; + const SUB: &str = "subscription { nullable }"; + + let schema = RootNode::new(Query, EmptyMutation::new(), Subscription); + + let expected = Ok(( + graphql_value!({ + "nullable": {"message": ["Ai", "Ambarendya!"]}, + }), + vec![], + )); + + assert_eq!( + execute(QRY, None, &schema, &graphql_vars! {}, &()).await, + expected, + ); + assert_eq!( + execute_sync(QRY, None, &schema, &graphql_vars! {}, &()), + expected, + ); + assert_eq!( + resolve_into_stream(SUB, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + expected, + ); + } + + #[tokio::test] + async fn resolves_fallible() { + const QRY: &str = "{ fallible }"; + const SUB: &str = "subscription { fallible }"; + + let schema = RootNode::new(Query, EmptyMutation::new(), Subscription); + + let expected = Ok(( + graphql_value!({ + "fallible": {"message": ["Ai", "Ambarendya!"]}, + }), + vec![], + )); + + assert_eq!( + execute(QRY, None, &schema, &graphql_vars! {}, &()).await, + expected, + ); + assert_eq!( + execute_sync(QRY, None, &schema, &graphql_vars! {}, &()), + expected, + ); + assert_eq!( + resolve_into_stream(SUB, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + expected, + ); + } + + #[tokio::test] + async fn resolves_fields() { + const QRY: &str = r#"{ + object { message } + nullable { message } + fallible { message } + }"#; + const SUB1: &str = r#"subscription { + object { message } + }"#; + const SUB2: &str = r#"subscription { + nullable { message } + }"#; + const SUB3: &str = r#"subscription { + fallible { message } + }"#; + + let schema = RootNode::new(Query, EmptyMutation::new(), Subscription); + + let expected = Ok(( + graphql_value!({ + "object": {"message": ["Ai", "Ambarendya!"]}, + "nullable": {"message": ["Ai", "Ambarendya!"]}, + "fallible": {"message": ["Ai", "Ambarendya!"]}, + }), + vec![], + )); + + assert_eq!( + execute(QRY, None, &schema, &graphql_vars! {}, &()).await, + expected, + ); + assert_eq!( + execute_sync(QRY, None, &schema, &graphql_vars! {}, &()), + expected, + ); + assert_eq!( + resolve_into_stream(SUB1, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + Ok(( + graphql_value!({ + "object": {"message": ["Ai", "Ambarendya!"]}, + }), + vec![], + )), + ); + assert_eq!( + resolve_into_stream(SUB2, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + Ok(( + graphql_value!({ + "nullable": {"message": ["Ai", "Ambarendya!"]}, + }), + vec![], + )), + ); + assert_eq!( + resolve_into_stream(SUB3, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + Ok(( + graphql_value!({ + "fallible": {"message": ["Ai", "Ambarendya!"]}, + }), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_unknown_fields_as_null() { + const QRY: &str = r#"{ + object { message, friend } + nullable { message, mellon } + fallible { message, freund } + }"#; + const SUB1: &str = r#"subscription { + object { message, friend } + }"#; + const SUB2: &str = r#"subscription { + nullable { message, mellon } + }"#; + const SUB3: &str = r#"subscription { + fallible { message, freund } + }"#; + + let schema = RootNode::new(Query, EmptyMutation::new(), Subscription); + + let expected = Ok(( + graphql_value!({ + "object": { + "message": ["Ai", "Ambarendya!"], + "friend": null, + }, + "nullable": { + "message": ["Ai", "Ambarendya!"], + "mellon": null, + }, + "fallible": { + "message": ["Ai", "Ambarendya!"], + "freund": null, + }, + }), + vec![], + )); + + assert_eq!( + execute(QRY, None, &schema, &graphql_vars! {}, &()).await, + expected, + ); + assert_eq!( + execute_sync(QRY, None, &schema, &graphql_vars! {}, &()), + expected, + ); + assert_eq!( + resolve_into_stream(SUB1, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + Ok(( + graphql_value!({ + "object": { + "message": ["Ai", "Ambarendya!"], + "friend": null, + }, + }), + vec![], + )), + ); + assert_eq!( + resolve_into_stream(SUB2, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + Ok(( + graphql_value!({ + "nullable": { + "message": ["Ai", "Ambarendya!"], + "mellon": null, + }, + }), + vec![], + )), + ); + assert_eq!( + resolve_into_stream(SUB3, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + Ok(( + graphql_value!({ + "fallible": { + "message": ["Ai", "Ambarendya!"], + "freund": null, + }, + }), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_nested_object_fields() { + const QRY: &str = "{ nested { envelope { message } } }"; + const SUB: &str = "subscription { nested { envelope { message } } }"; + + let schema = RootNode::new(Query, EmptyMutation::new(), Subscription); + + let expected = Ok(( + graphql_value!({ + "nested": {"envelope": {"message": ["Ai", "Ambarendya!"]}}, + }), + vec![], + )); + + assert_eq!( + execute(QRY, None, &schema, &graphql_vars! {}, &()).await, + expected, + ); + assert_eq!( + execute_sync(QRY, None, &schema, &graphql_vars! {}, &()), + expected, + ); + assert_eq!( + resolve_into_stream(SUB, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + expected, + ); + } + + #[tokio::test] + async fn resolves_nested_unknown_object_fields() { + const QRY: &str = "{ nested { envelope { message, foo } } }"; + const SUB: &str = "subscription { nested { envelope { message, foo } } }"; + + let schema = RootNode::new(Query, EmptyMutation::new(), Subscription); + + let expected = Ok(( + graphql_value!({ + "nested": {"envelope": { + "message": ["Ai", "Ambarendya!"], + "foo": null, + }}, + }), + vec![], + )); + + assert_eq!( + execute(QRY, None, &schema, &graphql_vars! {}, &()).await, + expected, + ); + assert_eq!( + execute_sync(QRY, None, &schema, &graphql_vars! {}, &()), + expected, + ); + assert_eq!( + resolve_into_stream(SUB, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + expected, + ); + } + + #[tokio::test] + async fn resolves_nested_aliased_object_fields() { + const QRY: &str = "{ nested { e: envelope { m: message } } }"; + const SUB: &str = "subscription { nested { e: envelope { m: message } } }"; + + let schema = RootNode::new(Query, EmptyMutation::new(), Subscription); + + let expected = Ok(( + graphql_value!({ + "nested": {"e": {"m": ["Ai", "Ambarendya!"]}}, + }), + vec![], + )); + + assert_eq!( + execute(QRY, None, &schema, &graphql_vars! {}, &()).await, + expected, + ); + assert_eq!( + execute_sync(QRY, None, &schema, &graphql_vars! {}, &()), + expected, + ); + assert_eq!( + resolve_into_stream(SUB, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + expected, + ); + } + + #[tokio::test] + async fn allows_fields_on_null() { + const QRY: &str = "{ null { message } }"; + const SUB: &str = "subscription { null { message } }"; + + let schema = RootNode::new(Query, EmptyMutation::new(), Subscription); + + let expected = Ok((graphql_value!({ "null": null }), vec![])); + + assert_eq!( + execute(QRY, None, &schema, &graphql_vars! {}, &()).await, + expected, + ); + assert_eq!( + execute_sync(QRY, None, &schema, &graphql_vars! {}, &()), + expected, + ); + assert_eq!( + resolve_into_stream(SUB, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await, + expected, + ); + } + + #[tokio::test] + async fn errors_selecting_fields_on_leaf_value() { + let schema = RootNode::new(Query, EmptyMutation::new(), Subscription); + + let expected = Ok(Some("cannot select fields on a leaf opaque JSON value")); + + for qry in [ + "{ bool { message } }", + "{ int { message } }", + "{ float { message } }", + "{ string { message } }", + "{ array { message } }", + "{ object { message { body } } }", + "{ nested { envelope { message { theme } } } }", + ] { + let res = execute(qry, None, &schema, &graphql_vars! {}, &()).await; + assert_eq!( + res.as_ref() + .map(|(_, errs)| errs.first().map(|e| e.error().message())), + expected, + "query: {}\nactual result: {:?}", + qry, + res, + ); + + let res = execute_sync(qry, None, &schema, &graphql_vars! {}, &()); + assert_eq!( + res.as_ref() + .map(|(_, errs)| errs.first().map(|e| e.error().message())), + expected, + "query: {}\nactual result: {:?}", + qry, + res, + ); + + let sub = format!("subscription {}", qry); + let res = resolve_into_stream(&sub, None, &schema, &graphql_vars! {}, &()) + .then(|s| extract_next(s)) + .await; + assert_eq!( + res.as_ref() + .map(|(_, errs)| errs.first().map(|e| e.error().message())), + expected, + "query: {}\nactual result: {:?}", + qry, + res, + ); + } + } + + #[tokio::test] + async fn represents_scalar() { + const QRY: &str = r#"{ __type(name: "Json") { kind } }"#; + + let schema = RootNode::new(Query, EmptyMutation::new(), Subscription); + + let expected = Ok((graphql_value!({"__type": {"kind": "SCALAR"}}), vec![])); + + assert_eq!( + execute(QRY, None, &schema, &graphql_vars! {}, &()).await, + expected, + ); + assert_eq!( + execute_sync(QRY, None, &schema, &graphql_vars! {}, &()), + expected, + ); + } + } + + mod as_input { + use serde::{Deserialize, Serialize}; + + use crate::{ + execute, graphql_object, graphql_vars, EmptyMutation, EmptySubscription, RootNode, + }; + + use super::super::Json; + + #[derive(Debug, Deserialize, Serialize)] + struct Message { + message: Vec, + } + + #[derive(Debug, Deserialize, Serialize)] + struct Envelope { + envelope: Message, + } + + struct Query; + + #[graphql_object] + impl Query { + fn any(arg: Json) -> Json { + arg + } + + fn bool(arg: Json) -> Json { + arg + } + + fn int(arg: Json) -> Json { + arg + } + + fn float(arg: Json) -> Json { + arg + } + + fn string(arg: Json) -> Json { + arg + } + + fn array(arg: Json>) -> Json> { + arg + } + + fn object(arg: Json) -> Json { + arg + } + } + + #[tokio::test] + async fn accepts_null() { + const DOC: &str = r#"{ + null: any(arg: null) + }"#; + + let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new()); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({ "null": None }), vec![])), + ); + } + + #[tokio::test] + async fn accepts_bool() { + const DOC: &str = r#"{ + bool(arg: true) + }"#; + + let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new()); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"bool": true}), vec![])), + ); + } + + #[tokio::test] + async fn accepts_int() { + const DOC: &str = r#"{ + int(arg: 42) + }"#; + + let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new()); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"int": 42}), vec![])), + ); + } + + #[tokio::test] + async fn accepts_float() { + const DOC: &str = r#"{ + float(arg: 3.14) + }"#; + + let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new()); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"float": 3.14}), vec![])), + ); + } + + #[tokio::test] + async fn accepts_string() { + const DOC: &str = r#"{ + string(arg: "Galadriel") + }"#; + + let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new()); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"string": "Galadriel"}), vec![])), + ); + } + + #[tokio::test] + async fn accepts_array() { + const DOC: &str = r#"{ + array(arg: ["Ai", "Ambarendya!"]) + }"#; + + let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new()); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"array": ["Ai", "Ambarendya!"]}), vec![])), + ); + } + + #[tokio::test] + async fn accepts_object() { + const DOC: &str = r#"{ + object(arg: {envelope: {message: ["Ai", "Ambarendya!"]}}) + }"#; + + let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new()); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({ + "object": {"envelope": {"message": ["Ai", "Ambarendya!"]}}, + }), + vec![], + )), + ); + } + + #[tokio::test] + async fn errors_on_invalid_object() { + const DOC: &str = r#"{ + object(arg: {envelope: ["Ai", "Ambarendya!"]}) + }"#; + + let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new()); + + let res = execute(DOC, None, &schema, &graphql_vars! {}, &()).await; + assert_eq!( + res.as_ref() + .map(|(_, errs)| errs.first().map(|e| e.error().message())), + Ok(Some(r#"invalid type: string "Ai", expected a sequence"#)), + ); + } + } +} + +#[cfg(test)] +mod typed_test { + mod as_output { + use serde_json::{json, Value}; + + use crate::{ + execute, execute_sync, graphql_vars, DefaultScalarValue, EmptyMutation, + EmptySubscription, RootNode, + }; + + use super::super::{Info, Typed}; + + const SDL: &str = r#" + type Bar { + location: String + capacity: Int + open: Boolean! + rating: Float + foo: Foo + } + type Foo { + id: [ID] + message: String + bar: Bar + } + "#; + + #[tokio::test] + async fn resolves() { + const QRY: &str = r#"{ + id + message + bar { + location + capacity + open + rating + foo { + message + } + } + }"#; + + let data = Typed::wrap(json!({ + "id": ["foo-1"], + "message": "hello world", + "bar": { + "location": "downtown", + "capacity": 80, + "open": true, + "rating": 4.5, + "foo": { + "message": "drink more", + }, + }, + })); + let schema = >::new_with_info( + data.clone(), + EmptyMutation::new(), + EmptySubscription::new(), + Info::parse("Foo", SDL).unwrap(), + (), + (), + ); + + let expected = Ok((data.into_inner().into(), vec![])); + + assert_eq!( + execute(QRY, None, &schema, &graphql_vars! {}, &()).await, + expected, + ); + assert_eq!( + execute_sync(QRY, None, &schema, &graphql_vars! {}, &()), + expected, + ); + } + } +} + +//------------------------------------------------------------------------------ + +/* + +impl GraphQLValue for Json { + type Context = (); + type TypeInfo = TypeInfo; + + fn type_name<'i>(&self, info: &'i Self::TypeInfo) -> Option<&'i str> { + Some(info.name.as_str()) + } + + fn resolve( + &self, + info: &Self::TypeInfo, + selection: Option<&[Selection]>, + executor: &Executor, + ) -> ExecutionResult { + if let Some(sel) = selection { + // resolve this value as an object + let mut res = juniper::Object::with_capacity(sel.len()); + Ok( + if resolve_selection_set_into(self, info, sel, executor, &mut res) { + Value::Object(res) + } else { + Value::null() + }, + ) + } else { + // resolve this value as leaf + match self { + Json::Null => Ok(Value::null()), + Json::Bool(value) => executor.resolve::(&(), value), + Json::Number(value) => { + if value.is_f64() { + executor.resolve::(&(), &value.as_f64().unwrap()) + } else if value.is_i64() { + executor.resolve::(&(), &(value.as_i64().unwrap() as i32)) + } else if value.is_u64() { + executor.resolve::(&(), &(value.as_u64().unwrap() as i32)) + } else { + panic!("invalid number") + } + } + Json::String(value) => executor.resolve::(&(), value), + _ => Err(FieldError::new("not a leaf value", Value::Null)), + } + } + } + + fn resolve_field( + &self, + _info: &Self::TypeInfo, + field_name: &str, + _: &Arguments, + executor: &Executor, + ) -> ExecutionResult { + match self { + Json::Object(fields) => { + let field_value = fields.get(field_name); + match field_value { + None => Ok(Value::null()), + Some(field_value) => { + let current_type = executor.current_type(); + let field_info = &TypeInfo { + schema: None, + name: current_type + .innermost_concrete() + .name() + .unwrap() + .to_string(), + }; + if current_type.list_contents().is_some() { + match field_value { + Json::Null => Ok(Value::null()), + Json::Array(field_value) => { + executor.resolve::>(field_info, field_value) + } + _ => Err(FieldError::new("not an array", Value::Null)), + } + } else { + executor.resolve::(field_info, &field_value) + } + } + } + } + _ => Err(FieldError::new("not an object value", Value::Null)), + } + } +} +*/ +/* +#[cfg(test)] +mod tests { + use juniper::{ + execute_sync, graphql_object, graphql_value, + integrations::serde_json::{TypeInfo, TypedJson, TypedJsonInfo}, + EmptyMutation, EmptySubscription, FieldResult, RootNode, ToInputValue, Variables, + }; + use serde_json::json; + + #[test] + fn sdl_type_info() { + let sdl = r#" + type Bar { + location: String + capacity: Int + open: Boolean! + rating: Float + foo: Foo + } + type Foo { + message: String + bar: Bar + } + "#; + + let info = TypeInfo { + name: "Foo".to_string(), + schema: Some(sdl.to_string()), + }; + + let data = json!({ + "message": "hello world", + "bar": { + "location": "downtown", + "capacity": 80, + "open": true, + "rating": 4.5, + "foo": { + "message": "drink more" + } + }, + }); + + let schema = >::new_with_info( + data, + EmptyMutation::new(), + EmptySubscription::new(), + info, + (), + (), + ); + + // print!("{}", schema.as_schema_language()); + + let query = r#"{ + message + bar { + location + capacity + open + rating + foo { + message + } + } + }"#; + + assert_eq!( + execute_sync(query, None, &schema, &graphql_vars! {}, &()), + Ok(( + graphql_value!({ + "message": "hello world", + "bar": { + "location": "downtown", + "capacity": 80, + "open": true, + "rating": 4.5, + "foo": { + "message": "drink more" + } + } + }), + vec![], + )), + ); + } + + #[test] + fn required_field() { + let sdl = r#" + type Bar { + location: String + open: Boolean! + } + type Foo { + message: String + bar: Bar + } + "#; + + let info = TypeInfo { + name: "Foo".to_string(), + schema: Some(sdl.to_string()), + }; + + let data = json!({ + "message": "hello world", + "bar": { + "capacity": 80, + }, + }); + + let schema = >::new_with_info( + data, + EmptyMutation::new(), + EmptySubscription::new(), + info, + (), + (), + ); + + let query = r#"{ + message + bar { + location + open + } + }"#; + + assert_eq!( + execute_sync(query, None, &schema, &graphql_vars! {}, &()), + Ok(( + graphql_value!({ + "message": "hello world", + "bar": None, + }), + vec![], + )), + ); + } + + #[test] + fn array_field() { + let sdl = r#" + type Bar { + location: [String] + open: [Boolean!] + } + type Foo { + message: [String] + bar: [Bar] + } + "#; + + let info = TypeInfo { + name: "Foo".to_string(), + schema: Some(sdl.to_string()), + }; + + let data = json!({ + "message": ["hello world"], + "bar": [{ + "location": ["Tampa"], + }], + }); + + let schema: RootNode<_, _, _> = RootNode::new_with_info( + data, + EmptyMutation::new(), + EmptySubscription::new(), + info, + (), + (), + ); + + // print!("{}", schema.as_schema_language()); + + let query = r#"{ + message + bar { + location + } + }"#; + + assert_eq!( + execute_sync(query, None, &schema, &graphql_vars! {}, &()), + Ok(( + graphql_value!({ + "message": ["hello world"], + "bar": [{ + "location": ["Tampa"], + }], + }), + vec![], + )), + ); + } + + #[test] + fn test_as_field_of_output_type() { + // We need a Foo wrapper associate a static SDL to the Foo type which + struct Foo; + impl TypedJsonInfo for Foo { + fn type_name() -> &'static str { + "Foo" + } + fn schema() -> &'static str { + r#" + type Foo { + message: [String] + } + "# + } + } + + struct Query; + #[graphql_object] + impl Query { + fn foo() -> FieldResult> { + let data = json!({"message": ["Hello", "World"] }); + Ok(TypedJson::new(data)) + } + } + let schema = juniper::RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new()); + // Run the executor. + let (res, _errors) = juniper::execute_sync( + "query { foo { message } }", + None, + &schema, + &graphql_vars! {}, + &(), + ) + .unwrap(); + + // Ensure the value matches. + assert_eq!( + res, + graphql_value!({ + "foo": {"message":["Hello", "World"]}, + }) + ); + } + + #[test] + fn test_as_field_of_input_type() { + #[derive(Debug, Clone, PartialEq)] + struct Foo; + impl TypedJsonInfo for Foo { + fn type_name() -> &'static str { + "Foo" + } + fn schema() -> &'static str { + r#" + input Foo { + message: [String] + } + "# + } + } + + struct Query; + #[graphql_object()] + impl Query { + fn foo(value: TypedJson) -> FieldResult { + Ok(value == TypedJson::new(json!({"message":["Hello", "World"]}))) + } + } + let schema = juniper::RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new()); + + let vars = vec![( + "value".to_owned(), + graphql_value!({ + "message":["Hello", "World"], + }) + .to_input_value(), + )] + .into_iter() + .collect(); + + // Run the executor. + let (res, _errors) = juniper::execute_sync( + "query example($value:Foo!){ foo(value: $value) }", + None, + &schema, + &vars, + &(), + ) + .unwrap(); + + // Ensure the value matches. + assert_eq!( + res, + graphql_value!({ + "foo": true, + }), + ); + } +} +*/ diff --git a/juniper/src/integrations/mod.rs b/juniper/src/integrations/mod.rs index d966997f2..5ed6d4404 100644 --- a/juniper/src/integrations/mod.rs +++ b/juniper/src/integrations/mod.rs @@ -6,6 +6,8 @@ pub mod bson; pub mod chrono; #[cfg(feature = "chrono-tz")] pub mod chrono_tz; +#[cfg(feature = "json")] +pub mod json; #[doc(hidden)] pub mod serde; #[cfg(feature = "time")] diff --git a/juniper/src/lib.rs b/juniper/src/lib.rs index d864cd101..6b5d1107b 100644 --- a/juniper/src/lib.rs +++ b/juniper/src/lib.rs @@ -142,6 +142,9 @@ where QueryT: GraphQLType, MutationT: GraphQLType, SubscriptionT: GraphQLType, + QueryT::TypeInfo: Sized, + MutationT::TypeInfo: Sized, + SubscriptionT::TypeInfo: Sized, { let document = parse_document_source(document_source, &root_node.schema)?; @@ -178,12 +181,12 @@ pub async fn execute<'a, S, QueryT, MutationT, SubscriptionT>( ) -> Result<(Value, Vec>), GraphQLError<'a>> where QueryT: GraphQLTypeAsync, - QueryT::TypeInfo: Sync, + QueryT::TypeInfo: Sync + Sized, QueryT::Context: Sync, MutationT: GraphQLTypeAsync, - MutationT::TypeInfo: Sync, + MutationT::TypeInfo: Sync + Sized, SubscriptionT: GraphQLType + Sync, - SubscriptionT::TypeInfo: Sync, + SubscriptionT::TypeInfo: Sync + Sized, S: ScalarValue + Send + Sync, { let document = parse_document_source(document_source, &root_node.schema)?; @@ -222,12 +225,12 @@ pub async fn resolve_into_stream<'a, S, QueryT, MutationT, SubscriptionT>( ) -> Result<(Value>, Vec>), GraphQLError<'a>> where QueryT: GraphQLTypeAsync, - QueryT::TypeInfo: Sync, + QueryT::TypeInfo: Sync + Sized, QueryT::Context: Sync, MutationT: GraphQLTypeAsync, - MutationT::TypeInfo: Sync, + MutationT::TypeInfo: Sync + Sized, SubscriptionT: GraphQLSubscriptionType, - SubscriptionT::TypeInfo: Sync, + SubscriptionT::TypeInfo: Sync + Sized, S: ScalarValue + Send + Sync, { let document: crate::ast::OwnedDocument<'a, S> = @@ -268,6 +271,9 @@ where QueryT: GraphQLType, MutationT: GraphQLType, SubscriptionT: GraphQLType, + QueryT::TypeInfo: Sized, + MutationT::TypeInfo: Sized, + SubscriptionT::TypeInfo: Sized, { execute_sync( match format { diff --git a/juniper/src/parser/value.rs b/juniper/src/parser/value.rs index 3ff6d5efc..a65528fbf 100644 --- a/juniper/src/parser/value.rs +++ b/juniper/src/parser/value.rs @@ -113,6 +113,17 @@ where }, _, ) => Ok(parser.next_token()?.map(|_| InputValue::enum_value(name))), + // This hack is required as Juniper doesn't allow at the moment + // for custom defined types to tweak into parsing. + // TODO: Redesign parsing layer to allow such things. + #[cfg(feature = "json")] + ( + &Spanning { + item: Token::CurlyOpen, + .. + }, + Some(&MetaType::Scalar(ref s)), + ) if s.name == "Json" => parse_object_literal(parser, is_const, schema, None), _ => Err(parser.next_token()?.map(ParseError::UnexpectedToken)), } } diff --git a/juniper/src/schema/model.rs b/juniper/src/schema/model.rs index 5544fbbdd..d3deb636d 100644 --- a/juniper/src/schema/model.rs +++ b/juniper/src/schema/model.rs @@ -28,6 +28,9 @@ pub struct RootNode< SubscriptionT: GraphQLType, S = DefaultScalarValue, > where + QueryT::TypeInfo: Sized, + MutationT::TypeInfo: Sized, + SubscriptionT::TypeInfo: Sized, S: ScalarValue, { #[doc(hidden)] @@ -134,6 +137,9 @@ where QueryT: GraphQLType, MutationT: GraphQLType, SubscriptionT: GraphQLType, + QueryT::TypeInfo: Sized, + MutationT::TypeInfo: Sized, + SubscriptionT::TypeInfo: Sized, S: ScalarValue + 'a, { /// Construct a new root node from query and mutation nodes, diff --git a/juniper/src/schema/schema.rs b/juniper/src/schema/schema.rs index 30906478f..ed11b065a 100644 --- a/juniper/src/schema/schema.rs +++ b/juniper/src/schema/schema.rs @@ -24,6 +24,9 @@ where QueryT: GraphQLType, MutationT: GraphQLType, SubscriptionT: GraphQLType, + QueryT::TypeInfo: Sized, + MutationT::TypeInfo: Sized, + SubscriptionT::TypeInfo: Sized, { fn name(info: &Self::TypeInfo) -> Option<&str> { QueryT::name(info) @@ -44,6 +47,9 @@ where QueryT: GraphQLType, MutationT: GraphQLType, SubscriptionT: GraphQLType, + QueryT::TypeInfo: Sized, + MutationT::TypeInfo: Sized, + SubscriptionT::TypeInfo: Sized, { type Context = QueryT::Context; type TypeInfo = QueryT::TypeInfo; @@ -102,12 +108,12 @@ impl<'a, S, QueryT, MutationT, SubscriptionT> GraphQLValueAsync for RootNode<'a, QueryT, MutationT, SubscriptionT, S> where QueryT: GraphQLTypeAsync, - QueryT::TypeInfo: Sync, + QueryT::TypeInfo: Sync + Sized, QueryT::Context: Sync + 'a, MutationT: GraphQLTypeAsync, - MutationT::TypeInfo: Sync, + MutationT::TypeInfo: Sync + Sized, SubscriptionT: GraphQLType + Sync, - SubscriptionT::TypeInfo: Sync, + SubscriptionT::TypeInfo: Sync + Sized, S: ScalarValue + Send + Sync, { fn resolve_field_async<'b>( diff --git a/juniper/src/tests/mod.rs b/juniper/src/tests/mod.rs index 3e42ec7da..639fce471 100644 --- a/juniper/src/tests/mod.rs +++ b/juniper/src/tests/mod.rs @@ -11,3 +11,5 @@ mod schema_introspection; mod subscriptions; #[cfg(test)] mod type_info_tests; +#[cfg(test)] +pub mod util; diff --git a/juniper/src/tests/util.rs b/juniper/src/tests/util.rs new file mode 100644 index 000000000..c5c225c54 --- /dev/null +++ b/juniper/src/tests/util.rs @@ -0,0 +1,41 @@ +//! Helper utilities to use in tests all over the crate. + +use std::pin::Pin; + +use futures::{future, stream, StreamExt as _}; + +use crate::{ExecutionError, GraphQLError, ScalarValue, Value, ValuesStream}; + +/// Shortcut for a [`Box`]ed stream of items. +pub type Stream = Pin + Send>>; + +/// Returns a [`Stream`] out of the given single value. +pub fn stream(val: T) -> Stream { + Box::pin(stream::once(future::ready(val))) +} + +/// Extracts a single next value from the result returned by +/// [`juniper::resolve_into_stream()`] and transforms it into a regular +/// [`Value`]. +pub async fn extract_next<'a, S: ScalarValue>( + input: Result<(Value>, Vec>), GraphQLError<'a>>, +) -> Result<(Value, Vec>), GraphQLError<'a>> { + let (stream, errs) = input?; + if !errs.is_empty() { + return Ok((Value::Null, errs)); + } + + if let Value::Object(obj) = stream { + for (name, mut val) in obj { + if let Value::Scalar(ref mut stream) = val { + return match stream.next().await { + Some(Ok(val)) => Ok((graphql_value!({ name: val }), vec![])), + Some(Err(e)) => Ok((Value::Null, vec![e])), + None => Ok((Value::Null, vec![])), + }; + } + } + } + + panic!("expected to get `Value::Object` containing a `Stream`") +} diff --git a/juniper/src/types/async_await.rs b/juniper/src/types/async_await.rs index fdc4a037e..823baf042 100644 --- a/juniper/src/types/async_await.rs +++ b/juniper/src/types/async_await.rs @@ -268,6 +268,16 @@ where .await; let value = match res { + // This hack is required as Juniper doesn't allow at the + // moment for custom defined types to tweak into + // resolving. + // TODO: Redesign resolving layer to allow such things. + #[cfg(feature = "json")] + Ok(Value::Null) + if is_non_null && meta_field.field_type.name() == Some("Json") => + { + Some(Value::Null) + } Ok(Value::Null) if is_non_null => None, Ok(v) => Some(v), Err(e) => { diff --git a/juniper/src/types/base.rs b/juniper/src/types/base.rs index 175be9293..f8ce57639 100644 --- a/juniper/src/types/base.rs +++ b/juniper/src/types/base.rs @@ -175,7 +175,7 @@ where /// It can be used to implement a schema that is partly dynamic, meaning that it can use /// information that is not known at compile time, for instance by reading it from a /// configuration file at startup. - type TypeInfo; + type TypeInfo: ?Sized; /// Returns name of the [`GraphQLType`] exposed by this [`GraphQLValue`]. /// @@ -493,6 +493,16 @@ where ); match field_result { + // This hack is required as Juniper doesn't allow at the + // moment for custom defined types to tweak into resolving. + // TODO: Redesign resolving layer to allow such things. + #[cfg(feature = "json")] + Ok(Value::Null) + if meta_field.field_type.is_non_null() + && meta_field.field_type.name() == Some("Json") => + { + merge_key_into(result, response_name, Value::Null) + } Ok(Value::Null) if meta_field.field_type.is_non_null() => return false, Ok(v) => merge_key_into(result, response_name, v), Err(e) => { diff --git a/juniper/src/types/subscriptions.rs b/juniper/src/types/subscriptions.rs index e99a5aafa..62c3738b1 100644 --- a/juniper/src/types/subscriptions.rs +++ b/juniper/src/types/subscriptions.rs @@ -329,6 +329,16 @@ where .await; match res { + // This hack is required as Juniper doesn't allow at the + // moment for custom defined types to to tweak into + // resolving. + // TODO: Redesign resolving layer to allow such things. + #[cfg(feature = "json")] + Ok(Value::Null) + if is_non_null && meta_field.field_type.name() == Some("Json") => + { + merge_key_into(&mut object, response_name, Value::Null) + } Ok(Value::Null) if is_non_null => { return Value::Null; } diff --git a/juniper/src/types/utilities.rs b/juniper/src/types/utilities.rs index 2ed03b1f3..cd49cebe1 100644 --- a/juniper/src/types/utilities.rs +++ b/juniper/src/types/utilities.rs @@ -1,7 +1,7 @@ use crate::{ ast::InputValue, schema::{ - meta::{EnumMeta, InputObjectMeta, MetaType}, + meta::{EnumMeta, InputObjectMeta, MetaType, ScalarMeta}, model::{SchemaType, TypeType}, }, value::ScalarValue, @@ -16,18 +16,32 @@ pub fn is_valid_literal_value( where S: ScalarValue, { - match *arg_type { - TypeType::NonNull(ref inner) => { + match arg_type { + TypeType::NonNull(inner) => { if arg_value.is_null() { + // This hack is required as Juniper doesn't allow at the moment + // for custom defined types to tweak into parsing validation. + // TODO: Redesign parsing layer to allow such things. + #[cfg(feature = "json")] + if let TypeType::Concrete(t) = &**inner { + if let MetaType::Scalar(ScalarMeta { name, .. }) = t { + if name == "Json" { + if let Some(parse_fn) = t.input_value_parse_fn() { + return parse_fn(arg_value).is_ok(); + } + } + } + } + false } else { is_valid_literal_value(schema, inner, arg_value) } } - TypeType::List(ref inner, expected_size) => match *arg_value { - InputValue::List(ref items) => { + TypeType::List(inner, expected_size) => match arg_value { + InputValue::List(items) => { if let Some(expected) = expected_size { - if items.len() != expected { + if items.len() != *expected { return false; } } @@ -35,9 +49,9 @@ where .iter() .all(|i| is_valid_literal_value(schema, inner, &i.item)) } - ref v => { + v => { if let Some(expected) = expected_size { - if expected != 1 { + if *expected != 1 { return false; } } @@ -47,13 +61,23 @@ where TypeType::Concrete(t) => { // Even though InputValue::String can be parsed into an enum, they // are not valid as enum *literals* in a GraphQL query. - if let (&InputValue::Scalar(_), Some(&MetaType::Enum(EnumMeta { .. }))) = - (arg_value, arg_type.to_concrete()) - { + if let (&InputValue::Scalar(_), MetaType::Enum(EnumMeta { .. })) = (arg_value, t) { return false; } - match *arg_value { + // This hack is required as Juniper doesn't allow at the moment + // for custom defined types to tweak into parsing validation. + // TODO: Redesign parsing layer to allow such things. + #[cfg(feature = "json")] + if let MetaType::Scalar(ScalarMeta { name, .. }) = t { + if name == "Json" { + if let Some(parse_fn) = t.input_value_parse_fn() { + return parse_fn(arg_value).is_ok(); + } + } + } + + match arg_value { InputValue::Null | InputValue::Variable(_) => true, ref v @ InputValue::Scalar(_) | ref v @ InputValue::Enum(_) => { if let Some(parse_fn) = t.input_value_parse_fn() { @@ -63,11 +87,8 @@ where } } InputValue::List(_) => false, - InputValue::Object(ref obj) => { - if let MetaType::InputObject(InputObjectMeta { - ref input_fields, .. - }) = *t - { + InputValue::Object(obj) => { + if let MetaType::InputObject(InputObjectMeta { input_fields, .. }) = t { let mut remaining_required_fields = input_fields .iter() .filter_map(|f| { @@ -81,13 +102,13 @@ where let all_types_ok = obj.iter().all(|&(ref key, ref value)| { remaining_required_fields.remove(&key.item); - if let Some(ref arg_type) = input_fields + if let Some(arg_type) = input_fields .iter() .filter(|f| f.name == key.item) .map(|f| schema.make_type(&f.arg_type)) .next() { - is_valid_literal_value(schema, arg_type, &value.item) + is_valid_literal_value(schema, &arg_type, &value.item) } else { false } diff --git a/juniper/src/validation/rules/fields_on_correct_type.rs b/juniper/src/validation/rules/fields_on_correct_type.rs index ad90cb257..e28ded82f 100644 --- a/juniper/src/validation/rules/fields_on_correct_type.rs +++ b/juniper/src/validation/rules/fields_on_correct_type.rs @@ -58,6 +58,14 @@ where } } + // This hack is required as Juniper doesn't allow at the + // moment for custom defined types to tweak into validation. + // TODO: Redesign validation layer to allow such things. + #[cfg(feature = "json")] + if type_name == "Json" { + return; + } + context.report_error( &error_message(field_name.item, type_name), &[field_name.start], diff --git a/juniper/src/validation/rules/scalar_leafs.rs b/juniper/src/validation/rules/scalar_leafs.rs index fe5ac7778..8328974b4 100644 --- a/juniper/src/validation/rules/scalar_leafs.rs +++ b/juniper/src/validation/rules/scalar_leafs.rs @@ -21,13 +21,24 @@ where let error = if let (Some(field_type), Some(field_type_literal)) = (ctx.current_type(), ctx.current_type_literal()) { - match (field_type.is_leaf(), &field.item.selection_set) { - (true, &Some(_)) => Some(RuleError::new( - &no_allowed_error_message(field_name, &format!("{}", field_type_literal)), - &[field.start], - )), - (false, &None) => Some(RuleError::new( - &required_error_message(field_name, &format!("{}", field_type_literal)), + match (field_type.is_leaf(), field.item.selection_set.is_some()) { + (true, true) => { + let field_type = field_type_literal.to_string(); + let field_type = field_type.as_str(); + // This hack is required as Juniper doesn't allow at the + // moment for custom defined types to tweak into validation. + // TODO: Redesign validation layer to allow such things. + if cfg!(feature = "json") && (field_type == "Json" || field_type == "Json!") { + None + } else { + Some(RuleError::new( + &no_allowed_error_message(field_name, field_type), + &[field.start], + )) + } + } + (false, false) => Some(RuleError::new( + &required_error_message(field_name, &field_type_literal.to_string()), &[field.start], )), _ => None, diff --git a/juniper_actix/examples/actix_server.rs b/juniper_actix/examples/actix_server.rs index 5a13b79a2..665e063ed 100644 --- a/juniper_actix/examples/actix_server.rs +++ b/juniper_actix/examples/actix_server.rs @@ -67,7 +67,7 @@ impl Database { // To make our Database usable by Juniper, we have to implement a marker trait. impl juniper::Context for Database {} -// Queries represent the callable funcitons +// Queries represent the callable functions struct Query; #[graphql_object(context = Database)] impl Query { diff --git a/tests/integration/src/codegen/subscription_attr.rs b/tests/integration/src/codegen/subscription_attr.rs index 306b8c8e3..211faa721 100644 --- a/tests/integration/src/codegen/subscription_attr.rs +++ b/tests/integration/src/codegen/subscription_attr.rs @@ -1797,4 +1797,64 @@ mod executor { )), ); } + + #[tokio::test] + async fn test_integration_json() { + use juniper::integrations::json::{TypedJson, TypedJsonInfo}; + use serde_json::json; + + struct Foo; + impl TypedJsonInfo for Foo { + fn type_name() -> &'static str { + "Foo" + } + fn schema() -> &'static str { + r#" + type Foo { + message: [String] + } + "# + } + } + + struct Query; + #[graphql_object] + impl Query { + fn zero() -> FieldResult { + Ok(0) + } + } + + struct Subscription; + #[graphql_subscription(scalar = S: ScalarValue)] + impl Subscription { + // TODO: Make work for `Stream<'e, &'e str>`. + async fn foo<'e, S>( + &self, + _executor: &'e Executor<'_, '_, (), S>, + ) -> Stream<'static, TypedJson> + where + S: ScalarValue, + { + let data = TypedJson::new(json!({"message": ["Hello World"] })); + Box::pin(stream::once(future::ready(data))) + } + } + + let schema = juniper::RootNode::new(Query, EmptyMutation::new(), Subscription); + + const DOC: &str = r#"subscription { + foo { message } + }"#; + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok(( + graphql_value!({"foo":{"message": ["Hello World"] }}), + vec![] + )), + ); + } }