diff --git a/Cargo.lock b/Cargo.lock index d3409726d6..c8675bf2cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1649,9 +1649,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.32" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c0596c1eac1f9e04ed902702e9878208b336edc9d6fddc8a48387349bab3666" +checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" dependencies = [ "crc32fast", "miniz_oxide 0.8.0", diff --git a/npm/package-lock.json b/npm/package-lock.json index 287681fbdd..acba9bbde7 100644 --- a/npm/package-lock.json +++ b/npm/package-lock.json @@ -843,9 +843,9 @@ } }, "node_modules/tsx": { - "version": "4.17.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.17.0.tgz", - "integrity": "sha512-eN4mnDA5UMKDt4YZixo9tBioibaMBpoxBkD+rIPAjVmYERSG0/dWEY1CEFuV89CgASlKL499q8AhmkMnnjtOJg==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.18.0.tgz", + "integrity": "sha512-a1jaKBSVQkd6yEc1/NI7G6yHFfefIcuf3QJST7ZEyn4oQnxLYrZR5uZAM8UrwUa3Ge8suiZHcNS1gNrEvmobqg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/src/core/blueprint/from_config.rs b/src/core/blueprint/from_config.rs index 697f6e2d06..af450b56e0 100644 --- a/src/core/blueprint/from_config.rs +++ b/src/core/blueprint/from_config.rs @@ -1,6 +1,7 @@ use std::collections::{BTreeMap, BTreeSet}; use async_graphql::dynamic::SchemaBuilder; +use indexmap::IndexMap; use self::telemetry::to_opentelemetry; use super::{Server, TypeLike}; @@ -70,7 +71,7 @@ pub fn apply_batching(mut blueprint: Blueprint) -> Blueprint { pub fn to_json_schema_for_field(field: &Field, config: &Config) -> JsonSchema { to_json_schema(field, config) } -pub fn to_json_schema_for_args(args: &BTreeMap, config: &Config) -> JsonSchema { +pub fn to_json_schema_for_args(args: &IndexMap, config: &Config) -> JsonSchema { let mut schema_fields = BTreeMap::new(); for (name, arg) in args.iter() { schema_fields.insert(name.clone(), to_json_schema(arg, config)); diff --git a/src/core/blueprint/into_schema.rs b/src/core/blueprint/into_schema.rs index cbfdb681f3..31fe06a964 100644 --- a/src/core/blueprint/into_schema.rs +++ b/src/core/blueprint/into_schema.rs @@ -1,7 +1,6 @@ use std::borrow::Cow; use std::sync::Arc; -use anyhow::{bail, Result}; use async_graphql::dynamic::{self, FieldFuture, FieldValue, SchemaBuilder}; use async_graphql::ErrorExtensions; use async_graphql_value::ConstValue; @@ -11,7 +10,7 @@ use tracing::Instrument; use crate::core::blueprint::{Blueprint, Definition, Type}; use crate::core::http::RequestContext; -use crate::core::ir::{EvalContext, ResolverContext, TypeName}; +use crate::core::ir::{EvalContext, ResolverContext, TypedValue}; use crate::core::scalar; fn to_type_ref(type_of: &Type) -> dynamic::TypeRef { @@ -59,27 +58,21 @@ fn set_default_value( } } -fn to_field_value<'a>( - ctx: &mut EvalContext<'a, ResolverContext<'a>>, - value: async_graphql::Value, -) -> Result> { - let type_name = ctx.type_name.take(); - - Ok(match (value, type_name) { - // NOTE: Mostly type_name is going to be None so we should keep that as the first check. - (value, None) => FieldValue::from(value), - (ConstValue::List(values), Some(TypeName::Vec(names))) => FieldValue::list( - values - .into_iter() - .zip(names) - .map(|(value, type_name)| FieldValue::from(value).with_type(type_name)), - ), - (value @ ConstValue::Object(_), Some(TypeName::Single(type_name))) => { - FieldValue::from(value).with_type(type_name) +fn to_field_value(value: async_graphql::Value) -> FieldValue<'static> { + match value { + ConstValue::List(vec) => FieldValue::list(vec.into_iter().map(to_field_value)), + value => { + let type_name = value.get_type_name().map(|s| s.to_string()); + + let field_value = FieldValue::from(value); + + if let Some(type_name) = type_name { + field_value.with_type(type_name) + } else { + field_value + } } - (ConstValue::Null, _) => FieldValue::NULL, - (_, Some(_)) => bail!("Failed to match type_name"), - }) + } } fn to_type(def: &Definition) -> dynamic::Type { @@ -131,7 +124,7 @@ fn to_type(def: &Definition) -> dynamic::Type { if let ConstValue::Null = value { Ok(FieldValue::NONE) } else { - Ok(Some(to_field_value(ctx, value)?)) + Ok(Some(to_field_value(value))) } } .instrument(span) diff --git a/src/core/config/config.rs b/src/core/config/config.rs index 702715493a..cb22208d47 100644 --- a/src/core/config/config.rs +++ b/src/core/config/config.rs @@ -1,4 +1,4 @@ -use std::collections::{BTreeMap, BTreeSet, HashSet}; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::fmt::{self, Display}; use std::num::NonZeroU64; @@ -6,6 +6,7 @@ use anyhow::Result; use async_graphql::parser::types::{ConstDirective, ServiceDocument}; use async_graphql::Positioned; use derive_setters::Setters; +use indexmap::IndexMap; use serde::{Deserialize, Serialize}; use serde_json::Value; use tailcall_macros::{CustomResolver, DirectiveDefinition, InputDefinition}; @@ -254,7 +255,8 @@ pub struct Field { /// /// Map of argument name and its definition. #[serde(default, skip_serializing_if = "is_default")] - pub args: BTreeMap, + #[schemars(with = "HashMap::")] + pub args: IndexMap, /// /// Publicly visible documentation for the field. diff --git a/src/core/config/from_document.rs b/src/core/config/from_document.rs index da6a94847e..5835a79542 100644 --- a/src/core/config/from_document.rs +++ b/src/core/config/from_document.rs @@ -8,6 +8,7 @@ use async_graphql::parser::types::{ use async_graphql::parser::Positioned; use async_graphql::Name; use async_graphql_value::ConstValue; +use indexmap::IndexMap; use super::telemetry::Telemetry; use super::Alias; @@ -294,7 +295,7 @@ fn to_field(field_definition: &FieldDefinition) -> Valid fn to_input_object_field(field_definition: &InputValueDefinition) -> Valid { to_common_field( field_definition, - BTreeMap::new(), + IndexMap::new(), field_definition .default_value .as_ref() @@ -303,7 +304,7 @@ fn to_input_object_field(field_definition: &InputValueDefinition) -> Valid( field: &F, - args: BTreeMap, + args: IndexMap, default_value: Option, ) -> Valid where @@ -356,8 +357,8 @@ fn to_type_of(type_: &Type) -> String { BaseType::List(ty) => to_type_of(ty), } } -fn to_args(field_definition: &FieldDefinition) -> BTreeMap { - let mut args: BTreeMap = BTreeMap::new(); +fn to_args(field_definition: &FieldDefinition) -> IndexMap { + let mut args = IndexMap::new(); for arg in field_definition.arguments.iter() { let arg_name = pos_name_to_string(&arg.node.name); diff --git a/src/core/config/transformer/snapshots/tailcall__core__config__transformer__union_input_type__tests__union_in_type.snap b/src/core/config/transformer/snapshots/tailcall__core__config__transformer__union_input_type__tests__union_in_type.snap index a18578c838..78dd4c5006 100644 --- a/src/core/config/transformer/snapshots/tailcall__core__config__transformer__union_input_type__tests__union_in_type.snap +++ b/src/core/config/transformer/snapshots/tailcall__core__config__transformer__union_input_type__tests__union_in_type.snap @@ -66,13 +66,13 @@ type NU { } type Query { - testVar0Var0(nnu: NNU__nu0, nu: NU__u0!): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") - testVar0Var1(nnu: NNU__nu0, nu: NU__u1!): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") - testVar0Var2(nnu: NNU__nu0, nu: NU__u2!): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") - testVar1Var0(nnu: NNU__nu1, nu: NU__u0!): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") - testVar1Var1(nnu: NNU__nu1, nu: NU__u1!): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") - testVar1Var2(nnu: NNU__nu1, nu: NU__u2!): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") - testVar2Var0(nnu: NNU__nu2, nu: NU__u0!): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") - testVar2Var1(nnu: NNU__nu2, nu: NU__u1!): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") - testVar2Var2(nnu: NNU__nu2, nu: NU__u2!): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") + testVar0Var0(nu: NU__u0!, nnu: NNU__nu0): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") + testVar0Var1(nu: NU__u0!, nnu: NNU__nu1): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") + testVar0Var2(nu: NU__u0!, nnu: NNU__nu2): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") + testVar1Var0(nu: NU__u1!, nnu: NNU__nu0): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") + testVar1Var1(nu: NU__u1!, nnu: NNU__nu1): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") + testVar1Var2(nu: NU__u1!, nnu: NNU__nu2): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") + testVar2Var0(nu: NU__u2!, nnu: NNU__nu0): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") + testVar2Var1(nu: NU__u2!, nnu: NNU__nu1): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") + testVar2Var2(nu: NU__u2!, nnu: NNU__nu2): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") } diff --git a/src/core/generator/tests/snapshots/json_to_config_spec__add_cart.json.snap b/src/core/generator/tests/snapshots/json_to_config_spec__add_cart.json.snap index 5360dbccf8..7811cf43a2 100644 --- a/src/core/generator/tests/snapshots/json_to_config_spec__add_cart.json.snap +++ b/src/core/generator/tests/snapshots/json_to_config_spec__add_cart.json.snap @@ -17,7 +17,7 @@ input T4 { } type Mutation { - addCart(addCartInput: T4, code: String): T2 @http(baseURL: "https://dummyjson.com", body: "{{.args.addCartInput}}", method: "POST", path: "/carts/add", query: [{key: "code", value: "{{.args.code}}"}]) + addCart(code: String, addCartInput: T4): T2 @http(baseURL: "https://dummyjson.com", body: "{{.args.addCartInput}}", method: "POST", path: "/carts/add", query: [{key: "code", value: "{{.args.code}}"}]) } type T1 { diff --git a/src/core/ir/discriminator.rs b/src/core/ir/discriminator.rs index b8907c70c8..bfd7511392 100644 --- a/src/core/ir/discriminator.rs +++ b/src/core/ir/discriminator.rs @@ -8,17 +8,40 @@ use indenter::indented; use indexmap::IndexMap; use crate::core::config::Type; +use crate::core::json::{JsonLike, JsonObjectLike}; use crate::core::valid::{Cause, Valid, Validator}; -/// Represents the type name for the resolved value. -/// It is used when the GraphQL executor needs to resolve values of a union -/// type. In order to select the correct fields, the executor must know the -/// exact type name for each resolved value. When the output is a list of a -/// union type, it should resolve the exact type for every entry in the list. -#[derive(PartialEq, Eq, Debug, Clone)] -pub enum TypeName { - Single(String), - Vec(Vec), +pub trait TypedValue<'a> { + type Error; + + fn get_type_name(&'a self) -> Option<&'a str>; + fn set_type_name(&'a mut self, type_name: String) -> Result<(), Self::Error>; +} + +const TYPENAME_FIELD: &str = "__typename"; + +impl<'json, T> TypedValue<'json> for T +where + T: JsonLike<'json>, + T::JsonObject<'json>: JsonObjectLike<'json, Value = T>, +{ + type Error = anyhow::Error; + + fn get_type_name(&'json self) -> Option<&'json str> { + self.as_object() + .and_then(|obj| obj.get_key(TYPENAME_FIELD)) + .and_then(|val| val.as_str()) + } + + fn set_type_name(&'json mut self, type_name: String) -> Result<(), Self::Error> { + if let Some(obj) = self.as_object_mut() { + obj.insert_key(TYPENAME_FIELD, T::string(type_name.into())); + + Ok(()) + } else { + bail!("Expected object") + } + } } /// Resolver for type member of a union. @@ -214,22 +237,7 @@ impl Discriminator { Valid::succeed(discriminator) } - pub fn resolve_type(&self, value: &Value) -> Result { - if let Value::List(list) = value { - let results: Result> = list - .iter() - .map(|item| Ok(self.resolve_type_for_single(item)?.to_string())) - .collect(); - - Ok(TypeName::Vec(results?)) - } else { - Ok(TypeName::Single( - self.resolve_type_for_single(value)?.to_string(), - )) - } - } - - fn resolve_type_for_single(&self, value: &Value) -> Result<&str> { + pub fn resolve_type(&self, value: &Value) -> Result<&str> { let Value::Object(obj) = value else { bail!("Value expected to be object"); }; @@ -346,7 +354,6 @@ mod tests { use super::Discriminator; use crate::core::config::{Field, Type}; - use crate::core::ir::discriminator::TypeName; use crate::core::valid::Validator; #[test] @@ -361,14 +368,14 @@ mod tests { discriminator .resolve_type(&Value::from_json(json!({ "foo": "test" })).unwrap()) .unwrap(), - TypeName::Single("Foo".to_string()) + "Foo" ); assert_eq!( discriminator .resolve_type(&Value::from_json(json!({ "bar": "test" })).unwrap()) .unwrap(), - TypeName::Single("Bar".to_string()) + "Bar" ); // ambiguous cases @@ -376,21 +383,21 @@ mod tests { discriminator .resolve_type(&Value::from_json(json!({ "foo": "test", "bar": "test" })).unwrap()) .unwrap(), - TypeName::Single("Foo".to_string()) + "Foo" ); assert_eq!( discriminator .resolve_type(&Value::from_json(json!({})).unwrap()) .unwrap(), - TypeName::Single("Foo".to_string()) + "Foo" ); assert_eq!( discriminator .resolve_type(&Value::from_json(json!({ "unknown": { "foo": "bar" }})).unwrap()) .unwrap(), - TypeName::Single("Foo".to_string()) + "Foo" ); } @@ -408,14 +415,14 @@ mod tests { discriminator .resolve_type(&Value::from_json(json!({ "foo": "test" })).unwrap()) .unwrap(), - TypeName::Single("Foo".to_string()) + "Foo" ); assert_eq!( discriminator .resolve_type(&Value::from_json(json!({ "bar": "test" })).unwrap()) .unwrap(), - TypeName::Single("Bar".to_string()) + "Bar" ); // ambiguous cases @@ -423,21 +430,21 @@ mod tests { discriminator .resolve_type(&Value::from_json(json!({ "foo": "test", "bar": "test" })).unwrap()) .unwrap(), - TypeName::Single("Foo".to_string()) + "Foo" ); assert_eq!( discriminator .resolve_type(&Value::from_json(json!({})).unwrap()) .unwrap(), - TypeName::Single("Bar".to_string()) + "Bar" ); assert_eq!( discriminator .resolve_type(&Value::from_json(json!({ "unknown": { "foo": "bar" }})).unwrap()) .unwrap(), - TypeName::Single("Bar".to_string()) + "Bar" ); } @@ -466,21 +473,21 @@ mod tests { discriminator .resolve_type(&Value::from_json(json!({ "a": 1, "ab": 1, "abab": 1 })).unwrap()) .unwrap(), - TypeName::Single("A".to_string()) + "A" ); assert_eq!( discriminator .resolve_type(&Value::from_json(json!({ "b": 1, "ab": 1, "abab": 1 })).unwrap()) .unwrap(), - TypeName::Single("B".to_string()) + "B" ); assert_eq!( discriminator .resolve_type(&Value::from_json(json!({ "c": 1, "ac": 1 })).unwrap()) .unwrap(), - TypeName::Single("C".to_string()) + "C" ); // ambiguous cases @@ -488,21 +495,21 @@ mod tests { discriminator .resolve_type(&Value::from_json(json!({ "a": 1, "b": 1, "c": 1 })).unwrap()) .unwrap(), - TypeName::Single("A".to_string()) + "A" ); assert_eq!( discriminator .resolve_type(&Value::from_json(json!({})).unwrap()) .unwrap(), - TypeName::Single("C".to_string()) + "C" ); assert_eq!( discriminator .resolve_type(&Value::from_json(json!({ "unknown": { "foo": "bar" }})).unwrap()) .unwrap(), - TypeName::Single("C".to_string()) + "C" ); } @@ -528,14 +535,14 @@ mod tests { &Value::from_json(json!({ "a": 123, "b": true, "foo": "test" })).unwrap() ) .unwrap(), - TypeName::Single("Foo".to_string()) + "Foo" ); assert_eq!( discriminator .resolve_type(&Value::from_json(json!({ "bar": "test" })).unwrap()) .unwrap(), - TypeName::Single("Bar".to_string()) + "Bar" ); // ambiguous cases @@ -543,21 +550,21 @@ mod tests { discriminator .resolve_type(&Value::from_json(json!({ "foo": "test", "bar": "test" })).unwrap()) .unwrap(), - TypeName::Single("Foo".to_string()) + "Foo" ); assert_eq!( discriminator .resolve_type(&Value::from_json(json!({})).unwrap()) .unwrap(), - TypeName::Single("Foo".to_string()) + "Foo" ); assert_eq!( discriminator .resolve_type(&Value::from_json(json!({ "unknown": { "foo": "bar" }})).unwrap()) .unwrap(), - TypeName::Single("Foo".to_string()) + "Foo" ); // ambiguous cases @@ -565,21 +572,21 @@ mod tests { discriminator .resolve_type(&Value::from_json(json!({ "foo": "test", "bar": "test" })).unwrap()) .unwrap(), - TypeName::Single("Foo".to_string()) + "Foo" ); assert_eq!( discriminator .resolve_type(&Value::from_json(json!({})).unwrap()) .unwrap(), - TypeName::Single("Foo".to_string()) + "Foo" ); assert_eq!( discriminator .resolve_type(&Value::from_json(json!({ "unknown": { "foo": "bar" }})).unwrap()) .unwrap(), - TypeName::Single("Foo".to_string()) + "Foo" ); } @@ -599,14 +606,14 @@ mod tests { discriminator .resolve_type(&Value::from_json(json!({ "b": 123, "foo": "test" })).unwrap()) .unwrap(), - TypeName::Single("Foo".to_string()) + "Foo" ); assert_eq!( discriminator .resolve_type(&Value::from_json(json!({ "bar": "test" })).unwrap()) .unwrap(), - TypeName::Single("Bar".to_string()) + "Bar" ); assert_eq!( @@ -615,7 +622,7 @@ mod tests { &Value::from_json(json!({ "unknown": { "foo": "bar" }, "a": 1 })).unwrap() ) .unwrap(), - TypeName::Single("Foo".to_string()) + "Foo" ); // ambiguous cases @@ -623,21 +630,21 @@ mod tests { discriminator .resolve_type(&Value::from_json(json!({ "foo": "test", "bar": "test" })).unwrap()) .unwrap(), - TypeName::Single("Foo".to_string()) + "Foo" ); assert_eq!( discriminator .resolve_type(&Value::from_json(json!({})).unwrap()) .unwrap(), - TypeName::Single("Foo".to_string()) + "Foo" ); assert_eq!( discriminator .resolve_type(&Value::from_json(json!({ "unknown": { "foo": "bar" }})).unwrap()) .unwrap(), - TypeName::Single("Foo".to_string()) + "Foo" ); } @@ -667,21 +674,21 @@ mod tests { discriminator .resolve_type(&Value::from_json(json!({ "a": 1 })).unwrap()) .unwrap(), - TypeName::Single("A".to_string()) + "A" ); assert_eq!( discriminator .resolve_type(&Value::from_json(json!({ "b": 1, "aa": 1 })).unwrap()) .unwrap(), - TypeName::Single("B".to_string()) + "B" ); assert_eq!( discriminator .resolve_type(&Value::from_json(json!({ "c": 1, "aaa": 1 })).unwrap()) .unwrap(), - TypeName::Single("C".to_string()) + "C" ); // ambiguous cases @@ -691,21 +698,21 @@ mod tests { &Value::from_json(json!({ "shared": 1, "a": 1, "b": 1, "c": 1 })).unwrap() ) .unwrap(), - TypeName::Single("A".to_string()) + "A" ); assert_eq!( discriminator .resolve_type(&Value::from_json(json!({})).unwrap()) .unwrap(), - TypeName::Single("A".to_string()) + "A" ); assert_eq!( discriminator .resolve_type(&Value::from_json(json!({ "unknown": { "foo": "bar" }})).unwrap()) .unwrap(), - TypeName::Single("A".to_string()) + "A" ); } @@ -766,14 +773,14 @@ mod tests { discriminator .resolve_type(&Value::from_json(json!({ "usual": 1 })).unwrap()) .unwrap(), - TypeName::Single("Var_Var".to_string()) + "Var_Var" ); assert_eq!( discriminator .resolve_type(&Value::from_json(json!({ "usual": 1, "payload": 1 })).unwrap()) .unwrap(), - TypeName::Single("Var0_Var".to_string()) + "Var0_Var" ); assert_eq!( @@ -782,14 +789,14 @@ mod tests { &Value::from_json(json!({ "usual": 1, "command": 2, "useless": 1 })).unwrap() ) .unwrap(), - TypeName::Single("Var1_Var".to_string()) + "Var1_Var" ); assert_eq!( discriminator .resolve_type(&Value::from_json(json!({ "usual": 1, "flag": true })).unwrap()) .unwrap(), - TypeName::Single("Var_Var0".to_string()) + "Var_Var0" ); assert_eq!( @@ -799,7 +806,7 @@ mod tests { .unwrap() ) .unwrap(), - TypeName::Single("Var_Var1".to_string()) + "Var_Var1" ); assert_eq!( @@ -808,7 +815,7 @@ mod tests { &Value::from_json(json!({ "usual": 1, "payload": 1, "flag": true })).unwrap() ) .unwrap(), - TypeName::Single("Var0_Var0".to_string()) + "Var0_Var0" ); assert_eq!( @@ -818,7 +825,7 @@ mod tests { .unwrap() ) .unwrap(), - TypeName::Single("Var0_Var1".to_string()) + "Var0_Var1" ); assert_eq!( @@ -827,7 +834,7 @@ mod tests { &Value::from_json(json!({ "usual": 1, "command": 1, "flag": true })).unwrap() ) .unwrap(), - TypeName::Single("Var1_Var0".to_string()) + "Var1_Var0" ); assert_eq!( @@ -837,7 +844,7 @@ mod tests { .unwrap() ) .unwrap(), - TypeName::Single("Var1_Var1".to_string()) + "Var1_Var1" ); // ambiguous cases @@ -855,14 +862,14 @@ mod tests { discriminator .resolve_type(&Value::from_json(json!({})).unwrap()) .unwrap(), - TypeName::Single("Var_Var".to_string()) + "Var_Var" ); assert_eq!( discriminator .resolve_type(&Value::from_json(json!({ "unknown": { "foo": "bar" }})).unwrap()) .unwrap(), - TypeName::Single("Var_Var".to_string()) + "Var_Var" ); } @@ -901,14 +908,14 @@ mod tests { &Value::from_json(json!({ "uniqueA1": "value", "common": 1 })).unwrap() ) .unwrap(), - TypeName::Single("TypeA".to_string()) + "TypeA" ); assert_eq!( discriminator .resolve_type(&Value::from_json(json!({ "uniqueB1": true, "common": 2 })).unwrap()) .unwrap(), - TypeName::Single("TypeB".to_string()) + "TypeB" ); assert_eq!( @@ -918,7 +925,7 @@ mod tests { .unwrap() ) .unwrap(), - TypeName::Single("TypeC".to_string()) + "TypeC" ); assert_eq!( @@ -930,7 +937,7 @@ mod tests { .unwrap() ) .unwrap(), - TypeName::Single("TypeD".to_string()) + "TypeD" ); // ambiguous cases @@ -943,21 +950,21 @@ mod tests { .unwrap() ) .unwrap(), - TypeName::Single("TypeA".to_string()) + "TypeA" ); assert_eq!( discriminator .resolve_type(&Value::from_json(json!({})).unwrap()) .unwrap(), - TypeName::Single("TypeA".to_string()) + "TypeA" ); assert_eq!( discriminator .resolve_type(&Value::from_json(json!({ "unknown": { "foo": "bar" }})).unwrap()) .unwrap(), - TypeName::Single("TypeA".to_string()) + "TypeA" ); } @@ -996,7 +1003,7 @@ mod tests { &Value::from_json(json!({ "field1": "value", "field2": "value" })).unwrap() ) .unwrap(), - TypeName::Single("TypeA".to_string()) + "TypeA" ); assert_eq!( @@ -1005,7 +1012,7 @@ mod tests { &Value::from_json(json!({ "field2": "value", "field3": "value" })).unwrap() ) .unwrap(), - TypeName::Single("TypeB".to_string()) + "TypeB" ); assert_eq!( @@ -1014,7 +1021,7 @@ mod tests { &Value::from_json(json!({ "field1": "value", "field3": "value" })).unwrap() ) .unwrap(), - TypeName::Single("TypeC".to_string()) + "TypeC" ); assert_eq!( @@ -1026,7 +1033,7 @@ mod tests { .unwrap() ) .unwrap(), - TypeName::Single("TypeD".to_string()) + "TypeD" ); // ambiguous cases @@ -1047,14 +1054,14 @@ mod tests { discriminator .resolve_type(&Value::from_json(json!({})).unwrap()) .unwrap(), - TypeName::Single("TypeA".to_string()) + "TypeA" ); assert_eq!( discriminator .resolve_type(&Value::from_json(json!({ "unknown": { "foo": "bar" }})).unwrap()) .unwrap(), - TypeName::Single("TypeA".to_string()) + "TypeA" ); } diff --git a/src/core/ir/eval.rs b/src/core/ir/eval.rs index 595cb57dbd..6bbf8871bd 100644 --- a/src/core/ir/eval.rs +++ b/src/core/ir/eval.rs @@ -5,8 +5,8 @@ use async_graphql_value::ConstValue; use super::eval_io::eval_io; use super::model::{Cache, CacheKey, Map, IR}; -use super::{Error, EvalContext, ResolverContextLike}; -use crate::core::json::JsonLike; +use super::{Error, EvalContext, ResolverContextLike, TypedValue}; +use crate::core::json::{JsonLike, JsonLikeList}; use crate::core::serde_value_ext::ValueExt; // Fake trait to capture proper lifetimes. @@ -89,9 +89,13 @@ impl IR { second.eval(ctx).await } IR::Discriminate(discriminator, expr) => expr.eval(ctx).await.and_then(|value| { - let type_name = discriminator.resolve_type(&value)?; + let value = value.map(|mut value| { + let type_name = discriminator.resolve_type(&value)?; - ctx.set_type_name(type_name); + value.set_type_name(type_name.to_string())?; + + anyhow::Ok(value) + })?; Ok(value) }), diff --git a/src/core/ir/eval_context.rs b/src/core/ir/eval_context.rs index a1afbcf26e..597335c00e 100644 --- a/src/core/ir/eval_context.rs +++ b/src/core/ir/eval_context.rs @@ -5,7 +5,6 @@ use std::sync::Arc; use async_graphql::{ServerError, Value}; use reqwest::header::HeaderMap; -use super::discriminator::TypeName; use super::{GraphQLOperationContext, RelatedFields, ResolverContextLike, SelectionField}; use crate::core::document::print_directives; use crate::core::http::RequestContext; @@ -25,12 +24,6 @@ pub struct EvalContext<'a, Ctx: ResolverContextLike> { // Overridden Arguments for Async GraphQL Context graphql_ctx_args: Option>, - - /// Type name of resolved data that is calculated - /// dynamically based on the shape of the value itself. - /// Required for proper Union type resolutions. - /// More details at [TypeName] - pub type_name: Option, } impl<'a, Ctx: ResolverContextLike> EvalContext<'a, Ctx> { @@ -56,7 +49,6 @@ impl<'a, Ctx: ResolverContextLike> EvalContext<'a, Ctx> { graphql_ctx, graphql_ctx_value: None, graphql_ctx_args: None, - type_name: None, } } @@ -114,10 +106,6 @@ impl<'a, Ctx: ResolverContextLike> EvalContext<'a, Ctx> { pub fn add_error(&self, error: ServerError) { self.graphql_ctx.add_error(error) } - - pub fn set_type_name(&mut self, type_name: TypeName) { - self.type_name = Some(type_name); - } } impl<'a, Ctx: ResolverContextLike> GraphQLOperationContext for EvalContext<'a, Ctx> { diff --git a/src/core/ir/resolver_context_like.rs b/src/core/ir/resolver_context_like.rs index 6733a43875..190f91e5b2 100644 --- a/src/core/ir/resolver_context_like.rs +++ b/src/core/ir/resolver_context_like.rs @@ -95,7 +95,7 @@ impl SelectionField { fn from_jit_field( field: &crate::core::jit::Field, ConstValue>, ) -> SelectionField { - let name = field.name.clone(); + let name = field.output_name.to_string(); let selection_set = field .nested_iter(field.type_of.name()) .map(Self::from_jit_field) diff --git a/src/core/jit/builder.rs b/src/core/jit/builder.rs index 16a85e0ae6..0d9a0c88ea 100644 --- a/src/core/jit/builder.rs +++ b/src/core/jit/builder.rs @@ -206,18 +206,18 @@ impl Builder { Some(Flat::new(id.clone())), fragments, ); - let name = gql_field - .alias - .as_ref() - .map(|alias| alias.node.to_string()) - .unwrap_or(field_name.to_string()); let ir = match field_def { QueryField::Field((field_def, _)) => field_def.resolver.clone(), _ => None, }; let flat_field = Field { id, - name, + name: field_name.to_string(), + output_name: gql_field + .alias + .as_ref() + .map(|a| a.node.to_string()) + .unwrap_or(field_name.to_owned()), ir, type_of, type_condition: type_condition.to_string(), @@ -231,8 +231,26 @@ impl Builder { fields.push(flat_field); fields = fields.merge_right(child_fields); - } else { - // TODO: error if the field is not found in the schema + } else if field_name == "__typename" { + let flat_field = Field { + id: FieldId::new(self.field_id.next()), + name: field_name.to_string(), + output_name: field_name.to_string(), + ir: None, + type_of: crate::core::blueprint::Type::NamedType { + name: "String".to_owned(), + non_null: true, + }, + type_condition: type_condition.to_string(), + skip, + include, + args: Vec::new(), + pos: selection.pos.into(), + extensions: exts.clone(), + directives, + }; + + fields.push(flat_field); } } Selection::FragmentSpread(Positioned { node: fragment_spread, .. }) => { @@ -404,6 +422,21 @@ mod tests { insta::assert_debug_snapshot!(plan.into_nested()); } + #[test] + fn test_alias_query() { + let plan = plan( + r#" + query { + articles: posts { author: user { identifier: id } } + } + "#, + &Variables::new(), + ); + + assert!(plan.is_query()); + insta::assert_debug_snapshot!(plan.into_nested()); + } + #[test] fn test_simple_mutation() { let plan = plan( diff --git a/src/core/jit/common/jp.rs b/src/core/jit/common/jp.rs index de31dc9924..521ecdfc8c 100644 --- a/src/core/jit/common/jp.rs +++ b/src/core/jit/common/jp.rs @@ -5,7 +5,6 @@ use serde::Deserialize; use crate::core::blueprint::Blueprint; use crate::core::config::{Config, ConfigModule}; use crate::core::jit::builder::Builder; -use crate::core::jit::exec::TypedValue; use crate::core::jit::store::{Data, Store}; use crate::core::jit::synth::Synth; use crate::core::jit::{self, OperationPlan, Positioned, Variables}; @@ -26,7 +25,7 @@ struct TestData { users: Vec, } -type Entry = Data, Positioned>>; +type Entry = Data>>; struct ProcessedTestData { posts: Value, @@ -75,7 +74,6 @@ impl<'a, Value: JsonLike<'a> + Deserialize<'a> + Clone + 'a> TestData { Value::null() } }) - .map(TypedValue::new) .map(Ok) .map(Data::Single) .enumerate() @@ -130,7 +128,7 @@ impl< .to_owned(); let store = [ - (posts_id, Data::Single(Ok(TypedValue::new(posts)))), + (posts_id, Data::Single(Ok(posts))), (users_id, Data::Multiple(users)), ] .into_iter() diff --git a/src/core/jit/context.rs b/src/core/jit/context.rs index d5ff29cd13..f52b34223b 100644 --- a/src/core/jit/context.rs +++ b/src/core/jit/context.rs @@ -2,6 +2,7 @@ use std::sync::{Arc, Mutex, MutexGuard}; use async_graphql::{Name, ServerError}; use async_graphql_value::ConstValue; +use indexmap::IndexMap; use super::error::*; use super::{Field, Nested, OperationPlan, Positioned}; @@ -39,8 +40,8 @@ pub struct Context<'a, Input, Output> { request: &'a RequestContext, } impl<'a, Input: Clone, Output> Context<'a, Input, Output> { - pub fn new(field: &'a Field, Input>, env: &'a RequestContext) -> Self { - Self { value: None, args: None, field, request: env } + pub fn new(field: &'a Field, Input>, request: &'a RequestContext) -> Self { + Self { request, value: None, args: Self::build_args(field), field } } pub fn with_value_and_field( @@ -48,19 +49,11 @@ impl<'a, Input: Clone, Output> Context<'a, Input, Output> { value: &'a Output, field: &'a Field, Input>, ) -> Self { - Self { args: None, value: Some(value), field, request: self.request } - } - - pub fn with_args(&self, args: indexmap::IndexMap<&str, Input>) -> Self { - let mut map = indexmap::IndexMap::new(); - for (key, value) in args { - map.insert(Name::new(key), value); - } Self { - value: self.value, - args: Some(map), - field: self.field, request: self.request, + args: Self::build_args(field), + value: Some(value), + field, } } @@ -71,6 +64,26 @@ impl<'a, Input: Clone, Output> Context<'a, Input, Output> { pub fn field(&self) -> &Field, Input> { self.field } + + fn build_args(field: &Field, Input>) -> Option> { + let mut arg_map = IndexMap::new(); + + for arg in field.args.iter() { + let name = arg.name.as_str(); + let value = arg + .value + .clone() + // TODO: default value resolution should happen in the InputResolver + .or_else(|| arg.default_value.clone()); + if let Some(value) = value { + arg_map.insert(Name::new(name), value); + } else if !arg.type_of.is_nullable() { + // TODO: throw error here + todo!() + } + } + Some(arg_map) + } } impl<'a> ResolverContextLike for Context<'a, ConstValue, ConstValue> { diff --git a/src/core/jit/error.rs b/src/core/jit/error.rs index 5bef9c43c6..b47380097c 100644 --- a/src/core/jit/error.rs +++ b/src/core/jit/error.rs @@ -27,9 +27,7 @@ pub enum ValidationError { // TODO: replace with sane error message. Right now, it's defined as is only for compatibility // with async_graphql error message for this case #[error(r#"internal: invalid value for scalar "{type_of}", expected "FieldValue::Value""#)] - ScalarInvalid { type_of: String, path: String }, - #[error("TypeName shape doesn't satisfy the processed object")] - TypeNameMismatch, + ScalarInvalid { type_of: String }, #[error(r#"internal: invalid item for enum "{type_of}""#)] EnumInvalid { type_of: String }, #[error("internal: non-null types require a return value")] diff --git a/src/core/jit/exec.rs b/src/core/jit/exec.rs index 6b4f855151..aa378f08fd 100644 --- a/src/core/jit/exec.rs +++ b/src/core/jit/exec.rs @@ -8,12 +8,12 @@ use futures_util::future::join_all; use super::context::{Context, RequestContext}; use super::{DataPath, OperationPlan, Positioned, Response, Store}; use crate::core::ir::model::IR; -use crate::core::ir::TypeName; +use crate::core::ir::TypedValue; use crate::core::jit; use crate::core::jit::synth::Synth; use crate::core::json::{JsonLike, JsonObjectLike}; -type SharedStore = Arc, Positioned>>>>; +type SharedStore = Arc>>>>; /// /// Default GraphQL executor that takes in a GraphQL Request and produces a @@ -31,10 +31,10 @@ where Exec: IRExecutor, { pub fn new(plan: OperationPlan, exec: Exec) -> Self { - Self { exec, ctx: RequestContext::new(plan) } + Self { exec, ctx: RequestContext::new(plan.clone()) } } - pub async fn store(&self) -> Store, Positioned>> { + pub async fn store(&self) -> Store>> { let store = Arc::new(Mutex::new(Store::new())); let mut ctx = ExecutorInner::new(store.clone(), &self.exec, &self.ctx); ctx.init().await; @@ -59,7 +59,7 @@ struct ExecutorInner<'a, Input, Output, Error, Exec> { impl<'a, Input, Output, Error, Exec> ExecutorInner<'a, Input, Output, Error, Exec> where - Output: for<'i> JsonLike<'i> + Debug, + for<'i> Output: JsonLike<'i> + TypedValue<'i> + Debug, Input: Clone + Debug, Exec: IRExecutor, { @@ -73,25 +73,9 @@ where async fn init(&mut self) { join_all(self.request.plan().as_nested().iter().map(|field| async { - let mut arg_map = indexmap::IndexMap::new(); - for arg in field.args.iter() { - let name = arg.name.as_str(); - let value: Option = arg - .value - .clone() - // TODO: default value resolution should happen in the InputResolver - .or_else(|| arg.default_value.clone()); - - if let Some(value) = value { - arg_map.insert(name, value); - } else if !arg.type_of.is_nullable() { - // TODO: throw error here - todo!() - } - } + let ctx = Context::new(field, self.request); // TODO: with_args should be called on inside iter_field on any level, not only // for root fields - let ctx = Context::new(field, self.request).with_args(arg_map); self.execute(&ctx, DataPath::new()).await })) .await; @@ -101,22 +85,17 @@ where &'b self, ctx: &'b Context<'b, Input, Output>, data_path: &DataPath, - result: TypedValueRef<'b, Output>, + value: &'b Output, ) -> Result<(), Error> { let field = ctx.field(); - let TypedValueRef { value, type_name } = result; // Array // Check if the field expects a list if field.type_of.is_list() { // Check if the value is an array if let Some(array) = value.as_array() { join_all(array.iter().enumerate().map(|(index, value)| { - let type_name = match &type_name { - Some(TypeName::Single(type_name)) => type_name, /* TODO: should throw */ - // ValidationError - Some(TypeName::Vec(v)) => &v[index], - None => field.type_of.name(), - }; + let type_name = value.get_type_name().unwrap_or(field.type_of.name()); + join_all(field.nested_iter(type_name).map(|field| { let ctx = ctx.with_value_and_field(value, field); let data_path = data_path.clone().with_index(index); @@ -132,11 +111,7 @@ where // TODO: Validate if the value is an Object // Has to be an Object, we don't do anything while executing if its a Scalar else { - let type_name = match &type_name { - Some(TypeName::Single(type_name)) => type_name, - Some(TypeName::Vec(_)) => panic!("TypeName type mismatch"), /* TODO: should throw ValidationError */ - None => field.type_of.name(), - }; + let type_name = value.get_type_name().unwrap_or(field.type_of.name()); join_all(field.nested_iter(type_name).map(|child| { let ctx = ctx.with_value_and_field(value, child); @@ -159,8 +134,8 @@ where if let Some(ir) = &field.ir { let result = self.ir_exec.execute(ir, ctx).await; - if let Ok(ref result) = result { - self.iter_field(ctx, &data_path, result.as_ref()).await?; + if let Ok(value) = &result { + self.iter_field(ctx, &data_path, value).await?; } let mut store = self.store.lock().unwrap(); @@ -176,7 +151,7 @@ where let default_obj = Output::object(Output::JsonObject::new()); let value = ctx .value() - .and_then(|v| v.get_key(&field.name)) + .and_then(|v| v.get_key(&field.output_name)) // in case there is no value we still put some dumb empty value anyway // to force execution of the nested fields even when parent object is not present. // For async_graphql it's done by `fix_dangling_resolvers` fn that basically creates @@ -185,49 +160,13 @@ where // here without doing the "fix" .unwrap_or(&default_obj); - let result = TypedValueRef { value, type_name: None }; - - self.iter_field(ctx, &data_path, result).await?; + self.iter_field(ctx, &data_path, value).await?; } Ok(()) } } -#[derive(Clone)] -pub struct TypedValue { - pub value: V, - pub type_name: Option, -} - -pub struct TypedValueRef<'a, V> { - pub value: &'a V, - pub type_name: Option<&'a TypeName>, -} - -impl TypedValue { - pub fn new(value: V) -> Self { - Self { value, type_name: None } - } - - pub fn as_ref(&self) -> TypedValueRef<'_, V> { - TypedValueRef { value: &self.value, type_name: self.type_name.as_ref() } - } -} - -impl<'a, V> TypedValueRef<'a, V> { - pub fn new(value: &'a V) -> Self { - Self { value, type_name: None } - } - - pub fn map<'out, U>(&self, map: impl FnOnce(&V) -> &'out U) -> TypedValueRef<'out, U> - where - 'a: 'out, - { - TypedValueRef { value: map(self.value), type_name: self.type_name } - } -} - /// Executor for IR pub trait IRExecutor { type Input; @@ -237,5 +176,5 @@ pub trait IRExecutor { &'a self, ir: &'a IR, ctx: &'a Context<'a, Self::Input, Self::Output>, - ) -> Result, Self::Error>; + ) -> Result; } diff --git a/src/core/jit/exec_const.rs b/src/core/jit/exec_const.rs index 45f530d367..df6f90f64f 100644 --- a/src/core/jit/exec_const.rs +++ b/src/core/jit/exec_const.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use async_graphql_value::ConstValue; use super::context::Context; -use super::exec::{Executor, IRExecutor, TypedValue}; +use super::exec::{Executor, IRExecutor}; use super::{Error, OperationPlan, Request, Response, Result}; use crate::core::app_context::AppContext; use crate::core::http::RequestContext; @@ -56,13 +56,10 @@ impl<'ctx> IRExecutor for ConstValueExec<'ctx> { &'a self, ir: &'a IR, ctx: &'a Context<'a, Self::Input, Self::Output>, - ) -> Result> { + ) -> Result { let req_context = &self.req_context; let mut eval_ctx = EvalContext::new(req_context, ctx); - Ok(ir - .eval(&mut eval_ctx) - .await - .map(|value| TypedValue { value, type_name: eval_ctx.type_name.take() })?) + Ok(ir.eval(&mut eval_ctx).await?) } } diff --git a/src/core/jit/model.rs b/src/core/jit/model.rs index a2d8a3e8cf..2fe51567fa 100644 --- a/src/core/jit/model.rs +++ b/src/core/jit/model.rs @@ -108,7 +108,11 @@ impl FieldId { #[derive(Clone)] pub struct Field { pub id: FieldId, + /// Name of key in the value object for this field pub name: String, + /// Output name (i.e. with alias) that should be used for the result value + /// of this field + pub output_name: String, pub ir: Option, pub type_of: crate::core::blueprint::Type, /// Specifies the name of type used in condition to fetch that field @@ -158,6 +162,7 @@ impl Field, Input> { Ok(Field { id: self.id, name: self.name, + output_name: self.output_name, ir: self.ir, type_of: self.type_of, type_condition: self.type_condition, @@ -187,6 +192,7 @@ impl Field { Ok(Field { id: self.id, name: self.name, + output_name: self.output_name, ir: self.ir, type_of: self.type_of, type_condition: self.type_condition, @@ -262,6 +268,7 @@ impl Field { Field { id: self.id, name: self.name, + output_name: self.output_name, ir: self.ir, type_of: self.type_of, type_condition: self.type_condition, @@ -280,6 +287,7 @@ impl Debug for Field { let mut debug_struct = f.debug_struct("Field"); debug_struct.field("id", &self.id); debug_struct.field("name", &self.name); + debug_struct.field("output_name", &self.output_name); if self.ir.is_some() { debug_struct.field("ir", &"Some(..)"); } @@ -298,6 +306,7 @@ impl Debug for Field { debug_struct.field("include", &self.include); } debug_struct.field("directives", &self.directives); + debug_struct.finish() } } diff --git a/src/core/jit/response.rs b/src/core/jit/response.rs index 9d78ca8aea..1b4bf12ec3 100644 --- a/src/core/jit/response.rs +++ b/src/core/jit/response.rs @@ -103,7 +103,9 @@ mod test { Pos { line: 1, column: 2 }, ); let error2 = Positioned::new( - jit::Error::Validation(jit::ValidationError::TypeNameMismatch), + jit::Error::Validation(jit::ValidationError::EnumInvalid { + type_of: "EnumDef".to_string(), + }), Pos { line: 3, column: 4 }, ); diff --git a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__alias_query.snap b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__alias_query.snap new file mode 100644 index 0000000000..0e0d13d8e2 --- /dev/null +++ b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__alias_query.snap @@ -0,0 +1,44 @@ +--- +source: src/core/jit/builder.rs +expression: plan.into_nested() +--- +[ + Field { + id: 0, + name: "posts", + output_name: "articles", + ir: "Some(..)", + type_of: [Post], + type_condition: "Query", + extensions: Some( + Nested( + [ + Field { + id: 1, + name: "user", + output_name: "author", + ir: "Some(..)", + type_of: User, + type_condition: "Post", + extensions: Some( + Nested( + [ + Field { + id: 2, + name: "id", + output_name: "identifier", + type_of: ID!, + type_condition: "User", + directives: [], + }, + ], + ), + ), + directives: [], + }, + ], + ), + ), + directives: [], + }, +] diff --git a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__default_value.snap b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__default_value.snap index 9bfb9e3660..b9ce57c2f4 100644 --- a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__default_value.snap +++ b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__default_value.snap @@ -6,6 +6,7 @@ expression: plan.into_nested() Field { id: 0, name: "createPost", + output_name: "createPost", ir: "Some(..)", type_of: Post, type_condition: "Mutation", @@ -44,6 +45,7 @@ expression: plan.into_nested() Field { id: 1, name: "id", + output_name: "id", type_of: ID!, type_condition: "Post", directives: [], diff --git a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__directives.snap b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__directives.snap index 624169f64e..a93aafa00d 100644 --- a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__directives.snap +++ b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__directives.snap @@ -6,6 +6,7 @@ expression: plan.into_nested() Field { id: 0, name: "users", + output_name: "users", ir: "Some(..)", type_of: [User], type_condition: "Query", @@ -15,6 +16,7 @@ expression: plan.into_nested() Field { id: 1, name: "id", + output_name: "id", type_of: ID!, type_condition: "User", directives: [ @@ -34,6 +36,7 @@ expression: plan.into_nested() Field { id: 2, name: "name", + output_name: "name", type_of: String!, type_condition: "User", include: Some( diff --git a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__fragments.snap b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__fragments.snap index cb7089873b..40266d97e5 100644 --- a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__fragments.snap +++ b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__fragments.snap @@ -6,6 +6,7 @@ expression: plan.into_nested() Field { id: 0, name: "user", + output_name: "user", ir: "Some(..)", type_of: User, type_condition: "Query", @@ -28,6 +29,7 @@ expression: plan.into_nested() Field { id: 1, name: "name", + output_name: "name", type_of: String!, type_condition: "User", directives: [], @@ -35,6 +37,7 @@ expression: plan.into_nested() Field { id: 2, name: "email", + output_name: "email", type_of: String!, type_condition: "User", directives: [], @@ -42,6 +45,7 @@ expression: plan.into_nested() Field { id: 3, name: "phone", + output_name: "phone", type_of: String, type_condition: "User", directives: [], @@ -49,6 +53,7 @@ expression: plan.into_nested() Field { id: 4, name: "title", + output_name: "title", type_of: String!, type_condition: "Post", directives: [], @@ -56,6 +61,7 @@ expression: plan.into_nested() Field { id: 5, name: "body", + output_name: "body", type_of: String!, type_condition: "Post", directives: [], diff --git a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__from_document.snap b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__from_document.snap index e984aa64c0..c1e6d168b7 100644 --- a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__from_document.snap +++ b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__from_document.snap @@ -6,6 +6,7 @@ expression: plan.into_nested() Field { id: 0, name: "posts", + output_name: "posts", ir: "Some(..)", type_of: [Post], type_condition: "Query", @@ -15,6 +16,7 @@ expression: plan.into_nested() Field { id: 1, name: "user", + output_name: "user", ir: "Some(..)", type_of: User, type_condition: "Post", @@ -24,6 +26,7 @@ expression: plan.into_nested() Field { id: 2, name: "id", + output_name: "id", type_of: ID!, type_condition: "User", directives: [], @@ -31,6 +34,7 @@ expression: plan.into_nested() Field { id: 3, name: "name", + output_name: "name", type_of: String!, type_condition: "User", directives: [], diff --git a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__multiple_operations.snap b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__multiple_operations.snap index 4f9c230fc0..31fc82f0ba 100644 --- a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__multiple_operations.snap +++ b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__multiple_operations.snap @@ -6,6 +6,7 @@ expression: plan.into_nested() Field { id: 0, name: "user", + output_name: "user", ir: "Some(..)", type_of: User, type_condition: "Query", @@ -28,6 +29,7 @@ expression: plan.into_nested() Field { id: 1, name: "id", + output_name: "id", type_of: ID!, type_condition: "User", directives: [], @@ -35,6 +37,7 @@ expression: plan.into_nested() Field { id: 2, name: "username", + output_name: "username", type_of: String!, type_condition: "User", directives: [], @@ -47,6 +50,7 @@ expression: plan.into_nested() Field { id: 3, name: "posts", + output_name: "posts", ir: "Some(..)", type_of: [Post], type_condition: "Query", @@ -56,6 +60,7 @@ expression: plan.into_nested() Field { id: 4, name: "id", + output_name: "id", type_of: ID!, type_condition: "Post", directives: [], @@ -63,6 +68,7 @@ expression: plan.into_nested() Field { id: 5, name: "title", + output_name: "title", type_of: String!, type_condition: "Post", directives: [], diff --git a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__resolving_operation-2.snap b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__resolving_operation-2.snap index a73a36b7d9..84141b1a62 100644 --- a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__resolving_operation-2.snap +++ b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__resolving_operation-2.snap @@ -6,6 +6,7 @@ expression: plan.into_nested() Field { id: 0, name: "createPost", + output_name: "createPost", ir: "Some(..)", type_of: Post, type_condition: "Mutation", @@ -44,6 +45,7 @@ expression: plan.into_nested() Field { id: 1, name: "id", + output_name: "id", type_of: ID!, type_condition: "Post", directives: [], @@ -51,6 +53,7 @@ expression: plan.into_nested() Field { id: 2, name: "userId", + output_name: "userId", type_of: ID!, type_condition: "Post", directives: [], @@ -58,6 +61,7 @@ expression: plan.into_nested() Field { id: 3, name: "title", + output_name: "title", type_of: String!, type_condition: "Post", directives: [], @@ -65,6 +69,7 @@ expression: plan.into_nested() Field { id: 4, name: "body", + output_name: "body", type_of: String!, type_condition: "Post", directives: [], diff --git a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__resolving_operation.snap b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__resolving_operation.snap index 2df3bc5121..9a433b4292 100644 --- a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__resolving_operation.snap +++ b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__resolving_operation.snap @@ -6,6 +6,7 @@ expression: plan.into_nested() Field { id: 0, name: "posts", + output_name: "posts", ir: "Some(..)", type_of: [Post], type_condition: "Query", @@ -15,6 +16,7 @@ expression: plan.into_nested() Field { id: 1, name: "id", + output_name: "id", type_of: ID!, type_condition: "Post", directives: [], @@ -22,6 +24,7 @@ expression: plan.into_nested() Field { id: 2, name: "userId", + output_name: "userId", type_of: ID!, type_condition: "Post", directives: [], @@ -29,6 +32,7 @@ expression: plan.into_nested() Field { id: 3, name: "title", + output_name: "title", type_of: String!, type_condition: "Post", directives: [], diff --git a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__simple_mutation.snap b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__simple_mutation.snap index 7f8bbbe66b..d447afdc34 100644 --- a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__simple_mutation.snap +++ b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__simple_mutation.snap @@ -6,6 +6,7 @@ expression: plan.into_nested() Field { id: 0, name: "createUser", + output_name: "createUser", ir: "Some(..)", type_of: User, type_condition: "Mutation", @@ -59,6 +60,7 @@ expression: plan.into_nested() Field { id: 1, name: "id", + output_name: "id", type_of: ID!, type_condition: "User", directives: [], @@ -66,6 +68,7 @@ expression: plan.into_nested() Field { id: 2, name: "name", + output_name: "name", type_of: String!, type_condition: "User", directives: [], @@ -73,6 +76,7 @@ expression: plan.into_nested() Field { id: 3, name: "email", + output_name: "email", type_of: String!, type_condition: "User", directives: [], @@ -80,6 +84,7 @@ expression: plan.into_nested() Field { id: 4, name: "phone", + output_name: "phone", type_of: String, type_condition: "User", directives: [], @@ -87,6 +92,7 @@ expression: plan.into_nested() Field { id: 5, name: "website", + output_name: "website", type_of: String, type_condition: "User", directives: [], @@ -94,6 +100,7 @@ expression: plan.into_nested() Field { id: 6, name: "username", + output_name: "username", type_of: String!, type_condition: "User", directives: [], diff --git a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__simple_query.snap b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__simple_query.snap index 1dc043d477..305af5ecbe 100644 --- a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__simple_query.snap +++ b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__simple_query.snap @@ -6,6 +6,7 @@ expression: plan.into_nested() Field { id: 0, name: "posts", + output_name: "posts", ir: "Some(..)", type_of: [Post], type_condition: "Query", @@ -15,6 +16,7 @@ expression: plan.into_nested() Field { id: 1, name: "user", + output_name: "user", ir: "Some(..)", type_of: User, type_condition: "Post", @@ -24,6 +26,7 @@ expression: plan.into_nested() Field { id: 2, name: "id", + output_name: "id", type_of: ID!, type_condition: "User", directives: [], diff --git a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__unions.snap b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__unions.snap index 9432388d5d..5d1a5b0b6f 100644 --- a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__unions.snap +++ b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__unions.snap @@ -6,6 +6,7 @@ expression: plan.into_nested() Field { id: 0, name: "getUserIdOrEmail", + output_name: "getUserIdOrEmail", ir: "Some(..)", type_of: UserIdOrEmail, type_condition: "Query", @@ -28,6 +29,7 @@ expression: plan.into_nested() Field { id: 1, name: "id", + output_name: "id", type_of: ID!, type_condition: "UserId", directives: [], @@ -35,6 +37,7 @@ expression: plan.into_nested() Field { id: 2, name: "email", + output_name: "email", type_of: String!, type_condition: "UserEmail", directives: [], diff --git a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__variables.snap b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__variables.snap index 733a1018c8..0c7d65e877 100644 --- a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__variables.snap +++ b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__variables.snap @@ -6,6 +6,7 @@ expression: plan.into_nested() Field { id: 0, name: "user", + output_name: "user", ir: "Some(..)", type_of: User, type_condition: "Query", @@ -28,6 +29,7 @@ expression: plan.into_nested() Field { id: 1, name: "id", + output_name: "id", type_of: ID!, type_condition: "User", directives: [], @@ -35,6 +37,7 @@ expression: plan.into_nested() Field { id: 2, name: "name", + output_name: "name", type_of: String!, type_condition: "User", directives: [], diff --git a/src/core/jit/snapshots/tailcall__core__jit__response__test__conversion_to_async_graphql.snap b/src/core/jit/snapshots/tailcall__core__jit__response__test__conversion_to_async_graphql.snap index e3efb724ec..1b362296bd 100644 --- a/src/core/jit/snapshots/tailcall__core__jit__response__test__conversion_to_async_graphql.snap +++ b/src/core/jit/snapshots/tailcall__core__jit__response__test__conversion_to_async_graphql.snap @@ -11,7 +11,7 @@ Response { }, errors: [ ServerError { - message: "TypeName shape doesn't satisfy the processed object", + message: "internal: invalid item for enum \"EnumDef\"", locations: [ Pos(3:4), ], diff --git a/src/core/jit/synth/synth.rs b/src/core/jit/synth/synth.rs index 23ed77d578..bcc0fb13e0 100644 --- a/src/core/jit/synth/synth.rs +++ b/src/core/jit/synth/synth.rs @@ -1,12 +1,11 @@ -use crate::core::ir::TypeName; -use crate::core::jit::exec::{TypedValue, TypedValueRef}; +use crate::core::ir::TypedValue; use crate::core::jit::model::{Field, Nested, OperationPlan, Variable, Variables}; use crate::core::jit::store::{Data, DataPath, Store}; use crate::core::jit::{Error, PathSegment, Positioned, ValidationError}; use crate::core::json::{JsonLike, JsonObjectLike}; use crate::core::scalar; -type ValueStore = Store, Positioned>>; +type ValueStore = Store>>; pub struct Synth { plan: OperationPlan, @@ -45,7 +44,7 @@ impl Synth { impl<'a, Value> Synth where - Value: JsonLike<'a> + Clone, + Value: JsonLike<'a> + Clone + std::fmt::Debug, Value::JsonObject<'a>: JsonObjectLike<'a, Value = Value>, { #[inline(always)] @@ -62,7 +61,8 @@ where continue; } let val = self.iter(child, None, &DataPath::new())?; - data.insert_key(child.name.as_str(), val); + + data.insert_key(&child.output_name, val); } Ok(Value::object(data)) @@ -78,7 +78,7 @@ where fn iter( &'a self, node: &'a Field, Value>, - result: Option>, + value: Option<&'a Value>, data_path: &DataPath, ) -> Result> { match self.store.get(&node.id) { @@ -96,15 +96,12 @@ where match data { Data::Single(result) => { - let result = match result { - Ok(result) => result, - Err(err) => return Err(err.clone()), - }; + let value = result.as_ref().map_err(Clone::clone)?; - if !Self::is_array(&node.type_of, &result.value) { + if !Self::is_array(&node.type_of, value) { return Ok(Value::null()); } - self.iter_inner(node, result.as_ref(), data_path) + self.iter_inner(node, value, data_path) } _ => { // TODO: should bailout instead of returning Null @@ -112,8 +109,8 @@ where } } } - None => match result { - Some(result) => self.iter_inner(node, result, data_path), + None => match value { + Some(value) => self.iter_inner(node, value, data_path), None => Ok(Value::null()), }, } @@ -123,15 +120,13 @@ where fn iter_inner( &'a self, node: &'a Field, Value>, - result: TypedValueRef<'a, Value>, + value: &'a Value, data_path: &DataPath, ) -> Result> { if !self.include(node) { return Ok(Value::null()); } - let TypedValueRef { type_name, value } = result; - let eval_result = if value.is_null() { if node.type_of.is_nullable() { Ok(Value::null()) @@ -148,11 +143,10 @@ where if scalar.validate(value) { Ok(value.clone()) } else { - Err(ValidationError::ScalarInvalid { - type_of: node.type_of.name().to_string(), - path: node.name.to_string(), - } - .into()) + Err( + ValidationError::ScalarInvalid { type_of: node.type_of.name().to_string() } + .into(), + ) } } else if self.plan.field_is_enum(node) { if value @@ -172,20 +166,7 @@ where (_, Some(obj)) => { let mut ans = Value::JsonObject::new(); - let type_name = match &type_name { - Some(TypeName::Single(type_name)) => type_name, - Some(TypeName::Vec(v)) => { - if let Some(index) = data_path.as_slice().last() { - &v[*index] - } else { - return Err(Positioned::new( - ValidationError::TypeNameMismatch.into(), - node.pos, - )); - } - } - None => node.type_of.name(), - }; + let type_name = value.get_type_name().unwrap_or(node.type_of.name()); for child in node.nested_iter(type_name) { // all checks for skip must occur in `iter_inner` @@ -193,11 +174,7 @@ where let include = self.include(child); if include { let val = obj.get_key(child.name.as_str()); - - ans.insert_key( - child.name.as_str(), - self.iter(child, val.map(TypedValueRef::new), data_path)?, - ); + ans.insert_key(&child.output_name, self.iter(child, val, data_path)?); } } @@ -206,11 +183,7 @@ where (Some(arr), _) => { let mut ans = vec![]; for (i, val) in arr.iter().enumerate() { - let val = self.iter_inner( - node, - result.map(|_| val), - &data_path.clone().with_index(i), - )?; + let val = self.iter_inner(node, val, &data_path.clone().with_index(i))?; ans.push(val) } Ok(Value::array(ans)) @@ -234,7 +207,7 @@ where let mut parent = self.plan.find_field(node.id.clone()); while let Some(field) = parent { - path.push(PathSegment::Field(field.name.to_string())); + path.push(PathSegment::Field(field.output_name.to_string())); parent = field .parent() .and_then(|id| self.plan.find_field(id.clone())); @@ -257,7 +230,6 @@ mod tests { use crate::core::config::{Config, ConfigModule}; use crate::core::jit::builder::Builder; use crate::core::jit::common::JP; - use crate::core::jit::exec::TypedValue; use crate::core::jit::model::{FieldId, Variables}; use crate::core::jit::store::{Data, Store}; use crate::core::jit::synth::Synth; @@ -313,22 +285,20 @@ mod tests { } impl TestData { - fn into_value<'a, Value: Deserialize<'a>>(self) -> Data> { + fn into_value<'a, Value: Deserialize<'a>>(self) -> Data { match self { - Self::Posts => Data::Single(TypedValue::new(serde_json::from_str(POSTS).unwrap())), - Self::User1 => Data::Single(TypedValue::new(serde_json::from_str(USER1).unwrap())), + Self::Posts => Data::Single(serde_json::from_str(POSTS).unwrap()), + Self::User1 => Data::Single(serde_json::from_str(USER1).unwrap()), TestData::UsersData => Data::Multiple( vec![ - Data::Single(TypedValue::new(serde_json::from_str(USER1).unwrap())), - Data::Single(TypedValue::new(serde_json::from_str(USER2).unwrap())), + Data::Single(serde_json::from_str(USER1).unwrap()), + Data::Single(serde_json::from_str(USER2).unwrap()), ] .into_iter() .enumerate() .collect(), ), - TestData::Users => { - Data::Single(TypedValue::new(serde_json::from_str(USERS).unwrap())) - } + TestData::Users => Data::Single(serde_json::from_str(USERS).unwrap()), } } } diff --git a/src/core/json/borrow.rs b/src/core/json/borrow.rs index 7e6001cf2b..59fd376d7c 100644 --- a/src/core/json/borrow.rs +++ b/src/core/json/borrow.rs @@ -47,10 +47,31 @@ impl<'ctx> JsonLike<'ctx> for Value<'ctx> { } } + fn into_array(self) -> Option> { + match self { + Value::Array(array) => Some(array), + _ => None, + } + } + fn as_object(&self) -> Option<&Self::JsonObject<'_>> { self.as_object() } + fn as_object_mut(&mut self) -> Option<&mut Self::JsonObject<'ctx>> { + match self { + Value::Object(obj) => Some(obj), + _ => None, + } + } + + fn into_object(self) -> Option> { + match self { + Value::Object(obj) => Some(obj), + _ => None, + } + } + fn as_str(&self) -> Option<&str> { self.as_str() } diff --git a/src/core/json/graphql.rs b/src/core/json/graphql.rs index 0810f70c1e..a41609a80a 100644 --- a/src/core/json/graphql.rs +++ b/src/core/json/graphql.rs @@ -15,7 +15,7 @@ impl<'obj, Value: JsonLike<'obj> + Clone> JsonObjectLike<'obj> for IndexMap Option<&Self::Value> { - self.get(&Name::new(key)) + self.get(key) } fn insert_key(&mut self, key: &'obj str, value: Self::Value) { @@ -33,6 +33,13 @@ impl<'json> JsonLike<'json> for ConstValue { } } + fn into_array(self) -> Option> { + match self { + ConstValue::List(seq) => Some(seq), + _ => None, + } + } + fn as_str(&self) -> Option<&str> { match self { ConstValue::String(s) => Some(s), @@ -110,6 +117,20 @@ impl<'json> JsonLike<'json> for ConstValue { } } + fn as_object_mut(&mut self) -> Option<&mut Self::JsonObject<'_>> { + match self { + ConstValue::Object(map) => Some(map), + _ => None, + } + } + + fn into_object(self) -> Option> { + match self { + ConstValue::Object(map) => Some(map), + _ => None, + } + } + fn object(obj: Self::JsonObject<'json>) -> Self { ConstValue::Object(obj) } diff --git a/src/core/json/json_like.rs b/src/core/json/json_like.rs index 1b74bea3c8..2b10d39eb0 100644 --- a/src/core/json/json_like.rs +++ b/src/core/json/json_like.rs @@ -31,7 +31,10 @@ pub trait JsonLike<'json>: Sized { // Operators fn as_array(&self) -> Option<&Vec>; + fn into_array(self) -> Option>; fn as_object(&self) -> Option<&Self::JsonObject<'_>>; + fn as_object_mut(&mut self) -> Option<&mut Self::JsonObject<'json>>; + fn into_object(self) -> Option>; fn as_str(&self) -> Option<&str>; fn as_i64(&self) -> Option; fn as_u64(&self) -> Option; diff --git a/src/core/json/json_like_list.rs b/src/core/json/json_like_list.rs new file mode 100644 index 0000000000..6743b380a8 --- /dev/null +++ b/src/core/json/json_like_list.rs @@ -0,0 +1,28 @@ +use super::JsonLike; + +pub trait JsonLikeList<'json>: JsonLike<'json> { + fn map(self, mut mapper: impl FnMut(Self) -> Result) -> Result { + if self.as_array().is_some() { + let new = self + .into_array() + .unwrap() + .into_iter() + .map(mapper) + .collect::>()?; + + Ok(Self::array(new)) + } else { + mapper(self) + } + } + + fn try_for_each(&self, mut f: impl FnMut(&Self) -> Result<(), Err>) -> Result<(), Err> { + if let Some(arr) = self.as_array() { + arr.iter().try_for_each(f) + } else { + f(self) + } + } +} + +impl<'json, T: JsonLike<'json>> JsonLikeList<'json> for T {} diff --git a/src/core/json/mod.rs b/src/core/json/mod.rs index 95efa530d0..c1a11d6919 100644 --- a/src/core/json/mod.rs +++ b/src/core/json/mod.rs @@ -1,12 +1,14 @@ mod borrow; mod graphql; mod json_like; +mod json_like_list; mod json_schema; mod serde; use std::collections::HashMap; pub use json_like::*; +pub use json_like_list::*; pub use json_schema::*; // Highly micro-optimized and benchmarked version of get_path_all diff --git a/src/core/json/serde.rs b/src/core/json/serde.rs index 0064e7b557..8ee36499c8 100644 --- a/src/core/json/serde.rs +++ b/src/core/json/serde.rs @@ -26,6 +26,14 @@ impl<'json> JsonLike<'json> for serde_json::Value { self.as_array() } + fn into_array(self) -> Option> { + if let Self::Array(vec) = self { + Some(vec) + } else { + None + } + } + fn as_str(&self) -> Option<&str> { self.as_str() } @@ -85,6 +93,18 @@ impl<'json> JsonLike<'json> for serde_json::Value { self.as_object() } + fn as_object_mut(&mut self) -> Option<&mut Self::JsonObject<'_>> { + self.as_object_mut() + } + + fn into_object(self) -> Option> { + if let Self::Object(obj) = self { + Some(obj) + } else { + None + } + } + fn object(obj: Self::JsonObject<'json>) -> Self { serde_json::Value::Object(obj) } diff --git a/tests/core/snapshots/call-mutation.md_client.snap b/tests/core/snapshots/call-mutation.md_client.snap index 86b6c44f11..4485a75813 100644 --- a/tests/core/snapshots/call-mutation.md_client.snap +++ b/tests/core/snapshots/call-mutation.md_client.snap @@ -26,7 +26,7 @@ scalar JSON type Mutation { attachPostToFirstUser(postId: Int!): User - attachPostToUser(postId: Int!, userId: Int!): User + attachPostToUser(userId: Int!, postId: Int!): User insertMockedPost: Post insertPost(input: PostInput): Post insertPostToFirstUser(input: PostInputWithoutUserId): Post diff --git a/tests/core/snapshots/call-mutation.md_merged.snap b/tests/core/snapshots/call-mutation.md_merged.snap index ee40791749..ccdcdba79b 100644 --- a/tests/core/snapshots/call-mutation.md_merged.snap +++ b/tests/core/snapshots/call-mutation.md_merged.snap @@ -22,7 +22,7 @@ input PostInputWithoutUserId { type Mutation { attachPostToFirstUser(postId: Int!): User @call(steps: [{mutation: "attachPostToUser", args: {postId: "{{.args.postId}}", userId: 1}}]) - attachPostToUser(postId: Int!, userId: Int!): User + attachPostToUser(userId: Int!, postId: Int!): User @http(body: "{\"postId\":{{.args.postId}}}", method: "PATCH", path: "/users/{{.args.userId}}") insertMockedPost: Post @call(steps: [{mutation: "insertPost", args: {input: {body: "post-body", title: "post-title", userId: 1}}}]) diff --git a/tests/core/snapshots/graphql-conformance-003.md_client.snap b/tests/core/snapshots/graphql-conformance-003.md_client.snap index 0c28ea612b..dacaf4db9f 100644 --- a/tests/core/snapshots/graphql-conformance-003.md_client.snap +++ b/tests/core/snapshots/graphql-conformance-003.md_client.snap @@ -45,7 +45,7 @@ scalar Url type User { id: ID! name: String! - profilePic(height: Int, size: Int, width: Int): String! + profilePic(size: Int, width: Int, height: Int): String! } schema { diff --git a/tests/core/snapshots/graphql-conformance-003.md_merged.snap b/tests/core/snapshots/graphql-conformance-003.md_merged.snap index d704383baf..db0f96da00 100644 --- a/tests/core/snapshots/graphql-conformance-003.md_merged.snap +++ b/tests/core/snapshots/graphql-conformance-003.md_merged.snap @@ -15,5 +15,5 @@ type Query { type User { id: ID! name: String! - profilePic(height: Int, size: Int, width: Int): String! + profilePic(size: Int, width: Int, height: Int): String! } diff --git a/tests/core/snapshots/graphql-conformance-015.md_client.snap b/tests/core/snapshots/graphql-conformance-015.md_client.snap index 1e26b06309..770537c269 100644 --- a/tests/core/snapshots/graphql-conformance-015.md_client.snap +++ b/tests/core/snapshots/graphql-conformance-015.md_client.snap @@ -47,7 +47,7 @@ type User { featuredVideoPreview(video: VideoSize! = {}): String! id: ID! name: String! - profilePic(height: Int = 100, size: Int! = 100, width: Int): String! + profilePic(size: Int! = 100, width: Int, height: Int = 100): String! searchComments(query: [String]! = [["today"]]): String! } diff --git a/tests/core/snapshots/graphql-conformance-015.md_merged.snap b/tests/core/snapshots/graphql-conformance-015.md_merged.snap index a9166dcdc9..28820191aa 100644 --- a/tests/core/snapshots/graphql-conformance-015.md_merged.snap +++ b/tests/core/snapshots/graphql-conformance-015.md_merged.snap @@ -25,7 +25,7 @@ type User { @expr(body: "video_{{.value.id}}_{{.args.video.width}}_{{.args.video.height}}_{{.args.video.hdr}}") id: ID! name: String! - profilePic(height: Int = 100, size: Int! = 100, width: Int): String! + profilePic(size: Int! = 100, width: Int, height: Int = 100): String! @expr(body: "{{.value.id}}_{{.args.size}}_{{.args.width}}_{{.args.height}}") searchComments(query: [String]! = [["today"]]): String! @expr(body: "video_{{.value.id}}_{{.args.query}}") } diff --git a/tests/core/snapshots/graphql-conformance-http-003.md_client.snap b/tests/core/snapshots/graphql-conformance-http-003.md_client.snap index 0c28ea612b..dacaf4db9f 100644 --- a/tests/core/snapshots/graphql-conformance-http-003.md_client.snap +++ b/tests/core/snapshots/graphql-conformance-http-003.md_client.snap @@ -45,7 +45,7 @@ scalar Url type User { id: ID! name: String! - profilePic(height: Int, size: Int, width: Int): String! + profilePic(size: Int, width: Int, height: Int): String! } schema { diff --git a/tests/core/snapshots/graphql-conformance-http-003.md_merged.snap b/tests/core/snapshots/graphql-conformance-http-003.md_merged.snap index 621a90aea1..15c61094e1 100644 --- a/tests/core/snapshots/graphql-conformance-http-003.md_merged.snap +++ b/tests/core/snapshots/graphql-conformance-http-003.md_merged.snap @@ -15,7 +15,7 @@ type Query { type User { id: ID! name: String! - profilePic(height: Int, size: Int, width: Int): String! + profilePic(size: Int, width: Int, height: Int): String! @http( path: "/pic" query: [ diff --git a/tests/core/snapshots/graphql-conformance-http-004.md_client.snap b/tests/core/snapshots/graphql-conformance-http-004.md_client.snap index 0c28ea612b..dacaf4db9f 100644 --- a/tests/core/snapshots/graphql-conformance-http-004.md_client.snap +++ b/tests/core/snapshots/graphql-conformance-http-004.md_client.snap @@ -45,7 +45,7 @@ scalar Url type User { id: ID! name: String! - profilePic(height: Int, size: Int, width: Int): String! + profilePic(size: Int, width: Int, height: Int): String! } schema { diff --git a/tests/core/snapshots/graphql-conformance-http-004.md_merged.snap b/tests/core/snapshots/graphql-conformance-http-004.md_merged.snap index 621a90aea1..15c61094e1 100644 --- a/tests/core/snapshots/graphql-conformance-http-004.md_merged.snap +++ b/tests/core/snapshots/graphql-conformance-http-004.md_merged.snap @@ -15,7 +15,7 @@ type Query { type User { id: ID! name: String! - profilePic(height: Int, size: Int, width: Int): String! + profilePic(size: Int, width: Int, height: Int): String! @http( path: "/pic" query: [ diff --git a/tests/core/snapshots/graphql-conformance-http-006.md_client.snap b/tests/core/snapshots/graphql-conformance-http-006.md_client.snap index b746f0b983..ba98860b7c 100644 --- a/tests/core/snapshots/graphql-conformance-http-006.md_client.snap +++ b/tests/core/snapshots/graphql-conformance-http-006.md_client.snap @@ -47,7 +47,7 @@ type User { id: ID! mutualFriends(first: Int): [User!]! name: String! - profilePic(height: Int, size: Int, width: Int): String! + profilePic(size: Int, width: Int, height: Int): String! } schema { diff --git a/tests/core/snapshots/graphql-conformance-http-006.md_merged.snap b/tests/core/snapshots/graphql-conformance-http-006.md_merged.snap index 5ead572f39..02b99f0acb 100644 --- a/tests/core/snapshots/graphql-conformance-http-006.md_merged.snap +++ b/tests/core/snapshots/graphql-conformance-http-006.md_merged.snap @@ -22,6 +22,6 @@ type User { query: [{key: "id", value: "{{.value.id}}"}, {key: "first", value: "{{.args.first}}"}] ) name: String! - profilePic(height: Int, size: Int, width: Int): String! + profilePic(size: Int, width: Int, height: Int): String! @expr(body: "{{.value.id}}_{{.args.size}}_{{.args.width}}_{{.args.height}}") } diff --git a/tests/core/snapshots/graphql-conformance-http-015.md_client.snap b/tests/core/snapshots/graphql-conformance-http-015.md_client.snap index b0ad1b799d..6f41f83b4d 100644 --- a/tests/core/snapshots/graphql-conformance-http-015.md_client.snap +++ b/tests/core/snapshots/graphql-conformance-http-015.md_client.snap @@ -47,7 +47,7 @@ type User { featuredVideoPreview(video: VideoSize! = {}): String! id: ID! name: String! - profilePic(height: Int = 100, size: Int! = 100, width: Int): String! + profilePic(size: Int! = 100, width: Int, height: Int = 100): String! searchComments(query: [String]! = [["today"]]): String! } diff --git a/tests/core/snapshots/graphql-conformance-http-015.md_merged.snap b/tests/core/snapshots/graphql-conformance-http-015.md_merged.snap index 880d478b00..38b944a41b 100644 --- a/tests/core/snapshots/graphql-conformance-http-015.md_merged.snap +++ b/tests/core/snapshots/graphql-conformance-http-015.md_merged.snap @@ -25,7 +25,7 @@ type User { @expr(body: "video_{{.value.id}}_{{.args.video.width}}_{{.args.video.height}}_{{.args.video.hdr}}") id: ID! name: String! - profilePic(height: Int = 100, size: Int! = 100, width: Int): String! + profilePic(size: Int! = 100, width: Int, height: Int = 100): String! @expr(body: "{{.value.id}}_{{.args.size}}_{{.args.width}}_{{.args.height}}") searchComments(query: [String]! = [["today"]]): String! @expr(body: "video_{{.value.id}}_{{.args.query}}") } diff --git a/tests/core/snapshots/test-union.md_5.snap b/tests/core/snapshots/test-union.md_5.snap new file mode 100644 index 0000000000..442efd36fc --- /dev/null +++ b/tests/core/snapshots/test-union.md_5.snap @@ -0,0 +1,45 @@ +--- +source: tests/core/spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": { + "foo": { + "__typename": "Foo" + }, + "bar": { + "__typename": "Bar" + }, + "arr": [ + { + "__typename": "Foo" + }, + { + "__typename": "Bar" + }, + { + "__typename": "Foo" + }, + { + "__typename": "Foo" + }, + { + "__typename": "Bar" + } + ], + "nested": { + "foo": { + "__typename": "Foo" + }, + "bar": { + "__typename": "Bar" + } + } + } + } +} diff --git a/tests/core/snapshots/yaml-union-in-type.md_client.snap b/tests/core/snapshots/yaml-union-in-type.md_client.snap index 0910863dd4..27c018f483 100644 --- a/tests/core/snapshots/yaml-union-in-type.md_client.snap +++ b/tests/core/snapshots/yaml-union-in-type.md_client.snap @@ -60,15 +60,15 @@ input NU__u2 { scalar PhoneNumber type Query { - testVar0Var0(nnu: NNU__nu0, nu: NU__u0!): U - testVar0Var1(nnu: NNU__nu0, nu: NU__u1!): U - testVar0Var2(nnu: NNU__nu0, nu: NU__u2!): U - testVar1Var0(nnu: NNU__nu1, nu: NU__u0!): U - testVar1Var1(nnu: NNU__nu1, nu: NU__u1!): U - testVar1Var2(nnu: NNU__nu1, nu: NU__u2!): U - testVar2Var0(nnu: NNU__nu2, nu: NU__u0!): U - testVar2Var1(nnu: NNU__nu2, nu: NU__u1!): U - testVar2Var2(nnu: NNU__nu2, nu: NU__u2!): U + testVar0Var0(nu: NU__u0!, nnu: NNU__nu0): U + testVar0Var1(nu: NU__u0!, nnu: NNU__nu1): U + testVar0Var2(nu: NU__u0!, nnu: NNU__nu2): U + testVar1Var0(nu: NU__u1!, nnu: NNU__nu0): U + testVar1Var1(nu: NU__u1!, nnu: NNU__nu1): U + testVar1Var2(nu: NU__u1!, nnu: NNU__nu2): U + testVar2Var0(nu: NU__u2!, nnu: NNU__nu0): U + testVar2Var1(nu: NU__u2!, nnu: NNU__nu1): U + testVar2Var2(nu: NU__u2!, nnu: NNU__nu2): U } type T1 { diff --git a/tests/core/snapshots/yaml-union-in-type.md_merged.snap b/tests/core/snapshots/yaml-union-in-type.md_merged.snap index 9d92d84dba..75f49f1e71 100644 --- a/tests/core/snapshots/yaml-union-in-type.md_merged.snap +++ b/tests/core/snapshots/yaml-union-in-type.md_merged.snap @@ -66,15 +66,15 @@ type NU { } type Query { - testVar0Var0(nnu: NNU__nu0, nu: NU__u0!): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") - testVar0Var1(nnu: NNU__nu0, nu: NU__u1!): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") - testVar0Var2(nnu: NNU__nu0, nu: NU__u2!): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") - testVar1Var0(nnu: NNU__nu1, nu: NU__u0!): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") - testVar1Var1(nnu: NNU__nu1, nu: NU__u1!): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") - testVar1Var2(nnu: NNU__nu1, nu: NU__u2!): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") - testVar2Var0(nnu: NNU__nu2, nu: NU__u0!): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") - testVar2Var1(nnu: NNU__nu2, nu: NU__u1!): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") - testVar2Var2(nnu: NNU__nu2, nu: NU__u2!): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") + testVar0Var0(nu: NU__u0!, nnu: NNU__nu0): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") + testVar0Var1(nu: NU__u0!, nnu: NNU__nu1): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") + testVar0Var2(nu: NU__u0!, nnu: NNU__nu2): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") + testVar1Var0(nu: NU__u1!, nnu: NNU__nu0): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") + testVar1Var1(nu: NU__u1!, nnu: NNU__nu1): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") + testVar1Var2(nu: NU__u1!, nnu: NNU__nu2): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") + testVar2Var0(nu: NU__u2!, nnu: NNU__nu0): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") + testVar2Var1(nu: NU__u2!, nnu: NNU__nu1): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") + testVar2Var2(nu: NU__u2!, nnu: NNU__nu2): U @http(baseURL: "http://localhost", path: "/users/{{args.nu.u}}") } type T1 { diff --git a/tests/execution/test-union.md b/tests/execution/test-union.md index 8d7eea5383..907920a1ee 100644 --- a/tests/execution/test-union.md +++ b/tests/execution/test-union.md @@ -37,6 +37,7 @@ type Query { - request: method: GET url: http://jsonplaceholder.typicode.com/foo + expectedHits: 2 response: status: 200 body: @@ -45,6 +46,7 @@ type Query { - request: method: GET url: http://jsonplaceholder.typicode.com/bar + expectedHits: 2 response: status: 200 body: @@ -53,6 +55,7 @@ type Query { - request: method: GET url: http://jsonplaceholder.typicode.com/nested + expectedHits: 2 response: status: 200 body: @@ -64,6 +67,7 @@ type Query { - request: method: GET url: http://jsonplaceholder.typicode.com/arr + expectedHits: 2 response: status: 200 body: @@ -155,4 +159,28 @@ type Query { } } } + +- method: POST + url: http://localhost:8080/graphql + body: + query: > + query { + foo { + __typename + } + bar { + __typename + } + arr { + __typename + } + nested { + foo { + __typename + } + bar { + __typename + } + } + } ```