Skip to content

Commit 38495b7

Browse files
committed
feat: support using serde_json::Value as input/output fields
1 parent d2bdcd9 commit 38495b7

File tree

1 file changed

+241
-8
lines changed

1 file changed

+241
-8
lines changed

juniper/src/integrations/json.rs

Lines changed: 241 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@ use graphql_parser::{
66
schema::{Definition, TypeDefinition},
77
};
88
use juniper::{
9-
meta::{Field, MetaType},
9+
marker::{IsOutputType, IsInputType},
10+
meta::{Field, MetaType, Argument},
11+
types::base::resolve_selection_set_into,
1012
Arguments, ExecutionResult, Executor, FieldError, GraphQLType, GraphQLValue, Registry,
11-
ScalarValue, Selection, Value, GraphQLValueAsync, BoxFuture,
13+
ScalarValue, Selection, Value, GraphQLValueAsync, BoxFuture, FromInputValue, InputValue,
1214
};
1315
use serde_json::Value as Json;
1416

15-
use crate::{types::base::resolve_selection_set_into};
1617

1718
// Used to describe the graphql type of a `serde_json::Value` using the GraphQL
1819
// schema definition language.
@@ -33,8 +34,10 @@ impl TypeInfo {
3334
S: ScalarValue + 'r,
3435
{
3536
let mut fields = Vec::new();
37+
let mut input_fields = Vec::new();
3638
let s = self.schema.clone().unwrap_or_default();
3739
let ast = parse_schema::<&str>(s.as_str()).unwrap();
40+
let mut is_input_object = false;
3841
for d in &ast.definitions {
3942
match &d {
4043
Definition::TypeDefinition(d) => match d {
@@ -50,14 +53,40 @@ impl TypeInfo {
5053
}
5154
}
5255
}
56+
TypeDefinition::InputObject(d) => {
57+
if d.name == self.name {
58+
is_input_object = true;
59+
for field in &d.fields {
60+
let f = self.build_field(
61+
registry,
62+
field.name,
63+
field.value_type.clone(),
64+
true,
65+
);
66+
67+
input_fields.push(Argument {
68+
name: field.name.to_string(),
69+
description: field.description.clone(),
70+
arg_type: f.field_type,
71+
default_value: None,
72+
});
73+
}
74+
}
75+
}
5376
_ => todo!(),
5477
},
5578
_ => {}
5679
}
5780
}
58-
registry
59-
.build_object_type::<Json>(self, &fields)
60-
.into_meta()
81+
if is_input_object {
82+
registry
83+
.build_input_object_type::<Json>(self, &input_fields)
84+
.into_meta()
85+
} else {
86+
registry
87+
.build_object_type::<Json>(self, &fields)
88+
.into_meta()
89+
}
6190
}
6291

6392
fn build_field<'r, 't, S, T>(
@@ -142,6 +171,54 @@ impl<S: ScalarValue> GraphQLType<S> for Json {
142171
}
143172
}
144173

174+
impl<S> IsOutputType<S> for Json where S: ScalarValue {}
175+
176+
impl<S> IsInputType<S> for Json where S: ScalarValue {}
177+
178+
impl<S> FromInputValue<S> for Json where S: ScalarValue
179+
{
180+
fn from_input_value(v: &InputValue<S>) -> Option<Self> {
181+
match v {
182+
InputValue::Null => {
183+
Some(Json::Null)
184+
}
185+
InputValue::Scalar(x) => {
186+
Some(if let Some(i) = x.as_int() {
187+
Json::Number(serde_json::Number::from(i))
188+
} else if let Some(f) = x.as_float() {
189+
Json::Number(serde_json::Number::from_f64(f).expect("f64 to convert"))
190+
} else if let Some(b) = x.as_boolean() {
191+
Json::Bool(b)
192+
} else if let Some(s) = x.as_str() {
193+
Json::String(s.to_string())
194+
} else {
195+
unreachable!("`ScalarValue` must represent at least one of the GraphQL spec types")
196+
})
197+
}
198+
InputValue::Enum(x) => {
199+
Some(Json::String(x.clone()))
200+
}
201+
InputValue::List(ls) => {
202+
let v: Vec<Json> = ls.iter().filter_map(|i| i.item.convert()).collect();
203+
Some(Json::Array(v))
204+
}
205+
InputValue::Object(fields) => {
206+
let mut obj = serde_json::Map::new();
207+
for field in fields {
208+
let v: Option<Json> = field.1.item.convert();
209+
if let Some(v) = v {
210+
obj.insert(field.0.item.clone(), v);
211+
}
212+
}
213+
Some(Json::Object(obj))
214+
}
215+
InputValue::Variable(_) => {
216+
None
217+
}
218+
}
219+
}
220+
}
221+
145222
impl<S: ScalarValue> GraphQLValue<S> for Json {
146223
type Context = ();
147224
type TypeInfo = TypeInfo;
@@ -264,11 +341,15 @@ impl<S> GraphQLValueAsync<S> for Json
264341
#[cfg(test)]
265342
mod tests {
266343
use juniper::{
267-
execute_sync, graphql_value, EmptyMutation, EmptySubscription, RootNode, Variables,
344+
marker::{IsOutputType, IsInputType},
345+
meta::MetaType,
346+
integrations::json::TypeInfo,
347+
execute_sync, graphql_object, graphql_value, EmptyMutation, EmptySubscription, RootNode, Variables,
348+
ScalarValue, GraphQLValue, GraphQLType, Selection, Executor, ExecutionResult, FieldResult,
349+
GraphQLValueAsync, Registry, ToInputValue, FromInputValue, InputValue,
268350
};
269351
use serde_json::json;
270352

271-
use super::TypeInfo;
272353

273354
#[test]
274355
fn sdl_type_info() {
@@ -503,5 +584,157 @@ mod tests {
503584
)),
504585
);
505586
}
587+
588+
#[test]
589+
fn test_as_field_of_output_type() {
590+
// We need a Foo wrapper associate a static SDL to the Foo type which
591+
// wraps the serde_json::Value. Would be nice if a macro could code gen this.
592+
struct Foo(serde_json::Value);
593+
impl<S> IsOutputType<S> for Foo where S: ScalarValue {}
594+
impl<S> GraphQLValueAsync<S> for Foo where S: ScalarValue + Send + Sync {}
595+
impl<S> GraphQLType<S> for Foo where S: ScalarValue
596+
{
597+
fn name(_info: &Self::TypeInfo) -> Option<&str> {
598+
Some("Foo")
599+
}
600+
fn meta<'r>(_info: &Self::TypeInfo, registry: &mut Registry<'r, S>) -> MetaType<'r, S>
601+
where S: 'r,
602+
{
603+
TypeInfo {
604+
name: "Foo".to_string(),
605+
schema: Some(r#"
606+
type Foo {
607+
message: [String]
608+
}
609+
"#.to_string()),
610+
}.meta(registry)
611+
}
612+
}
613+
impl<S> GraphQLValue<S> for Foo where S: ScalarValue
614+
{
615+
type Context = ();
616+
type TypeInfo = ();
617+
fn type_name<'i>(&self, info: &'i Self::TypeInfo) -> Option<&'i str> {
618+
<Self as GraphQLType>::name(info)
619+
}
620+
fn resolve(
621+
&self,
622+
_info: &Self::TypeInfo,
623+
_selection: Option<&[Selection<S>]>,
624+
executor: &Executor<Self::Context, S>,
625+
) -> ExecutionResult<S> {
626+
executor.resolve(&TypeInfo { schema: None, name: "Foo".to_string() }, &self.0)
627+
}
628+
}
629+
630+
struct Query;
631+
#[graphql_object()]
632+
impl Query {
633+
fn foo() -> FieldResult<Foo> {
634+
let data = json!({"message": ["Hello", "World"] });
635+
Ok(Foo(data))
636+
}
637+
}
638+
let schema = juniper::RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new());
639+
// Run the executor.
640+
let (res, _errors) = juniper::execute_sync(
641+
"query { foo { message } }",
642+
None,
643+
&schema,
644+
&Variables::new(),
645+
&(),
646+
).unwrap();
647+
648+
// Ensure the value matches.
649+
assert_eq!(
650+
res,
651+
graphql_value!({
652+
"foo": {"message":["Hello", "World"]},
653+
})
654+
);
655+
}
656+
657+
658+
#[test]
659+
fn test_as_field_of_input_type() {
660+
// We need a Foo wrapper associate a static SDL to the Foo type which
661+
// wraps the serde_json::Value. Would be nice if a macro could code gen this.
662+
663+
#[derive(Debug, Clone, PartialEq)]
664+
struct Foo(serde_json::Value);
665+
impl<S> IsInputType<S> for Foo where S: ScalarValue {}
666+
impl<S> GraphQLValueAsync<S> for Foo where S: ScalarValue + Send + Sync {}
667+
impl<S> FromInputValue<S> for Foo where S: ScalarValue {
668+
fn from_input_value(v: &InputValue<S>) -> Option<Self> {
669+
<serde_json::Value as FromInputValue<S>>::from_input_value(v).map(|x| Foo(x))
670+
}
671+
}
672+
impl<S> GraphQLType<S> for Foo where S: ScalarValue
673+
{
674+
fn name(_info: &Self::TypeInfo) -> Option<&str> {
675+
Some("Foo")
676+
}
677+
fn meta<'r>(_info: &Self::TypeInfo, registry: &mut Registry<'r, S>) -> MetaType<'r, S>
678+
where S: 'r,
679+
{
680+
TypeInfo {
681+
name: "Foo".to_string(),
682+
schema: Some(r#"
683+
input Foo {
684+
message: [String]
685+
}
686+
"#.to_string()),
687+
}.meta(registry)
688+
}
689+
}
690+
impl<S> GraphQLValue<S> for Foo where S: ScalarValue
691+
{
692+
type Context = ();
693+
type TypeInfo = ();
694+
fn type_name<'i>(&self, info: &'i Self::TypeInfo) -> Option<&'i str> {
695+
<Self as GraphQLType>::name(info)
696+
}
697+
fn resolve(
698+
&self,
699+
_info: &Self::TypeInfo,
700+
_selection: Option<&[Selection<S>]>,
701+
executor: &Executor<Self::Context, S>,
702+
) -> ExecutionResult<S> {
703+
executor.resolve(&TypeInfo { schema: None, name: "Foo".to_string() }, &self.0)
704+
}
705+
}
706+
707+
struct Query;
708+
#[graphql_object()]
709+
impl Query {
710+
fn foo(value: Foo) -> FieldResult<bool> {
711+
Ok(value == Foo(json!({"message":["Hello", "World"]})))
712+
}
713+
}
714+
let schema = juniper::RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new());
715+
716+
let vars = vec![("value".to_owned(), graphql_value!({
717+
"message":["Hello", "World"],
718+
}).to_input_value())]
719+
.into_iter()
720+
.collect();
721+
722+
// Run the executor.
723+
let (res, _errors) = juniper::execute_sync(
724+
"query example($value:Foo!){ foo(value: $value) }",
725+
None,
726+
&schema,
727+
&vars,
728+
&(),
729+
).unwrap();
730+
731+
// Ensure the value matches.
732+
assert_eq!(
733+
res,
734+
graphql_value!({
735+
"foo": true,
736+
})
737+
);
738+
}
506739
}
507740

0 commit comments

Comments
 (0)