From 7d591cf03d51251068a066cbbb1308f041c29e64 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Aug 2024 20:11:12 +0000 Subject: [PATCH 01/14] chore(deps): update dependency wrangler to v3.72.3 --- tailcall-cloudflare/package-lock.json | 28 +++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tailcall-cloudflare/package-lock.json b/tailcall-cloudflare/package-lock.json index 5c115e6071..6521dc4ffa 100644 --- a/tailcall-cloudflare/package-lock.json +++ b/tailcall-cloudflare/package-lock.json @@ -130,9 +130,9 @@ } }, "node_modules/@cloudflare/workers-shared": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-shared/-/workers-shared-0.3.0.tgz", - "integrity": "sha512-cqtLW1QiBC/ABaZIhAdyGCsnHHY6pAb6hsVUZg82Co2gQtf/faxRYV1FgpCwUYroTdk6A66xUMSTmFqreKCJow==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-shared/-/workers-shared-0.4.0.tgz", + "integrity": "sha512-XAFOldVQsbxQ7mjbqX2q1dNIgcLbKSytk41pwuZTn9e0p7OeTpFTosJef8uwosL6CcOAHqcW1f1HJxyjwmtGxw==", "dev": true, "engines": { "node": ">=16.7.0" @@ -2214,13 +2214,13 @@ } }, "node_modules/wrangler": { - "version": "3.72.2", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.72.2.tgz", - "integrity": "sha512-7nxkJ4md+KtESNJ/0DwTM7bHZP+uNRpJT5gMDT9WllP9UVzYdtXCTF+p4CHtxIReUpe6pOi7tb05hK9/Q6WaiA==", + "version": "3.72.3", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.72.3.tgz", + "integrity": "sha512-EBlJGOcwanbzFkiJkRB47WKhvevh1AZK0ty0MyD0gptsgWnAxBfmFGiBuzOuRXbvH45ZrFrTqgi8c67EwcV1nA==", "dev": true, "dependencies": { "@cloudflare/kv-asset-handler": "0.3.4", - "@cloudflare/workers-shared": "0.3.0", + "@cloudflare/workers-shared": "0.4.0", "@esbuild-plugins/node-globals-polyfill": "^0.2.3", "@esbuild-plugins/node-modules-polyfill": "^0.2.2", "blake3-wasm": "^2.1.5", @@ -2762,9 +2762,9 @@ "optional": true }, "@cloudflare/workers-shared": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-shared/-/workers-shared-0.3.0.tgz", - "integrity": "sha512-cqtLW1QiBC/ABaZIhAdyGCsnHHY6pAb6hsVUZg82Co2gQtf/faxRYV1FgpCwUYroTdk6A66xUMSTmFqreKCJow==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-shared/-/workers-shared-0.4.0.tgz", + "integrity": "sha512-XAFOldVQsbxQ7mjbqX2q1dNIgcLbKSytk41pwuZTn9e0p7OeTpFTosJef8uwosL6CcOAHqcW1f1HJxyjwmtGxw==", "dev": true }, "@cloudflare/workers-types": { @@ -4104,13 +4104,13 @@ } }, "wrangler": { - "version": "3.72.2", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.72.2.tgz", - "integrity": "sha512-7nxkJ4md+KtESNJ/0DwTM7bHZP+uNRpJT5gMDT9WllP9UVzYdtXCTF+p4CHtxIReUpe6pOi7tb05hK9/Q6WaiA==", + "version": "3.72.3", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.72.3.tgz", + "integrity": "sha512-EBlJGOcwanbzFkiJkRB47WKhvevh1AZK0ty0MyD0gptsgWnAxBfmFGiBuzOuRXbvH45ZrFrTqgi8c67EwcV1nA==", "dev": true, "requires": { "@cloudflare/kv-asset-handler": "0.3.4", - "@cloudflare/workers-shared": "0.3.0", + "@cloudflare/workers-shared": "0.4.0", "@esbuild-plugins/node-globals-polyfill": "^0.2.3", "@esbuild-plugins/node-modules-polyfill": "^0.2.2", "blake3-wasm": "^2.1.5", From 751e9a61121a0cfafdea7cd88d70f95401215f9f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 28 Aug 2024 08:52:38 +0000 Subject: [PATCH 02/14] fix(deps): update rust crate serde_json_borrow to 0.6.0 --- Cargo.lock | 14 +++++++------- Cargo.toml | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c8675bf2cf..3daea4f8fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2376,7 +2376,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.10", + "socket2 0.5.7", "tokio", "tower-service", "tracing", @@ -3727,7 +3727,7 @@ dependencies = [ "bincode", "either", "fnv", - "itertools 0.10.5", + "itertools 0.11.0", "lazy_static", "nom", "quick-xml", @@ -3990,7 +3990,7 @@ checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" dependencies = [ "bytes", "heck", - "itertools 0.10.5", + "itertools 0.11.0", "log", "multimap", "once_cell", @@ -4010,7 +4010,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.11.0", "proc-macro2", "quote", "syn 2.0.76", @@ -4023,7 +4023,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18bec9b0adc4eba778b33684b7ba3e7137789434769ee3ce3930463ef904cfca" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.11.0", "proc-macro2", "quote", "syn 2.0.76", @@ -5041,9 +5041,9 @@ dependencies = [ [[package]] name = "serde_json_borrow" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a60291362be3646d15fb0b5a5bddfd8003ebf013b2186a3c60a534fd35d6a26" +checksum = "176a77dea19cf9b2cfe7f9e31966112ef8282a709af7c0a0fb28fc6347c7ba78" dependencies = [ "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index ae057e4ea9..b387d921ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -160,7 +160,7 @@ datatest-stable = "0.2.9" tokio-test = "0.4.4" base64 = "0.22.1" tailcall-hasher = { path = "tailcall-hasher" } -serde_json_borrow = "0.5.0" +serde_json_borrow = "0.6.0" pluralizer = "0.4.0" path-clean = "=1.0.1" pathdiff = "0.2.1" From aa669c4e9932c6e502402322c4d46ff002bfb39f Mon Sep 17 00:00:00 2001 From: laststylebender <43403528+laststylebender14@users.noreply.github.com> Date: Wed, 28 Aug 2024 21:19:29 +0530 Subject: [PATCH 03/14] feat(2560): merge unknows types (#2567) --- src/cli/generator/config.rs | 1 - .../transformer/merge_types/similarity.rs | 32 +++++++++- ...type_merger__test__merge_to_supertype.snap | 17 ++++++ .../transformer/merge_types/type_merger.rs | 59 ++++++++++++++++++- .../fixtures/generator/simple-json.json | 2 +- tests/cli/fixtures/generator/gen_deezer.md | 2 +- .../generator/gen_json_proto_mix_config.md | 2 +- .../fixtures/generator/gen_jsonplaceholder.md | 2 +- 8 files changed, 108 insertions(+), 9 deletions(-) create mode 100644 src/core/config/transformer/merge_types/snapshots/tailcall__core__config__transformer__merge_types__type_merger__test__merge_to_supertype.snap diff --git a/src/cli/generator/config.rs b/src/cli/generator/config.rs index dab568cb42..a5f4859781 100644 --- a/src/cli/generator/config.rs +++ b/src/cli/generator/config.rs @@ -50,7 +50,6 @@ pub struct PresetConfig { pub tree_shake: Option, pub unwrap_single_field_types: Option, } - #[derive(Deserialize, Serialize, Debug, Default)] #[serde(transparent)] pub struct Location( diff --git a/src/core/config/transformer/merge_types/similarity.rs b/src/core/config/transformer/merge_types/similarity.rs index 7bc81368a0..396bdc8953 100644 --- a/src/core/config/transformer/merge_types/similarity.rs +++ b/src/core/config/transformer/merge_types/similarity.rs @@ -1,6 +1,7 @@ use super::pair_map::PairMap; use super::pair_set::PairSet; use crate::core::config::{Config, Type}; +use crate::core::scalar::Scalar; use crate::core::valid::{Valid, Validator}; /// Given Two types,it tells similarity between two types based on a specified @@ -63,7 +64,11 @@ impl<'a> Similarity<'a> { if config.is_scalar(&field_1_type_of) && config.is_scalar(&field_2_type_of) { // if field type_of is scalar and they don't match then we can't merge // types. - if field_1_type_of == field_2_type_of { + let json_scalar = Scalar::JSON.to_string(); + if field_1_type_of == field_2_type_of + || field_1_type_of == json_scalar + || field_2_type_of == json_scalar + { if field_1.list == field_2.list { same_field_count += 1; } else { @@ -513,4 +518,29 @@ mod test { // Assert that merging incompatible list and non-list fields fails assert!(result.is_err()) } + + #[test] + fn test_unknown_types_similarity() { + let sdl = r#" + type A { + primarySubcategoryId: String + } + type B { + primarySubcategoryId: JSON + } + "#; + let config = Config::from_sdl(sdl).to_result().unwrap(); + + let mut similarity = Similarity::new(&config); + + let result = similarity + .similarity( + ("B", config.types.get("B").unwrap()), + ("A", config.types.get("A").unwrap()), + 0.9, + ) + .to_result() + .unwrap(); + assert!(result); + } } diff --git a/src/core/config/transformer/merge_types/snapshots/tailcall__core__config__transformer__merge_types__type_merger__test__merge_to_supertype.snap b/src/core/config/transformer/merge_types/snapshots/tailcall__core__config__transformer__merge_types__type_merger__test__merge_to_supertype.snap new file mode 100644 index 0000000000..88ce824973 --- /dev/null +++ b/src/core/config/transformer/merge_types/snapshots/tailcall__core__config__transformer__merge_types__type_merger__test__merge_to_supertype.snap @@ -0,0 +1,17 @@ +--- +source: src/core/config/transformer/merge_types/type_merger.rs +expression: config.to_sdl() +--- +schema @server @upstream { + query: Query +} + +type M1 { + id: Int + name: JSON +} + +type Query { + bar: M1 + foo: M1 +} diff --git a/src/core/config/transformer/merge_types/type_merger.rs b/src/core/config/transformer/merge_types/type_merger.rs index e494cde350..f735af0b43 100644 --- a/src/core/config/transformer/merge_types/type_merger.rs +++ b/src/core/config/transformer/merge_types/type_merger.rs @@ -4,6 +4,7 @@ use super::mergeable_types::MergeableTypes; use super::similarity::Similarity; use crate::core::config::{Config, Type}; use crate::core::merge_right::MergeRight; +use crate::core::scalar::Scalar; use crate::core::transform::Transform; use crate::core::valid::{Valid, Validator}; @@ -57,7 +58,6 @@ impl TypeMerger { if let Some(type_info_2) = config.types.get(type_name_2) { let threshold = mergeable_types.get_threshold(type_name_1, type_name_2); - visited_types.insert(type_name_1.clone()); let is_similar = stat_gen .similarity( @@ -66,6 +66,7 @@ impl TypeMerger { threshold, ) .to_result(); + if let Ok(similar) = is_similar { if similar { visited_types.insert(type_name_2.clone()); @@ -187,8 +188,34 @@ impl TypeMerger { } } -fn merge_type(type_: &Type, merge_into: Type) -> Type { - merge_into.merge_right(type_.clone()) +fn merge_type(type_: &Type, mut merge_into: Type) -> Type { + // Merge the simple fields using `merge_right`. + merge_into.added_fields = merge_into + .added_fields + .merge_right(type_.added_fields.clone()); + merge_into.implements = merge_into.implements.merge_right(type_.implements.clone()); + merge_into.cache = merge_into.cache.merge_right(type_.cache.clone()); + merge_into.protected = merge_into.protected.merge_right(type_.protected.clone()); + merge_into.doc = merge_into.doc.merge_right(type_.doc.clone()); + + // Handle field output type merging correctly. + type_.fields.iter().for_each(|(key, new_field)| { + merge_into + .fields + .entry(key.to_owned()) + .and_modify(|existing_field| { + let mut merged_field = existing_field.clone().merge_right(new_field.clone()); + if existing_field.type_of == Scalar::JSON.to_string() + || new_field.type_of == Scalar::JSON.to_string() + { + merged_field.type_of = Scalar::JSON.to_string(); + } + *existing_field = merged_field; + }) + .or_insert_with(|| new_field.to_owned()); + }); + + merge_into } impl Transform for TypeMerger { @@ -379,4 +406,30 @@ mod test { let config = TypeMerger::default().transform(config).to_result().unwrap(); insta::assert_snapshot!(config.to_sdl()); } + + #[test] + fn test_merge_to_supertype() { + let sdl = r#" + schema { + query: Query + } + + type Bar { + id: Int + name: JSON + } + type Foo { + id: Int + name: String + } + type Query { + foo: Foo + bar: Bar + } + "#; + + let config = Config::from_sdl(sdl).to_result().unwrap(); + let config = TypeMerger::default().transform(config).to_result().unwrap(); + insta::assert_snapshot!(config.to_sdl()); + } } diff --git a/tailcall-fixtures/fixtures/generator/simple-json.json b/tailcall-fixtures/fixtures/generator/simple-json.json index 77995628c8..9e1d8f61cf 100644 --- a/tailcall-fixtures/fixtures/generator/simple-json.json +++ b/tailcall-fixtures/fixtures/generator/simple-json.json @@ -23,7 +23,7 @@ } ], "preset": { - "mergeType": 1, + "mergeType": 1.0, "consolidateURL": 0.5 }, "output": { diff --git a/tests/cli/fixtures/generator/gen_deezer.md b/tests/cli/fixtures/generator/gen_deezer.md index 6fb4f47ac9..74cfe25711 100644 --- a/tests/cli/fixtures/generator/gen_deezer.md +++ b/tests/cli/fixtures/generator/gen_deezer.md @@ -51,7 +51,7 @@ } ], "preset": { - "mergeType": 1, + "mergeType": 1.0, "consolidateURL": 0.5, "treeShake": true, "inferTypeNames": true diff --git a/tests/cli/fixtures/generator/gen_json_proto_mix_config.md b/tests/cli/fixtures/generator/gen_json_proto_mix_config.md index 1da85764b2..be7a4f639d 100644 --- a/tests/cli/fixtures/generator/gen_json_proto_mix_config.md +++ b/tests/cli/fixtures/generator/gen_json_proto_mix_config.md @@ -14,7 +14,7 @@ } ], "preset": { - "mergeType": 1, + "mergeType": 1.0, "consolidateURL": 0.5, "inferTypeNames": true, "treeShake": true diff --git a/tests/cli/fixtures/generator/gen_jsonplaceholder.md b/tests/cli/fixtures/generator/gen_jsonplaceholder.md index 2feead7b4f..d3d2d4202a 100644 --- a/tests/cli/fixtures/generator/gen_jsonplaceholder.md +++ b/tests/cli/fixtures/generator/gen_jsonplaceholder.md @@ -69,7 +69,7 @@ } ], "preset": { - "mergeType": 1, + "mergeType": 1.0, "consolidateURL": 0.5, "treeShake": true, "inferTypeNames": true From 6caf1793fef82d73ce1a4e835a6180ed5dd58dbf Mon Sep 17 00:00:00 2001 From: Kiryl Mialeshka <8974488+meskill@users.noreply.github.com> Date: Wed, 28 Aug 2024 18:14:19 +0200 Subject: [PATCH 04/14] fix(grpc): handle union shared fields (#2757) Co-authored-by: Tushar Mathur --- src/core/blueprint/index.rs | 21 + src/core/config/config.rs | 3 + src/core/generator/from_proto.rs | 8 +- ...erator__from_proto__test__oneof_types.snap | 16 +- src/core/ir/resolver_context_like.rs | 6 +- src/core/jit/builder.rs | 6 +- src/core/jit/exec.rs | 34 +- src/core/jit/model.rs | 64 +- ...ore__jit__builder__tests__alias_query.snap | 12 +- ...e__jit__builder__tests__default_value.snap | 8 +- ...core__jit__builder__tests__directives.snap | 12 +- ..._core__jit__builder__tests__fragments.snap | 24 +- ...e__jit__builder__tests__from_document.snap | 16 +- ...__builder__tests__multiple_operations.snap | 24 +- ...builder__tests__resolving_operation-2.snap | 20 +- ...__builder__tests__resolving_operation.snap | 16 +- ..._jit__builder__tests__simple_mutation.snap | 28 +- ...re__jit__builder__tests__simple_query.snap | 12 +- ...ll__core__jit__builder__tests__unions.snap | 12 +- ..._core__jit__builder__tests__variables.snap | 12 +- ...nth__tests__json_placeholder_typename.snap | 808 ++++++++++++++++++ src/core/jit/synth/synth.rs | 22 +- .../core/snapshots/grpc-oneof.md_client.snap | 12 +- .../core/snapshots/grpc-oneof.md_merged.snap | 12 +- tests/execution/grpc-oneof.md | 18 +- 25 files changed, 1114 insertions(+), 112 deletions(-) create mode 100644 src/core/jit/synth/snapshots/tailcall__core__jit__synth__synth__tests__json_placeholder_typename.snap diff --git a/src/core/blueprint/index.rs b/src/core/blueprint/index.rs index c6e8632a19..d8ebe1a317 100644 --- a/src/core/blueprint/index.rs +++ b/src/core/blueprint/index.rs @@ -66,6 +66,18 @@ impl Index { pub fn get_mutation(&self) -> Option<&str> { self.schema.mutation.as_deref() } + + pub fn is_type_implements(&self, type_name: &str, type_or_interface: &str) -> bool { + if type_name == type_or_interface { + return true; + } + + if let Some((Definition::Object(obj), _)) = self.map.get(type_name) { + obj.implements.contains(type_or_interface) + } else { + false + } + } } impl From<&Blueprint> for Index { @@ -232,4 +244,13 @@ mod test { index.schema.mutation = None; assert_eq!(index.get_mutation(), None); } + + #[test] + fn test_is_type_implements() { + let index = setup(); + + assert!(index.is_type_implements("User", "Node")); + assert!(index.is_type_implements("Post", "Post")); + assert!(!index.is_type_implements("Node", "User")); + } } diff --git a/src/core/config/config.rs b/src/core/config/config.rs index cb22208d47..5f15e7fc78 100644 --- a/src/core/config/config.rs +++ b/src/core/config/config.rs @@ -962,6 +962,9 @@ impl Config { stack.extend(field.args.values().map(|arg| arg.type_of.clone())); stack.push(field.type_of.clone()); } + for interface in typ.implements.iter() { + stack.push(interface.clone()) + } } } diff --git a/src/core/generator/from_proto.rs b/src/core/generator/from_proto.rs index b78c837ded..a7bd6ba215 100644 --- a/src/core/generator/from_proto.rs +++ b/src/core/generator/from_proto.rs @@ -127,7 +127,7 @@ impl Context { collect_types( type_name.clone(), - base_type, + base_type.clone(), &oneof_fields, &mut union_types, ); @@ -141,13 +141,17 @@ impl Context { } let mut union_ = Union::default(); + let interface_name = format!("{type_name}__Interface"); - for (type_name, ty) in union_types { + for (type_name, mut ty) in union_types { + ty.implements.insert(interface_name.clone()); union_.types.insert(type_name.clone()); self = self.insert_type(type_name, ty); } + // base interface type + self.config.types.insert(interface_name, base_type); self.config.unions.insert(type_name, union_); self diff --git a/src/core/generator/snapshots/tailcall__core__generator__from_proto__test__oneof_types.snap b/src/core/generator/snapshots/tailcall__core__generator__from_proto__test__oneof_types.snap index df9cc4e60d..098e30b2e2 100644 --- a/src/core/generator/snapshots/tailcall__core__generator__from_proto__test__oneof_types.snap +++ b/src/core/generator/snapshots/tailcall__core__generator__from_proto__test__oneof_types.snap @@ -62,6 +62,14 @@ input oneof__Request__Var__Var1 { usual: String } +interface oneof__Request__Interface { + usual: String +} + +interface oneof__Response__Interface { + usual: Int +} + union oneof__Request = oneof__Request__Var0__Var | oneof__Request__Var0__Var0 | oneof__Request__Var0__Var1 | oneof__Request__Var1__Var | oneof__Request__Var1__Var0 | oneof__Request__Var1__Var1 | oneof__Request__Var__Var | oneof__Request__Var__Var0 | oneof__Request__Var__Var1 union oneof__Response = oneof__Response__Var | oneof__Response__Var0 | oneof__Response__Var1 | oneof__Response__Var2 @@ -78,21 +86,21 @@ type oneof__Payload { payload: String } -type oneof__Response__Var { +type oneof__Response__Var implements oneof__Response__Interface { usual: Int } -type oneof__Response__Var0 { +type oneof__Response__Var0 implements oneof__Response__Interface { payload: oneof__Payload! usual: Int } -type oneof__Response__Var1 { +type oneof__Response__Var1 implements oneof__Response__Interface { command: oneof__Command! usual: Int } -type oneof__Response__Var2 { +type oneof__Response__Var2 implements oneof__Response__Interface { response: String! usual: Int } diff --git a/src/core/ir/resolver_context_like.rs b/src/core/ir/resolver_context_like.rs index 190f91e5b2..93dfaaf1a1 100644 --- a/src/core/ir/resolver_context_like.rs +++ b/src/core/ir/resolver_context_like.rs @@ -96,8 +96,12 @@ impl SelectionField { field: &crate::core::jit::Field, ConstValue>, ) -> SelectionField { let name = field.output_name.to_string(); + let type_name = field.type_of.name(); let selection_set = field - .nested_iter(field.type_of.name()) + .iter_only(|field| match &field.type_condition { + Some(type_condition) => type_condition == type_name, + None => true, + }) .map(Self::from_jit_field) .collect(); let args = field diff --git a/src/core/jit/builder.rs b/src/core/jit/builder.rs index 0d9a0c88ea..7e662d9c03 100644 --- a/src/core/jit/builder.rs +++ b/src/core/jit/builder.rs @@ -220,7 +220,7 @@ impl Builder { .unwrap_or(field_name.to_owned()), ir, type_of, - type_condition: type_condition.to_string(), + type_condition: Some(type_condition.to_string()), skip, include, args, @@ -241,7 +241,9 @@ impl Builder { name: "String".to_owned(), non_null: true, }, - type_condition: type_condition.to_string(), + // __typename has a special meaning and could be applied + // to any type + type_condition: None, skip, include, args: Vec::new(), diff --git a/src/core/jit/exec.rs b/src/core/jit/exec.rs index aa378f08fd..ee9a890bb2 100644 --- a/src/core/jit/exec.rs +++ b/src/core/jit/exec.rs @@ -94,13 +94,16 @@ where // 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 = 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); - async move { self.execute(&ctx, data_path).await } - })) + join_all( + self.request + .plan() + .field_iter_only(field, value) + .map(|field| { + let ctx = ctx.with_value_and_field(value, field); + let data_path = data_path.clone().with_index(index); + async move { self.execute(&ctx, data_path).await } + }), + ) })) .await; } @@ -111,13 +114,16 @@ 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 = 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); - let data_path = data_path.clone(); - async move { self.execute(&ctx, data_path).await } - })) + join_all( + self.request + .plan() + .field_iter_only(field, value) + .map(|child| { + let ctx = ctx.with_value_and_field(value, child); + let data_path = data_path.clone(); + async move { self.execute(&ctx, data_path).await } + }), + ) .await; } diff --git a/src/core/jit/model.rs b/src/core/jit/model.rs index bcee777ce4..0bf448dac6 100644 --- a/src/core/jit/model.rs +++ b/src/core/jit/model.rs @@ -10,6 +10,7 @@ use serde::{Deserialize, Serialize}; use super::Error; use crate::core::blueprint::Index; use crate::core::ir::model::IR; +use crate::core::ir::TypedValue; use crate::core::json::JsonLike; #[derive(Debug, Deserialize, Clone)] @@ -65,6 +66,14 @@ impl Field { skip == include } + + /// Returns the __typename of the value related to this field + pub fn value_type<'a, Output>(&'a self, value: &'a Output) -> &'a str + where + Output: TypedValue<'a>, + { + value.get_type_name().unwrap_or(self.type_of.name()) + } } #[derive(Debug, Clone)] @@ -138,7 +147,7 @@ pub struct Field { /// The type could be anything from graphql type system: /// interface, type, union, input type. /// See [spec](https://spec.graphql.org/October2021/#sec-Type-Conditions) - pub type_condition: String, + pub type_condition: Option, pub skip: Option, pub include: Option, pub args: Vec>, @@ -234,27 +243,15 @@ impl Field { } impl Field, Input> { - /// iters over children fields that are - /// related to passed `type_name` either - /// as direct field of the queried type or - /// field from fragment on type `type_name` - pub fn nested_iter<'a>( + /// iters over children fields that satisfies + /// passed filter_fn + pub fn iter_only<'a>( &'a self, - type_name: &'a str, + mut filter_fn: impl FnMut(&'a Field, Input>) -> bool + 'a, ) -> impl Iterator, Input>> + 'a { self.extensions .as_ref() - .map(move |nested| { - nested - .0 - .iter() - // TODO: handle Interface and Union types here - // Right now only exact type name is used to check the set of fields - // but with Interfaces/Unions we need to check if that specific type - // is member of some Interface/Union and if so call the fragments for - // the related Interfaces/Unions - .filter(move |field| field.type_condition == type_name) - }) + .map(move |nested| nested.0.iter().filter(move |&field| filter_fn(field))) .into_iter() .flatten() } @@ -351,7 +348,6 @@ pub struct OperationPlan { flat: Vec>, operation_type: OperationType, nested: Vec, Input>>, - // TODO: drop index from here. Embed all the necessary information in each field of the plan. pub index: Arc, } @@ -409,30 +405,37 @@ impl OperationPlan { Self { flat: fields, nested, operation_type, index } } + /// Returns a graphQL operation type pub fn operation_type(&self) -> OperationType { self.operation_type } + /// Check if current graphQL operation is query pub fn is_query(&self) -> bool { self.operation_type == OperationType::Query } + /// Returns a nested [Field] representation pub fn as_nested(&self) -> &[Field, Input>] { &self.nested } + /// Returns an owned version of [Field] representation pub fn into_nested(self) -> Vec, Input>> { self.nested } + /// Returns a flat [Field] representation pub fn as_parent(&self) -> &[Field] { &self.flat } + /// Search for a field with a specified [FieldId] pub fn find_field(&self, id: FieldId) -> Option<&Field> { self.flat.iter().find(|field| field.id == id) } + /// Search for a field by specified path of nested fields pub fn find_field_path>(&self, path: &[S]) -> Option<&Field> { match path.split_first() { None => None, @@ -447,18 +450,22 @@ impl OperationPlan { } } + /// Returns number of fields in plan pub fn size(&self) -> usize { self.flat.len() } + /// Check if the field is of scalar type pub fn field_is_scalar(&self, field: &Field) -> bool { self.index.type_is_scalar(field.type_of.name()) } + /// Check if the field is of enum type pub fn field_is_enum(&self, field: &Field) -> bool { self.index.type_is_enum(field.type_of.name()) } + /// Validate the value against enum variants of the field pub fn field_validate_enum_value( &self, field: &Field, @@ -466,6 +473,25 @@ impl OperationPlan { ) -> bool { self.index.validate_enum_value(field.type_of.name(), value) } + + /// Iterate over nested fields that are related to the __typename of the + /// value + pub fn field_iter_only<'a, Output>( + &'a self, + field: &'a Field, Input>, + value: &'a Output, + ) -> impl Iterator, Input>> + where + Output: TypedValue<'a>, + { + let value_type = field.value_type(value); + + field.iter_only(move |field| match &field.type_condition { + Some(type_condition) => self.index.is_type_implements(value_type, type_condition), + // if there is no type_condition restriction then use this field + None => true, + }) + } } #[derive(Clone, Debug)] 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 index 0e0d13d8e2..70ebd89e69 100644 --- 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 @@ -9,7 +9,9 @@ expression: plan.into_nested() output_name: "articles", ir: "Some(..)", type_of: [Post], - type_condition: "Query", + type_condition: Some( + "Query", + ), extensions: Some( Nested( [ @@ -19,7 +21,9 @@ expression: plan.into_nested() output_name: "author", ir: "Some(..)", type_of: User, - type_condition: "Post", + type_condition: Some( + "Post", + ), extensions: Some( Nested( [ @@ -28,7 +32,9 @@ expression: plan.into_nested() name: "id", output_name: "identifier", type_of: ID!, - type_condition: "User", + type_condition: Some( + "User", + ), 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 b9ce57c2f4..2ab1e80e91 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 @@ -9,7 +9,9 @@ expression: plan.into_nested() output_name: "createPost", ir: "Some(..)", type_of: Post, - type_condition: "Mutation", + type_condition: Some( + "Mutation", + ), args: [ Arg { id: 0, @@ -47,7 +49,9 @@ expression: plan.into_nested() name: "id", output_name: "id", type_of: ID!, - type_condition: "Post", + type_condition: Some( + "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 a93aafa00d..b35eadcd1d 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 @@ -9,7 +9,9 @@ expression: plan.into_nested() output_name: "users", ir: "Some(..)", type_of: [User], - type_condition: "Query", + type_condition: Some( + "Query", + ), extensions: Some( Nested( [ @@ -18,7 +20,9 @@ expression: plan.into_nested() name: "id", output_name: "id", type_of: ID!, - type_condition: "User", + type_condition: Some( + "User", + ), directives: [ Directive { name: "options", @@ -38,7 +42,9 @@ expression: plan.into_nested() name: "name", output_name: "name", type_of: String!, - type_condition: "User", + type_condition: Some( + "User", + ), include: Some( Variable( "includeName", 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 40266d97e5..b8201fa695 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 @@ -9,7 +9,9 @@ expression: plan.into_nested() output_name: "user", ir: "Some(..)", type_of: User, - type_condition: "Query", + type_condition: Some( + "Query", + ), args: [ Arg { id: 0, @@ -31,7 +33,9 @@ expression: plan.into_nested() name: "name", output_name: "name", type_of: String!, - type_condition: "User", + type_condition: Some( + "User", + ), directives: [], }, Field { @@ -39,7 +43,9 @@ expression: plan.into_nested() name: "email", output_name: "email", type_of: String!, - type_condition: "User", + type_condition: Some( + "User", + ), directives: [], }, Field { @@ -47,7 +53,9 @@ expression: plan.into_nested() name: "phone", output_name: "phone", type_of: String, - type_condition: "User", + type_condition: Some( + "User", + ), directives: [], }, Field { @@ -55,7 +63,9 @@ expression: plan.into_nested() name: "title", output_name: "title", type_of: String!, - type_condition: "Post", + type_condition: Some( + "Post", + ), directives: [], }, Field { @@ -63,7 +73,9 @@ expression: plan.into_nested() name: "body", output_name: "body", type_of: String!, - type_condition: "Post", + type_condition: Some( + "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 c1e6d168b7..a66c07dc6e 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 @@ -9,7 +9,9 @@ expression: plan.into_nested() output_name: "posts", ir: "Some(..)", type_of: [Post], - type_condition: "Query", + type_condition: Some( + "Query", + ), extensions: Some( Nested( [ @@ -19,7 +21,9 @@ expression: plan.into_nested() output_name: "user", ir: "Some(..)", type_of: User, - type_condition: "Post", + type_condition: Some( + "Post", + ), extensions: Some( Nested( [ @@ -28,7 +32,9 @@ expression: plan.into_nested() name: "id", output_name: "id", type_of: ID!, - type_condition: "User", + type_condition: Some( + "User", + ), directives: [], }, Field { @@ -36,7 +42,9 @@ expression: plan.into_nested() name: "name", output_name: "name", type_of: String!, - type_condition: "User", + type_condition: Some( + "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 31fc82f0ba..999533bf3e 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 @@ -9,7 +9,9 @@ expression: plan.into_nested() output_name: "user", ir: "Some(..)", type_of: User, - type_condition: "Query", + type_condition: Some( + "Query", + ), args: [ Arg { id: 0, @@ -31,7 +33,9 @@ expression: plan.into_nested() name: "id", output_name: "id", type_of: ID!, - type_condition: "User", + type_condition: Some( + "User", + ), directives: [], }, Field { @@ -39,7 +43,9 @@ expression: plan.into_nested() name: "username", output_name: "username", type_of: String!, - type_condition: "User", + type_condition: Some( + "User", + ), directives: [], }, ], @@ -53,7 +59,9 @@ expression: plan.into_nested() output_name: "posts", ir: "Some(..)", type_of: [Post], - type_condition: "Query", + type_condition: Some( + "Query", + ), extensions: Some( Nested( [ @@ -62,7 +70,9 @@ expression: plan.into_nested() name: "id", output_name: "id", type_of: ID!, - type_condition: "Post", + type_condition: Some( + "Post", + ), directives: [], }, Field { @@ -70,7 +80,9 @@ expression: plan.into_nested() name: "title", output_name: "title", type_of: String!, - type_condition: "Post", + type_condition: Some( + "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 84141b1a62..1ef844578d 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 @@ -9,7 +9,9 @@ expression: plan.into_nested() output_name: "createPost", ir: "Some(..)", type_of: Post, - type_condition: "Mutation", + type_condition: Some( + "Mutation", + ), args: [ Arg { id: 0, @@ -47,7 +49,9 @@ expression: plan.into_nested() name: "id", output_name: "id", type_of: ID!, - type_condition: "Post", + type_condition: Some( + "Post", + ), directives: [], }, Field { @@ -55,7 +59,9 @@ expression: plan.into_nested() name: "userId", output_name: "userId", type_of: ID!, - type_condition: "Post", + type_condition: Some( + "Post", + ), directives: [], }, Field { @@ -63,7 +69,9 @@ expression: plan.into_nested() name: "title", output_name: "title", type_of: String!, - type_condition: "Post", + type_condition: Some( + "Post", + ), directives: [], }, Field { @@ -71,7 +79,9 @@ expression: plan.into_nested() name: "body", output_name: "body", type_of: String!, - type_condition: "Post", + type_condition: Some( + "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 9a433b4292..8c61134907 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 @@ -9,7 +9,9 @@ expression: plan.into_nested() output_name: "posts", ir: "Some(..)", type_of: [Post], - type_condition: "Query", + type_condition: Some( + "Query", + ), extensions: Some( Nested( [ @@ -18,7 +20,9 @@ expression: plan.into_nested() name: "id", output_name: "id", type_of: ID!, - type_condition: "Post", + type_condition: Some( + "Post", + ), directives: [], }, Field { @@ -26,7 +30,9 @@ expression: plan.into_nested() name: "userId", output_name: "userId", type_of: ID!, - type_condition: "Post", + type_condition: Some( + "Post", + ), directives: [], }, Field { @@ -34,7 +40,9 @@ expression: plan.into_nested() name: "title", output_name: "title", type_of: String!, - type_condition: "Post", + type_condition: Some( + "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 d447afdc34..242fca4dfd 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 @@ -9,7 +9,9 @@ expression: plan.into_nested() output_name: "createUser", ir: "Some(..)", type_of: User, - type_condition: "Mutation", + type_condition: Some( + "Mutation", + ), args: [ Arg { id: 0, @@ -62,7 +64,9 @@ expression: plan.into_nested() name: "id", output_name: "id", type_of: ID!, - type_condition: "User", + type_condition: Some( + "User", + ), directives: [], }, Field { @@ -70,7 +74,9 @@ expression: plan.into_nested() name: "name", output_name: "name", type_of: String!, - type_condition: "User", + type_condition: Some( + "User", + ), directives: [], }, Field { @@ -78,7 +84,9 @@ expression: plan.into_nested() name: "email", output_name: "email", type_of: String!, - type_condition: "User", + type_condition: Some( + "User", + ), directives: [], }, Field { @@ -86,7 +94,9 @@ expression: plan.into_nested() name: "phone", output_name: "phone", type_of: String, - type_condition: "User", + type_condition: Some( + "User", + ), directives: [], }, Field { @@ -94,7 +104,9 @@ expression: plan.into_nested() name: "website", output_name: "website", type_of: String, - type_condition: "User", + type_condition: Some( + "User", + ), directives: [], }, Field { @@ -102,7 +114,9 @@ expression: plan.into_nested() name: "username", output_name: "username", type_of: String!, - type_condition: "User", + type_condition: Some( + "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 305af5ecbe..b4109ee4ac 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 @@ -9,7 +9,9 @@ expression: plan.into_nested() output_name: "posts", ir: "Some(..)", type_of: [Post], - type_condition: "Query", + type_condition: Some( + "Query", + ), extensions: Some( Nested( [ @@ -19,7 +21,9 @@ expression: plan.into_nested() output_name: "user", ir: "Some(..)", type_of: User, - type_condition: "Post", + type_condition: Some( + "Post", + ), extensions: Some( Nested( [ @@ -28,7 +32,9 @@ expression: plan.into_nested() name: "id", output_name: "id", type_of: ID!, - type_condition: "User", + type_condition: Some( + "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 5d1a5b0b6f..46b9485a94 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 @@ -9,7 +9,9 @@ expression: plan.into_nested() output_name: "getUserIdOrEmail", ir: "Some(..)", type_of: UserIdOrEmail, - type_condition: "Query", + type_condition: Some( + "Query", + ), args: [ Arg { id: 0, @@ -31,7 +33,9 @@ expression: plan.into_nested() name: "id", output_name: "id", type_of: ID!, - type_condition: "UserId", + type_condition: Some( + "UserId", + ), directives: [], }, Field { @@ -39,7 +43,9 @@ expression: plan.into_nested() name: "email", output_name: "email", type_of: String!, - type_condition: "UserEmail", + type_condition: Some( + "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 0c7d65e877..0c29d55b1f 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 @@ -9,7 +9,9 @@ expression: plan.into_nested() output_name: "user", ir: "Some(..)", type_of: User, - type_condition: "Query", + type_condition: Some( + "Query", + ), args: [ Arg { id: 0, @@ -31,7 +33,9 @@ expression: plan.into_nested() name: "id", output_name: "id", type_of: ID!, - type_condition: "User", + type_condition: Some( + "User", + ), directives: [], }, Field { @@ -39,7 +43,9 @@ expression: plan.into_nested() name: "name", output_name: "name", type_of: String!, - type_condition: "User", + type_condition: Some( + "User", + ), directives: [], }, ], diff --git a/src/core/jit/synth/snapshots/tailcall__core__jit__synth__synth__tests__json_placeholder_typename.snap b/src/core/jit/synth/snapshots/tailcall__core__jit__synth__synth__tests__json_placeholder_typename.snap new file mode 100644 index 0000000000..c00b1c6293 --- /dev/null +++ b/src/core/jit/synth/snapshots/tailcall__core__jit__synth__synth__tests__json_placeholder_typename.snap @@ -0,0 +1,808 @@ +--- +source: src/core/jit/synth/synth.rs +expression: "serde_json::to_string_pretty(&val).unwrap()" +--- +{ + "posts": [ + { + "id": 1, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 1 + } + }, + { + "id": 2, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 1 + } + }, + { + "id": 3, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 1 + } + }, + { + "id": 4, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 1 + } + }, + { + "id": 5, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 1 + } + }, + { + "id": 6, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 1 + } + }, + { + "id": 7, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 1 + } + }, + { + "id": 8, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 1 + } + }, + { + "id": 9, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 1 + } + }, + { + "id": 10, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 1 + } + }, + { + "id": 11, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 2 + } + }, + { + "id": 12, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 2 + } + }, + { + "id": 13, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 2 + } + }, + { + "id": 14, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 2 + } + }, + { + "id": 15, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 2 + } + }, + { + "id": 16, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 2 + } + }, + { + "id": 17, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 2 + } + }, + { + "id": 18, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 2 + } + }, + { + "id": 19, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 2 + } + }, + { + "id": 20, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 2 + } + }, + { + "id": 21, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 3 + } + }, + { + "id": 22, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 3 + } + }, + { + "id": 23, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 3 + } + }, + { + "id": 24, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 3 + } + }, + { + "id": 25, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 3 + } + }, + { + "id": 26, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 3 + } + }, + { + "id": 27, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 3 + } + }, + { + "id": 28, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 3 + } + }, + { + "id": 29, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 3 + } + }, + { + "id": 30, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 3 + } + }, + { + "id": 31, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 4 + } + }, + { + "id": 32, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 4 + } + }, + { + "id": 33, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 4 + } + }, + { + "id": 34, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 4 + } + }, + { + "id": 35, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 4 + } + }, + { + "id": 36, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 4 + } + }, + { + "id": 37, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 4 + } + }, + { + "id": 38, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 4 + } + }, + { + "id": 39, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 4 + } + }, + { + "id": 40, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 4 + } + }, + { + "id": 41, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 5 + } + }, + { + "id": 42, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 5 + } + }, + { + "id": 43, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 5 + } + }, + { + "id": 44, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 5 + } + }, + { + "id": 45, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 5 + } + }, + { + "id": 46, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 5 + } + }, + { + "id": 47, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 5 + } + }, + { + "id": 48, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 5 + } + }, + { + "id": 49, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 5 + } + }, + { + "id": 50, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 5 + } + }, + { + "id": 51, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 6 + } + }, + { + "id": 52, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 6 + } + }, + { + "id": 53, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 6 + } + }, + { + "id": 54, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 6 + } + }, + { + "id": 55, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 6 + } + }, + { + "id": 56, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 6 + } + }, + { + "id": 57, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 6 + } + }, + { + "id": 58, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 6 + } + }, + { + "id": 59, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 6 + } + }, + { + "id": 60, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 6 + } + }, + { + "id": 61, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 7 + } + }, + { + "id": 62, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 7 + } + }, + { + "id": 63, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 7 + } + }, + { + "id": 64, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 7 + } + }, + { + "id": 65, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 7 + } + }, + { + "id": 66, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 7 + } + }, + { + "id": 67, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 7 + } + }, + { + "id": 68, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 7 + } + }, + { + "id": 69, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 7 + } + }, + { + "id": 70, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 7 + } + }, + { + "id": 71, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 8 + } + }, + { + "id": 72, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 8 + } + }, + { + "id": 73, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 8 + } + }, + { + "id": 74, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 8 + } + }, + { + "id": 75, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 8 + } + }, + { + "id": 76, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 8 + } + }, + { + "id": 77, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 8 + } + }, + { + "id": 78, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 8 + } + }, + { + "id": 79, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 8 + } + }, + { + "id": 80, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 8 + } + }, + { + "id": 81, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 9 + } + }, + { + "id": 82, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 9 + } + }, + { + "id": 83, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 9 + } + }, + { + "id": 84, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 9 + } + }, + { + "id": 85, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 9 + } + }, + { + "id": 86, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 9 + } + }, + { + "id": 87, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 9 + } + }, + { + "id": 88, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 9 + } + }, + { + "id": 89, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 9 + } + }, + { + "id": 90, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 9 + } + }, + { + "id": 91, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 10 + } + }, + { + "id": 92, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 10 + } + }, + { + "id": 93, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 10 + } + }, + { + "id": 94, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 10 + } + }, + { + "id": 95, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 10 + } + }, + { + "id": 96, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 10 + } + }, + { + "id": 97, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 10 + } + }, + { + "id": 98, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 10 + } + }, + { + "id": 99, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 10 + } + }, + { + "id": 100, + "__typename": "Post", + "user": { + "__typename": "User", + "id": 10 + } + } + ] +} diff --git a/src/core/jit/synth/synth.rs b/src/core/jit/synth/synth.rs index 387952bb30..0161de44a8 100644 --- a/src/core/jit/synth/synth.rs +++ b/src/core/jit/synth/synth.rs @@ -1,4 +1,3 @@ -use crate::core::ir::TypedValue; use crate::core::jit::model::{Field, Nested, OperationPlan, Variables}; use crate::core::jit::store::{Data, DataPath, Store}; use crate::core::jit::{Error, PathSegment, Positioned, ValidationError}; @@ -164,15 +163,18 @@ where (_, Some(obj)) => { let mut ans = Value::JsonObject::new(); - let type_name = value.get_type_name().unwrap_or(node.type_of.name()); - - for child in node.nested_iter(type_name) { + for child in self.plan.field_iter_only(node, value) { // all checks for skip must occur in `iter_inner` // and include be checked before calling `iter` or recursing. let include = self.include(child); if include { - let val = obj.get_key(child.name.as_str()); - ans.insert_key(&child.output_name, self.iter(child, val, data_path)?); + let value = if child.name == "__typename" { + Value::string(node.value_type(value).into()) + } else { + let val = obj.get_key(child.name.as_str()); + self.iter(child, val, data_path)? + }; + ans.insert_key(&child.output_name, value); } } @@ -424,4 +426,12 @@ mod tests { let val: serde_json_borrow::Value = synth.synthesize().unwrap(); insta::assert_snapshot!(serde_json::to_string_pretty(&val).unwrap()) } + + #[test] + fn test_json_placeholder_typename() { + let jp = JP::init("{ posts { id __typename user { __typename id } } }", None); + let synth = jp.synth(); + let val: serde_json_borrow::Value = synth.synthesize().unwrap(); + insta::assert_snapshot!(serde_json::to_string_pretty(&val).unwrap()) + } } diff --git a/tests/core/snapshots/grpc-oneof.md_client.snap b/tests/core/snapshots/grpc-oneof.md_client.snap index 4b75edbacd..a3bd64ef14 100644 --- a/tests/core/snapshots/grpc-oneof.md_client.snap +++ b/tests/core/snapshots/grpc-oneof.md_client.snap @@ -116,21 +116,25 @@ input oneof__Request__Var__Var1 { union oneof__Response = oneof__Response__Var | oneof__Response__Var0 | oneof__Response__Var1 | oneof__Response__Var2 -type oneof__Response__Var { +interface oneof__Response__Interface { usual: Int } -type oneof__Response__Var0 { +type oneof__Response__Var implements oneof__Response__Interface { + usual: Int +} + +type oneof__Response__Var0 implements oneof__Response__Interface { payload: oneof__Payload! usual: Int } -type oneof__Response__Var1 { +type oneof__Response__Var1 implements oneof__Response__Interface { command: oneof__Command! usual: Int } -type oneof__Response__Var2 { +type oneof__Response__Var2 implements oneof__Response__Interface { response: String! usual: Int } diff --git a/tests/core/snapshots/grpc-oneof.md_merged.snap b/tests/core/snapshots/grpc-oneof.md_merged.snap index 2e65b34afa..679c54ba45 100644 --- a/tests/core/snapshots/grpc-oneof.md_merged.snap +++ b/tests/core/snapshots/grpc-oneof.md_merged.snap @@ -65,6 +65,10 @@ input oneof__Request__Var__Var1 { usual: String } +interface oneof__Response__Interface { + usual: Int +} + union oneof__Response = oneof__Response__Var | oneof__Response__Var0 | oneof__Response__Var1 | oneof__Response__Var2 type Query { @@ -96,21 +100,21 @@ type oneof__Payload { payload: String } -type oneof__Response__Var { +type oneof__Response__Var implements oneof__Response__Interface { usual: Int } -type oneof__Response__Var0 { +type oneof__Response__Var0 implements oneof__Response__Interface { payload: oneof__Payload! usual: Int } -type oneof__Response__Var1 { +type oneof__Response__Var1 implements oneof__Response__Interface { command: oneof__Command! usual: Int } -type oneof__Response__Var2 { +type oneof__Response__Var2 implements oneof__Response__Interface { response: String! usual: Int } diff --git a/tests/execution/grpc-oneof.md b/tests/execution/grpc-oneof.md index eb5857799e..3b23f10b19 100644 --- a/tests/execution/grpc-oneof.md +++ b/tests/execution/grpc-oneof.md @@ -102,6 +102,10 @@ input oneof__Request__Var__Var1 { usual: String } +interface oneof__Response__Interface { + usual: Int +} + union oneof__Response = oneof__Response__Var | oneof__Response__Var0 | oneof__Response__Var1 | oneof__Response__Var2 type Query { @@ -133,21 +137,21 @@ type oneof__Payload { payload: String } -type oneof__Response__Var { +type oneof__Response__Var implements oneof__Response__Interface { usual: Int } -type oneof__Response__Var0 { +type oneof__Response__Var0 implements oneof__Response__Interface { payload: oneof__Payload! usual: Int } -type oneof__Response__Var1 { +type oneof__Response__Var1 implements oneof__Response__Interface { command: oneof__Command! usual: Int } -type oneof__Response__Var2 { +type oneof__Response__Var2 implements oneof__Response__Interface { response: String! usual: Int } @@ -169,9 +173,9 @@ type oneof__Response__Var2 { query: > query { oneof__OneOfService__GetOneOfVar1(request: { command: { command: "start" } }) { - # TODO: check that it's possible to get shared field from Union like that - # outside of the fragment - usual + ... on oneof__Response__Interface { + usual + } ... on oneof__Response__Var1 { command { command From 1d34275c34a68c4182a9c7d5f7cb1f2225e5ac98 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 28 Aug 2024 19:46:37 +0000 Subject: [PATCH 05/14] fix(deps): update dependency type-fest to v4.26.0 --- npm/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/npm/package-lock.json b/npm/package-lock.json index 70ea8d98db..7835e97807 100644 --- a/npm/package-lock.json +++ b/npm/package-lock.json @@ -863,9 +863,9 @@ } }, "node_modules/type-fest": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.25.0.tgz", - "integrity": "sha512-bRkIGlXsnGBRBQRAY56UXBm//9qH4bmJfFvq83gSz41N282df+fjy8ofcEgc1sM8geNt5cl6mC2g9Fht1cs8Aw==", + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.0.tgz", + "integrity": "sha512-OduNjVJsFbifKb57UqZ2EMP1i4u64Xwow3NYXUtBbD4vIwJdQd4+xl8YDou1dlm4DVrtwT/7Ky8z8WyCULVfxw==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" From a94f77f4a8c3c7aa47c98695f2bea0ef3679b51d Mon Sep 17 00:00:00 2001 From: Sandipsinh Dilipsinh Rathod <62684960+ssddOnTop@users.noreply.github.com> Date: Thu, 29 Aug 2024 10:42:49 +0000 Subject: [PATCH 06/14] chore: cli tests (#2767) Co-authored-by: Tushar Mathur --- ...re__jit__response__test__with_error-2.snap | 23 ------ tests/cli/gen.rs | 14 +--- ...__fixtures__generator__gen_deezer.md.snap} | 0 ...nerator__gen_json_proto_mix_config.md.snap | 81 +++++++++++++++++++ ...s__generator__gen_jsonplaceholder.md.snap} | 0 5 files changed, 83 insertions(+), 35 deletions(-) delete mode 100644 src/core/jit/snapshots/tailcall__core__jit__response__test__with_error-2.snap rename tests/cli/snapshots/{cli_spec__test__generator_spec__tests__cli__fixtures__generator__gen_deezer.json.snap => cli_spec__test__generator_spec__tests__cli__fixtures__generator__gen_deezer.md.snap} (100%) create mode 100644 tests/cli/snapshots/cli_spec__test__generator_spec__tests__cli__fixtures__generator__gen_json_proto_mix_config.md.snap rename tests/cli/snapshots/{cli_spec__test__generator_spec__tests__cli__fixtures__generator__gen_jsonplaceholder.json.snap => cli_spec__test__generator_spec__tests__cli__fixtures__generator__gen_jsonplaceholder.md.snap} (100%) diff --git a/src/core/jit/snapshots/tailcall__core__jit__response__test__with_error-2.snap b/src/core/jit/snapshots/tailcall__core__jit__response__test__with_error-2.snap deleted file mode 100644 index 6ec6584bca..0000000000 --- a/src/core/jit/snapshots/tailcall__core__jit__response__test__with_error-2.snap +++ /dev/null @@ -1,23 +0,0 @@ ---- -source: src/core/jit/response.rs -expression: response.into_async_graphql() ---- -Response { - data: Null, - extensions: {}, - cache_control: CacheControl { - public: true, - max_age: 0, - }, - errors: [ - ServerError { - message: "internal: non-null types require a return value", - locations: [ - Pos(1:2), - ], - path: [], - extensions: None, - }, - ], - http_headers: {}, -} diff --git a/tests/cli/gen.rs b/tests/cli/gen.rs index 90955a5fa1..030ff69c40 100644 --- a/tests/cli/gen.rs +++ b/tests/cli/gen.rs @@ -335,18 +335,8 @@ pub mod test { } } async fn test_generator(path: &Path) -> datatest_stable::Result<()> { - if let Some(extension) = path.extension() { - if extension == "json" - && path - .file_name() - .and_then(|v| v.to_str()) - .map(|v| v.starts_with("gen")) - .unwrap_or_default() - { - let spec = ExecutionSpec::from_source(path, std::fs::read_to_string(path)?)?; - generator_spec::run_test(path, spec).await?; - } - } + let spec = ExecutionSpec::from_source(path, std::fs::read_to_string(path)?)?; + generator_spec::run_test(path, spec).await?; Ok(()) } pub fn run(path: &Path) -> datatest_stable::Result<()> { diff --git a/tests/cli/snapshots/cli_spec__test__generator_spec__tests__cli__fixtures__generator__gen_deezer.json.snap b/tests/cli/snapshots/cli_spec__test__generator_spec__tests__cli__fixtures__generator__gen_deezer.md.snap similarity index 100% rename from tests/cli/snapshots/cli_spec__test__generator_spec__tests__cli__fixtures__generator__gen_deezer.json.snap rename to tests/cli/snapshots/cli_spec__test__generator_spec__tests__cli__fixtures__generator__gen_deezer.md.snap diff --git a/tests/cli/snapshots/cli_spec__test__generator_spec__tests__cli__fixtures__generator__gen_json_proto_mix_config.md.snap b/tests/cli/snapshots/cli_spec__test__generator_spec__tests__cli__fixtures__generator__gen_json_proto_mix_config.md.snap new file mode 100644 index 0000000000..74ddc5e649 --- /dev/null +++ b/tests/cli/snapshots/cli_spec__test__generator_spec__tests__cli__fixtures__generator__gen_json_proto_mix_config.md.snap @@ -0,0 +1,81 @@ +--- +source: tests/cli/gen.rs +expression: config.to_sdl() +--- +schema @server @upstream(baseURL: "https://jsonplaceholder.typicode.com") { + query: Query +} + +input Id { + id: Int +} + +input news__MultipleNewsId @addField(name: "ids", path: ["ids", "id"]) { + ids: [Id]@omit +} + +input news__NewsInput { + body: String + id: Int + postImage: String + status: news__Status + title: String +} + +enum news__Status { + DELETED + DRAFT + PUBLISHED +} + +type Address { + city: String + geo: Geo + street: String + suite: String + zipcode: String +} + +type Company { + bs: String + catchPhrase: String + name: String +} + +type Geo { + lat: String + lng: String +} + +type News { + body: String + id: Int + postImage: String + status: news__Status + title: String +} + +type NewsNewsServiceGetMultipleNew { + news: [News] +} + +type Query { + news__NewsService__AddNews(news: news__NewsInput!): News @grpc(body: "{{.args.news}}", method: "news.NewsService.AddNews") + news__NewsService__DeleteNews(newsId: news__NewsId!): Empty @grpc(body: "{{.args.newsId}}", method: "news.NewsService.DeleteNews") + news__NewsService__EditNews(news: news__NewsInput!): News @grpc(body: "{{.args.news}}", method: "news.NewsService.EditNews") + news__NewsService__GetAllNews: NewsNewsServiceGetMultipleNew @grpc(method: "news.NewsService.GetAllNews") + news__NewsService__GetMultipleNews(multipleNewsId: news__MultipleNewsId!): NewsNewsServiceGetMultipleNew @grpc(body: "{{.args.multipleNewsId}}", method: "news.NewsService.GetMultipleNews") + news__NewsService__GetNews(newsId: news__NewsId!): News @grpc(body: "{{.args.newsId}}", method: "news.NewsService.GetNews") + users: [User] @http(path: "/users") +} + +type User { + address: Address + company: Company + email: String + id: Int + name: String + phone: String + username: String + website: String +} diff --git a/tests/cli/snapshots/cli_spec__test__generator_spec__tests__cli__fixtures__generator__gen_jsonplaceholder.json.snap b/tests/cli/snapshots/cli_spec__test__generator_spec__tests__cli__fixtures__generator__gen_jsonplaceholder.md.snap similarity index 100% rename from tests/cli/snapshots/cli_spec__test__generator_spec__tests__cli__fixtures__generator__gen_jsonplaceholder.json.snap rename to tests/cli/snapshots/cli_spec__test__generator_spec__tests__cli__fixtures__generator__gen_jsonplaceholder.md.snap From 7ed198cb217d4a6b240bebe81d70471ae4a3939e Mon Sep 17 00:00:00 2001 From: laststylebender <43403528+laststylebender14@users.noreply.github.com> Date: Thu, 29 Aug 2024 17:08:55 +0530 Subject: [PATCH 07/14] feat: add allowed headers in configuration (#2706) Co-authored-by: Tushar Mathur --- benches/from_json_bench.rs | 1 + src/cli/generator/config.rs | 4 + src/cli/generator/generator.rs | 1 + src/core/generator/from_json.rs | 84 +++++++---- src/core/generator/generator.rs | 136 +++++++++++------- .../generator/json/operation_generator.rs | 51 +++---- src/core/generator/json/schema_generator.rs | 35 ++++- ...son__tests__generate_config_from_json.snap | 22 +-- ...erate_from_config_from_multiple_jsons.snap | 18 +-- ...test__should_generate_combined_config.snap | 6 +- ...est__should_generate_config_from_json.snap | 6 +- .../json/incompatible_properties.json | 7 +- .../json/incompatible_root_object.json | 2 +- .../generator/tests/fixtures/json/list.json | 2 +- .../json/list_incompatible_object.json | 2 +- .../tests/fixtures/json/nested_list.json | 2 +- .../fixtures/json/nested_same_properties.json | 2 +- .../generator/tests/json_to_config_spec.rs | 8 +- ...ig_spec__incompatible_properties.json.snap | 4 +- ...g_spec__incompatible_root_object.json.snap | 2 +- .../json_to_config_spec__list.json.snap | 2 +- ...g_spec__list_incompatible_object.json.snap | 2 +- ...json_to_config_spec__nested_list.json.snap | 2 +- ...fig_spec__nested_same_properties.json.snap | 2 +- .../fixtures/generator/gen_jsonplaceholder.md | 4 + ...rator__gen_json_proto_mix_config.json.snap | 81 +++++++++++ ...es__generator__gen_jsonplaceholder.md.snap | 2 +- 27 files changed, 332 insertions(+), 158 deletions(-) create mode 100644 tests/cli/snapshots/cli_spec__test__generator_spec__tests__cli__fixtures__generator__gen_json_proto_mix_config.json.snap diff --git a/benches/from_json_bench.rs b/benches/from_json_bench.rs index 4e43769d56..e953d9459d 100644 --- a/benches/from_json_bench.rs +++ b/benches/from_json_bench.rs @@ -27,6 +27,7 @@ pub fn benchmark_from_json_method(c: &mut Criterion) { res_body: reqs[0].clone(), field_name: "f1".to_string(), is_mutation: false, + headers: None, }]; let config_generator = Generator::default().inputs(cfg_gen_reqs); diff --git a/src/cli/generator/config.rs b/src/cli/generator/config.rs index a5f4859781..cc9296d5cc 100644 --- a/src/cli/generator/config.rs +++ b/src/cli/generator/config.rs @@ -196,6 +196,10 @@ impl Location { } impl Headers { + pub fn into_btree_map(self) -> Option> { + self.0 + } + pub fn as_btree_map(&self) -> &Option> { &self.0 } diff --git a/src/cli/generator/generator.rs b/src/cli/generator/generator.rs index f34ce02616..96333fbbdc 100644 --- a/src/cli/generator/generator.rs +++ b/src/cli/generator/generator.rs @@ -135,6 +135,7 @@ impl Generator { res_body: serde_json::from_str(&response.content)?, field_name, is_mutation, + headers: headers.into_btree_map(), }); } Source::Proto { src } => { diff --git a/src/core/generator/from_json.rs b/src/core/generator/from_json.rs index 538b7068e4..ce82cbe87d 100644 --- a/src/core/generator/from_json.rs +++ b/src/core/generator/from_json.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{BTreeMap, BTreeSet, HashMap}; use convert_case::{Case, Casing}; use serde_json::Value; @@ -10,6 +10,7 @@ use crate::core::config::transformer::RenameTypes; use crate::core::config::{Config, GraphQLOperationType}; use crate::core::http::Method; use crate::core::merge_right::MergeRight; +use crate::core::mustache::TemplateString; use crate::core::transform::{Transform, TransformerOps}; use crate::core::valid::{Valid, Validator}; @@ -20,27 +21,46 @@ pub struct RequestSample { pub res_body: Value, pub field_name: String, pub operation_type: GraphQLOperationType, + pub headers: Option>, } impl RequestSample { - #[allow(clippy::too_many_arguments)] - pub fn new>( - url: Url, - method: Method, - body: serde_json::Value, - resp: Value, - field_name: T, - operation_type: GraphQLOperationType, - ) -> Self { + pub fn new(url: Url, response_body: Value, field_name: String) -> Self { Self { url, - method, - req_body: body, - res_body: resp, - field_name: field_name.into(), - operation_type, + field_name, + res_body: response_body, + method: Default::default(), + req_body: Default::default(), + headers: Default::default(), + operation_type: Default::default(), } } + + pub fn with_method(mut self, method: Method) -> Self { + self.method = method; + self + } + + pub fn with_req_body(mut self, req_body: Value) -> Self { + self.req_body = req_body; + self + } + + pub fn with_headers(mut self, headers: Option>) -> Self { + self.headers = headers; + self + } + + pub fn with_is_mutation(mut self, is_mutation: bool) -> Self { + let operation_type = if is_mutation { + GraphQLOperationType::Mutation + } else { + GraphQLOperationType::Query + }; + self.operation_type = operation_type; + self + } } pub struct FromJsonGenerator<'a> { @@ -89,12 +109,23 @@ impl Transform for FromJsonGenerator<'_> { ), }; + // collect the required header keys + let header_keys = sample.headers.as_ref().map(|headers_inner| { + headers_inner + .iter() + .map(|(k, _)| k.to_owned()) + .collect::>() + }); + let mut rename_types = HashMap::new(); rename_types.insert(existing_name, suggested_name); // these transformations are required in order to generate a base config. GraphQLTypesGenerator::new(sample, type_name_gen) - .pipe(json::SchemaGenerator::new(&sample.operation_type)) + .pipe(json::SchemaGenerator::new( + &sample.operation_type, + &header_keys, + )) .pipe(json::FieldBaseUrlGenerator::new( &sample.url, &sample.operation_type, @@ -113,10 +144,8 @@ impl Transform for FromJsonGenerator<'_> { #[cfg(test)] mod tests { use crate::core::config::transformer::Preset; - use crate::core::config::GraphQLOperationType; use crate::core::generator::generator::test::JsonFixture; use crate::core::generator::{FromJsonGenerator, NameGenerator, RequestSample}; - use crate::core::http::Method; use crate::core::transform::TransformerOps; use crate::core::valid::Validator; @@ -130,17 +159,16 @@ mod tests { "src/core/generator/tests/fixtures/json/nested_same_properties.json", "src/core/generator/tests/fixtures/json/incompatible_root_object.json", ]; - let field_name_generator = NameGenerator::new("f"); for fixture in fixtures { - let JsonFixture { url, response } = JsonFixture::read(fixture).await?; - request_samples.push(RequestSample::new( - url.parse()?, - Method::GET, - serde_json::Value::Null, - response, - field_name_generator.next(), - GraphQLOperationType::Query, - )); + let JsonFixture { request, response, is_mutation, field_name } = + JsonFixture::read(fixture).await?; + let req_sample = RequestSample::new(request.url, response, field_name) + .with_method(request.method) + .with_headers(request.headers) + .with_is_mutation(is_mutation) + .with_req_body(request.body.unwrap_or_default()); + + request_samples.push(req_sample); } let config = diff --git a/src/core/generator/generator.rs b/src/core/generator/generator.rs index 9420f87a6a..fee6674d91 100644 --- a/src/core/generator/generator.rs +++ b/src/core/generator/generator.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeMap; + use derive_setters::Setters; use prost_reflect::prost_types::FileDescriptorSet; use prost_reflect::DescriptorPool; @@ -6,9 +8,10 @@ use url::Url; use super::from_proto::from_proto; use super::{FromJsonGenerator, NameGenerator, RequestSample}; -use crate::core::config::{self, Config, ConfigModule, GraphQLOperationType, Link, LinkType}; +use crate::core::config::{self, Config, ConfigModule, Link, LinkType}; use crate::core::http::Method; use crate::core::merge_right::MergeRight; +use crate::core::mustache::TemplateString; use crate::core::proto_reader::ProtoMetadata; use crate::core::transform::{Transform, TransformerOps}; use crate::core::valid::Validator; @@ -34,6 +37,7 @@ pub enum Input { res_body: Value, field_name: String, is_mutation: bool, + headers: Option>, }, Proto(ProtoMetadata), Config { @@ -103,29 +107,25 @@ impl Generator { } Input::Json { url, - res_body: response, - field_name, - is_mutation, method, req_body, + res_body, + field_name, + is_mutation, + headers, } => { - let operation_type = if *is_mutation { - GraphQLOperationType::Mutation - } else { - GraphQLOperationType::Query - }; - - let request_sample = RequestSample::new( + let req_sample = RequestSample::new( url.to_owned(), - method.to_owned(), - req_body.to_owned(), - response.to_owned(), - field_name, - operation_type.to_owned(), - ); - config = config.merge_right( - self.generate_from_json(&type_name_generator, &[request_sample])?, - ); + res_body.to_owned(), + field_name.to_owned(), + ) + .with_method(method.to_owned()) + .with_headers(headers.to_owned()) + .with_is_mutation(is_mutation.to_owned()) + .with_req_body(req_body.to_owned()); + + config = config + .merge_right(self.generate_from_json(&type_name_generator, &[req_sample])?); } Input::Proto(proto_input) => { config = @@ -164,24 +164,39 @@ fn resolve_file_descriptor_set( #[cfg(test)] pub mod test { + use std::collections::BTreeMap; + use prost_reflect::prost_types::FileDescriptorSet; use serde::{Deserialize, Deserializer}; use serde_json::Value; + use url::Url; use super::Generator; use crate::core::config::transformer::Preset; use crate::core::generator::generator::Input; - use crate::core::generator::NameGenerator; use crate::core::http::Method; + use crate::core::mustache::TemplateString; use crate::core::proto_reader::ProtoMetadata; fn compile_protobuf(files: &[&str]) -> anyhow::Result { Ok(protox::compile(files, [tailcall_fixtures::protobuf::SELF])?) } + #[derive(Deserialize)] + pub struct Request { + pub url: Url, + #[serde(default)] + pub method: Method, + #[serde(default)] + pub body: Option, + pub headers: Option>, + } + pub struct JsonFixture { - pub url: String, - pub response: serde_json::Value, + pub request: Request, + pub response: Value, + pub is_mutation: bool, + pub field_name: String, } impl JsonFixture { @@ -199,12 +214,11 @@ pub mod test { { let json_content: Value = Value::deserialize(deserializer)?; - let url = json_content + let req_value = json_content .get("request") - .and_then(|req| req.get("url")) - .and_then(|url| url.as_str()) - .ok_or_else(|| serde::de::Error::missing_field("request.url"))? - .to_string(); + .ok_or_else(|| serde::de::Error::missing_field("request"))?; + + let request = serde_json::from_value(req_value.to_owned()).unwrap(); let response = json_content .get("response") @@ -212,7 +226,24 @@ pub mod test { .cloned() .ok_or_else(|| serde::de::Error::missing_field("response.body"))?; - Ok(JsonFixture { url, response }) + // if is mutation isn't present, then mark it as false. + let is_mutation = json_content + .get("is_mutation") + .and_then(|is_mutation| is_mutation.as_bool().to_owned()) + .unwrap_or_default(); + + let field_name = json_content + .get("fieldName") + .ok_or_else(|| serde::de::Error::missing_field("fieldName"))? + .as_str() + .unwrap_or_default(); + + Ok(JsonFixture { + request, + response, + is_mutation, + field_name: field_name.to_owned(), + }) } } @@ -247,18 +278,19 @@ pub mod test { #[tokio::test] async fn should_generate_config_from_json() -> anyhow::Result<()> { - let parsed_content = JsonFixture::read( + let JsonFixture { request, response, field_name, is_mutation } = JsonFixture::read( "src/core/generator/tests/fixtures/json/incompatible_properties.json", ) .await?; let cfg_module = Generator::default() .inputs(vec![Input::Json { - url: parsed_content.url.parse()?, - method: Method::GET, - req_body: serde_json::Value::Null, - res_body: parsed_content.response, - field_name: "f1".to_string(), - is_mutation: false, + url: request.url, + method: request.method, + req_body: request.body.unwrap_or_default(), + res_body: response, + field_name, + is_mutation, + headers: request.headers, }]) .transformers(vec![Box::new(Preset::default())]) .generate(true)?; @@ -283,17 +315,18 @@ pub mod test { }; // Json Input - let parsed_content = JsonFixture::read( + let JsonFixture { request, response, field_name, is_mutation } = JsonFixture::read( "src/core/generator/tests/fixtures/json/incompatible_properties.json", ) .await?; let json_input = Input::Json { - url: parsed_content.url.parse()?, - method: Method::GET, - req_body: serde_json::Value::Null, - res_body: parsed_content.response, - field_name: "f1".to_string(), - is_mutation: false, + url: request.url, + method: request.method, + req_body: request.body.unwrap_or_default(), + res_body: response, + field_name, + is_mutation, + headers: request.headers, }; // Combine inputs @@ -315,16 +348,17 @@ pub mod test { "src/core/generator/tests/fixtures/json/list_incompatible_object.json", "src/core/generator/tests/fixtures/json/list.json", ]; - let field_name_generator = NameGenerator::new("f"); for json_path in json_fixtures { - let parsed_content = JsonFixture::read(json_path).await?; + let JsonFixture { request, response, field_name, is_mutation } = + JsonFixture::read(json_path).await?; inputs.push(Input::Json { - url: parsed_content.url.parse()?, - method: Method::GET, - req_body: serde_json::Value::Null, - res_body: parsed_content.response, - field_name: field_name_generator.next(), - is_mutation: false, + url: request.url, + method: request.method, + req_body: request.body.unwrap_or_default(), + res_body: response, + field_name, + is_mutation, + headers: request.headers, }); } diff --git a/src/core/generator/json/operation_generator.rs b/src/core/generator/json/operation_generator.rs index dad9302686..6097cd808a 100644 --- a/src/core/generator/json/operation_generator.rs +++ b/src/core/generator/json/operation_generator.rs @@ -69,23 +69,18 @@ mod test { use std::collections::BTreeMap; use super::OperationTypeGenerator; - use crate::core::config::{Config, Field, GraphQLOperationType, Type}; + use crate::core::config::{Config, Field, Type}; use crate::core::generator::{NameGenerator, RequestSample}; use crate::core::http::Method; use crate::core::valid::Validator; #[test] fn test_query() { - let sample = RequestSample::new( - "https://jsonplaceholder.typicode.com/comments?postId=1" - .parse() - .unwrap(), - Method::GET, - serde_json::Value::Null, - serde_json::Value::Null, - "postComments", - GraphQLOperationType::Query, - ); + let url = "https://jsonplaceholder.typicode.com/comments?postId=1" + .parse() + .unwrap(); + + let sample = RequestSample::new(url, Default::default(), "postComments".into()); let config = Config::default(); let config = OperationTypeGenerator .generate(&sample, "T44", &NameGenerator::new("Input"), config) @@ -97,16 +92,11 @@ mod test { #[test] fn test_append_field_if_operation_type_exists() { - let sample = RequestSample::new( - "https://jsonplaceholder.typicode.com/comments?postId=1" - .parse() - .unwrap(), - Method::GET, - serde_json::Value::Null, - serde_json::Value::Null, - "postComments", - GraphQLOperationType::Query, - ); + let url = "https://jsonplaceholder.typicode.com/comments?postId=1" + .parse() + .unwrap(); + + let sample = RequestSample::new(url, Default::default(), "postComments".into()); let mut config = Config::default(); let mut fields = BTreeMap::default(); fields.insert( @@ -136,16 +126,15 @@ mod test { } "#; - let sample = RequestSample::new( - "https://jsonplaceholder.typicode.com/posts" - .parse() - .unwrap(), - Method::POST, - serde_json::from_str(body).unwrap(), - serde_json::Value::Null, - "postComments", - GraphQLOperationType::Mutation, - ); + let url = "https://jsonplaceholder.typicode.com/posts" + .parse() + .unwrap(); + + let sample = RequestSample::new(url, Default::default(), "postComments".into()) + .with_method(Method::POST) + .with_req_body(serde_json::from_str(body).unwrap()) + .with_is_mutation(true); + let config = Config::default(); let config = OperationTypeGenerator .generate(&sample, "T44", &NameGenerator::new("Input"), config) diff --git a/src/core/generator/json/schema_generator.rs b/src/core/generator/json/schema_generator.rs index c722590584..a2d5a778a3 100644 --- a/src/core/generator/json/schema_generator.rs +++ b/src/core/generator/json/schema_generator.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeSet; + use convert_case::{Case, Casing}; use crate::core::config::{Config, GraphQLOperationType}; @@ -6,11 +8,15 @@ use crate::core::valid::Valid; pub struct SchemaGenerator<'a> { operation_type: &'a GraphQLOperationType, + header_keys: &'a Option>, } impl<'a> SchemaGenerator<'a> { - pub fn new(operation_type: &'a GraphQLOperationType) -> Self { - Self { operation_type } + pub fn new( + operation_type: &'a GraphQLOperationType, + header_keys: &'a Option>, + ) -> Self { + Self { operation_type, header_keys } } } @@ -34,12 +40,18 @@ impl Transform for SchemaGenerator<'_> { ); } } + + // Add allowed headers setting on upstream + config.upstream = config.upstream.allowed_headers(self.header_keys.to_owned()); + Valid::succeed(config) } } #[cfg(test)] mod test { + use std::collections::BTreeSet; + use super::SchemaGenerator; use crate::core::config::GraphQLOperationType; use crate::core::transform::Transform; @@ -47,7 +59,7 @@ mod test { #[test] fn test_schema_generator_with_mutation() { - let schema_gen = SchemaGenerator::new(&GraphQLOperationType::Mutation); + let schema_gen = SchemaGenerator::new(&GraphQLOperationType::Mutation, &None); let config = schema_gen .transform(Default::default()) .to_result() @@ -60,13 +72,28 @@ mod test { #[test] fn test_schema_generator_with_query() { - let schema_gen = SchemaGenerator::new(&GraphQLOperationType::Query); + let schema_gen = SchemaGenerator::new(&GraphQLOperationType::Query, &None); + let config = schema_gen + .transform(Default::default()) + .to_result() + .unwrap(); + assert!(config.schema.query.is_some()); + assert_eq!(config.schema.query, Some("Query".to_owned())); + + assert!(config.schema.mutation.is_none()); + } + + #[test] + fn test_schema_generator_with_headers() { + let expected_header_keys = Some(BTreeSet::from(["X-Custom-Header".to_owned()])); + let schema_gen = SchemaGenerator::new(&GraphQLOperationType::Query, &expected_header_keys); let config = schema_gen .transform(Default::default()) .to_result() .unwrap(); assert!(config.schema.query.is_some()); assert_eq!(config.schema.query, Some("Query".to_owned())); + assert_eq!(config.upstream.allowed_headers, expected_header_keys); assert!(config.schema.mutation.is_none()); } diff --git a/src/core/generator/snapshots/tailcall__core__generator__from_json__tests__generate_config_from_json.snap b/src/core/generator/snapshots/tailcall__core__generator__from_json__tests__generate_config_from_json.snap index bd0acf2499..a16119dc42 100644 --- a/src/core/generator/snapshots/tailcall__core__generator__from_json__tests__generate_config_from_json.snap +++ b/src/core/generator/snapshots/tailcall__core__generator__from_json__tests__generate_config_from_json.snap @@ -2,7 +2,7 @@ source: src/core/generator/from_json.rs expression: config.to_sdl() --- -schema @server @upstream(baseURL: "https://example.com") { +schema @server @upstream(allowedHeaders: ["authorization"], baseURL: "https://example.com") { query: Query } @@ -15,17 +15,17 @@ type Container { age: Int } -type F1 { +type InCompatibleProperty { campaignTemplates: JSON colors: [JSON] } -type F3 { - people: [Person] +type NestedSameProperty { + container: T7 } -type F4 { - container: T7 +type NestedUser { + people: [Person] } type Person { @@ -35,11 +35,11 @@ type Person { } type Query { - f1: F1 @http(path: "/") - f2: [JSON] @http(path: "/api/v2/users") - f3(children: Boolean): F3 @http(path: "/users", query: [{key: "children", value: "{{.args.children}}"}]) - f4: F4 @http(path: "/") - f5: JSON @http(path: "/") + inCompatibleObjects: [JSON] @http(path: "/api/v2/users") + inCompatibleProperties: InCompatibleProperty @http(path: "/") + inCompatibleRootObject: JSON @http(path: "/") + nestedSameProperties: NestedSameProperty @http(path: "/") + nestedUsers(children: Boolean): NestedUser @http(path: "/users", query: [{key: "children", value: "{{.args.children}}"}]) } type T6 { diff --git a/src/core/generator/snapshots/tailcall__core__generator__generator__test__generate_from_config_from_multiple_jsons.snap b/src/core/generator/snapshots/tailcall__core__generator__generator__test__generate_from_config_from_multiple_jsons.snap index eaa7c94a94..c3247b1a49 100644 --- a/src/core/generator/snapshots/tailcall__core__generator__generator__test__generate_from_config_from_multiple_jsons.snap +++ b/src/core/generator/snapshots/tailcall__core__generator__generator__test__generate_from_config_from_multiple_jsons.snap @@ -2,23 +2,23 @@ source: src/core/generator/generator.rs expression: cfg_module.config().to_sdl() --- -schema @server @upstream(baseURL: "https://example.com") { +schema @server @upstream(allowedHeaders: ["authorization"], baseURL: "https://example.com") { query: Query } -type F1 { +type InCompatibleProperty { campaignTemplates: JSON colors: [JSON] } -type F3 { +type Query { + inCompatibleObjects: [JSON] @http(path: "/api/v2/users") + inCompatibleProperties: InCompatibleProperty @http(path: "/") + userData: [Userdatum] @http(path: "/users") +} + +type Userdatum { adult: Boolean age: Int name: String } - -type Query { - f1: F1 @http(path: "/") - f2: [JSON] @http(path: "/api/v2/users") - f3: [F3] @http(path: "/users") -} diff --git a/src/core/generator/snapshots/tailcall__core__generator__generator__test__should_generate_combined_config.snap b/src/core/generator/snapshots/tailcall__core__generator__generator__test__should_generate_combined_config.snap index b26c81ddb8..98e1f7616a 100644 --- a/src/core/generator/snapshots/tailcall__core__generator__generator__test__should_generate_combined_config.snap +++ b/src/core/generator/snapshots/tailcall__core__generator__generator__test__should_generate_combined_config.snap @@ -2,7 +2,7 @@ source: src/core/generator/generator.rs expression: cfg_module.config().to_sdl() --- -schema @server(hostname: "0.0.0.0", port: 8000) @upstream(baseURL: "https://example.com", httpCache: 42) @link(src: "../../../tailcall-fixtures/fixtures/protobuf/news.proto", type: Protobuf) { +schema @server(hostname: "0.0.0.0", port: 8000) @upstream(allowedHeaders: ["authorization"], baseURL: "https://example.com", httpCache: 42) @link(src: "../../../tailcall-fixtures/fixtures/protobuf/news.proto", type: Protobuf) { query: Query } @@ -43,7 +43,7 @@ type Comment { title: String! @expr(body: "{{.value.email}}: {{.value.name}}") } -type F1 { +type InCompatibleProperty { campaignTemplates: JSON colors: [JSON] } @@ -78,7 +78,7 @@ type Post { } type Query { - f1: F1 @http(path: "/") + inCompatibleProperties: InCompatibleProperty @http(path: "/") news__NewsService__AddNews(news: news__NewsInput!): News @grpc(body: "{{.args.news}}", method: "news.NewsService.AddNews") news__NewsService__DeleteNews(newsId: news__NewsId!): Empty @grpc(body: "{{.args.newsId}}", method: "news.NewsService.DeleteNews") news__NewsService__EditNews(news: news__NewsInput!): News @grpc(body: "{{.args.news}}", method: "news.NewsService.EditNews") diff --git a/src/core/generator/snapshots/tailcall__core__generator__generator__test__should_generate_config_from_json.snap b/src/core/generator/snapshots/tailcall__core__generator__generator__test__should_generate_config_from_json.snap index 86c904b875..b3c9cd036f 100644 --- a/src/core/generator/snapshots/tailcall__core__generator__generator__test__should_generate_config_from_json.snap +++ b/src/core/generator/snapshots/tailcall__core__generator__generator__test__should_generate_config_from_json.snap @@ -2,15 +2,15 @@ source: src/core/generator/generator.rs expression: cfg_module.config().to_sdl() --- -schema @server @upstream(baseURL: "https://example.com") { +schema @server @upstream(allowedHeaders: ["authorization"], baseURL: "https://example.com") { query: Query } -type F1 { +type InCompatibleProperty { campaignTemplates: JSON colors: [JSON] } type Query { - f1: F1 @http(path: "/") + inCompatibleProperties: InCompatibleProperty @http(path: "/") } diff --git a/src/core/generator/tests/fixtures/json/incompatible_properties.json b/src/core/generator/tests/fixtures/json/incompatible_properties.json index 62515b94d2..c696725bb4 100644 --- a/src/core/generator/tests/fixtures/json/incompatible_properties.json +++ b/src/core/generator/tests/fixtures/json/incompatible_properties.json @@ -1,8 +1,11 @@ { "request": { - "url": "https://example.com" + "url": "https://example.com", + "headers": { + "authorization": "Bearer TESTTEST" + } }, - "fieldName": "test", + "fieldName": "inCompatibleProperties", "response": { "status": 200, "body": { diff --git a/src/core/generator/tests/fixtures/json/incompatible_root_object.json b/src/core/generator/tests/fixtures/json/incompatible_root_object.json index 621bad481f..15f18f052b 100644 --- a/src/core/generator/tests/fixtures/json/incompatible_root_object.json +++ b/src/core/generator/tests/fixtures/json/incompatible_root_object.json @@ -2,7 +2,7 @@ "request": { "url": "https://example.com/" }, - "fieldName": "test", + "fieldName": "inCompatibleRootObject", "response": { "status": 200, "body": { diff --git a/src/core/generator/tests/fixtures/json/list.json b/src/core/generator/tests/fixtures/json/list.json index b0cd73718d..fbc75fad1f 100644 --- a/src/core/generator/tests/fixtures/json/list.json +++ b/src/core/generator/tests/fixtures/json/list.json @@ -2,7 +2,7 @@ "request": { "url": "https://example.com/users" }, - "fieldName": "users", + "fieldName": "userData", "response": { "status": 200, "body": [ diff --git a/src/core/generator/tests/fixtures/json/list_incompatible_object.json b/src/core/generator/tests/fixtures/json/list_incompatible_object.json index 6db9bb77b1..da26c03ae8 100644 --- a/src/core/generator/tests/fixtures/json/list_incompatible_object.json +++ b/src/core/generator/tests/fixtures/json/list_incompatible_object.json @@ -2,7 +2,7 @@ "request": { "url": "https://example.com/api/v2/users" }, - "fieldName": "users", + "fieldName": "inCompatibleObjects", "response": { "status": 200, "body": [ diff --git a/src/core/generator/tests/fixtures/json/nested_list.json b/src/core/generator/tests/fixtures/json/nested_list.json index 20afacbb7d..9adaea32e9 100644 --- a/src/core/generator/tests/fixtures/json/nested_list.json +++ b/src/core/generator/tests/fixtures/json/nested_list.json @@ -2,7 +2,7 @@ "request": { "url": "https://example.com/users?children=true" }, - "fieldName": "users", + "fieldName": "nestedUsers", "response": { "status": 200, "body": { diff --git a/src/core/generator/tests/fixtures/json/nested_same_properties.json b/src/core/generator/tests/fixtures/json/nested_same_properties.json index 6a225bff3d..3cca0494ec 100644 --- a/src/core/generator/tests/fixtures/json/nested_same_properties.json +++ b/src/core/generator/tests/fixtures/json/nested_same_properties.json @@ -2,7 +2,7 @@ "request": { "url": "https://example.com" }, - "fieldName": "test", + "fieldName": "nestedSameProperties", "response": { "status": 200, "body": { diff --git a/src/core/generator/tests/json_to_config_spec.rs b/src/core/generator/tests/json_to_config_spec.rs index 4502470059..89b6d0cba8 100644 --- a/src/core/generator/tests/json_to_config_spec.rs +++ b/src/core/generator/tests/json_to_config_spec.rs @@ -6,15 +6,16 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use tailcall::core::generator::{Generator, Input}; use tailcall::core::http::Method; +use tailcall::core::mustache::TemplateString; use url::Url; -#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct APIRequest { #[serde(default)] pub method: Method, pub url: Url, #[serde(default)] - pub headers: BTreeMap, + pub headers: Option>, #[serde(default, rename = "body")] pub body: Option, } @@ -31,7 +32,7 @@ pub struct APIResponse { #[serde(default = "default::status")] pub status: u16, #[serde(default)] - pub headers: BTreeMap, + pub headers: BTreeMap, #[serde(default, rename = "body")] pub body: Option, } @@ -77,6 +78,7 @@ fn test_spec(path: &Path, json_data: JsonFixture) -> anyhow::Result<()> { res_body: resp_body, field_name, is_mutation: is_mutation.unwrap_or_default(), + headers: request.headers, }]); let cfg = if is_mutation.unwrap_or_default() { diff --git a/src/core/generator/tests/snapshots/json_to_config_spec__incompatible_properties.json.snap b/src/core/generator/tests/snapshots/json_to_config_spec__incompatible_properties.json.snap index c8a0677a8a..e7cf286cff 100644 --- a/src/core/generator/tests/snapshots/json_to_config_spec__incompatible_properties.json.snap +++ b/src/core/generator/tests/snapshots/json_to_config_spec__incompatible_properties.json.snap @@ -2,12 +2,12 @@ source: src/core/generator/tests/json_to_config_spec.rs expression: cfg.to_sdl() --- -schema @server @upstream { +schema @server @upstream(allowedHeaders: ["authorization"]) { query: Query } type Query { - test: T1 @http(baseURL: "https://example.com", path: "/") + inCompatibleProperties: T1 @http(baseURL: "https://example.com", path: "/") } type T1 { diff --git a/src/core/generator/tests/snapshots/json_to_config_spec__incompatible_root_object.json.snap b/src/core/generator/tests/snapshots/json_to_config_spec__incompatible_root_object.json.snap index 60ecb88432..52584e3814 100644 --- a/src/core/generator/tests/snapshots/json_to_config_spec__incompatible_root_object.json.snap +++ b/src/core/generator/tests/snapshots/json_to_config_spec__incompatible_root_object.json.snap @@ -7,5 +7,5 @@ schema @server @upstream { } type Query { - test: JSON @http(baseURL: "https://example.com", path: "/") + inCompatibleRootObject: JSON @http(baseURL: "https://example.com", path: "/") } diff --git a/src/core/generator/tests/snapshots/json_to_config_spec__list.json.snap b/src/core/generator/tests/snapshots/json_to_config_spec__list.json.snap index 6f3800361d..4c9dfc6ec5 100644 --- a/src/core/generator/tests/snapshots/json_to_config_spec__list.json.snap +++ b/src/core/generator/tests/snapshots/json_to_config_spec__list.json.snap @@ -7,7 +7,7 @@ schema @server @upstream { } type Query { - users: [T1] @http(baseURL: "https://example.com", path: "/users") + userData: [T1] @http(baseURL: "https://example.com", path: "/users") } type T1 { diff --git a/src/core/generator/tests/snapshots/json_to_config_spec__list_incompatible_object.json.snap b/src/core/generator/tests/snapshots/json_to_config_spec__list_incompatible_object.json.snap index e10e701a8b..6b8a9af188 100644 --- a/src/core/generator/tests/snapshots/json_to_config_spec__list_incompatible_object.json.snap +++ b/src/core/generator/tests/snapshots/json_to_config_spec__list_incompatible_object.json.snap @@ -7,5 +7,5 @@ schema @server @upstream { } type Query { - users: [JSON] @http(baseURL: "https://example.com", path: "/api/v2/users") + inCompatibleObjects: [JSON] @http(baseURL: "https://example.com", path: "/api/v2/users") } diff --git a/src/core/generator/tests/snapshots/json_to_config_spec__nested_list.json.snap b/src/core/generator/tests/snapshots/json_to_config_spec__nested_list.json.snap index bf742c8856..82af15e8ce 100644 --- a/src/core/generator/tests/snapshots/json_to_config_spec__nested_list.json.snap +++ b/src/core/generator/tests/snapshots/json_to_config_spec__nested_list.json.snap @@ -7,7 +7,7 @@ schema @server @upstream { } type Query { - users(children: Boolean): T3 @http(baseURL: "https://example.com", path: "/users", query: [{key: "children", value: "{{.args.children}}"}]) + nestedUsers(children: Boolean): T3 @http(baseURL: "https://example.com", path: "/users", query: [{key: "children", value: "{{.args.children}}"}]) } type T1 { diff --git a/src/core/generator/tests/snapshots/json_to_config_spec__nested_same_properties.json.snap b/src/core/generator/tests/snapshots/json_to_config_spec__nested_same_properties.json.snap index a656c20610..ec6e7906d9 100644 --- a/src/core/generator/tests/snapshots/json_to_config_spec__nested_same_properties.json.snap +++ b/src/core/generator/tests/snapshots/json_to_config_spec__nested_same_properties.json.snap @@ -7,7 +7,7 @@ schema @server @upstream { } type Query { - test: T4 @http(baseURL: "https://example.com", path: "/") + nestedSameProperties: T4 @http(baseURL: "https://example.com", path: "/") } type T1 { diff --git a/tests/cli/fixtures/generator/gen_jsonplaceholder.md b/tests/cli/fixtures/generator/gen_jsonplaceholder.md index d3d2d4202a..e7bfca0aac 100644 --- a/tests/cli/fixtures/generator/gen_jsonplaceholder.md +++ b/tests/cli/fixtures/generator/gen_jsonplaceholder.md @@ -4,6 +4,10 @@ { "curl": { "src": "https://jsonplaceholder.typicode.com/posts/1", + "headers": { + "Content-Type": "application/json", + "Accept": "application/json" + }, "fieldName": "post" } }, diff --git a/tests/cli/snapshots/cli_spec__test__generator_spec__tests__cli__fixtures__generator__gen_json_proto_mix_config.json.snap b/tests/cli/snapshots/cli_spec__test__generator_spec__tests__cli__fixtures__generator__gen_json_proto_mix_config.json.snap new file mode 100644 index 0000000000..1b2059b5bb --- /dev/null +++ b/tests/cli/snapshots/cli_spec__test__generator_spec__tests__cli__fixtures__generator__gen_json_proto_mix_config.json.snap @@ -0,0 +1,81 @@ +--- +source: tests/cli/gen.rs +expression: config.to_sdl() +--- +schema @server @upstream(baseURL: "https://jsonplaceholder.typicode.com") { + query: Query +} + +input Id { + id: Int +} + +input news__MultipleNewsId { + ids: [Id] +} + +input news__NewsInput { + body: String + id: Int + postImage: String + status: news__Status + title: String +} + +enum news__Status { + DELETED + DRAFT + PUBLISHED +} + +type Address { + city: String + geo: Geo + street: String + suite: String + zipcode: String +} + +type Company { + bs: String + catchPhrase: String + name: String +} + +type Geo { + lat: String + lng: String +} + +type News { + body: String + id: Int + postImage: String + status: news__Status + title: String +} + +type NewsNewsServiceGetMultipleNew { + news: [News] +} + +type Query { + news__NewsService__AddNews(news: news__NewsInput!): News! @grpc(body: "{{.args.news}}", method: "news.NewsService.AddNews") + news__NewsService__DeleteNews(newsId: news__NewsId!): Empty! @grpc(body: "{{.args.newsId}}", method: "news.NewsService.DeleteNews") + news__NewsService__EditNews(news: news__NewsInput!): News! @grpc(body: "{{.args.news}}", method: "news.NewsService.EditNews") + news__NewsService__GetAllNews: NewsNewsServiceGetMultipleNew! @grpc(method: "news.NewsService.GetAllNews") + news__NewsService__GetMultipleNews(multipleNewsId: news__MultipleNewsId!): NewsNewsServiceGetMultipleNew! @grpc(body: "{{.args.multipleNewsId}}", method: "news.NewsService.GetMultipleNews") + news__NewsService__GetNews(newsId: news__NewsId!): News! @grpc(body: "{{.args.newsId}}", method: "news.NewsService.GetNews") + users: [User] @http(path: "/users") +} + +type User { + address: Address + company: Company + email: String + id: Int + name: String + phone: String + username: String + website: String +} diff --git a/tests/cli/snapshots/cli_spec__test__generator_spec__tests__cli__fixtures__generator__gen_jsonplaceholder.md.snap b/tests/cli/snapshots/cli_spec__test__generator_spec__tests__cli__fixtures__generator__gen_jsonplaceholder.md.snap index 47bdfb6296..b0d6a758e3 100644 --- a/tests/cli/snapshots/cli_spec__test__generator_spec__tests__cli__fixtures__generator__gen_jsonplaceholder.md.snap +++ b/tests/cli/snapshots/cli_spec__test__generator_spec__tests__cli__fixtures__generator__gen_jsonplaceholder.md.snap @@ -2,7 +2,7 @@ source: tests/cli/gen.rs expression: config.to_sdl() --- -schema @server @upstream(baseURL: "https://jsonplaceholder.typicode.com") { +schema @server @upstream(allowedHeaders: ["Accept", "Content-Type"], baseURL: "https://jsonplaceholder.typicode.com") { query: Query } From 5dab91ba4e1bb0b0e0d36e981730fd42833efa45 Mon Sep 17 00:00:00 2001 From: Kiryl Mialeshka <8974488+meskill@users.noreply.github.com> Date: Thu, 29 Aug 2024 14:07:14 +0200 Subject: [PATCH 08/14] fix(config): representation for nested list (#2747) Co-authored-by: Tushar Mathur --- core/blueprint/wrapping_type.rs | 175 +++++++++++++ examples/jsonplaceholder.json | 77 ++++-- examples/jsonplaceholder.yml | 62 +++-- examples/jsonplaceholder_batch.json | 67 +++-- examples/jsonplaceholder_batch.yml | 54 ++-- generated/.tailcallrc.schema.json | 67 +++-- src/cli/llm/infer_type_name.rs | 2 +- src/cli/tc/init.rs | 8 +- src/core/app_context.rs | 3 +- src/core/blueprint/blueprint.rs | 59 +---- src/core/blueprint/compress.rs | 19 +- src/core/blueprint/definitions.rs | 39 +-- src/core/blueprint/from_config.rs | 72 +++--- src/core/blueprint/into_schema.rs | 35 +-- src/core/blueprint/mod.rs | 73 +----- src/core/blueprint/mustache.rs | 28 +-- src/core/blueprint/operators/call.rs | 4 +- src/core/blueprint/operators/enum_alias.rs | 2 +- src/core/blueprint/operators/expr.rs | 2 +- src/core/blueprint/operators/graphql.rs | 4 +- src/core/blueprint/operators/grpc.rs | 6 +- src/core/blueprint/operators/protected.rs | 2 +- src/core/blueprint/schema.rs | 2 +- src/core/blueprint/union_resolver.rs | 4 +- src/core/blueprint/wrapping_type.rs | 175 +++++++++++++ src/core/config/config.rs | 51 ++-- src/core/config/from_document.rs | 25 +- src/core/config/into_document.rs | 59 +---- src/core/config/npo/tracker.rs | 4 +- src/core/config/transformer/ambiguous_type.rs | 60 +++-- .../transformer/flatten_single_field.rs | 4 +- .../config/transformer/improve_type_names.rs | 11 +- .../transformer/merge_types/similarity.rs | 167 ++++++------- .../transformer/merge_types/type_merger.rs | 58 +++-- src/core/config/transformer/rename_types.rs | 10 +- .../config/transformer/union_input_type.rs | 16 +- src/core/generator/from_proto.rs | 47 ++-- .../json/field_base_url_generator.rs | 12 +- .../json/http_directive_generator.rs | 18 +- .../generator/json/operation_generator.rs | 22 +- src/core/generator/json/types_generator.rs | 24 +- src/core/ir/discriminator.rs | 233 ++++++++++++------ src/core/jit/builder.rs | 6 +- src/core/jit/model.rs | 4 +- src/core/jit/synth/synth.rs | 4 +- src/core/mod.rs | 1 + .../configs/yaml-nested-unions-recursive.yaml | 28 ++- .../fixtures/configs/yaml-nested-unions.yaml | 28 ++- .../configs/yaml-recursive-input.yaml | 15 +- .../fixtures/configs/yaml-union-in-type.yaml | 40 +-- .../fixtures/configs/yaml-union.yaml | 22 +- .../graphql-conformance-015.md_client.snap | 2 +- .../graphql-conformance-015.md_merged.snap | 2 +- .../graphql-conformance-016.md_0.snap | 44 ++++ .../graphql-conformance-016.md_1.snap | 15 ++ .../graphql-conformance-016.md_2.snap | 24 ++ .../graphql-conformance-016.md_client.snap | 53 ++++ .../graphql-conformance-016.md_merged.snap | 20 ++ ...raphql-conformance-http-015.md_client.snap | 2 +- ...raphql-conformance-http-015.md_merged.snap | 2 +- .../graphql-conformance-http-016.md_0.snap | 44 ++++ .../graphql-conformance-http-016.md_1.snap | 15 ++ .../graphql-conformance-http-016.md_2.snap | 24 ++ ...raphql-conformance-http-016.md_client.snap | 53 ++++ ...raphql-conformance-http-016.md_merged.snap | 19 ++ .../test-grpc-nested-data.md_error.snap | 2 +- .../snapshots/test-list-args.md_client.snap | 2 +- .../snapshots/test-list-args.md_merged.snap | 2 +- tests/execution/batching-disabled.md | 22 +- tests/execution/batching.md | 12 +- tests/execution/cache-control.md | 16 +- tests/execution/custom-headers.md | 4 +- tests/execution/env-value.md | 30 ++- tests/execution/graphql-conformance-016.md | 30 ++- .../execution/graphql-conformance-http-016.md | 28 ++- tests/execution/https.md | 12 +- tests/execution/recursive-types-json.md | 39 ++- tests/execution/ref-other-nested.md | 20 +- .../execution/request-to-upstream-batching.md | 18 +- tests/execution/simple-query.md | 12 +- tests/execution/test-enum-empty.md | 10 +- tests/execution/test-interface-from-json.md | 16 +- tests/execution/test-static-value.md | 12 +- tests/execution/upstream-batching.md | 16 +- tests/execution/with-args-url.md | 18 +- tests/execution/yaml-nested-unions.md | 28 ++- tests/execution/yaml-union-in-type.md | 40 +-- tests/execution/yaml-union.md | 30 ++- 88 files changed, 1810 insertions(+), 938 deletions(-) create mode 100644 core/blueprint/wrapping_type.rs create mode 100644 src/core/blueprint/wrapping_type.rs create mode 100644 tests/core/snapshots/graphql-conformance-016.md_0.snap create mode 100644 tests/core/snapshots/graphql-conformance-016.md_1.snap create mode 100644 tests/core/snapshots/graphql-conformance-016.md_2.snap create mode 100644 tests/core/snapshots/graphql-conformance-016.md_client.snap create mode 100644 tests/core/snapshots/graphql-conformance-016.md_merged.snap create mode 100644 tests/core/snapshots/graphql-conformance-http-016.md_0.snap create mode 100644 tests/core/snapshots/graphql-conformance-http-016.md_1.snap create mode 100644 tests/core/snapshots/graphql-conformance-http-016.md_2.snap create mode 100644 tests/core/snapshots/graphql-conformance-http-016.md_client.snap create mode 100644 tests/core/snapshots/graphql-conformance-http-016.md_merged.snap diff --git a/core/blueprint/wrapping_type.rs b/core/blueprint/wrapping_type.rs new file mode 100644 index 0000000000..ec7198a23c --- /dev/null +++ b/core/blueprint/wrapping_type.rs @@ -0,0 +1,175 @@ +use std::fmt::Formatter; +use std::ops::Deref; + +use async_graphql::parser::types as async_graphql_types; +use async_graphql::Name; +use serde::{Deserialize, Serialize}; + +use crate::core::is_default; + +/// Type to represent GraphQL type usage with modifiers +/// [spec](https://spec.graphql.org/October2021/#sec-Wrapping-Types) +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema)] +#[serde(untagged)] +pub enum Type { + Named { + /// Name of the type + name: String, + /// Flag to indicate the type is required. + #[serde(rename = "required", default, skip_serializing_if = "is_default")] + non_null: bool, + }, + List { + /// Type is a list + #[serde(rename = "list")] + of_type: Box, + /// Flag to indicate the type is required. + #[serde(rename = "required", default, skip_serializing_if = "is_default")] + non_null: bool, + }, +} + +impl std::fmt::Debug for Type { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Type::Named { name, non_null } => { + if *non_null { + write!(f, "{}!", name) + } else { + write!(f, "{}", name) + } + } + Type::List { of_type, non_null } => { + if *non_null { + write!(f, "[{:?}]!", of_type) + } else { + write!(f, "[{:?}]", of_type) + } + } + } + } +} + +impl Default for Type { + fn default() -> Self { + Type::Named { name: "JSON".to_string(), non_null: false } + } +} + +impl Type { + /// gets the name of the type + pub fn name(&self) -> &String { + match self { + Type::Named { name, .. } => name, + Type::List { of_type, .. } => of_type.name(), + } + } + + /// checks if the type is nullable + pub fn is_nullable(&self) -> bool { + !match self { + Type::Named { non_null, .. } => *non_null, + Type::List { non_null, .. } => *non_null, + } + } + /// checks if the type is a list + pub fn is_list(&self) -> bool { + matches!(self, Type::List { .. }) + } + + /// convert this type into NonNull type + pub fn into_required(self) -> Self { + match self { + Type::Named { name, .. } => Self::Named { name, non_null: true }, + Type::List { of_type, .. } => Self::List { of_type, non_null: true }, + } + } + + /// convert this into nullable type + pub fn into_nullable(self) -> Self { + match self { + Type::Named { name, .. } => Self::Named { name, non_null: false }, + Type::List { of_type, .. } => Self::List { of_type, non_null: false }, + } + } + + /// create a nullable list type from this type + pub fn into_list(self) -> Self { + Type::List { of_type: Box::new(self), non_null: false } + } + + /// convert this type from list to non-list for any level of nesting + pub fn into_single(self) -> Self { + match self { + Type::Named { .. } => self, + Type::List { of_type, .. } => of_type.into_single(), + } + } + + /// replace the name of the underlying type + pub fn with_name(self, name: String) -> Self { + match self { + Type::Named { non_null, .. } => Type::Named { name, non_null }, + Type::List { of_type, non_null } => { + Type::List { of_type: Box::new(of_type.with_name(name)), non_null } + } + } + } +} + +impl From<&async_graphql_types::Type> for Type { + fn from(value: &async_graphql_types::Type) -> Self { + let non_null = !value.nullable; + + match &value.base { + async_graphql_types::BaseType::Named(name) => { + Self::Named { name: name.to_string(), non_null } + } + async_graphql_types::BaseType::List(type_) => { + Self::List { of_type: Box::new(type_.as_ref().into()), non_null } + } + } + } +} + +impl From<&Type> for async_graphql_types::Type { + fn from(value: &Type) -> Self { + let nullable = value.is_nullable(); + + let base = match value { + Type::Named { name, .. } => async_graphql_types::BaseType::Named(Name::new(name)), + Type::List { of_type, .. } => async_graphql_types::BaseType::List(Box::new( + async_graphql_types::Type::from(of_type.deref()), + )), + }; + + async_graphql_types::Type { base, nullable } + } +} + +impl From<&Type> for async_graphql::dynamic::TypeRef { + fn from(value: &Type) -> Self { + let nullable = value.is_nullable(); + + let base = match value { + Type::Named { name, .. } => { + async_graphql::dynamic::TypeRef::Named(name.to_owned().into()) + } + Type::List { of_type, .. } => async_graphql::dynamic::TypeRef::List(Box::new( + async_graphql::dynamic::TypeRef::from(of_type.deref()), + )), + }; + + if nullable { + base + } else { + async_graphql::dynamic::TypeRef::NonNull(Box::new(base)) + } + } +} + +impl From for Type { + fn from(value: String) -> Self { + Self::Named { name: value, non_null: false } + } +} \ No newline at end of file diff --git a/examples/jsonplaceholder.json b/examples/jsonplaceholder.json index cce3f0d6b4..6c74931c9a 100644 --- a/examples/jsonplaceholder.json +++ b/examples/jsonplaceholder.json @@ -15,44 +15,61 @@ "Post": { "fields": { "body": { - "type": "String", - "required": true + "type": { + "name": "String", + "required": true + } }, "id": { - "type": "Int", - "required": true + "type": { + "name": "Int", + "required": true + } }, "title": { - "type": "String", - "required": true + "type": { + "name": "String", + "required": true + } }, "user": { - "type": "User", + "type": { + "name": "User" + }, "http": { "path": "/users/{{value.userId}}" } }, "userId": { - "type": "Int", - "required": true + "type": { + "name": "Int", + "required": true + } } } }, "Query": { "fields": { "posts": { - "type": "Post", - "list": true, + "type": { + "list": { + "name": "Post" + } + }, "http": { "path": "/posts" } }, "user": { - "type": "User", + "type": { + "name": "User" + }, "args": { "id": { - "type": "Int", - "required": true + "type": { + "name": "Int", + "required": true + } } }, "http": { @@ -64,26 +81,38 @@ "User": { "fields": { "email": { - "type": "String", - "required": true + "type": { + "name": "String", + "required": true + } }, "id": { - "type": "Int", - "required": true + "type": { + "name": "Int", + "required": true + } }, "name": { - "type": "String", - "required": true + "type": { + "name": "String", + "required": true + } }, "phone": { - "type": "String" + "type": { + "name": "String" + } }, "username": { - "type": "String", - "required": true + "type": { + "name": "String", + "required": true + } }, "website": { - "type": "String" + "type": { + "name": "String" + } } } } diff --git a/examples/jsonplaceholder.yml b/examples/jsonplaceholder.yml index 57f712130f..0cd95b6ea1 100644 --- a/examples/jsonplaceholder.yml +++ b/examples/jsonplaceholder.yml @@ -10,51 +10,65 @@ types: Post: fields: body: - type: String - required: true + type: + name: String + required: true id: - type: Int - required: true + type: + name: Int + required: true title: - type: String - required: true + type: + name: String + required: true user: - type: User + type: + name: User http: path: /users/{{value.userId}} userId: - type: Int - required: true + type: + name: Int + required: true Query: fields: posts: - type: Post - list: true + type: + list: + name: Post http: path: /posts user: - type: User + type: + name: User args: id: - type: Int - required: true + type: + name: Int + required: true http: path: /users/{{args.id}} User: fields: email: - type: String - required: true + type: + name: String + required: true id: - type: Int - required: true + type: + name: Int + required: true name: - type: String - required: true + type: + name: String + required: true phone: - type: String + type: + name: String username: - type: String - required: true + type: + name: String + required: true website: - type: String + type: + name: String diff --git a/examples/jsonplaceholder_batch.json b/examples/jsonplaceholder_batch.json index 2c633c3ffb..9c6565d918 100644 --- a/examples/jsonplaceholder_batch.json +++ b/examples/jsonplaceholder_batch.json @@ -18,19 +18,27 @@ "Post": { "fields": { "body": { - "type": "String", - "required": true + "type": { + "name": "String", + "required": true + } }, "id": { - "type": "Int", - "required": true + "type": { + "name": "Int", + "required": true + } }, "title": { - "type": "String", - "required": true + "type": { + "name": "String", + "required": true + } }, "user": { - "type": "User", + "type": { + "name": "User" + }, "http": { "path": "/users", "query": [ @@ -43,16 +51,21 @@ } }, "userId": { - "type": "Int", - "required": true + "type": { + "name": "Int", + "required": true + } } } }, "Query": { "fields": { "posts": { - "type": "Post", - "list": true, + "type": { + "list": { + "name": "Post" + } + }, "http": { "path": "/posts" } @@ -62,26 +75,38 @@ "User": { "fields": { "email": { - "type": "String", - "required": true + "type": { + "name": "String", + "required": true + } }, "id": { - "type": "Int", - "required": true + "type": { + "name": "Int", + "required": true + } }, "name": { - "type": "String", - "required": true + "type": { + "name": "String", + "required": true + } }, "phone": { - "type": "String" + "type": { + "name": "String" + } }, "username": { - "type": "String", - "required": true + "type": { + "name": "String", + "required": true + } }, "website": { - "type": "String" + "type": { + "name": "String" + } } } } diff --git a/examples/jsonplaceholder_batch.yml b/examples/jsonplaceholder_batch.yml index 8a28c8ebb1..520db3b74c 100644 --- a/examples/jsonplaceholder_batch.yml +++ b/examples/jsonplaceholder_batch.yml @@ -13,16 +13,20 @@ types: Post: fields: body: - type: String - required: true + type: + name: String + required: true id: - type: Int - required: true + type: + name: Int + required: true title: - type: String - required: true + type: + name: String + required: true user: - type: User + type: + name: User http: path: /users query: @@ -31,30 +35,38 @@ types: batchKey: - id userId: - type: Int - required: true + type: + name: Int + required: true Query: fields: posts: - type: Post - list: true + type: + list: + name: Post http: path: /posts User: fields: email: - type: String - required: true + type: + name: String + required: true id: - type: Int - required: true + type: + name: Int + required: true name: - type: String - required: true + type: + name: String + required: true phone: - type: String + type: + name: String username: - type: String - required: true + type: + name: String + required: true website: - type: String + type: + name: String diff --git a/generated/.tailcallrc.schema.json b/generated/.tailcallrc.schema.json index 468b392e0e..76e5cab3c2 100644 --- a/generated/.tailcallrc.schema.json +++ b/generated/.tailcallrc.schema.json @@ -160,9 +160,6 @@ "null" ] }, - "list": { - "type": "boolean" - }, "modify": { "anyOf": [ { @@ -173,11 +170,8 @@ } ] }, - "required": { - "type": "boolean" - }, "type": { - "type": "string" + "$ref": "#/definitions/Type2" } } }, @@ -475,14 +469,6 @@ "null" ] }, - "list": { - "description": "Flag to indicate the type is a list.", - "type": "boolean" - }, - "list_type_required": { - "description": "Flag to indicate if the type inside the list is required.", - "type": "boolean" - }, "modify": { "description": "Allows modifying existing fields.", "anyOf": [ @@ -517,13 +503,13 @@ } ] }, - "required": { - "description": "Flag to indicate the type is required.", - "type": "boolean" - }, "type": { "description": "Refers to the type of the value the field can be resolved to.", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/Type2" + } + ] } } }, @@ -1290,6 +1276,47 @@ } } }, + "Type2": { + "description": "Type to represent GraphQL type usage with modifiers [spec](https://spec.graphql.org/October2021/#sec-Wrapping-Types)", + "anyOf": [ + { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "Name of the type", + "type": "string" + }, + "required": { + "description": "Flag to indicate the type is required.", + "type": "boolean" + } + } + }, + { + "type": "object", + "required": [ + "list" + ], + "properties": { + "list": { + "description": "Type is a list", + "allOf": [ + { + "$ref": "#/definitions/Type2" + } + ] + }, + "required": { + "description": "Flag to indicate the type is required.", + "type": "boolean" + } + } + } + ] + }, "UInt128": { "title": "UInt128", "description": "Field whose value is a 128-bit unsigned integer." diff --git a/src/cli/llm/infer_type_name.rs b/src/cli/llm/infer_type_name.rs index cfba78ff77..975841500e 100644 --- a/src/cli/llm/infer_type_name.rs +++ b/src/cli/llm/infer_type_name.rs @@ -93,7 +93,7 @@ impl InferTypeName { fields: type_ .fields .iter() - .map(|(k, v)| (k.clone(), v.type_of.clone())) + .map(|(k, v)| (k.clone(), v.type_of.name().to_owned())) .collect(), }; diff --git a/src/cli/tc/init.rs b/src/cli/tc/init.rs index e1a2b7672a..59ef041880 100644 --- a/src/cli/tc/init.rs +++ b/src/cli/tc/init.rs @@ -5,9 +5,10 @@ use anyhow::Result; use super::helpers::{FILE_NAME, JSON_FILE_NAME, YML_FILE_NAME}; use crate::cli::runtime::{confirm_and_write, create_directory, select_prompt}; -use crate::core::config::{Config, Expr, Field, Resolver, RootSchema, Source, Type}; +use crate::core::config::{Config, Expr, Field, Resolver, RootSchema, Source}; use crate::core::merge_right::MergeRight; use crate::core::runtime::TargetRuntime; +use crate::core::{config, Type}; pub(super) async fn init_command(runtime: TargetRuntime, folder_path: &str) -> Result<()> { create_directory(folder_path).await?; @@ -73,13 +74,12 @@ async fn confirm_and_write_yml( fn main_config() -> Config { let field = Field { - type_of: "String".to_string(), - required: true, + type_of: Type::from("String".to_owned()).into_required(), resolver: Some(Resolver::Expr(Expr { body: "Hello, World!".into() })), ..Default::default() }; - let query_type = Type { + let query_type = config::Type { fields: BTreeMap::from([("greet".into(), field)]), ..Default::default() }; diff --git a/src/core/app_context.rs b/src/core/app_context.rs index 1f6516be36..1e06f46b1d 100644 --- a/src/core/app_context.rs +++ b/src/core/app_context.rs @@ -6,7 +6,6 @@ use hyper::body::Bytes; use crate::core::async_graphql_hyper::OperationId; use crate::core::auth::context::GlobalAuthContext; -use crate::core::blueprint::Type::ListType; use crate::core::blueprint::{Blueprint, Definition, SchemaModifiers}; use crate::core::data_loader::{DataLoader, DedupeResult}; use crate::core::graphql::GraphqlDataLoader; @@ -53,7 +52,7 @@ impl AppContext { let data_loader = HttpDataLoader::new( runtime.clone(), group_by.clone(), - matches!(of_type, ListType { .. }), + of_type.is_list(), ) .to_data_loader(upstream_batch.clone().unwrap_or_default()); diff --git a/src/core/blueprint/blueprint.rs b/src/core/blueprint/blueprint.rs index 6fa14f5c67..307989c761 100644 --- a/src/core/blueprint/blueprint.rs +++ b/src/core/blueprint/blueprint.rs @@ -1,5 +1,4 @@ use std::collections::{BTreeSet, HashMap}; -use std::fmt::Formatter; use std::sync::Arc; use async_graphql::dynamic::{Schema, SchemaBuilder}; @@ -12,8 +11,8 @@ use super::telemetry::Telemetry; use super::{GlobalTimeout, Index}; use crate::core::blueprint::{Server, Upstream}; use crate::core::ir::model::IR; -use crate::core::scalar; use crate::core::schema_extension::SchemaExtension; +use crate::core::{scalar, Type}; /// Blueprint is an intermediary representation that allows us to generate /// graphQL APIs. It can only be generated from a valid Config. @@ -28,62 +27,6 @@ pub struct Blueprint { pub telemetry: Telemetry, } -#[derive(Clone)] -pub enum Type { - NamedType { name: String, non_null: bool }, - ListType { of_type: Box, non_null: bool }, -} - -impl std::fmt::Debug for Type { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Type::NamedType { name, non_null } => { - if *non_null { - write!(f, "{}!", name) - } else { - write!(f, "{}", name) - } - } - Type::ListType { of_type, non_null } => { - if *non_null { - write!(f, "[{:?}]!", of_type) - } else { - write!(f, "[{:?}]", of_type) - } - } - } - } -} - -impl Default for Type { - fn default() -> Self { - Type::NamedType { name: "JSON".to_string(), non_null: false } - } -} - -impl Type { - /// gets the name of the type - pub fn name(&self) -> &str { - match self { - Type::NamedType { name, .. } => name, - Type::ListType { of_type, .. } => of_type.name(), - } - } - - /// checks if the type is nullable - pub fn is_nullable(&self) -> bool { - !match self { - Type::NamedType { non_null, .. } => *non_null, - Type::ListType { non_null, .. } => *non_null, - } - } - - /// checks if the type is a list - pub fn is_list(&self) -> bool { - matches!(self, Type::ListType { .. }) - } -} - #[derive(Clone, Debug)] pub enum Definition { Interface(InterfaceTypeDefinition), diff --git a/src/core/blueprint/compress.rs b/src/core/blueprint/compress.rs index 18c40de5bf..cf6feec483 100644 --- a/src/core/blueprint/compress.rs +++ b/src/core/blueprint/compress.rs @@ -42,12 +42,12 @@ pub fn compress(mut blueprint: Blueprint) -> Blueprint { blueprint } -fn build_dependency_graph(blueprint: &Blueprint) -> HashMap<&str, Vec<&str>> { - let mut graph: HashMap<&str, Vec<&str>> = HashMap::new(); +fn build_dependency_graph(blueprint: &Blueprint) -> HashMap<&str, Vec<&String>> { + let mut graph = HashMap::new(); for def in &blueprint.definitions { let type_name = def.name(); - let mut dependencies: Vec<&str> = Vec::new(); + let mut dependencies = Vec::new(); match def { Definition::Object(def) => { @@ -55,7 +55,7 @@ fn build_dependency_graph(blueprint: &Blueprint) -> HashMap<&str, Vec<&str>> { for field in &def.fields { dependencies.extend(field.args.iter().map(|arg| arg.of_type.name())); } - dependencies.extend(def.implements.iter().map(|s| s.as_str())); + dependencies.extend(def.implements.iter()); } Definition::Interface(def) => { dependencies.extend(def.fields.iter().map(|field| field.of_type.name())); @@ -71,13 +71,13 @@ fn build_dependency_graph(blueprint: &Blueprint) -> HashMap<&str, Vec<&str>> { dependencies.extend(def.fields.iter().map(|field| field.of_type.name())); } Definition::Enum(def) => { - dependencies.extend(def.enum_values.iter().map(|value| value.name.as_str())); + dependencies.extend(def.enum_values.iter().map(|value| &value.name)); } Definition::Union(def) => { - dependencies.extend(def.types.iter().map(|s| s.as_str())); + dependencies.extend(def.types.iter()); } Definition::Scalar(sc) => { - dependencies.push(sc.name.as_str()); + dependencies.push(&sc.name); } } @@ -87,7 +87,10 @@ fn build_dependency_graph(blueprint: &Blueprint) -> HashMap<&str, Vec<&str>> { } // Function to perform DFS and identify all reachable types -fn identify_referenced_types(graph: &HashMap<&str, Vec<&str>>, root: Vec<&str>) -> HashSet { +fn identify_referenced_types( + graph: &HashMap<&str, Vec<&String>>, + root: Vec<&str>, +) -> HashSet { let mut stack = root; let mut referenced_types = HashSet::new(); diff --git a/src/core/blueprint/definitions.rs b/src/core/blueprint/definitions.rs index d8df056f1f..7a029afc08 100644 --- a/src/core/blueprint/definitions.rs +++ b/src/core/blueprint/definitions.rs @@ -4,14 +4,13 @@ use async_graphql_value::ConstValue; use regex::Regex; use union_resolver::update_union_resolver; -use crate::core::blueprint::Type::ListType; use crate::core::blueprint::*; use crate::core::config::{Config, Enum, Field, GraphQLOperationType, Protected, Union}; use crate::core::directive::DirectiveCodec; use crate::core::ir::model::{Cache, IR}; use crate::core::try_fold::TryFold; use crate::core::valid::{Valid, Validator}; -use crate::core::{config, scalar}; +use crate::core::{config, scalar, Type}; pub fn to_scalar_type_definition(name: &str) -> Valid { if scalar::Scalar::is_predefined(name) { @@ -84,6 +83,7 @@ struct ProcessPathContext<'a> { path: &'a [String], field: &'a config::Field, type_info: &'a config::Type, + // TODO: does it even used other than as false? is_required: bool, config_module: &'a ConfigModule, invalid_path_handler: &'a InvalidPathHandler, @@ -105,7 +105,7 @@ fn process_field_within_type(context: ProcessFieldWithinTypeContext) -> Valid Valid Valid Valid Valid { if let Some((field_name, remaining_path)) = path.split_first() { if field_name.parse::().is_ok() { let mut modified_field = field.clone(); - modified_field.list = false; + // TODO: does it required? + modified_field.type_of = modified_field.type_of.into_single(); return process_path(ProcessPathContext { config_module, type_info, @@ -201,7 +202,7 @@ fn process_path(context: ProcessPathContext) -> Valid { .fields .get(field_name) .map(|_| type_info) - .or_else(|| config_module.find_type(&field.type_of)); + .or_else(|| config_module.find_type(field.type_of.name())); if let Some(type_info) = target_type_info { return process_field_within_type(ProcessFieldWithinTypeContext { @@ -219,7 +220,11 @@ fn process_path(context: ProcessPathContext) -> Valid { return invalid_path_handler(field_name, path, context.original_path); } - Valid::succeed(to_type(field, Some(is_required))) + Valid::succeed(if is_required { + field.type_of.clone().into_required() + } else { + field.type_of.clone().into_nullable() + }) } fn to_enum_type_definition((name, eu): (&String, &Enum)) -> Definition { @@ -264,7 +269,7 @@ fn update_args<'a>( Valid::succeed(InputFieldDefinition { name: name.clone(), description: arg.doc.clone(), - of_type: to_type(arg, None), + of_type: arg.type_of.clone(), default_value: arg.default_value.clone(), }) }) @@ -272,7 +277,7 @@ fn update_args<'a>( name: name.to_string(), description: field.doc.clone(), args, - of_type: to_type(*field, None), + of_type: field.type_of.clone(), directives: Vec::new(), resolver: None, default_value: field.default_value.clone(), @@ -281,7 +286,7 @@ fn update_args<'a>( ) } -fn item_is_numberic(list: &[String]) -> bool { +fn item_is_numeric(list: &[String]) -> bool { list.iter().any(|s| { let re = Regex::new(r"^\d+$").unwrap(); re.is_match(s) @@ -292,14 +297,14 @@ fn update_resolver_from_path( context: &ProcessPathContext, base_field: blueprint::FieldDefinition, ) -> Valid { - let has_index = item_is_numberic(context.path); + let has_index = item_is_numeric(context.path); process_path(context.clone()).and_then(|of_type| { let mut updated_base_field = base_field; let resolver = IR::ContextPath(context.path.to_owned()); if has_index { updated_base_field.of_type = - Type::NamedType { name: of_type.name().to_string(), non_null: false } + Type::Named { name: of_type.name().to_string(), non_null: false } } else { updated_base_field.of_type = of_type; } @@ -352,7 +357,7 @@ pub fn update_cache_resolvers<'a>( } fn validate_field_type_exist(config: &Config, field: &Field) -> Valid<(), String> { - let field_type = &field.type_of; + let field_type = field.type_of.name(); if !scalar::Scalar::is_predefined(field_type) && !config.contains(field_type) { Valid::fail(format!("Undeclared type '{field_type}' was found")) } else { diff --git a/src/core/blueprint/from_config.rs b/src/core/blueprint/from_config.rs index af450b56e0..ef16968692 100644 --- a/src/core/blueprint/from_config.rs +++ b/src/core/blueprint/from_config.rs @@ -4,15 +4,16 @@ use async_graphql::dynamic::SchemaBuilder; use indexmap::IndexMap; use self::telemetry::to_opentelemetry; -use super::{Server, TypeLike}; +use super::Server; use crate::core::blueprint::compress::compress; use crate::core::blueprint::*; use crate::core::config::transformer::Required; -use crate::core::config::{Arg, Batch, Config, ConfigModule, Field}; +use crate::core::config::{Arg, Batch, Config, ConfigModule}; use crate::core::ir::model::{IO, IR}; use crate::core::json::JsonSchema; use crate::core::try_fold::TryFold; use crate::core::valid::{Valid, ValidationError, Validator}; +use crate::core::Type; pub fn config_blueprint<'a>() -> TryFold<'a, ConfigModule, Blueprint, String> { let server = TryFoldConfig::::new(|config_module, blueprint| { @@ -68,55 +69,46 @@ pub fn apply_batching(mut blueprint: 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: &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)); + schema_fields.insert(name.clone(), to_json_schema(&arg.type_of, config)); } JsonSchema::Obj(schema_fields) } -fn to_json_schema(field: &T, config: &Config) -> JsonSchema -where - T: TypeLike, -{ - let type_of = field.name(); - let list = field.list(); - let required = field.non_null(); - let type_ = config.find_type(type_of); - let type_enum_ = config.find_enum(type_of); - let schema = if let Some(type_) = type_ { - let mut schema_fields = BTreeMap::new(); - for (name, field) in type_.fields.iter() { - if field.resolver.is_none() { - schema_fields.insert(name.clone(), to_json_schema_for_field(field, config)); +pub fn to_json_schema(type_of: &Type, config: &Config) -> JsonSchema { + let json_schema = match type_of { + Type::Named { name, .. } => { + let type_ = config.find_type(name); + let type_enum_ = config.find_enum(name); + + if let Some(type_) = type_ { + let mut schema_fields = BTreeMap::new(); + for (name, field) in type_.fields.iter() { + if field.resolver.is_none() { + schema_fields.insert(name.clone(), to_json_schema(&field.type_of, config)); + } + } + JsonSchema::Obj(schema_fields) + } else if let Some(type_enum_) = type_enum_ { + JsonSchema::Enum( + type_enum_ + .variants + .iter() + .map(|variant| variant.name.clone()) + .collect::>(), + ) + } else { + JsonSchema::from_scalar_type(name) } } - JsonSchema::Obj(schema_fields) - } else if let Some(type_enum_) = type_enum_ { - JsonSchema::Enum( - type_enum_ - .variants - .iter() - .map(|variant| variant.name.clone()) - .collect::>(), - ) - } else { - JsonSchema::from_scalar_type(type_of) + Type::List { of_type, .. } => JsonSchema::Arr(Box::new(to_json_schema(of_type, config))), }; - if !required { - if list { - JsonSchema::Opt(Box::new(JsonSchema::Arr(Box::new(schema)))) - } else { - JsonSchema::Opt(Box::new(schema)) - } - } else if list { - JsonSchema::Arr(Box::new(schema)) + if type_of.is_nullable() { + JsonSchema::Opt(Box::new(json_schema)) } else { - schema + json_schema } } diff --git a/src/core/blueprint/into_schema.rs b/src/core/blueprint/into_schema.rs index 31fe06a964..ff45a464f3 100644 --- a/src/core/blueprint/into_schema.rs +++ b/src/core/blueprint/into_schema.rs @@ -1,40 +1,17 @@ -use std::borrow::Cow; use std::sync::Arc; -use async_graphql::dynamic::{self, FieldFuture, FieldValue, SchemaBuilder}; +use async_graphql::dynamic::{self, FieldFuture, FieldValue, SchemaBuilder, TypeRef}; use async_graphql::ErrorExtensions; use async_graphql_value::ConstValue; use futures_util::TryFutureExt; use strum::IntoEnumIterator; use tracing::Instrument; -use crate::core::blueprint::{Blueprint, Definition, Type}; +use crate::core::blueprint::{Blueprint, Definition}; use crate::core::http::RequestContext; use crate::core::ir::{EvalContext, ResolverContext, TypedValue}; use crate::core::scalar; -fn to_type_ref(type_of: &Type) -> dynamic::TypeRef { - match type_of { - Type::NamedType { name, non_null } => { - if *non_null { - dynamic::TypeRef::NonNull(Box::from(dynamic::TypeRef::Named(Cow::Owned( - name.clone(), - )))) - } else { - dynamic::TypeRef::Named(Cow::Owned(name.clone())) - } - } - Type::ListType { of_type, non_null } => { - let inner = Box::new(to_type_ref(of_type)); - if *non_null { - dynamic::TypeRef::NonNull(Box::from(dynamic::TypeRef::List(inner))) - } else { - dynamic::TypeRef::List(inner) - } - } - } -} - /// We set the default value for an `InputValue` by reading it from the /// blueprint and assigning it to the provided `InputValue` during the /// generation of the `async_graphql::Schema`. The `InputValue` represents the @@ -81,7 +58,7 @@ fn to_type(def: &Definition) -> dynamic::Type { let mut object = dynamic::Object::new(def.name.clone()); for field in def.fields.iter() { let field = field.clone(); - let type_ref = to_type_ref(&field.of_type); + let type_ref = TypeRef::from(&field.of_type); let field_name = &field.name.clone(); let mut dyn_schema_field = dynamic::Field::new( @@ -144,7 +121,7 @@ fn to_type(def: &Definition) -> dynamic::Type { } for arg in field.args.iter() { dyn_schema_field = dyn_schema_field.argument(set_default_value( - dynamic::InputValue::new(arg.name.clone(), to_type_ref(&arg.of_type)), + dynamic::InputValue::new(arg.name.clone(), TypeRef::from(&arg.of_type)), arg.default_value.clone(), )); } @@ -164,7 +141,7 @@ fn to_type(def: &Definition) -> dynamic::Type { for field in def.fields.iter() { interface = interface.field(dynamic::InterfaceField::new( field.name.clone(), - to_type_ref(&field.of_type), + TypeRef::from(&field.of_type), )); } @@ -174,7 +151,7 @@ fn to_type(def: &Definition) -> dynamic::Type { let mut input_object = dynamic::InputObject::new(def.name.clone()); for field in def.fields.iter() { let mut input_field = - dynamic::InputValue::new(field.name.clone(), to_type_ref(&field.of_type)); + dynamic::InputValue::new(field.name.clone(), TypeRef::from(&field.of_type)); if let Some(description) = &field.description { input_field = input_field.description(description); } diff --git a/src/core/blueprint/mod.rs b/src/core/blueprint/mod.rs index 245c4c20ad..0260384c67 100644 --- a/src/core/blueprint/mod.rs +++ b/src/core/blueprint/mod.rs @@ -16,6 +16,7 @@ pub mod telemetry; mod timeout; mod union_resolver; mod upstream; +mod wrapping_type; pub use auth::*; pub use blueprint::*; @@ -30,77 +31,9 @@ pub use schema::*; pub use server::*; pub use timeout::GlobalTimeout; pub use upstream::*; +pub use wrapping_type::Type; -use crate::core::config::{Arg, ConfigModule, Field}; +use crate::core::config::ConfigModule; use crate::core::try_fold::TryFold; pub type TryFoldConfig<'a, A> = TryFold<'a, ConfigModule, A, String>; - -pub(crate) trait TypeLike { - fn name(&self) -> &str; - fn list(&self) -> bool; - fn non_null(&self) -> bool; - fn list_type_required(&self) -> bool; -} - -impl TypeLike for Field { - fn name(&self) -> &str { - &self.type_of - } - - fn list(&self) -> bool { - self.list - } - - fn non_null(&self) -> bool { - self.required - } - - fn list_type_required(&self) -> bool { - self.list_type_required - } -} - -impl TypeLike for Arg { - fn name(&self) -> &str { - &self.type_of - } - - fn list(&self) -> bool { - self.list - } - - fn non_null(&self) -> bool { - self.required - } - - fn list_type_required(&self) -> bool { - false - } -} - -pub(crate) fn to_type(field: &T, override_non_null: Option) -> Type -where - T: TypeLike, -{ - let name = field.name(); - let list = field.list(); - let list_type_required = field.list_type_required(); - let non_null = if let Some(non_null) = override_non_null { - non_null - } else { - field.non_null() - }; - - if list { - Type::ListType { - of_type: Box::new(Type::NamedType { - name: name.to_string(), - non_null: list_type_required, - }), - non_null, - } - } else { - Type::NamedType { name: name.to_string(), non_null } - } -} diff --git a/src/core/blueprint/mustache.rs b/src/core/blueprint/mustache.rs index 76c8a716dc..a53e88f9e6 100644 --- a/src/core/blueprint/mustache.rs +++ b/src/core/blueprint/mustache.rs @@ -1,4 +1,4 @@ -use super::{to_type, FieldDefinition}; +use super::FieldDefinition; use crate::core::config::{self, Config}; use crate::core::ir::model::{IO, IR}; use crate::core::scalar; @@ -25,7 +25,7 @@ impl<'a> MustachePartsValidator<'a> { parts[0..parts.len() - len + 1].join(".").as_str() ) })?; - let val_type = to_type(field, None); + let val_type = &field.type_of; if !is_query && val_type.is_nullable() { return Err(format!("value '{}' is a nullable type", item.as_str())); @@ -37,7 +37,7 @@ impl<'a> MustachePartsValidator<'a> { type_of = self .config - .find_type(&field.type_of) + .find_type(val_type.name()) .ok_or_else(|| format!("no type '{}' found", parts.join(".").as_str()))?; len -= 1; @@ -181,24 +181,25 @@ impl FieldDefinition { mod test { use super::MustachePartsValidator; use crate::core::blueprint::{FieldDefinition, InputFieldDefinition}; - use crate::core::config::{Config, Field, Type}; + use crate::core::config::{self, Config, Field}; use crate::core::valid::Validator; + use crate::core::Type; fn initialize_test_config_and_field() -> (Config, FieldDefinition) { let mut config = Config::default(); - let mut t1_type = Type::default(); + let mut t1_type = config::Type::default(); t1_type.fields.insert( "numbers".to_owned(), - Field { type_of: "Int".to_owned(), list: true, ..Default::default() }, + Field { + type_of: Type::from("Int".to_owned()).into_list(), + ..Default::default() + }, ); config.types.insert("T1".to_string(), t1_type); - let type_ = crate::core::blueprint::Type::ListType { - of_type: Box::new(crate::core::blueprint::Type::NamedType { - name: "Int".to_string(), - non_null: false, - }), + let type_ = Type::List { + of_type: Box::new(Type::Named { name: "Int".to_string(), non_null: false }), non_null: false, }; @@ -210,10 +211,7 @@ mod test { default_value: None, description: None, }], - of_type: crate::core::blueprint::Type::NamedType { - name: "T1".to_string(), - non_null: false, - }, + of_type: Type::Named { name: "T1".to_string(), non_null: false }, resolver: None, directives: vec![], description: None, diff --git a/src/core/blueprint/operators/call.rs b/src/core/blueprint/operators/call.rs index 5ea3cf31f3..3db52535b0 100644 --- a/src/core/blueprint/operators/call.rs +++ b/src/core/blueprint/operators/call.rs @@ -38,7 +38,7 @@ fn compile_call( .args .iter() .filter_map(|(k, arg)| { - if arg.required && !args.clone().any(|(k1, _)| k1.eq(k)) { + if !arg.type_of.is_nullable() && !args.clone().any(|(k1, _)| k1.eq(k)) { Some(k) } else { None @@ -64,7 +64,7 @@ fn compile_call( object_name, config_module, type_of, - &field.type_of, + field.type_of.name(), ) .and_then(|b_field| { if b_field.resolver.is_none() { diff --git a/src/core/blueprint/operators/enum_alias.rs b/src/core/blueprint/operators/enum_alias.rs index 1c78f3665b..cc1406eed1 100644 --- a/src/core/blueprint/operators/enum_alias.rs +++ b/src/core/blueprint/operators/enum_alias.rs @@ -12,7 +12,7 @@ pub fn update_enum_alias<'a>( { TryFold::<(&ConfigModule, &Field, &config::Type, &'a str), FieldDefinition, String>::new( |(config, field, _, _), mut b_field| { - let enum_type = config.enums.get(&field.type_of); + let enum_type = config.enums.get(field.type_of.name()); if let Some(enum_type) = enum_type { let has_alias = enum_type.variants.iter().any(|v| v.alias.is_some()); if !has_alias { diff --git a/src/core/blueprint/operators/expr.rs b/src/core/blueprint/operators/expr.rs index a429bf9e3b..de90d424b8 100644 --- a/src/core/blueprint/operators/expr.rs +++ b/src/core/blueprint/operators/expr.rs @@ -13,7 +13,7 @@ fn validate_data_with_schema( field: &config::Field, gql_value: ConstValue, ) -> Valid<(), String> { - match to_json_schema_for_field(field, config) + match to_json_schema(&field.type_of, config) .validate(&gql_value) .to_result() { diff --git a/src/core/blueprint/operators/graphql.rs b/src/core/blueprint/operators/graphql.rs index bf5f42e9c4..0c7fba35af 100644 --- a/src/core/blueprint/operators/graphql.rs +++ b/src/core/blueprint/operators/graphql.rs @@ -27,7 +27,7 @@ fn create_related_fields( if !field.has_resolver() { map.insert( name.clone(), - create_related_fields(config, &field.type_of, visited), + create_related_fields(config, field.type_of.name(), visited), ); } } @@ -84,7 +84,7 @@ pub fn update_graphql<'a>( return Valid::succeed(b_field); }; - compile_graphql(config, operation_type, &field.type_of, graphql) + compile_graphql(config, operation_type, field.type_of.name(), graphql) .map(|resolver| b_field.resolver(Some(resolver))) .and_then(|b_field| b_field.validate_field(type_of, config).map_to(b_field)) }, diff --git a/src/core/blueprint/operators/grpc.rs b/src/core/blueprint/operators/grpc.rs index 3c170b74be..abf21c9582 100644 --- a/src/core/blueprint/operators/grpc.rs +++ b/src/core/blueprint/operators/grpc.rs @@ -3,7 +3,7 @@ use std::fmt::Display; use prost_reflect::prost_types::FileDescriptorSet; use prost_reflect::FieldDescriptor; -use crate::core::blueprint::{FieldDefinition, TypeLike}; +use crate::core::blueprint::FieldDefinition; use crate::core::config::group_by::GroupBy; use crate::core::config::{Config, ConfigModule, Field, GraphQLOperationType, Grpc, Resolver}; use crate::core::grpc::protobuf::{ProtobufOperation, ProtobufSet}; @@ -55,7 +55,7 @@ fn to_operation( } fn json_schema_from_field(config: &Config, field: &Field) -> FieldSchema { - let field_schema = crate::core::blueprint::to_json_schema_for_field(field, config); + let field_schema = crate::core::blueprint::to_json_schema(&field.type_of, config); let args_schema = crate::core::blueprint::to_json_schema_for_args(&field.args, config); FieldSchema { args: args_schema, field: field_schema } } @@ -179,7 +179,7 @@ pub fn compile_grpc(inputs: CompileGrpc) -> Valid { let validation = if validate_with_schema { let field_schema = json_schema_from_field(config_module, field); if grpc.batch_key.is_empty() { - validate_schema(field_schema, &operation, field.name()).unit() + validate_schema(field_schema, &operation, field.type_of.name()).unit() } else { validate_group_by(&field_schema, &operation, grpc.batch_key.clone()).unit() } diff --git a/src/core/blueprint/operators/protected.rs b/src/core/blueprint/operators/protected.rs index d492fc1bee..87580dbcaa 100644 --- a/src/core/blueprint/operators/protected.rs +++ b/src/core/blueprint/operators/protected.rs @@ -13,7 +13,7 @@ pub fn update_protected<'a>( if field.protected.is_some() // check the field itself has marked as protected || type_.protected.is_some() // check the type that contains current field || config // check that output type of the field is protected - .find_type(&field.type_of) + .find_type(field.type_of.name()) .and_then(|type_| type_.protected.as_ref()) .is_some() { diff --git a/src/core/blueprint/schema.rs b/src/core/blueprint/schema.rs index 2bf73b1dca..ce92bd307e 100644 --- a/src/core/blueprint/schema.rs +++ b/src/core/blueprint/schema.rs @@ -52,7 +52,7 @@ pub fn validate_field_has_resolver( Valid::<(), String>::fail("No resolver has been found in the schema".to_owned()) .when(|| { if !field.has_resolver() { - let type_name = &field.type_of; + let type_name = field.type_of.name(); if let Some(ty) = types.get(type_name) { let res = validate_type_has_resolvers(type_name, ty, types, visited); return !res.is_succeed(); diff --git a/src/core/blueprint/union_resolver.rs b/src/core/blueprint/union_resolver.rs index e2bc6529c0..544ef76ebf 100644 --- a/src/core/blueprint/union_resolver.rs +++ b/src/core/blueprint/union_resolver.rs @@ -31,11 +31,11 @@ pub fn update_union_resolver<'a>( { TryFold::<(&ConfigModule, &Field, &config::Type, &str), FieldDefinition, String>::new( |(config, field, _, _), mut b_field| { - let Some(union_) = config.find_union(&field.type_of) else { + let Some(union_) = config.find_union(field.type_of.name()) else { return Valid::succeed(b_field); }; - compile_union_resolver(config, &field.type_of, union_).map(|discriminator| { + compile_union_resolver(config, field.type_of.name(), union_).map(|discriminator| { b_field.resolver = Some( b_field .resolver diff --git a/src/core/blueprint/wrapping_type.rs b/src/core/blueprint/wrapping_type.rs new file mode 100644 index 0000000000..39cbe499ea --- /dev/null +++ b/src/core/blueprint/wrapping_type.rs @@ -0,0 +1,175 @@ +use std::fmt::Formatter; +use std::ops::Deref; + +use async_graphql::parser::types as async_graphql_types; +use async_graphql::Name; +use serde::{Deserialize, Serialize}; + +use crate::core::is_default; + +/// Type to represent GraphQL type usage with modifiers +/// [spec](https://spec.graphql.org/October2021/#sec-Wrapping-Types) +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema)] +#[serde(untagged)] +pub enum Type { + Named { + /// Name of the type + name: String, + /// Flag to indicate the type is required. + #[serde(rename = "required", default, skip_serializing_if = "is_default")] + non_null: bool, + }, + List { + /// Type is a list + #[serde(rename = "list")] + of_type: Box, + /// Flag to indicate the type is required. + #[serde(rename = "required", default, skip_serializing_if = "is_default")] + non_null: bool, + }, +} + +impl std::fmt::Debug for Type { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Type::Named { name, non_null } => { + if *non_null { + write!(f, "{}!", name) + } else { + write!(f, "{}", name) + } + } + Type::List { of_type, non_null } => { + if *non_null { + write!(f, "[{:?}]!", of_type) + } else { + write!(f, "[{:?}]", of_type) + } + } + } + } +} + +impl Default for Type { + fn default() -> Self { + Type::Named { name: "JSON".to_string(), non_null: false } + } +} + +impl Type { + /// gets the name of the type + pub fn name(&self) -> &String { + match self { + Type::Named { name, .. } => name, + Type::List { of_type, .. } => of_type.name(), + } + } + + /// checks if the type is nullable + pub fn is_nullable(&self) -> bool { + !match self { + Type::Named { non_null, .. } => *non_null, + Type::List { non_null, .. } => *non_null, + } + } + /// checks if the type is a list + pub fn is_list(&self) -> bool { + matches!(self, Type::List { .. }) + } + + /// convert this type into NonNull type + pub fn into_required(self) -> Self { + match self { + Type::Named { name, .. } => Self::Named { name, non_null: true }, + Type::List { of_type, .. } => Self::List { of_type, non_null: true }, + } + } + + /// convert this into nullable type + pub fn into_nullable(self) -> Self { + match self { + Type::Named { name, .. } => Self::Named { name, non_null: false }, + Type::List { of_type, .. } => Self::List { of_type, non_null: false }, + } + } + + /// create a nullable list type from this type + pub fn into_list(self) -> Self { + Type::List { of_type: Box::new(self), non_null: false } + } + + /// convert this type from list to non-list for any level of nesting + pub fn into_single(self) -> Self { + match self { + Type::Named { .. } => self, + Type::List { of_type, .. } => of_type.into_single(), + } + } + + /// replace the name of the underlying type + pub fn with_name(self, name: String) -> Self { + match self { + Type::Named { non_null, .. } => Type::Named { name, non_null }, + Type::List { of_type, non_null } => { + Type::List { of_type: Box::new(of_type.with_name(name)), non_null } + } + } + } +} + +impl From<&async_graphql_types::Type> for Type { + fn from(value: &async_graphql_types::Type) -> Self { + let non_null = !value.nullable; + + match &value.base { + async_graphql_types::BaseType::Named(name) => { + Self::Named { name: name.to_string(), non_null } + } + async_graphql_types::BaseType::List(type_) => { + Self::List { of_type: Box::new(type_.as_ref().into()), non_null } + } + } + } +} + +impl From<&Type> for async_graphql_types::Type { + fn from(value: &Type) -> Self { + let nullable = value.is_nullable(); + + let base = match value { + Type::Named { name, .. } => async_graphql_types::BaseType::Named(Name::new(name)), + Type::List { of_type, .. } => async_graphql_types::BaseType::List(Box::new( + async_graphql_types::Type::from(of_type.deref()), + )), + }; + + async_graphql_types::Type { base, nullable } + } +} + +impl From<&Type> for async_graphql::dynamic::TypeRef { + fn from(value: &Type) -> Self { + let nullable = value.is_nullable(); + + let base = match value { + Type::Named { name, .. } => { + async_graphql::dynamic::TypeRef::Named(name.to_owned().into()) + } + Type::List { of_type, .. } => async_graphql::dynamic::TypeRef::List(Box::new( + async_graphql::dynamic::TypeRef::from(of_type.deref()), + )), + }; + + if nullable { + base + } else { + async_graphql::dynamic::TypeRef::NonNull(Box::new(base)) + } + } +} + +impl From for Type { + fn from(value: String) -> Self { + Self::Named { name: value, non_null: false } + } +} diff --git a/src/core/config/config.rs b/src/core/config/config.rs index 5f15e7fc78..952de581a3 100644 --- a/src/core/config/config.rs +++ b/src/core/config/config.rs @@ -123,7 +123,7 @@ impl Display for Type { writeln!(f, "{{")?; for (field_name, field) in &self.fields { - writeln!(f, " {}: {},", field_name, field.type_of)?; + writeln!(f, " {}: {:?},", field_name, field.type_of)?; } writeln!(f, "}}") } @@ -235,22 +235,7 @@ pub struct Field { /// /// Refers to the type of the value the field can be resolved to. #[serde(rename = "type", default, skip_serializing_if = "is_default")] - pub type_of: String, - - /// - /// Flag to indicate the type is a list. - #[serde(default, skip_serializing_if = "is_default")] - pub list: bool, - - /// - /// Flag to indicate the type is required. - #[serde(default, skip_serializing_if = "is_default")] - pub required: bool, - - /// - /// Flag to indicate if the type inside the list is required. - #[serde(default, skip_serializing_if = "is_default")] - pub list_type_required: bool, + pub type_of: crate::core::Type, /// /// Map of argument name and its definition. @@ -318,29 +303,25 @@ impl Field { false } } - pub fn into_list(mut self) -> Self { - self.list = true; - self - } pub fn int() -> Self { - Self { type_of: "Int".to_string(), ..Default::default() } + Self { type_of: "Int".to_string().into(), ..Default::default() } } pub fn string() -> Self { - Self { type_of: "String".to_string(), ..Default::default() } + Self { type_of: "String".to_string().into(), ..Default::default() } } pub fn float() -> Self { - Self { type_of: "Float".to_string(), ..Default::default() } + Self { type_of: "Float".to_string().into(), ..Default::default() } } pub fn boolean() -> Self { - Self { type_of: "Boolean".to_string(), ..Default::default() } + Self { type_of: "Boolean".to_string().into(), ..Default::default() } } pub fn id() -> Self { - Self { type_of: "ID".to_string(), ..Default::default() } + Self { type_of: "ID".to_string().into(), ..Default::default() } } pub fn is_omitted(&self) -> bool { @@ -397,11 +378,7 @@ pub struct Inline { #[derive(Default, Serialize, Deserialize, Clone, Debug, PartialEq, Eq, schemars::JsonSchema)] pub struct Arg { #[serde(rename = "type")] - pub type_of: String, - #[serde(default, skip_serializing_if = "is_default")] - pub list: bool, - #[serde(default, skip_serializing_if = "is_default")] - pub required: bool, + pub type_of: crate::core::Type, #[serde(default, skip_serializing_if = "is_default")] pub doc: Option, #[serde(default, skip_serializing_if = "is_default")] @@ -843,8 +820,8 @@ impl Config { } else if let Some(type_) = self.find_type(type_of) { types.insert(type_of.into()); for (_, field) in type_.fields.iter() { - if !types.contains(&field.type_of) && !self.is_scalar(&field.type_of) { - types = self.find_connections(&field.type_of, types); + if !types.contains(field.type_of.name()) && !self.is_scalar(field.type_of.name()) { + types = self.find_connections(field.type_of.name(), types); } } } @@ -865,8 +842,8 @@ impl Config { pub fn input_types(&self) -> HashSet { self.arguments() .iter() - .filter(|(_, arg)| !self.is_scalar(&arg.type_of)) - .map(|(_, arg)| arg.type_of.as_str()) + .filter(|(_, arg)| !self.is_scalar(arg.type_of.name())) + .map(|(_, arg)| arg.type_of.name()) .fold(HashSet::new(), |types, type_of| { self.find_connections(type_of, types) }) @@ -959,8 +936,8 @@ impl Config { } else if let Some(typ) = self.types.get(&type_name) { set.insert(type_name); for field in typ.fields.values() { - stack.extend(field.args.values().map(|arg| arg.type_of.clone())); - stack.push(field.type_of.clone()); + stack.extend(field.args.values().map(|arg| arg.type_of.name().to_owned())); + stack.push(field.type_of.name().clone()); } for interface in typ.implements.iter() { stack.push(interface.clone()) diff --git a/src/core/config/from_document.rs b/src/core/config/from_document.rs index 5835a79542..3fa2af4a06 100644 --- a/src/core/config/from_document.rs +++ b/src/core/config/from_document.rs @@ -1,7 +1,7 @@ use std::collections::{BTreeMap, BTreeSet}; use async_graphql::parser::types::{ - BaseType, ConstDirective, EnumType, FieldDefinition, InputObjectType, InputValueDefinition, + ConstDirective, EnumType, FieldDefinition, InputObjectType, InputValueDefinition, InterfaceType, ObjectType, SchemaDefinition, ServiceDocument, Type, TypeDefinition, TypeKind, TypeSystemDefinition, UnionType, }; @@ -311,8 +311,6 @@ where F: FieldLike + HasName, { let type_of = field.type_of(); - let base = &type_of.base; - let nullable = &type_of.nullable; let description = field.description(); let directives = field.directives(); let default_value = default_value @@ -320,10 +318,6 @@ where .transpose() .map_err(|err| ValidationError::new(err.to_string())) .into(); - - let type_of = to_type_of(type_of); - let list = matches!(&base, BaseType::List(_)); - let list_type_required = matches!(&base, BaseType::List(type_of) if !type_of.nullable); let doc = description.to_owned().map(|pos| pos.node); config::Resolver::from_directives(directives) @@ -334,10 +328,7 @@ where .fuse(default_value) .map( |(resolver, cache, omit, modify, protected, default_value)| config::Field { - type_of, - list, - required: !nullable, - list_type_required, + type_of: type_of.into(), args, doc, modify, @@ -351,12 +342,6 @@ where .trace(pos_name_to_string(field.name()).as_str()) } -fn to_type_of(type_: &Type) -> String { - match &type_.base { - BaseType::Named(name) => name.to_string(), - BaseType::List(ty) => to_type_of(ty), - } -} fn to_args(field_definition: &FieldDefinition) -> IndexMap { let mut args = IndexMap::new(); @@ -369,9 +354,7 @@ fn to_args(field_definition: &FieldDefinition) -> IndexMap args } fn to_arg(input_value_definition: &InputValueDefinition) -> config::Arg { - let type_of = to_type_of(&input_value_definition.ty.node); - let list = matches!(&input_value_definition.ty.node.base, BaseType::List(_)); - let required = !input_value_definition.ty.node.nullable; + let type_of = &input_value_definition.ty.node; let doc = input_value_definition .description .to_owned() @@ -386,7 +369,7 @@ fn to_arg(input_value_definition: &InputValueDefinition) -> config::Arg { } else { None }; - config::Arg { type_of, list, required, doc, modify, default_value } + config::Arg { type_of: type_of.into(), doc, modify, default_value } } fn to_union(union_type: UnionType, doc: &Option) -> Union { diff --git a/src/core/config/into_document.rs b/src/core/config/into_document.rs index a33fe457f3..203c007338 100644 --- a/src/core/config/into_document.rs +++ b/src/core/config/into_document.rs @@ -3,7 +3,6 @@ use async_graphql::{Pos, Positioned}; use async_graphql_value::{ConstValue, Name}; use super::Config; -use crate::core::blueprint::TypeLike; use crate::core::directive::DirectiveCodec; fn pos(a: A) -> Positioned { @@ -73,21 +72,13 @@ fn config_document(config: &Config) -> ServiceDocument { .clone() .iter() .map(|(name, field)| { + let type_of = &field.type_of; let directives = get_directives(field); - let base_type = if field.list { - BaseType::List(Box::new(Type { - nullable: !field.list_type_required, - base: BaseType::Named(Name::new(field.type_of.clone())), - })) - } else { - BaseType::Named(Name::new(field.type_of.clone())) - }; pos(FieldDefinition { description: field.doc.clone().map(pos), name: pos(Name::new(name.clone())), arguments: vec![], - ty: pos(Type { nullable: !field.required, base: base_type }), - + ty: pos(type_of.into()), directives, }) }) @@ -100,25 +91,13 @@ fn config_document(config: &Config) -> ServiceDocument { .clone() .iter() .map(|(name, field)| { + let type_of = &field.type_of; let directives = get_directives(field); - let base_type = if field.list { - async_graphql::parser::types::BaseType::List(Box::new(Type { - nullable: !field.list_type_required, - base: async_graphql::parser::types::BaseType::Named(Name::new( - field.type_of.clone(), - )), - })) - } else { - async_graphql::parser::types::BaseType::Named(Name::new( - field.type_of.clone(), - )) - }; pos(async_graphql::parser::types::InputValueDefinition { description: field.doc.clone().map(pos), name: pos(Name::new(name.clone())), - ty: pos(Type { nullable: !field.required, base: base_type }), - + ty: pos(type_of.into()), default_value: transform_default_value(field.default_value.clone()) .map(pos), directives, @@ -139,40 +118,17 @@ fn config_document(config: &Config) -> ServiceDocument { .fields .iter() .map(|(name, field)| { + let type_of = &field.type_of; let directives = get_directives(field); - let base_type = if field.list { - async_graphql::parser::types::BaseType::List(Box::new(Type { - nullable: !field.list_type_required, - base: async_graphql::parser::types::BaseType::Named(Name::new( - field.type_of.clone(), - )), - })) - } else { - async_graphql::parser::types::BaseType::Named(Name::new( - field.type_of.clone(), - )) - }; let args_map = field.args.clone(); let args = args_map .iter() .map(|(name, arg)| { - let base_type = if arg.list { - async_graphql::parser::types::BaseType::List(Box::new(Type { - nullable: !arg.list_type_required(), - base: async_graphql::parser::types::BaseType::Named( - Name::new(arg.type_of.clone()), - ), - })) - } else { - async_graphql::parser::types::BaseType::Named(Name::new( - arg.type_of.clone(), - )) - }; pos(async_graphql::parser::types::InputValueDefinition { description: arg.doc.clone().map(pos), name: pos(Name::new(name.clone())), - ty: pos(Type { nullable: !arg.required, base: base_type }), + ty: pos((&arg.type_of).into()), default_value: transform_default_value( arg.default_value.clone(), @@ -187,8 +143,7 @@ fn config_document(config: &Config) -> ServiceDocument { description: field.doc.clone().map(pos), name: pos(Name::new(name.clone())), arguments: args, - ty: pos(Type { nullable: !field.required, base: base_type }), - + ty: pos(type_of.into()), directives, }) }) diff --git a/src/core/config/npo/tracker.rs b/src/core/config/npo/tracker.rs index 553f4d1019..43ec037315 100644 --- a/src/core/config/npo/tracker.rs +++ b/src/core/config/npo/tracker.rs @@ -136,10 +136,10 @@ impl<'a> PathTracker<'a> { } else { let mut visited = visited.clone(); visited.insert((type_name, field_name)); - let is_list = is_list | field.list; + let is_list = is_list | field.type_of.is_list(); chunks = chunks.concat(self.iter( path, - TypeName::new(field.type_of.as_str()), + TypeName::new(field.type_of.name()), is_list, visited, )) diff --git a/src/core/config/transformer/ambiguous_type.rs b/src/core/config/transformer/ambiguous_type.rs index d7be2bdfb0..47da6187bb 100644 --- a/src/core/config/transformer/ambiguous_type.rs +++ b/src/core/config/transformer/ambiguous_type.rs @@ -79,13 +79,13 @@ impl Transform for AmbiguousType { for args in field.args.values() { // if arg is of output type then it should be changed to that of // newly created input type. - if output_types.contains(&args.type_of) - && !resolution_map.contains_key(&args.type_of) + if output_types.contains(args.type_of.name()) + && !resolution_map.contains_key(args.type_of.name()) { - let resolution = (self.resolver)(args.type_of.as_str()); + let resolution = (self.resolver)(args.type_of.name()); resolution_map = insert_resolution( resolution_map, - args.type_of.as_str(), + args.type_of.name(), resolution, ); } @@ -123,16 +123,21 @@ impl Transform for AmbiguousType { for k in keys { if let Some(ty) = config.types.get_mut(&k) { for field in ty.fields.values_mut() { - if let Some(resolution) = resolution_map.get(&field.type_of) { + if let Some(resolution) = resolution_map.get(field.type_of.name()) { if output_types.contains(&k) { - field.type_of.clone_from(&resolution.output); + field.type_of = field + .type_of + .clone() + .with_name(resolution.output.to_owned()); } else if input_types.contains(&k) { - field.type_of.clone_from(&resolution.input); + field.type_of = + field.type_of.clone().with_name(resolution.input.to_owned()); } } for arg in field.args.values_mut() { - if let Some(resolution) = resolution_map.get(&arg.type_of) { - arg.type_of.clone_from(&resolution.input); + if let Some(resolution) = resolution_map.get(arg.type_of.name()) { + arg.type_of = + arg.type_of.clone().with_name(resolution.input.clone()); } } } @@ -151,27 +156,36 @@ mod tests { use tailcall_fixtures::protobuf; use crate::core::config::transformer::AmbiguousType; - use crate::core::config::{Config, Type}; + use crate::core::config::{self, Config}; use crate::core::generator::{Generator, Input}; use crate::core::proto_reader::ProtoMetadata; use crate::core::transform::Transform; use crate::core::valid::Validator; + use crate::core::Type; fn build_qry(mut config: Config) -> Config { - let mut query = Type::default(); - let mut field1 = - crate::core::config::Field { type_of: "Type1".to_string(), ..Default::default() }; + let mut query = config::Type::default(); + let mut field1 = crate::core::config::Field { + type_of: "Type1".to_string().into(), + ..Default::default() + }; - let arg1 = crate::core::config::Arg { type_of: "Type1".to_string(), ..Default::default() }; + let arg1 = crate::core::config::Arg { + type_of: Type::from("Type1".to_string()), + ..Default::default() + }; field1.args.insert("arg1".to_string(), arg1); - let arg2 = crate::core::config::Arg { type_of: "Type2".to_string(), ..Default::default() }; + let arg2 = crate::core::config::Arg { + type_of: Type::from("Type2".to_string()), + ..Default::default() + }; field1.args.insert("arg2".to_string(), arg2); let mut field2 = field1.clone(); - field2.type_of = "Type2".to_string(); + field2.type_of = "Type2".to_string().into(); query.fields.insert("field1".to_string(), field1); query.fields.insert("field2".to_string(), field2); @@ -187,27 +201,27 @@ mod tests { // Create a ConfigModule instance with ambiguous types let mut config = Config::default(); - let mut type1 = Type::default(); - let mut type2 = Type::default(); - let mut type3 = Type::default(); + let mut type1 = config::Type::default(); + let mut type2 = config::Type::default(); + let mut type3 = config::Type::default(); type1.fields.insert( "name".to_string(), - crate::core::config::Field::default().type_of("String".to_string()), + crate::core::config::Field::default().type_of("String".to_string().into()), ); type2.fields.insert( "ty1".to_string(), - crate::core::config::Field::default().type_of("Type1".to_string()), + crate::core::config::Field::default().type_of("Type1".to_string().into()), ); type3.fields.insert( "ty1".to_string(), - crate::core::config::Field::default().type_of("Type1".to_string()), + crate::core::config::Field::default().type_of("Type1".to_string().into()), ); type3.fields.insert( "ty2".to_string(), - crate::core::config::Field::default().type_of("Type2".to_string()), + crate::core::config::Field::default().type_of("Type2".to_string().into()), ); config.types.insert("Type1".to_string(), type1); diff --git a/src/core/config/transformer/flatten_single_field.rs b/src/core/config/transformer/flatten_single_field.rs index 0ef2a1adff..d6d57ce457 100644 --- a/src/core/config/transformer/flatten_single_field.rs +++ b/src/core/config/transformer/flatten_single_field.rs @@ -31,7 +31,7 @@ fn get_single_field_path( let sub_path = get_single_field_path( config, sub_field_name, - &sub_field.type_of, + sub_field.type_of.name(), visited_types, ); if let Some(sub_path) = sub_path { @@ -63,7 +63,7 @@ impl Transform for FlattenSingleField { if let Some(path) = get_single_field_path( &origin_config, field_name, - &field.type_of, + field.type_of.name(), &mut visited_types, ) { if path.len() > 1 { diff --git a/src/core/config/transformer/improve_type_names.rs b/src/core/config/transformer/improve_type_names.rs index 8cd791bf9c..93f270eda2 100644 --- a/src/core/config/transformer/improve_type_names.rs +++ b/src/core/config/transformer/improve_type_names.rs @@ -76,14 +76,14 @@ impl<'a> CandidateGeneration<'a> { fn generate(mut self) -> CandidateConvergence<'a> { for (type_name, type_info) in self.config.types.iter() { for (field_name, field_info) in type_info.fields.iter() { - if self.config.is_scalar(&field_info.type_of) { + if self.config.is_scalar(field_info.type_of.name()) { // If field type is scalar then ignore type name inference. continue; } let inner_map = self .candidates - .entry(field_info.type_of.to_owned()) + .entry(field_info.type_of.name().to_owned()) .or_default(); let singularized_candidate = pluralizer::pluralize(field_name, 1, false); @@ -127,9 +127,12 @@ impl ImproveTypeNames { // Replace all the instances of old name in config. for actual_type in config.types.values_mut() { for actual_field in actual_type.fields.values_mut() { - if actual_field.type_of == old_type_name { + if actual_field.type_of.name() == &old_type_name { // Update the field's type with the new name - actual_field.type_of.clone_from(&new_type_name); + actual_field.type_of = actual_field + .type_of + .clone() + .with_name(new_type_name.to_owned()); } } } diff --git a/src/core/config/transformer/merge_types/similarity.rs b/src/core/config/transformer/merge_types/similarity.rs index 396bdc8953..37473c4225 100644 --- a/src/core/config/transformer/merge_types/similarity.rs +++ b/src/core/config/transformer/merge_types/similarity.rs @@ -58,18 +58,18 @@ impl<'a> Similarity<'a> { for (field_name_1, field_1) in type_1.fields.iter() { if let Some(field_2) = type_2.fields.get(field_name_1) { - let field_1_type_of = field_1.type_of.to_owned(); - let field_2_type_of = field_2.type_of.to_owned(); + let field_1_type_of = field_1.type_of.name(); + let field_2_type_of = field_2.type_of.name(); - if config.is_scalar(&field_1_type_of) && config.is_scalar(&field_2_type_of) { + if config.is_scalar(field_1_type_of) && config.is_scalar(field_2_type_of) { // if field type_of is scalar and they don't match then we can't merge // types. - let json_scalar = Scalar::JSON.to_string(); + let json_scalar = &Scalar::JSON.to_string(); if field_1_type_of == field_2_type_of || field_1_type_of == json_scalar || field_2_type_of == json_scalar { - if field_1.list == field_2.list { + if field_1.type_of.is_list() == field_2.type_of.is_list() { same_field_count += 1; } else { return Valid::fail("Type merge failed: The fields have different list types and cannot be merged.".to_string()); @@ -83,16 +83,16 @@ impl<'a> Similarity<'a> { } else if field_1_type_of == field_2_type_of { // in order to consider the fields to be exactly same. // it's output type must match (we can ignore the required bounds). - if field_1.list == field_2.list { + if field_1.type_of.is_list() == field_2.type_of.is_list() { // if they're of both of list type then they're probably of same type. same_field_count += 1; } else { // If the list properties don't match, we cannot merge these types. return Valid::fail("Type merge failed: The fields have different list types and cannot be merged.".to_string()); } - } else if let Some(type_1) = config.types.get(field_1_type_of.as_str()) { - if let Some(type_2) = config.types.get(field_2_type_of.as_str()) { - if visited_type.contains(&field_1_type_of, &field_2_type_of) { + } else if let Some(type_1) = config.types.get(field_1_type_of) { + if let Some(type_2) = config.types.get(field_2_type_of) { + if visited_type.contains(field_1_type_of, field_2_type_of) { // it's cyclic type, return true as they're the same. return Valid::succeed(true); } @@ -102,8 +102,8 @@ impl<'a> Similarity<'a> { let type_info = SimilarityTypeInfo { type_1, type_2, - type_1_name: field_1_type_of.as_str(), - type_2_name: field_2_type_of.as_str(), + type_1_name: field_1_type_of, + type_2_name: field_2_type_of, }; let is_nested_type_similar = @@ -138,57 +138,58 @@ impl<'a> Similarity<'a> { #[cfg(test)] mod test { use super::Similarity; - use crate::core::config::{Config, Field, Type}; + use crate::core::config::{config, Config, Field}; use crate::core::valid::Validator; + use crate::core::Type; #[test] fn should_return_error_when_same_field_has_different_scalar_type() { - let mut foo1 = Type::default(); + let mut foo1 = config::Type::default(); foo1.fields.insert( "a".to_owned(), - Field { type_of: "Int".to_owned(), ..Default::default() }, + Field { type_of: "Int".to_owned().into(), ..Default::default() }, ); foo1.fields.insert( "b".to_owned(), - Field { type_of: "String".to_owned(), ..Default::default() }, + Field { type_of: "String".to_owned().into(), ..Default::default() }, ); foo1.fields.insert( "c".to_owned(), - Field { type_of: "Bar1".to_owned(), ..Default::default() }, + Field { type_of: "Bar1".to_owned().into(), ..Default::default() }, ); - let mut foo2 = Type::default(); + let mut foo2 = config::Type::default(); foo2.fields.insert( "a".to_owned(), - Field { type_of: "Int".to_owned(), ..Default::default() }, + Field { type_of: "Int".to_owned().into(), ..Default::default() }, ); foo2.fields.insert( "b".to_owned(), - Field { type_of: "Float".to_owned(), ..Default::default() }, + Field { type_of: "Float".to_owned().into(), ..Default::default() }, ); foo2.fields.insert( "c".to_owned(), - Field { type_of: "Bar2".to_owned(), ..Default::default() }, + Field { type_of: "Bar2".to_owned().into(), ..Default::default() }, ); - let mut bar1 = Type::default(); + let mut bar1 = config::Type::default(); bar1.fields.insert( "a".to_owned(), - Field { type_of: "Int".to_owned(), ..Default::default() }, + Field { type_of: "Int".to_owned().into(), ..Default::default() }, ); bar1.fields.insert( "c".to_owned(), - Field { type_of: "Float".to_owned(), ..Default::default() }, + Field { type_of: "Float".to_owned().into(), ..Default::default() }, ); - let mut bar2 = Type::default(); + let mut bar2 = config::Type::default(); bar2.fields.insert( "a".to_owned(), - Field { type_of: "Int".to_owned(), ..Default::default() }, + Field { type_of: "Int".to_owned().into(), ..Default::default() }, ); bar2.fields.insert( "c".to_owned(), - Field { type_of: "String".to_owned(), ..Default::default() }, + Field { type_of: "String".to_owned().into(), ..Default::default() }, ); let mut cfg: Config = Config::default(); @@ -207,28 +208,28 @@ mod test { #[test] fn test_cyclic_type() { - let mut foo1 = Type::default(); + let mut foo1 = config::Type::default(); foo1.fields.insert( "a".to_owned(), - Field { type_of: "Bar1".to_owned(), ..Default::default() }, + Field { type_of: "Bar1".to_owned().into(), ..Default::default() }, ); - let mut foo2 = Type::default(); + let mut foo2 = config::Type::default(); foo2.fields.insert( "a".to_owned(), - Field { type_of: "Bar2".to_owned(), ..Default::default() }, + Field { type_of: "Bar2".to_owned().into(), ..Default::default() }, ); - let mut bar1 = Type::default(); + let mut bar1 = config::Type::default(); bar1.fields.insert( "a".to_owned(), - Field { type_of: "Foo1".to_owned(), ..Default::default() }, + Field { type_of: "Foo1".to_owned().into(), ..Default::default() }, ); - let mut bar2 = Type::default(); + let mut bar2 = config::Type::default(); bar2.fields.insert( "a".to_owned(), - Field { type_of: "Foo2".to_owned(), ..Default::default() }, + Field { type_of: "Foo2".to_owned().into(), ..Default::default() }, ); let mut cfg: Config = Config::default(); @@ -248,39 +249,39 @@ mod test { #[test] fn test_nested_types() { - let mut foo1 = Type::default(); + let mut foo1 = config::Type::default(); foo1.fields.insert( "a".to_owned(), - Field { type_of: "Bar1".to_owned(), ..Default::default() }, + Field { type_of: "Bar1".to_owned().into(), ..Default::default() }, ); - let mut foo2 = Type::default(); + let mut foo2 = config::Type::default(); foo2.fields.insert( "a".to_owned(), - Field { type_of: "Bar2".to_owned(), ..Default::default() }, + Field { type_of: "Bar2".to_owned().into(), ..Default::default() }, ); - let mut bar1 = Type::default(); + let mut bar1 = config::Type::default(); bar1.fields.insert( "a".to_owned(), - Field { type_of: "Far1".to_owned(), ..Default::default() }, + Field { type_of: "Far1".to_owned().into(), ..Default::default() }, ); - let mut bar2 = Type::default(); + let mut bar2 = config::Type::default(); bar2.fields.insert( "a".to_owned(), - Field { type_of: "Far2".to_owned(), ..Default::default() }, + Field { type_of: "Far2".to_owned().into(), ..Default::default() }, ); - let mut far1 = Type::default(); + let mut far1 = config::Type::default(); far1.fields.insert( "a".to_owned(), - Field { type_of: "Int".to_owned(), ..Default::default() }, + Field { type_of: "Int".to_owned().into(), ..Default::default() }, ); - let mut far2 = Type::default(); + let mut far2 = config::Type::default(); far2.fields.insert( "a".to_owned(), - Field { type_of: "Int".to_owned(), ..Default::default() }, + Field { type_of: "Int".to_owned().into(), ..Default::default() }, ); let mut cfg: Config = Config::default(); @@ -303,14 +304,13 @@ mod test { #[test] fn test_required_and_optional_fields() { let required_int_field = Field { - type_of: "Int".to_owned(), - required: true, + type_of: Type::from("Int".to_owned()).into_required(), ..Default::default() }; - let optional_int_field = Field { type_of: "Int".to_owned(), ..Default::default() }; + let optional_int_field = Field { type_of: "Int".to_owned().into(), ..Default::default() }; - let mut ty1 = Type::default(); + let mut ty1 = config::Type::default(); ty1.fields .insert("a".to_string(), required_int_field.clone()); ty1.fields @@ -318,7 +318,7 @@ mod test { ty1.fields .insert("c".to_string(), required_int_field.clone()); - let mut ty2 = Type::default(); + let mut ty2 = config::Type::default(); ty2.fields .insert("a".to_string(), optional_int_field.clone()); ty2.fields @@ -340,20 +340,20 @@ mod test { #[test] fn test_required_list_of_optional_int_vs_optional_list() { let required_int_field = Field { - type_of: "Int".to_owned(), - list: true, - required: true, + type_of: Type::from("Int".to_owned()).into_list().into_required(), ..Default::default() }; - let optional_int_field = - Field { type_of: "Int".to_owned(), list: true, ..Default::default() }; + let optional_int_field = Field { + type_of: Type::from("Int".to_owned()).into_list(), + ..Default::default() + }; - let mut ty1 = Type::default(); + let mut ty1 = config::Type::default(); ty1.fields .insert("a".to_string(), required_int_field.clone()); - let mut ty2 = Type::default(); + let mut ty2 = config::Type::default(); ty2.fields .insert("a".to_string(), optional_int_field.clone()); @@ -371,24 +371,20 @@ mod test { #[test] fn test_list_of_required_int_vs_required_list() { let required_int_field = Field { - type_of: "Int".to_owned(), - list: true, - list_type_required: true, + type_of: Type::from("Int".to_owned()).into_required().into_list(), ..Default::default() }; let optional_int_field = Field { - type_of: "Int".to_owned(), - list: true, - required: true, + type_of: Type::from("Int".to_owned()).into_required().into_list(), ..Default::default() }; - let mut ty1 = Type::default(); + let mut ty1 = config::Type::default(); ty1.fields .insert("a".to_string(), required_int_field.clone()); - let mut ty2 = Type::default(); + let mut ty2 = config::Type::default(); ty2.fields .insert("a".to_string(), optional_int_field.clone()); @@ -406,17 +402,15 @@ mod test { #[test] fn test_list_of_required_int_vs_list_of_required_int() { let required_int_field = Field { - type_of: "Int".to_owned(), - list: true, - list_type_required: true, + type_of: Type::from("Int".to_owned()).into_required().into_list(), ..Default::default() }; - let mut ty1 = Type::default(); + let mut ty1 = config::Type::default(); ty1.fields .insert("a".to_string(), required_int_field.clone()); - let mut ty2 = Type::default(); + let mut ty2 = config::Type::default(); ty2.fields .insert("a".to_string(), required_int_field.clone()); @@ -434,17 +428,15 @@ mod test { #[test] fn test_required_list_vs_required_list() { let required_int_field = Field { - type_of: "Int".to_owned(), - list: true, - required: true, + type_of: Type::from("Int".to_owned()).into_list().into_required(), ..Default::default() }; - let mut ty1 = Type::default(); + let mut ty1 = config::Type::default(); ty1.fields .insert("a".to_string(), required_int_field.clone()); - let mut ty2 = Type::default(); + let mut ty2 = config::Type::default(); ty2.fields .insert("a".to_string(), required_int_field.clone()); @@ -462,18 +454,18 @@ mod test { #[test] fn test_required_list_of_required_int_vs_required_list_of_required_int() { let required_int_field = Field { - type_of: "Int".to_owned(), - list: true, - required: true, - list_type_required: true, + type_of: Type::from("Int".to_owned()) + .into_required() + .into_list() + .into_required(), ..Default::default() }; - let mut ty1 = Type::default(); + let mut ty1 = config::Type::default(); ty1.fields .insert("a".to_string(), required_int_field.clone()); - let mut ty2 = Type::default(); + let mut ty2 = config::Type::default(); ty2.fields .insert("a".to_string(), required_int_field.clone()); @@ -491,16 +483,19 @@ mod test { #[test] fn test_merge_incompatible_list_and_non_list_fields() { // Define fields - let int_field = Field { type_of: "Int".to_owned(), ..Default::default() }; - let list_int_field = Field { type_of: "Int".to_owned(), list: true, ..Default::default() }; + let int_field = Field { type_of: "Int".to_owned().into(), ..Default::default() }; + let list_int_field = Field { + type_of: Type::from("Int".to_owned()).into_list(), + ..Default::default() + }; // Define types Foo and Bar - let mut foo = Type::default(); + let mut foo = config::Type::default(); foo.fields.insert("a".to_string(), int_field.clone()); foo.fields.insert("b".to_string(), int_field.clone()); foo.fields.insert("c".to_string(), list_int_field.clone()); - let mut bar = Type::default(); + let mut bar = config::Type::default(); bar.fields.insert("a".to_string(), int_field.clone()); bar.fields.insert("b".to_string(), int_field.clone()); bar.fields.insert("c".to_string(), int_field.clone()); diff --git a/src/core/config/transformer/merge_types/type_merger.rs b/src/core/config/transformer/merge_types/type_merger.rs index f735af0b43..dbd88ffb8f 100644 --- a/src/core/config/transformer/merge_types/type_merger.rs +++ b/src/core/config/transformer/merge_types/type_merger.rs @@ -113,17 +113,23 @@ impl TypeMerger { for type_info in config.types.values_mut() { for actual_field in type_info.fields.values_mut() { if let Some(merged_into_type_name) = - type_to_merge_type_mapping.get(actual_field.type_of.as_str()) + type_to_merge_type_mapping.get(actual_field.type_of.name()) { - actual_field.type_of = merged_into_type_name.to_string(); + actual_field.type_of = actual_field + .type_of + .clone() + .with_name(merged_into_type_name.to_string()); } // make the changes in the input arguments as well. for arg_ in actual_field.args.values_mut() { if let Some(merge_into_type_name) = - type_to_merge_type_mapping.get(arg_.type_of.as_str()) + type_to_merge_type_mapping.get(arg_.type_of.name()) { - arg_.type_of = merge_into_type_name.to_string(); + arg_.type_of = arg_ + .type_of + .clone() + .with_name(merge_into_type_name.to_owned()); } } } @@ -205,10 +211,10 @@ fn merge_type(type_: &Type, mut merge_into: Type) -> Type { .entry(key.to_owned()) .and_modify(|existing_field| { let mut merged_field = existing_field.clone().merge_right(new_field.clone()); - if existing_field.type_of == Scalar::JSON.to_string() - || new_field.type_of == Scalar::JSON.to_string() + if existing_field.type_of.name() == &Scalar::JSON.to_string() + || new_field.type_of.name() == &Scalar::JSON.to_string() { - merged_field.type_of = Scalar::JSON.to_string(); + merged_field.type_of = Scalar::JSON.to_string().into(); } *existing_field = merged_field; }) @@ -238,9 +244,9 @@ mod test { #[test] fn test_cyclic_merge_case() -> anyhow::Result<()> { - let str_field = Field { type_of: "String".to_owned(), ..Default::default() }; - let int_field = Field { type_of: "Int".to_owned(), ..Default::default() }; - let bool_field = Field { type_of: "Boolean".to_owned(), ..Default::default() }; + let str_field = Field { type_of: "String".to_owned().into(), ..Default::default() }; + let int_field = Field { type_of: "Int".to_owned().into(), ..Default::default() }; + let bool_field = Field { type_of: "Boolean".to_owned().into(), ..Default::default() }; let mut ty1 = Type::default(); ty1.fields.insert("body".to_string(), str_field.clone()); @@ -252,7 +258,7 @@ mod test { let mut ty2 = Type::default(); ty2.fields.insert( "t1".to_string(), - Field { type_of: "T1".to_string(), ..Default::default() }, + Field { type_of: "T1".to_string().into(), ..Default::default() }, ); ty2.fields .insert("is_verified".to_string(), bool_field.clone()); @@ -267,11 +273,11 @@ mod test { let mut q_type = Type::default(); q_type.fields.insert( "q1".to_string(), - Field { type_of: "T1".to_string(), ..Default::default() }, + Field { type_of: "T1".to_string().into(), ..Default::default() }, ); q_type.fields.insert( "q2".to_string(), - Field { type_of: "T2".to_string(), ..Default::default() }, + Field { type_of: "T2".to_string().into(), ..Default::default() }, ); config.types.insert("Query".to_owned(), q_type); @@ -286,11 +292,11 @@ mod test { #[test] fn test_type_merger() -> anyhow::Result<()> { - let str_field = Field { type_of: "String".to_owned(), ..Default::default() }; - let int_field = Field { type_of: "Int".to_owned(), ..Default::default() }; - let bool_field = Field { type_of: "Boolean".to_owned(), ..Default::default() }; - let float_field = Field { type_of: "Float".to_owned(), ..Default::default() }; - let id_field = Field { type_of: "ID".to_owned(), ..Default::default() }; + let str_field = Field { type_of: "String".to_owned().into(), ..Default::default() }; + let int_field = Field { type_of: "Int".to_owned().into(), ..Default::default() }; + let bool_field = Field { type_of: "Boolean".to_owned().into(), ..Default::default() }; + let float_field = Field { type_of: "Float".to_owned().into(), ..Default::default() }; + let id_field = Field { type_of: "ID".to_owned().into(), ..Default::default() }; let mut ty = Type::default(); ty.fields.insert("f1".to_string(), str_field.clone()); @@ -308,19 +314,19 @@ mod test { let mut q_type = Type::default(); q_type.fields.insert( "q1".to_string(), - Field { type_of: "T1".to_string(), ..Default::default() }, + Field { type_of: "T1".to_string().into(), ..Default::default() }, ); q_type.fields.insert( "q2".to_string(), - Field { type_of: "T2".to_string(), ..Default::default() }, + Field { type_of: "T2".to_string().into(), ..Default::default() }, ); q_type.fields.insert( "q3".to_string(), - Field { type_of: "T3".to_string(), ..Default::default() }, + Field { type_of: "T3".to_string().into(), ..Default::default() }, ); q_type.fields.insert( "q4".to_string(), - Field { type_of: "T4".to_string(), ..Default::default() }, + Field { type_of: "T4".to_string().into(), ..Default::default() }, ); config.types.insert("Query".to_owned(), q_type); @@ -361,8 +367,8 @@ mod test { #[test] fn test_fail_when_scalar_field_not_match() { - let str_field = Field { type_of: "String".to_owned(), ..Default::default() }; - let int_field = Field { type_of: "Int".to_owned(), ..Default::default() }; + let str_field = Field { type_of: "String".to_owned().into(), ..Default::default() }; + let int_field = Field { type_of: "Int".to_owned().into(), ..Default::default() }; let mut ty1 = Type::default(); ty1.fields.insert("a".to_string(), int_field.clone()); @@ -384,7 +390,7 @@ mod test { #[test] fn test_interface_types() { - let int_field = Field { type_of: "Int".to_owned(), ..Default::default() }; + let int_field = Field { type_of: "Int".to_owned().into(), ..Default::default() }; let mut ty1 = Type::default(); ty1.fields.insert("a".to_string(), int_field.clone()); @@ -413,7 +419,7 @@ mod test { schema { query: Query } - + type Bar { id: Int name: JSON diff --git a/src/core/config/transformer/rename_types.rs b/src/core/config/transformer/rename_types.rs index a189b5fbdf..07a0f90526 100644 --- a/src/core/config/transformer/rename_types.rs +++ b/src/core/config/transformer/rename_types.rs @@ -53,13 +53,15 @@ impl Transform for RenameTypes { for type_ in config.types.values_mut() { for field_ in type_.fields.values_mut() { // replace type of field. - if let Some(suggested_name) = lookup.get(&field_.type_of) { - field_.type_of = suggested_name.to_owned(); + if let Some(suggested_name) = lookup.get(field_.type_of.name()) { + field_.type_of = + field_.type_of.clone().with_name(suggested_name.to_owned()); } // replace type of argument. for arg_ in field_.args.values_mut() { - if let Some(suggested_name) = lookup.get(&arg_.type_of) { - arg_.type_of = suggested_name.clone(); + if let Some(suggested_name) = lookup.get(arg_.type_of.name()) { + arg_.type_of = + arg_.type_of.clone().with_name(suggested_name.to_owned()); } } } diff --git a/src/core/config/transformer/union_input_type.rs b/src/core/config/transformer/union_input_type.rs index 7bacb48838..b43132dc68 100644 --- a/src/core/config/transformer/union_input_type.rs +++ b/src/core/config/transformer/union_input_type.rs @@ -100,7 +100,7 @@ impl<'cfg> Visitor<'cfg> { field .args .values() - .for_each(|arg| self.collect_nested_unions_for_type(&arg.type_of)) + .for_each(|arg| self.collect_nested_unions_for_type(arg.type_of.name())) } /// Recursively walks over nested types and fills union_presence info @@ -135,7 +135,7 @@ impl<'cfg> Visitor<'cfg> { } else if let Some(type_) = self.config.types.get(type_name) { // first, recursively walk over nested fields to see if there any nested unions for field in type_.fields.values() { - self.collect_nested_unions_for_type(&field.type_of); + self.collect_nested_unions_for_type(field.type_of.name()); } // store any fields that contain union @@ -145,7 +145,7 @@ impl<'cfg> Visitor<'cfg> { // to multiple types. As separate loop to bypass borrow checker for (field_name, field) in &type_.fields { if let Some(UnionPresence::Union(union_types)) = - self.union_presence.get(&field.type_of) + self.union_presence.get(field.type_of.name()) { union_fields.push((field_name, union_types)); } @@ -204,11 +204,15 @@ impl<'cfg> Visitor<'cfg> { let args = &args[1..]; - if let Some(UnionPresence::Union(union_types)) = self.union_presence.get(&arg.type_of) { + if let Some(UnionPresence::Union(union_types)) = self.union_presence.get(arg.type_of.name()) + { // if the type is union walk over all type members and generate new separate // field for this variant for (i, type_) in union_types.iter().enumerate() { - let new_arg = Arg { type_of: type_.clone(), ..arg.clone() }; + let new_arg = Arg { + type_of: arg.type_of.clone().with_name(type_.to_owned()), + ..arg.clone() + }; current_field.args.insert(arg_name.to_string(), new_arg); self.walk_arguments( @@ -255,7 +259,7 @@ impl<'cfg> Visitor<'cfg> { .get_mut(*field_name) .expect("Only available fields could be in list of union_fields"); - field.type_of.clone_from(union_type); + field.type_of = field.type_of.clone().with_name(union_type.to_owned()); inner_create(type_name, new_type, union_fields, result); } diff --git a/src/core/generator/from_proto.rs b/src/core/generator/from_proto.rs index a7bd6ba215..ee52d01a78 100644 --- a/src/core/generator/from_proto.rs +++ b/src/core/generator/from_proto.rs @@ -13,9 +13,10 @@ use super::proto::comments_builder::CommentsBuilder; use super::proto::path_builder::PathBuilder; use super::proto::path_field::PathField; use crate::core::config::transformer::{AmbiguousType, TreeShake}; -use crate::core::config::{Arg, Config, Enum, Field, Grpc, Resolver, Type, Union, Variant}; +use crate::core::config::{self, Arg, Config, Enum, Field, Grpc, Resolver, Union, Variant}; use crate::core::transform::{Transform, TransformerOps}; use crate::core::valid::Validator; +use crate::core::Type; /// Assists in the mapping and retrieval of proto type names to custom formatted /// strings based on the descriptor type. @@ -56,7 +57,7 @@ impl Context { } /// Resolves the actual name and inserts the type. - fn insert_type(mut self, name: String, ty: Type) -> Self { + fn insert_type(mut self, name: String, ty: config::Type) -> Self { self.config.types.insert(name.to_string(), ty); self } @@ -64,16 +65,16 @@ impl Context { /// Converts oneof definitions in message to set of types with union fn insert_oneofs( mut self, - type_name: String, // name of the message - base_type: Type, // that's the type with fields that are not oneofs + type_name: String, // name of the message + base_type: config::Type, // that's the type with fields that are not oneofs oneof_fields: Vec>, /* there is multiple oneof definitions every - * one of which contains multiple fields */ + * one of which contains multiple fields */ ) -> Self { fn collect_types( type_name: String, - base_type: Type, + base_type: config::Type, oneof_fields: &[Vec<(String, Field)>], // currently processed set of oneof fields - output: &mut Vec<(String, Type)>, // newly generated types with their names + output: &mut Vec<(String, config::Type)>, // newly generated types with their names ) { let Some(one_of) = oneof_fields.first() else { output.push((type_name, base_type)); @@ -109,7 +110,7 @@ impl Context { let mut field = field.clone(); // mark this field as required to force type-check on specific variant of oneof - field.required = true; + field.type_of = field.type_of.into_required(); // add new field specific to this variant of oneof field new_type.fields.insert(field_name.clone(), field); @@ -260,7 +261,7 @@ impl Context { let mut oneof_fields: Vec<_> = message.oneof_decl.iter().map(|_| Vec::new()).collect(); - let mut ty = Type { + let mut ty = config::Type { doc: self.comments_builder.get_comments(&msg_path), ..Default::default() }; @@ -272,10 +273,12 @@ impl Context { let mut cfg_field = Field::default(); - let label = field.label(); - cfg_field.list = matches!(label, Label::Repeated); - // required only applicable for proto2 - cfg_field.required = matches!(label, Label::Required); + cfg_field.type_of = match field.label() { + Label::Optional => cfg_field.type_of, + // required only applicable for proto2 + Label::Required => cfg_field.type_of.into_required(), + Label::Repeated => cfg_field.type_of.into_list(), + }; if let Some(type_name) = &field.type_name { // check that current field is map. @@ -283,22 +286,20 @@ impl Context { // inside the nested type. It works only if we explore nested types // before the current type if self.map_types.contains(&type_name[1..]) { - cfg_field.type_of = "JSON".to_string(); - // drop list option since it is not relevant - // when using JSON representation - cfg_field.list = false; + // override type with single scalar + cfg_field.type_of = "JSON".to_string().into(); } else { // for non-primitive types let type_of = graphql_type_from_ref(type_name)? .into_object_type() .to_string(); - cfg_field.type_of = type_of; + cfg_field.type_of = cfg_field.type_of.with_name(type_of); } } else { let type_of = convert_primitive_type(field.r#type().as_str_name()); - cfg_field.type_of = type_of; + cfg_field.type_of = cfg_field.type_of.with_name(type_of); } let field_path = @@ -348,9 +349,7 @@ impl Context { let key = graphql_type.clone().into_field().to_string(); let type_of = graphql_type.into_object_type().to_string(); let val = Arg { - type_of, - list: false, - required: true, + type_of: Type::from(type_of).into_required(), /* Setting it not null by default. There's no way to infer this * from proto file */ doc: None, @@ -365,7 +364,7 @@ impl Context { let output_ty = get_output_type(method.output_type())? .into_object_type() .to_string(); - cfg_field.type_of = output_ty; + cfg_field.type_of = cfg_field.type_of.with_name(output_ty); cfg_field.resolver = Some(Resolver::Grpc(Grpc { base_url: None, @@ -385,7 +384,7 @@ impl Context { .entry(self.query.clone()) .or_insert_with(|| { self.config.schema.query = Some(self.query.clone()); - Type::default() + config::Type::default() }); ty.fields.insert(field_name.to_string(), cfg_field); diff --git a/src/core/generator/json/field_base_url_generator.rs b/src/core/generator/json/field_base_url_generator.rs index e30f19f449..1fec627b4d 100644 --- a/src/core/generator/json/field_base_url_generator.rs +++ b/src/core/generator/json/field_base_url_generator.rs @@ -65,7 +65,7 @@ mod test { query_type.fields.insert( "f1".to_string(), Field { - type_of: "Int".to_string(), + type_of: "Int".to_string().into(), resolver: Some(Resolver::Http(Http { path: "/day".to_string(), ..Default::default() @@ -76,7 +76,7 @@ mod test { query_type.fields.insert( "f2".to_string(), Field { - type_of: "String".to_string(), + type_of: "String".to_string().into(), resolver: Some(Resolver::Http(Http { path: "/month".to_string(), ..Default::default() @@ -87,7 +87,7 @@ mod test { query_type.fields.insert( "f3".to_string(), Field { - type_of: "String".to_string(), + type_of: "String".to_string().into(), resolver: Some(Resolver::Http(Http { path: "/status".to_string(), ..Default::default() @@ -112,7 +112,7 @@ mod test { query_type.fields.insert( "f1".to_string(), Field { - type_of: "Int".to_string(), + type_of: "Int".to_string().into(), resolver: Some(Resolver::Http(Http { base_url: Some("https://calender.com/api/v1/".to_string()), path: "/day".to_string(), @@ -124,7 +124,7 @@ mod test { query_type.fields.insert( "f2".to_string(), Field { - type_of: "String".to_string(), + type_of: "String".to_string().into(), resolver: Some(Resolver::Http(Http { path: "/month".to_string(), ..Default::default() @@ -135,7 +135,7 @@ mod test { query_type.fields.insert( "f3".to_string(), Field { - type_of: "String".to_string(), + type_of: "String".to_string().into(), resolver: None, ..Default::default() }, diff --git a/src/core/generator/json/http_directive_generator.rs b/src/core/generator/json/http_directive_generator.rs index e2f0c3b554..5dad164164 100644 --- a/src/core/generator/json/http_directive_generator.rs +++ b/src/core/generator/json/http_directive_generator.rs @@ -6,6 +6,7 @@ use url::Url; use crate::core::config::{Arg, Field, Http, URLQuery}; use crate::core::helpers::gql_type::detect_gql_data_type; +use crate::core::Type; #[derive(Debug)] struct QueryParamInfo { @@ -79,8 +80,7 @@ impl<'a> HttpDirectiveGenerator<'a> { let placeholder = format!("/{{{{.args.{}}}}}", arg_key); let arg = Arg { - type_of: type_of.to_string(), - required: true, + type_of: Type::from(type_of.to_owned()).into_required(), ..Default::default() }; @@ -100,13 +100,15 @@ impl<'a> HttpDirectiveGenerator<'a> { let url_utility = UrlUtility::new(self.url); for query in url_utility.get_query_params() { - let arg = Arg { - list: query.is_list, - type_of: query.data_type, - required: false, - ..Default::default() + let type_of = Type::from(query.data_type.clone()); + let type_of = if query.is_list { + type_of.into_list() + } else { + type_of }; + let arg = Arg { type_of, ..Default::default() }; + // Convert query key to camel case for better readability. let query_key = query.key.to_case(Case::Camel); let value: String = format!("{{{{.args.{}}}}}", query_key); @@ -200,7 +202,7 @@ mod test { let args: HashMap = field .args .iter() - .map(|(name, arg)| (name.to_string(), arg.type_of.clone())) + .map(|(name, arg)| (name.to_string(), arg.type_of.name().to_owned())) .collect::>(); let test_args = vec![ ("p1".to_string(), "Int".to_string()), diff --git a/src/core/generator/json/operation_generator.rs b/src/core/generator/json/operation_generator.rs index 6097cd808a..d80b10c33d 100644 --- a/src/core/generator/json/operation_generator.rs +++ b/src/core/generator/json/operation_generator.rs @@ -1,10 +1,11 @@ use convert_case::{Case, Casing}; use super::http_directive_generator::HttpDirectiveGenerator; -use crate::core::config::{Arg, Config, Field, GraphQLOperationType, Resolver, Type}; +use crate::core::config::{Arg, Config, Field, GraphQLOperationType, Resolver}; use crate::core::generator::json::types_generator::TypeGenerator; use crate::core::generator::{NameGenerator, RequestSample}; use crate::core::valid::Valid; +use crate::core::{config, Type}; pub struct OperationTypeGenerator; @@ -17,9 +18,13 @@ impl OperationTypeGenerator { name_generator: &NameGenerator, mut config: Config, ) -> Valid { + let type_of = Type::from(root_type.to_owned()); let mut field = Field { - list: request_sample.res_body.is_array(), - type_of: root_type.to_owned(), + type_of: if request_sample.res_body.is_array() { + type_of.into_list() + } else { + type_of + }, ..Default::default() }; @@ -39,9 +44,10 @@ impl OperationTypeGenerator { http.body = Some(format!("{{{{.args.{}}}}}", arg_name)); http.method = request_sample.method.to_owned(); } - field - .args - .insert(arg_name, Arg { type_of: root_ty, ..Default::default() }); + field.args.insert( + arg_name, + Arg { type_of: root_ty.into(), ..Default::default() }, + ); } // if type is already present, then append the new field to it else create one. @@ -54,7 +60,7 @@ impl OperationTypeGenerator { .fields .insert(request_sample.field_name.to_owned(), field); } else { - let mut ty = Type::default(); + let mut ty = config::Type::default(); ty.fields .insert(request_sample.field_name.to_owned(), field); config.types.insert(req_op.to_owned(), ty); @@ -101,7 +107,7 @@ mod test { let mut fields = BTreeMap::default(); fields.insert( "post".to_owned(), - Field { type_of: "Int".to_owned(), ..Default::default() }, + Field { type_of: "Int".to_owned().into(), ..Default::default() }, ); let type_ = Type { fields, ..Default::default() }; diff --git a/src/core/generator/json/types_generator.rs b/src/core/generator/json/types_generator.rs index 4e6ef5e17a..9e77fb7042 100644 --- a/src/core/generator/json/types_generator.rs +++ b/src/core/generator/json/types_generator.rs @@ -36,10 +36,10 @@ impl TypeMerger { for current_type in type_list { for (key, new_field) in current_type.fields { if let Some(existing_field) = ty.fields.get(&key) { - if existing_field.type_of.is_empty() - || existing_field.type_of == Scalar::Empty.to_string() - || (existing_field.type_of == Scalar::JSON.to_string() - && new_field.type_of != Scalar::Empty.to_string()) + if existing_field.type_of.name().is_empty() + || existing_field.type_of.name() == &Scalar::Empty.to_string() + || (existing_field.type_of.name() == &Scalar::JSON.to_string() + && new_field.type_of.name() != &Scalar::Empty.to_string()) { ty.fields.insert(key, new_field); } @@ -76,25 +76,29 @@ impl<'a> TypeGenerator<'a> { ) -> Type { let mut ty = Type::default(); for (json_property, json_val) in json_object { - let field = if !JSONValidator::is_graphql_compatible(json_val) { + let mut field = if !JSONValidator::is_graphql_compatible(json_val) { // if object, array is empty or object has in-compatible fields then // generate scalar for it. Field { - type_of: self.generate_scalar(config).to_string(), - list: json_val.is_array(), + type_of: self.generate_scalar(config).to_string().into(), ..Default::default() } } else { let mut field = Field::default(); if is_primitive(json_val) { - field.type_of = to_gql_type(json_val); + field.type_of = to_gql_type(json_val).into(); } else { let type_name = self.generate_types(json_val, config); - field.type_of = type_name; - field.list = json_val.is_array() + field.type_of = type_name.into(); } field }; + field.type_of = if json_val.is_array() { + field.type_of.into_list() + } else { + field.type_of + }; + ty.fields.insert(json_property.to_string(), field); } ty diff --git a/src/core/ir/discriminator.rs b/src/core/ir/discriminator.rs index bfd7511392..ee8f08057c 100644 --- a/src/core/ir/discriminator.rs +++ b/src/core/ir/discriminator.rs @@ -147,7 +147,7 @@ impl Discriminator { info.presented_in |= repr; // And information if it is required in this type. - if field.required { + if !field.type_of.is_nullable() { info.required_in |= repr; } } @@ -353,13 +353,14 @@ mod tests { use test_log::test; use super::Discriminator; - use crate::core::config::{Field, Type}; + use crate::core::config::Field; use crate::core::valid::Validator; + use crate::core::{config, Type}; #[test] fn test_single_distinct_field_optional() { - let foo = Type::default().fields(vec![("foo", Field::default())]); - let bar = Type::default().fields(vec![("bar", Field::default())]); + let foo = config::Type::default().fields(vec![("foo", Field::default())]); + let bar = config::Type::default().fields(vec![("bar", Field::default())]); let types = vec![("Foo", &foo), ("Bar", &bar)]; let discriminator = Discriminator::new("Test", &types).to_result().unwrap(); @@ -403,10 +404,14 @@ mod tests { #[test] fn test_single_distinct_field_required() { - let foo = - Type::default().fields(vec![("foo", Field { required: true, ..Field::default() })]); - let bar = - Type::default().fields(vec![("bar", Field { required: true, ..Field::default() })]); + let foo = config::Type::default().fields(vec![( + "foo", + Field { type_of: Type::default().into_required(), ..Field::default() }, + )]); + let bar = config::Type::default().fields(vec![( + "bar", + Field { type_of: Type::default().into_required(), ..Field::default() }, + )]); let types = vec![("Foo", &foo), ("Bar", &bar)]; let discriminator = Discriminator::new("Test", &types).to_result().unwrap(); @@ -450,20 +455,47 @@ mod tests { #[test] fn test_multiple_distinct_field_required() { - let a = Type::default().fields(vec![ - ("a", Field { required: true, ..Field::default() }), - ("ab", Field { required: true, ..Field::default() }), - ("abab", Field { required: true, ..Field::default() }), + let a = config::Type::default().fields(vec![ + ( + "a", + Field { type_of: Type::default().into_required(), ..Field::default() }, + ), + ( + "ab", + Field { type_of: Type::default().into_required(), ..Field::default() }, + ), + ( + "abab", + Field { type_of: Type::default().into_required(), ..Field::default() }, + ), ]); - let b = Type::default().fields(vec![ - ("b", Field { required: true, ..Field::default() }), - ("ab", Field { required: true, ..Field::default() }), - ("abab", Field { required: true, ..Field::default() }), - ("ac", Field { required: true, ..Field::default() }), + let b = config::Type::default().fields(vec![ + ( + "b", + Field { type_of: Type::default().into_required(), ..Field::default() }, + ), + ( + "ab", + Field { type_of: Type::default().into_required(), ..Field::default() }, + ), + ( + "abab", + Field { type_of: Type::default().into_required(), ..Field::default() }, + ), + ( + "ac", + Field { type_of: Type::default().into_required(), ..Field::default() }, + ), ]); - let c = Type::default().fields(vec![ - ("c", Field { required: true, ..Field::default() }), - ("ac", Field { required: true, ..Field::default() }), + let c = config::Type::default().fields(vec![ + ( + "c", + Field { type_of: Type::default().into_required(), ..Field::default() }, + ), + ( + "ac", + Field { type_of: Type::default().into_required(), ..Field::default() }, + ), ]); let types = vec![("A", &a), ("B", &b), ("C", &c)]; @@ -515,12 +547,12 @@ mod tests { #[test] fn test_single_distinct_field_optional_and_shared_fields() { - let foo = Type::default().fields(vec![ + let foo = config::Type::default().fields(vec![ ("a", Field::default()), ("b", Field::default()), ("foo", Field::default()), ]); - let bar = Type::default().fields(vec![ + let bar = config::Type::default().fields(vec![ ("a", Field::default()), ("b", Field::default()), ("bar", Field::default()), @@ -592,12 +624,12 @@ mod tests { #[test] fn test_multiple_distinct_fields() { - let foo = Type::default().fields(vec![ + let foo = config::Type::default().fields(vec![ ("a", Field::default()), ("b", Field::default()), ("foo", Field::default()), ]); - let bar = Type::default().fields(vec![("bar", Field::default())]); + let bar = config::Type::default().fields(vec![("bar", Field::default())]); let types = vec![("Foo", &foo), ("Bar", &bar)]; let discriminator = Discriminator::new("Test", &types).to_result().unwrap(); @@ -650,18 +682,18 @@ mod tests { #[test] fn test_fields_intersection() { - let a = Type::default().fields(vec![ + let a = config::Type::default().fields(vec![ ("shared", Field::default()), ("a", Field::default()), ("aa", Field::default()), ("aaa", Field::default()), ]); - let b = Type::default().fields(vec![ + let b = config::Type::default().fields(vec![ ("shared", Field::default()), ("b", Field::default()), ("aa", Field::default()), ]); - let c = Type::default().fields(vec![ + let c = config::Type::default().fields(vec![ ("shared", Field::default()), ("c", Field::default()), ("aaa", Field::default()), @@ -718,42 +750,78 @@ mod tests { #[test] fn test_fields_protobuf_oneof() { - let var_var = Type::default().fields(vec![("usual", Field::default())]); - let var0_var = Type::default().fields(vec![ + let var_var = config::Type::default().fields(vec![("usual", Field::default())]); + let var0_var = config::Type::default().fields(vec![ ("usual", Field::default()), - ("payload", Field { required: true, ..Field::default() }), + ( + "payload", + Field { type_of: Type::default().into_required(), ..Field::default() }, + ), ]); - let var1_var = Type::default().fields(vec![ + let var1_var = config::Type::default().fields(vec![ ("usual", Field::default()), - ("command", Field { required: true, ..Field::default() }), + ( + "command", + Field { type_of: Type::default().into_required(), ..Field::default() }, + ), ]); - let var_var0 = Type::default().fields(vec![ + let var_var0 = config::Type::default().fields(vec![ ("usual", Field::default()), - ("flag", Field { required: true, ..Field::default() }), + ( + "flag", + Field { type_of: Type::default().into_required(), ..Field::default() }, + ), ]); - let var_var1 = Type::default().fields(vec![ + let var_var1 = config::Type::default().fields(vec![ ("usual", Field::default()), - ("optPayload", Field { required: true, ..Field::default() }), + ( + "optPayload", + Field { type_of: Type::default().into_required(), ..Field::default() }, + ), ]); - let var0_var0 = Type::default().fields(vec![ + let var0_var0 = config::Type::default().fields(vec![ ("usual", Field::default()), - ("payload", Field { required: true, ..Field::default() }), - ("flag", Field { required: true, ..Field::default() }), + ( + "payload", + Field { type_of: Type::default().into_required(), ..Field::default() }, + ), + ( + "flag", + Field { type_of: Type::default().into_required(), ..Field::default() }, + ), ]); - let var1_var0 = Type::default().fields(vec![ + let var1_var0 = config::Type::default().fields(vec![ ("usual", Field::default()), - ("command", Field { required: true, ..Field::default() }), - ("flag", Field { required: true, ..Field::default() }), + ( + "command", + Field { type_of: Type::default().into_required(), ..Field::default() }, + ), + ( + "flag", + Field { type_of: Type::default().into_required(), ..Field::default() }, + ), ]); - let var0_var1 = Type::default().fields(vec![ + let var0_var1 = config::Type::default().fields(vec![ ("usual", Field::default()), - ("payload", Field { required: true, ..Field::default() }), - ("optPayload", Field { required: true, ..Field::default() }), + ( + "payload", + Field { type_of: Type::default().into_required(), ..Field::default() }, + ), + ( + "optPayload", + Field { type_of: Type::default().into_required(), ..Field::default() }, + ), ]); - let var1_var1 = Type::default().fields(vec![ + let var1_var1 = config::Type::default().fields(vec![ ("usual", Field::default()), - ("command", Field { required: true, ..Field::default() }), - ("optPayload", Field { required: true, ..Field::default() }), + ( + "command", + Field { type_of: Type::default().into_required(), ..Field::default() }, + ), + ( + "optPayload", + Field { type_of: Type::default().into_required(), ..Field::default() }, + ), ]); let types = vec![ ("Var_Var", &var_var), @@ -875,22 +943,28 @@ mod tests { #[test] fn test_additional_types() { - let type_a = Type::default().fields(vec![ + let type_a = config::Type::default().fields(vec![ ("uniqueA1", Field::default()), ("common", Field::default()), ]); - let type_b = Type::default().fields(vec![ - ("uniqueB1", Field { required: true, ..Field::default() }), + let type_b = config::Type::default().fields(vec![ + ( + "uniqueB1", + Field { type_of: Type::default().into_required(), ..Field::default() }, + ), ("common", Field::default()), ]); - let type_c = Type::default().fields(vec![ + let type_c = config::Type::default().fields(vec![ ("uniqueC1", Field::default()), ("uniqueC2", Field::default()), ]); - let type_d = Type::default().fields(vec![ + let type_d = config::Type::default().fields(vec![ ("uniqueD1", Field::default()), ("common", Field::default()), - ("uniqueD2", Field { required: true, ..Field::default() }), + ( + "uniqueD2", + Field { type_of: Type::default().into_required(), ..Field::default() }, + ), ]); let types = vec![ @@ -970,22 +1044,25 @@ mod tests { #[test] fn test_combination_of_shared_fields() { - let type_a = Type::default().fields(vec![ + let type_a = config::Type::default().fields(vec![ ("field1", Field::default()), ("field2", Field::default()), ]); - let type_b = Type::default().fields(vec![ + let type_b = config::Type::default().fields(vec![ ("field2", Field::default()), ("field3", Field::default()), ]); - let type_c = Type::default().fields(vec![ + let type_c = config::Type::default().fields(vec![ ("field1", Field::default()), ("field3", Field::default()), ]); - let type_d = Type::default().fields(vec![ + let type_d = config::Type::default().fields(vec![ ("field1", Field::default()), ("field2", Field::default()), - ("field4", Field { required: true, ..Field::default() }), + ( + "field4", + Field { type_of: Type::default().into_required(), ..Field::default() }, + ), ]); let types = vec![ @@ -1067,7 +1144,9 @@ mod tests { #[test] fn validation_number_of_types() { - let types: Vec<_> = (0..136).map(|i| (i.to_string(), Type::default())).collect(); + let types: Vec<_> = (0..136) + .map(|i| (i.to_string(), config::Type::default())) + .collect(); let union_types: Vec<_> = types .iter() .map(|(name, type_)| (name.as_str(), type_)) @@ -1089,21 +1168,33 @@ mod tests { #[test] fn test_validation_equal_types() { - let a = Type::default().fields(vec![("a", Field::default()), ("b", Field::default())]); - let b = Type::default().fields(vec![ - ("a", Field { required: true, ..Field::default() }), + let a = + config::Type::default().fields(vec![("a", Field::default()), ("b", Field::default())]); + let b = config::Type::default().fields(vec![ + ( + "a", + Field { type_of: Type::default().into_required(), ..Field::default() }, + ), ("b", Field::default()), ]); - let c = Type::default().fields(vec![("a", Field::default()), ("b", Field::default())]); - let d = Type::default().fields(vec![ + let c = + config::Type::default().fields(vec![("a", Field::default()), ("b", Field::default())]); + let d = config::Type::default().fields(vec![ ("a", Field::default()), ("b", Field::default()), - ("c", Field { required: true, ..Field::default() }), + ( + "c", + Field { type_of: Type::default().into_required(), ..Field::default() }, + ), ]); - let e = Type::default().fields(vec![("c", Field::default()), ("d", Field::default())]); - let f = Type::default().fields(vec![ + let e = + config::Type::default().fields(vec![("c", Field::default()), ("d", Field::default())]); + let f = config::Type::default().fields(vec![ ("c", Field::default()), - ("d", Field { required: true, ..Field::default() }), + ( + "d", + Field { type_of: Type::default().into_required(), ..Field::default() }, + ), ]); let types = vec![ @@ -1129,8 +1220,8 @@ mod tests { #[test] fn test_validation_non_object() { - let foo = Type::default().fields(vec![("foo", Field::default())]); - let bar = Type::default().fields(vec![("bar", Field::default())]); + let foo = config::Type::default().fields(vec![("foo", Field::default())]); + let bar = config::Type::default().fields(vec![("bar", Field::default())]); let types = vec![("Foo", &foo), ("Bar", &bar)]; let discriminator = Discriminator::new("Test", &types).to_result().unwrap(); diff --git a/src/core/jit/builder.rs b/src/core/jit/builder.rs index 7e662d9c03..0124aa4949 100644 --- a/src/core/jit/builder.rs +++ b/src/core/jit/builder.rs @@ -16,6 +16,7 @@ use crate::core::blueprint::{Blueprint, Index, QueryField}; use crate::core::counter::{Count, Counter}; use crate::core::jit::model::OperationPlan; use crate::core::merge_right::MergeRight; +use crate::core::Type; #[derive(PartialEq, strum_macros::Display)] enum Condition { @@ -237,10 +238,7 @@ impl Builder { 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_of: Type::Named { name: "String".to_owned(), non_null: true }, // __typename has a special meaning and could be applied // to any type type_condition: None, diff --git a/src/core/jit/model.rs b/src/core/jit/model.rs index 0bf448dac6..be2fdff0af 100644 --- a/src/core/jit/model.rs +++ b/src/core/jit/model.rs @@ -80,7 +80,7 @@ impl Field { pub struct Arg { pub id: ArgId, pub name: String, - pub type_of: crate::core::blueprint::Type, + pub type_of: crate::core::Type, pub value: Option, pub default_value: Option, } @@ -142,7 +142,7 @@ pub struct Field { /// of this field pub output_name: String, pub ir: Option, - pub type_of: crate::core::blueprint::Type, + pub type_of: crate::core::Type, /// Specifies the name of type used in condition to fetch that field /// The type could be anything from graphql type system: /// interface, type, union, input type. diff --git a/src/core/jit/synth/synth.rs b/src/core/jit/synth/synth.rs index 0161de44a8..e8b430d66b 100644 --- a/src/core/jit/synth/synth.rs +++ b/src/core/jit/synth/synth.rs @@ -122,8 +122,8 @@ where let eval_result = if value.is_null() { // check the nullability of this type unwrapping list modifier let is_nullable = match &node.type_of { - crate::core::blueprint::Type::NamedType { non_null, .. } => !*non_null, - crate::core::blueprint::Type::ListType { of_type, .. } => of_type.is_nullable(), + crate::core::Type::Named { non_null, .. } => !*non_null, + crate::core::Type::List { of_type, .. } => of_type.is_nullable(), }; if is_nullable { Ok(Value::null()) diff --git a/src/core/mod.rs b/src/core/mod.rs index a885e5ab4f..0ecaf7355d 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -48,6 +48,7 @@ use std::hash::Hash; use std::num::NonZeroU64; use async_graphql_value::ConstValue; +pub use blueprint::Type; pub use errata::Errata; pub use error::{Error, Result}; use http::Response; diff --git a/tailcall-fixtures/fixtures/configs/yaml-nested-unions-recursive.yaml b/tailcall-fixtures/fixtures/configs/yaml-nested-unions-recursive.yaml index 879ff5e416..e4c0974957 100644 --- a/tailcall-fixtures/fixtures/configs/yaml-nested-unions-recursive.yaml +++ b/tailcall-fixtures/fixtures/configs/yaml-nested-unions-recursive.yaml @@ -5,37 +5,45 @@ types: T1: fields: t1: - type: String + type: + name: String T2: fields: t2: - type: Int + type: + name: Int T3: fields: t3: - type: Boolean + type: + name: Boolean t33: - type: Float - required: true + type: + name: Float + required: true T4: fields: t4: - type: String + type: + name: String T5: fields: t5: - type: Boolean + type: + name: Boolean Query: fields: test: - type: U + type: + name: U args: u: - type: U - required: true + type: + name: U + required: true http: baseURL: http://localhost path: /users/{{args.u}} diff --git a/tailcall-fixtures/fixtures/configs/yaml-nested-unions.yaml b/tailcall-fixtures/fixtures/configs/yaml-nested-unions.yaml index b2086cb0db..6e7e0946c9 100644 --- a/tailcall-fixtures/fixtures/configs/yaml-nested-unions.yaml +++ b/tailcall-fixtures/fixtures/configs/yaml-nested-unions.yaml @@ -5,37 +5,45 @@ types: T1: fields: t1: - type: String + type: + name: String T2: fields: t2: - type: Int + type: + name: Int T3: fields: t3: - type: Boolean + type: + name: Boolean t33: - type: Float - required: true + type: + name: Float + required: true T4: fields: t4: - type: String + type: + name: String T5: fields: t5: - type: Boolean + type: + name: Boolean Query: fields: test: - type: U + type: + name: U args: u: - type: U - required: true + type: + name: U + required: true http: baseURL: http://localhost path: /users/{{args.u}} diff --git a/tailcall-fixtures/fixtures/configs/yaml-recursive-input.yaml b/tailcall-fixtures/fixtures/configs/yaml-recursive-input.yaml index 41448b3fa7..0f8aee6585 100644 --- a/tailcall-fixtures/fixtures/configs/yaml-recursive-input.yaml +++ b/tailcall-fixtures/fixtures/configs/yaml-recursive-input.yaml @@ -8,17 +8,21 @@ types: Bar: fields: name: - type: Foo + type: + name: Foo rec: - type: Bar + type: + name: Bar Query: fields: bars: - type: String + type: + name: String args: filter: - type: Bar + type: + name: Bar graphql: args: - key: baz @@ -28,4 +32,5 @@ types: Foo: fields: name: - type: String \ No newline at end of file + type: + name: String diff --git a/tailcall-fixtures/fixtures/configs/yaml-union-in-type.yaml b/tailcall-fixtures/fixtures/configs/yaml-union-in-type.yaml index 94ea2b0172..48d247ef1f 100644 --- a/tailcall-fixtures/fixtures/configs/yaml-union-in-type.yaml +++ b/tailcall-fixtures/fixtures/configs/yaml-union-in-type.yaml @@ -5,45 +5,57 @@ types: T1: fields: t1: - type: String + type: + name: String T2: fields: t2: - type: Int + type: + name: Int T3: fields: t3: - type: Boolean + type: + name: Boolean t33: - type: Float - required: true + type: + name: Float + required: true NU: fields: test: - type: String + type: + name: String u: - type: U + type: + name: U NNU: fields: other: - type: Int + type: + name: Int new: - type: Boolean + type: + name: Boolean nu: - type: NU + type: + name: NU Query: fields: test: - type: U + type: + name: U args: nu: - type: NU - required: true + type: + name: NU + required: true nnu: - type: NNU + type: + name: NNU http: baseURL: http://localhost path: /users/{{args.nu.u}} diff --git a/tailcall-fixtures/fixtures/configs/yaml-union.yaml b/tailcall-fixtures/fixtures/configs/yaml-union.yaml index f2d30c9678..890006f883 100644 --- a/tailcall-fixtures/fixtures/configs/yaml-union.yaml +++ b/tailcall-fixtures/fixtures/configs/yaml-union.yaml @@ -5,27 +5,33 @@ types: T1: fields: t1: - type: String + type: + name: String T2: fields: t2: - type: Int + type: + name: Int T3: fields: t3: - type: Boolean + type: + name: Boolean t33: - type: Float - required: true + type: + name: Float + required: true Query: fields: test: - type: U + type: + name: U args: u: - type: U - required: true + type: + name: U + required: true http: baseURL: http://localhost path: /users/{{args.u}} diff --git a/tests/core/snapshots/graphql-conformance-015.md_client.snap b/tests/core/snapshots/graphql-conformance-015.md_client.snap index 770537c269..fdbd6e202b 100644 --- a/tests/core/snapshots/graphql-conformance-015.md_client.snap +++ b/tests/core/snapshots/graphql-conformance-015.md_client.snap @@ -48,7 +48,7 @@ type User { id: ID! name: String! profilePic(size: Int! = 100, width: Int, height: Int = 100): String! - searchComments(query: [String]! = [["today"]]): String! + searchComments(query: [[String!]!]! = [["today"]]): String! } input VideoSize { diff --git a/tests/core/snapshots/graphql-conformance-015.md_merged.snap b/tests/core/snapshots/graphql-conformance-015.md_merged.snap index 28820191aa..d11302e3fc 100644 --- a/tests/core/snapshots/graphql-conformance-015.md_merged.snap +++ b/tests/core/snapshots/graphql-conformance-015.md_merged.snap @@ -27,5 +27,5 @@ type User { name: 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}}") + searchComments(query: [[String!]!]! = [["today"]]): String! @expr(body: "video_{{.value.id}}_{{.args.query}}") } diff --git a/tests/core/snapshots/graphql-conformance-016.md_0.snap b/tests/core/snapshots/graphql-conformance-016.md_0.snap new file mode 100644 index 0000000000..c4f532dc57 --- /dev/null +++ b/tests/core/snapshots/graphql-conformance-016.md_0.snap @@ -0,0 +1,44 @@ +--- +source: tests/core/spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": { + "userGroups": [ + [ + { + "id": 1, + "name": "user-1" + }, + { + "id": 2, + "name": "user-2" + }, + { + "id": 3, + "name": "user-3" + } + ], + [ + { + "id": 4, + "name": "user-4" + }, + { + "id": 5, + "name": "user-5" + }, + { + "id": 6, + "name": "user-6" + } + ] + ] + } + } +} diff --git a/tests/core/snapshots/graphql-conformance-016.md_1.snap b/tests/core/snapshots/graphql-conformance-016.md_1.snap new file mode 100644 index 0000000000..02b07d6d88 --- /dev/null +++ b/tests/core/snapshots/graphql-conformance-016.md_1.snap @@ -0,0 +1,15 @@ +--- +source: tests/core/spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": { + "addUsers": true + } + } +} diff --git a/tests/core/snapshots/graphql-conformance-016.md_2.snap b/tests/core/snapshots/graphql-conformance-016.md_2.snap new file mode 100644 index 0000000000..5d8e545f31 --- /dev/null +++ b/tests/core/snapshots/graphql-conformance-016.md_2.snap @@ -0,0 +1,24 @@ +--- +source: tests/core/spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": null, + "errors": [ + { + "message": " --> 3:5\n |\n3 | {\n | ^---\n |\n = expected selection", + "locations": [ + { + "line": 3, + "column": 5 + } + ] + } + ] + } +} diff --git a/tests/core/snapshots/graphql-conformance-016.md_client.snap b/tests/core/snapshots/graphql-conformance-016.md_client.snap new file mode 100644 index 0000000000..90e5a57faa --- /dev/null +++ b/tests/core/snapshots/graphql-conformance-016.md_client.snap @@ -0,0 +1,53 @@ +--- +source: tests/core/spec.rs +expression: formatted +--- +scalar Bytes + +scalar Date + +scalar DateTime + +scalar Email + +scalar Empty + +scalar Int128 + +scalar Int16 + +scalar Int32 + +scalar Int64 + +scalar Int8 + +scalar JSON + +scalar PhoneNumber + +type Query { + addUsers(userNames: [[String!]!]!): Boolean + userGroups: [[User!]!]! +} + +scalar UInt128 + +scalar UInt16 + +scalar UInt32 + +scalar UInt64 + +scalar UInt8 + +scalar Url + +type User { + id: ID! + name: String! +} + +schema { + query: Query +} diff --git a/tests/core/snapshots/graphql-conformance-016.md_merged.snap b/tests/core/snapshots/graphql-conformance-016.md_merged.snap new file mode 100644 index 0000000000..088141a132 --- /dev/null +++ b/tests/core/snapshots/graphql-conformance-016.md_merged.snap @@ -0,0 +1,20 @@ +--- +source: tests/core/spec.rs +expression: formatter +--- +schema + @server(hostname: "0.0.0.0", port: 8001, queryValidation: false) + @upstream(baseURL: "http://upstream/graphql", httpCache: 42) { + query: Query +} + +type Query { + addUsers(userNames: [[String!]!]!): Boolean + @graphQL(args: [{key: "userNames", value: "{{.args.userNames}}"}], name: "addUsers") + userGroups: [[User!]!]! @graphQL(name: "users") +} + +type User { + id: ID! + name: String! +} 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 6f41f83b4d..d275a8ae1d 100644 --- a/tests/core/snapshots/graphql-conformance-http-015.md_client.snap +++ b/tests/core/snapshots/graphql-conformance-http-015.md_client.snap @@ -48,7 +48,7 @@ type User { id: ID! name: String! profilePic(size: Int! = 100, width: Int, height: Int = 100): String! - searchComments(query: [String]! = [["today"]]): String! + searchComments(query: [[String!]!]! = [["today"]]): String! } input VideoSize { 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 38b944a41b..c3b08996ee 100644 --- a/tests/core/snapshots/graphql-conformance-http-015.md_merged.snap +++ b/tests/core/snapshots/graphql-conformance-http-015.md_merged.snap @@ -27,5 +27,5 @@ type User { name: 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}}") + searchComments(query: [[String!]!]! = [["today"]]): String! @expr(body: "video_{{.value.id}}_{{.args.query}}") } diff --git a/tests/core/snapshots/graphql-conformance-http-016.md_0.snap b/tests/core/snapshots/graphql-conformance-http-016.md_0.snap new file mode 100644 index 0000000000..c4f532dc57 --- /dev/null +++ b/tests/core/snapshots/graphql-conformance-http-016.md_0.snap @@ -0,0 +1,44 @@ +--- +source: tests/core/spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": { + "userGroups": [ + [ + { + "id": 1, + "name": "user-1" + }, + { + "id": 2, + "name": "user-2" + }, + { + "id": 3, + "name": "user-3" + } + ], + [ + { + "id": 4, + "name": "user-4" + }, + { + "id": 5, + "name": "user-5" + }, + { + "id": 6, + "name": "user-6" + } + ] + ] + } + } +} diff --git a/tests/core/snapshots/graphql-conformance-http-016.md_1.snap b/tests/core/snapshots/graphql-conformance-http-016.md_1.snap new file mode 100644 index 0000000000..02b07d6d88 --- /dev/null +++ b/tests/core/snapshots/graphql-conformance-http-016.md_1.snap @@ -0,0 +1,15 @@ +--- +source: tests/core/spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": { + "addUsers": true + } + } +} diff --git a/tests/core/snapshots/graphql-conformance-http-016.md_2.snap b/tests/core/snapshots/graphql-conformance-http-016.md_2.snap new file mode 100644 index 0000000000..5d8e545f31 --- /dev/null +++ b/tests/core/snapshots/graphql-conformance-http-016.md_2.snap @@ -0,0 +1,24 @@ +--- +source: tests/core/spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": null, + "errors": [ + { + "message": " --> 3:5\n |\n3 | {\n | ^---\n |\n = expected selection", + "locations": [ + { + "line": 3, + "column": 5 + } + ] + } + ] + } +} diff --git a/tests/core/snapshots/graphql-conformance-http-016.md_client.snap b/tests/core/snapshots/graphql-conformance-http-016.md_client.snap new file mode 100644 index 0000000000..90e5a57faa --- /dev/null +++ b/tests/core/snapshots/graphql-conformance-http-016.md_client.snap @@ -0,0 +1,53 @@ +--- +source: tests/core/spec.rs +expression: formatted +--- +scalar Bytes + +scalar Date + +scalar DateTime + +scalar Email + +scalar Empty + +scalar Int128 + +scalar Int16 + +scalar Int32 + +scalar Int64 + +scalar Int8 + +scalar JSON + +scalar PhoneNumber + +type Query { + addUsers(userNames: [[String!]!]!): Boolean + userGroups: [[User!]!]! +} + +scalar UInt128 + +scalar UInt16 + +scalar UInt32 + +scalar UInt64 + +scalar UInt8 + +scalar Url + +type User { + id: ID! + name: String! +} + +schema { + query: Query +} diff --git a/tests/core/snapshots/graphql-conformance-http-016.md_merged.snap b/tests/core/snapshots/graphql-conformance-http-016.md_merged.snap new file mode 100644 index 0000000000..affeb02e65 --- /dev/null +++ b/tests/core/snapshots/graphql-conformance-http-016.md_merged.snap @@ -0,0 +1,19 @@ +--- +source: tests/core/spec.rs +expression: formatter +--- +schema + @server(hostname: "0.0.0.0", port: 8001, queryValidation: false) + @upstream(baseURL: "http://upstream/", httpCache: 42) { + query: Query +} + +type Query { + addUsers(userNames: [[String!]!]!): Boolean @http(body: "{{.args.userNames}}", method: "POST", path: "/users") + userGroups: [[User!]!]! @http(path: "/users") +} + +type User { + id: ID! + name: String! +} diff --git a/tests/core/snapshots/test-grpc-nested-data.md_error.snap b/tests/core/snapshots/test-grpc-nested-data.md_error.snap index aeb1600dc1..7ae4b0af3a 100644 --- a/tests/core/snapshots/test-grpc-nested-data.md_error.snap +++ b/tests/core/snapshots/test-grpc-nested-data.md_error.snap @@ -4,7 +4,7 @@ expression: errors --- [ { - "message": "Type '{body: Option, id: Option, postImage: Option, title: Option}' is not assignable to type '[{body: Option, id: Option, postImage: Option, title: Option}]'", + "message": "Type '{body: Option, id: Option, postImage: Option, title: Option}' is not assignable to type '[Option<{body: Option, id: Option, postImage: Option, title: Option}>]'", "trace": [ "Query", "newsById", diff --git a/tests/core/snapshots/test-list-args.md_client.snap b/tests/core/snapshots/test-list-args.md_client.snap index 988028965e..0948784533 100644 --- a/tests/core/snapshots/test-list-args.md_client.snap +++ b/tests/core/snapshots/test-list-args.md_client.snap @@ -27,7 +27,7 @@ scalar JSON scalar PhoneNumber type Query { - f1(q: [Int]!): T1 + f1(q: [Int!]!): T1 } type T1 { diff --git a/tests/core/snapshots/test-list-args.md_merged.snap b/tests/core/snapshots/test-list-args.md_merged.snap index b71ad4b665..d5f569a781 100644 --- a/tests/core/snapshots/test-list-args.md_merged.snap +++ b/tests/core/snapshots/test-list-args.md_merged.snap @@ -7,7 +7,7 @@ schema @server(queryValidation: true) @upstream(baseURL: "http://localhost:3000" } type Query { - f1(q: [Int]!): T1 @http(path: "/api", query: [{key: "q", value: "{{.args.q}}"}]) + f1(q: [Int!]!): T1 @http(path: "/api", query: [{key: "q", value: "{{.args.q}}"}]) } type T1 { diff --git a/tests/execution/batching-disabled.md b/tests/execution/batching-disabled.md index cd21a6b94c..e14029cc6d 100644 --- a/tests/execution/batching-disabled.md +++ b/tests/execution/batching-disabled.md @@ -19,11 +19,15 @@ "Query": { "fields": { "user": { - "type": "User", + "type": { + "name": "User" + }, "args": { "id": { - "type": "Int", - "required": true + "type": { + "name": "Int", + "required": true + } } }, "http": { @@ -37,15 +41,21 @@ "User": { "fields": { "id": { - "type": "Int", + "type": { + "name": "Int" + }, "cache": null }, "name": { - "type": "String", + "type": { + "name": "String" + }, "cache": null }, "username": { - "type": "String", + "type": { + "name": "String" + }, "cache": null } }, diff --git a/tests/execution/batching.md b/tests/execution/batching.md index 262c653379..5dd3b44e57 100644 --- a/tests/execution/batching.md +++ b/tests/execution/batching.md @@ -13,7 +13,9 @@ "Query": { "fields": { "user": { - "type": "User", + "type": { + "name": "User" + }, "http": { "path": "/users/1", "baseURL": "http://jsonplaceholder.typicode.com" @@ -26,11 +28,15 @@ "User": { "fields": { "id": { - "type": "Int", + "type": { + "name": "Int" + }, "cache": null }, "name": { - "type": "String", + "type": { + "name": "String" + }, "cache": null } }, diff --git a/tests/execution/cache-control.md b/tests/execution/cache-control.md index 23c1588d34..e974722e5f 100644 --- a/tests/execution/cache-control.md +++ b/tests/execution/cache-control.md @@ -15,10 +15,14 @@ "Query": { "fields": { "user": { - "type": "User", + "type": { + "name": "User" + }, "args": { "id": { - "type": "Int" + "type": { + "name": "Int" + } } }, "http": { @@ -39,11 +43,15 @@ "User": { "fields": { "id": { - "type": "Int", + "type": { + "name": "Int" + }, "cache": null }, "name": { - "type": "String", + "type": { + "name": "String" + }, "cache": null } }, diff --git a/tests/execution/custom-headers.md b/tests/execution/custom-headers.md index f84209d759..3a0c4f486d 100644 --- a/tests/execution/custom-headers.md +++ b/tests/execution/custom-headers.md @@ -24,7 +24,9 @@ "Query": { "fields": { "greet": { - "type": "String", + "type": { + "name": "String" + }, "expr": { "body": "Hello World!" }, diff --git a/tests/execution/env-value.md b/tests/execution/env-value.md index 2c243e7a8b..6f9b412a2d 100644 --- a/tests/execution/env-value.md +++ b/tests/execution/env-value.md @@ -13,20 +13,28 @@ "Post": { "fields": { "body": { - "type": "String", + "type": { + "name": "String" + }, "cache": null }, "id": { - "type": "Int", + "type": { + "name": "Int" + }, "cache": null }, "title": { - "type": "String", + "type": { + "name": "String" + }, "cache": null }, "userId": { - "type": "Int", - "required": true, + "type": { + "name": "Int", + "required": true + }, "cache": null } }, @@ -35,21 +43,27 @@ "Query": { "fields": { "post1": { - "type": "Post", + "type": { + "name": "Post" + }, "http": { "path": "/posts/{{.env.ID}}" }, "cache": null }, "post2": { - "type": "Post", + "type": { + "name": "Post" + }, "http": { "path": "/posts/{{.env.POST_ID}}" }, "cache": null }, "post3": { - "type": "Post", + "type": { + "name": "Post" + }, "http": { "path": "/posts/{{.env.NESTED_POST_ID}}" }, diff --git a/tests/execution/graphql-conformance-016.md b/tests/execution/graphql-conformance-016.md index 4dd89c15e8..1d3a35dde9 100644 --- a/tests/execution/graphql-conformance-016.md +++ b/tests/execution/graphql-conformance-016.md @@ -1,11 +1,5 @@ ---- -skip: true ---- - # List of lists. -TODO: Skipped because Tailcall cannot extract a list of lists. - ```graphql @config schema @server(port: 8001, queryValidation: false, hostname: "0.0.0.0") @@ -15,6 +9,8 @@ schema type Query { userGroups: [[User!]!]! @graphQL(name: "users") + addUsers(userNames: [[String!]!]!): Boolean + @graphQL(name: "addUsers", args: [{key: "userNames", value: "{{.args.userNames}}"}]) } type User { @@ -27,13 +23,12 @@ type User { - request: method: POST url: http://upstream/graphql - textBody: {"query": "query { users { id name } }"} - expectedHits: 1 + textBody: '{ "query": "query { users { id name } }" }' response: status: 200 body: data: - userGroups: + users: - - id: 1 name: user-1 - id: 2 @@ -46,6 +41,15 @@ type User { name: user-5 - id: 6 name: user-6 +- request: + method: POST + url: http://upstream/graphql + textBody: '{ "query": "query { addUsers(userNames: [[\\\"user-1\\\", \\\"user-2\\\"], [\\\"user-3\\\", \\\"user-4\\\"]]) }" }' + response: + status: 200 + body: + data: + addUsers: true ``` ```yml @test @@ -60,6 +64,14 @@ type User { name } } + +- method: POST + url: http://localhost:8080/graphql + body: + query: | + query { + addUsers(userNames: [["user-1", "user-2"], ["user-3", "user-4"]]) + } # Negative - method: POST url: http://localhost:8080/graphql diff --git a/tests/execution/graphql-conformance-http-016.md b/tests/execution/graphql-conformance-http-016.md index eeae3922f7..e16f6e4399 100644 --- a/tests/execution/graphql-conformance-http-016.md +++ b/tests/execution/graphql-conformance-http-016.md @@ -1,11 +1,5 @@ ---- -skip: true ---- - # List of lists. -TODO: Skipped because Tailcall cannot extract a list of lists. - ```graphql @config schema @server(port: 8001, queryValidation: false, hostname: "0.0.0.0") @@ -15,6 +9,7 @@ schema type Query { userGroups: [[User!]!]! @http(path: "/users") + addUsers(userNames: [[String!]!]!): Boolean @http(path: "/users", method: POST, body: "{{.args.userNames}}") } type User { @@ -43,6 +38,19 @@ type User { name: user-5 - id: 6 name: user-6 + +- request: + method: POST + url: http://upstream/users + body: + - - user-1 + - user-2 + - - user-3 + - user-4 + expectedHits: 1 + response: + status: 200 + body: true ``` ```yml @test @@ -57,6 +65,14 @@ type User { name } } + +- method: POST + url: http://localhost:8080/graphql + body: + query: | + query { + addUsers(userNames: [["user-1", "user-2"], ["user-3", "user-4"]]) + } # Negative - method: POST url: http://localhost:8080/graphql diff --git a/tests/execution/https.md b/tests/execution/https.md index db8fb9e99c..d6442bb0ce 100644 --- a/tests/execution/https.md +++ b/tests/execution/https.md @@ -13,7 +13,9 @@ "Query": { "fields": { "firstUser": { - "type": "User", + "type": { + "name": "User" + }, "http": { "path": "/users/1", "baseURL": "https://jsonplaceholder.typicode.com" @@ -26,11 +28,15 @@ "User": { "fields": { "id": { - "type": "Int", + "type": { + "name": "Int" + }, "cache": null }, "name": { - "type": "String", + "type": { + "name": "String" + }, "cache": null } }, diff --git a/tests/execution/recursive-types-json.md b/tests/execution/recursive-types-json.md index dc3c790a84..114abaf3ad 100644 --- a/tests/execution/recursive-types-json.md +++ b/tests/execution/recursive-types-json.md @@ -15,7 +15,9 @@ "Query": { "fields": { "user": { - "type": "User", + "type": { + "name": "User" + }, "http": { "path": "/users/1" } @@ -27,10 +29,14 @@ "createUser": { "args": { "user": { - "type": "User" + "type": { + "name": "User" + } } }, - "type": "User", + "type": { + "name": "User" + }, "http": { "path": "/user", "method": "POST", @@ -42,16 +48,23 @@ "User": { "fields": { "id": { - "type": "Int", - "required": true + "type": { + "name": "Int", + "required": true + } }, "name": { - "type": "String", - "required": true + "type": { + "name": "String", + "required": true + } }, "connections": { - "type": "Connection", - "list": true, + "type": { + "list": { + "name": "Connection" + } + }, "http": { "path": "/connections/{{.value.id}}" } @@ -61,10 +74,14 @@ "Connection": { "fields": { "type": { - "type": "String" + "type": { + "name": "String" + } }, "user": { - "type": "User" + "type": { + "name": "User" + } } } } diff --git a/tests/execution/ref-other-nested.md b/tests/execution/ref-other-nested.md index ac702723a8..0bd8a7c28c 100644 --- a/tests/execution/ref-other-nested.md +++ b/tests/execution/ref-other-nested.md @@ -13,7 +13,9 @@ "Query": { "fields": { "firstUser": { - "type": "User1", + "type": { + "name": "User1" + }, "http": { "path": "/users/1", "baseURL": "https://jsonplaceholder.typicode.com" @@ -26,11 +28,15 @@ "User": { "fields": { "id": { - "type": "Int", + "type": { + "name": "Int" + }, "cache": null }, "name": { - "type": "String", + "type": { + "name": "String" + }, "cache": null } }, @@ -39,7 +45,9 @@ "User1": { "fields": { "user1": { - "type": "User2", + "type": { + "name": "User2" + }, "cache": null } }, @@ -48,7 +56,9 @@ "User2": { "fields": { "user2": { - "type": "User", + "type": { + "name": "User" + }, "http": { "path": "/users/1", "baseURL": "https://jsonplaceholder.typicode.com" diff --git a/tests/execution/request-to-upstream-batching.md b/tests/execution/request-to-upstream-batching.md index b8f6e6076e..552a5cb185 100644 --- a/tests/execution/request-to-upstream-batching.md +++ b/tests/execution/request-to-upstream-batching.md @@ -19,11 +19,15 @@ "Query": { "fields": { "user": { - "type": "User", + "type": { + "name": "User" + }, "args": { "id": { - "type": "Int", - "required": true + "type": { + "name": "Int", + "required": true + } } }, "http": { @@ -45,11 +49,15 @@ "User": { "fields": { "id": { - "type": "Int", + "type": { + "name": "Int" + }, "cache": null }, "name": { - "type": "String", + "type": { + "name": "String" + }, "cache": null } }, diff --git a/tests/execution/simple-query.md b/tests/execution/simple-query.md index 736a7c5e18..a5c1e6de7e 100644 --- a/tests/execution/simple-query.md +++ b/tests/execution/simple-query.md @@ -13,7 +13,9 @@ "Query": { "fields": { "firstUser": { - "type": "User", + "type": { + "name": "User" + }, "http": { "path": "/users/1" }, @@ -25,11 +27,15 @@ "User": { "fields": { "id": { - "type": "Int", + "type": { + "name": "Int" + }, "cache": null }, "name": { - "type": "String", + "type": { + "name": "String" + }, "cache": null } }, diff --git a/tests/execution/test-enum-empty.md b/tests/execution/test-enum-empty.md index 346dcef16d..adce03ea75 100644 --- a/tests/execution/test-enum-empty.md +++ b/tests/execution/test-enum-empty.md @@ -17,11 +17,15 @@ error: true "Query": { "fields": { "foo": { - "type": "Foo", + "type": { + "name": "Foo" + }, "args": { "val": { - "type": "String", - "required": true + "type": { + "name": "String", + "required": true + } } }, "expr": { diff --git a/tests/execution/test-interface-from-json.md b/tests/execution/test-interface-from-json.md index 4ae926dc57..9ba7bb9fa8 100644 --- a/tests/execution/test-interface-from-json.md +++ b/tests/execution/test-interface-from-json.md @@ -12,7 +12,9 @@ "IA": { "fields": { "a": { - "type": "String" + "type": { + "name": "String" + } } } }, @@ -20,17 +22,23 @@ "implements": ["IA"], "fields": { "a": { - "type": "String" + "type": { + "name": "String" + } }, "b": { - "type": "String" + "type": { + "name": "String" + } } } }, "Query": { "fields": { "bar": { - "type": "B", + "type": { + "name": "B" + }, "http": { "path": "/posts" } diff --git a/tests/execution/test-static-value.md b/tests/execution/test-static-value.md index 067aeb4989..530afe1298 100644 --- a/tests/execution/test-static-value.md +++ b/tests/execution/test-static-value.md @@ -11,7 +11,9 @@ "Query": { "fields": { "firstUser": { - "type": "User", + "type": { + "name": "User" + }, "http": { "path": "/users/1", "baseURL": "http://jsonplaceholder.typicode.com" @@ -24,11 +26,15 @@ "User": { "fields": { "id": { - "type": "Int", + "type": { + "name": "Int" + }, "cache": null }, "name": { - "type": "String", + "type": { + "name": "String" + }, "cache": null } }, diff --git a/tests/execution/upstream-batching.md b/tests/execution/upstream-batching.md index 92276a7dd5..b2b62dd5db 100644 --- a/tests/execution/upstream-batching.md +++ b/tests/execution/upstream-batching.md @@ -17,10 +17,14 @@ "Query": { "fields": { "user": { - "type": "User", + "type": { + "name": "User" + }, "args": { "id": { - "type": "Int" + "type": { + "name": "Int" + } } }, "http": { @@ -42,11 +46,15 @@ "User": { "fields": { "id": { - "type": "Int", + "type": { + "name": "Int" + }, "cache": null }, "name": { - "type": "String", + "type": { + "name": "String" + }, "cache": null } }, diff --git a/tests/execution/with-args-url.md b/tests/execution/with-args-url.md index d38147f920..e5ac5a388c 100644 --- a/tests/execution/with-args-url.md +++ b/tests/execution/with-args-url.md @@ -13,11 +13,15 @@ "Query": { "fields": { "user": { - "type": "User", + "type": { + "name": "User" + }, "args": { "id": { - "type": "Int", - "required": true + "type": { + "name": "Int", + "required": true + } } }, "http": { @@ -32,11 +36,15 @@ "User": { "fields": { "id": { - "type": "Int", + "type": { + "name": "Int" + }, "cache": null }, "name": { - "type": "String", + "type": { + "name": "String" + }, "cache": null } }, diff --git a/tests/execution/yaml-nested-unions.md b/tests/execution/yaml-nested-unions.md index b4b803b8c1..c066983f8a 100644 --- a/tests/execution/yaml-nested-unions.md +++ b/tests/execution/yaml-nested-unions.md @@ -8,37 +8,45 @@ types: T1: fields: t1: - type: String + type: + name: String T2: fields: t2: - type: Int + type: + name: Int T3: fields: t3: - type: Boolean + type: + name: Boolean t33: - type: Float - required: true + type: + name: Float + required: true T4: fields: t4: - type: String + type: + name: String T5: fields: t5: - type: Boolean + type: + name: Boolean Query: fields: test: - type: U + type: + name: U args: u: - type: U - required: true + type: + name: U + required: true http: baseURL: http://localhost path: /users/{{args.u}} diff --git a/tests/execution/yaml-union-in-type.md b/tests/execution/yaml-union-in-type.md index 3cda27711b..9b1a73af89 100644 --- a/tests/execution/yaml-union-in-type.md +++ b/tests/execution/yaml-union-in-type.md @@ -8,45 +8,57 @@ types: T1: fields: t1: - type: String + type: + name: String T2: fields: t2: - type: Int + type: + name: Int T3: fields: t3: - type: Boolean + type: + name: Boolean t33: - type: Float - required: true + type: + name: Float + required: true NU: fields: test: - type: String + type: + name: String u: - type: U + type: + name: U NNU: fields: other: - type: Int + type: + name: Int new: - type: Boolean + type: + name: Boolean nu: - type: NU + type: + name: NU Query: fields: test: - type: U + type: + name: U args: nu: - type: NU - required: true + type: + name: NU + required: true nnu: - type: NNU + type: + name: NNU http: baseURL: http://localhost path: /users/{{args.nu.u}} diff --git a/tests/execution/yaml-union.md b/tests/execution/yaml-union.md index b25f69934c..ac5db090a1 100644 --- a/tests/execution/yaml-union.md +++ b/tests/execution/yaml-union.md @@ -8,37 +8,43 @@ types: T1: fields: t1: - type: String + type: + name: String T2: fields: t2: - type: Int + type: + name: Int T3: fields: t3: - type: Boolean + type: + name: Boolean t33: - type: Float - required: true - + type: + name: Float + required: true NU: fields: u: - type: U + type: + name: U NNU: fields: nu: - type: NU - + type: + name: NU Query: fields: test: - type: U + type: + name: U args: u: - type: U - required: true + type: + name: U + required: true http: baseURL: http://localhost path: /users/{{args.u}}/ From 075e1f86749fa9614bafd4b33f99e5ee4d3ea450 Mon Sep 17 00:00:00 2001 From: Sandipsinh Dilipsinh Rathod <62684960+ssddOnTop@users.noreply.github.com> Date: Fri, 30 Aug 2024 08:38:34 +0000 Subject: [PATCH 09/14] feat(2426): support mustache template on `generate::config` (#2657) --- src/cli/generator/config.rs | 103 +++---------- src/cli/generator/generator.rs | 18 ++- src/core/generator/from_json.rs | 5 +- src/core/generator/generator.rs | 6 +- .../generator/tests/json_to_config_spec.rs | 5 +- src/core/mustache/mod.rs | 3 - src/core/mustache/template_string.rs | 139 ------------------ tests/cli/fixtures/generator/gen_deezer.md | 22 ++- .../fixtures/generator/gen_jsonplaceholder.md | 28 ++-- 9 files changed, 71 insertions(+), 258 deletions(-) delete mode 100644 src/core/mustache/template_string.rs diff --git a/src/cli/generator/config.rs b/src/cli/generator/config.rs index cc9296d5cc..a89a80fec2 100644 --- a/src/cli/generator/config.rs +++ b/src/cli/generator/config.rs @@ -10,9 +10,8 @@ use serde::{Deserialize, Serialize}; use url::Url; use crate::core::config::transformer::Preset; -use crate::core::config::{self, ConfigReaderContext}; +use crate::core::config::{self}; use crate::core::http::Method; -use crate::core::mustache::TemplateString; use crate::core::valid::{Valid, ValidateFrom, Validator}; #[derive(Deserialize, Serialize, Debug, Default, Setters)] @@ -36,7 +35,7 @@ pub struct LLMConfig { #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub secret: Option, + pub secret: Option, } #[derive(Clone, Deserialize, Serialize, Debug, Default)] @@ -59,10 +58,7 @@ pub struct Location( #[derive(Deserialize, Serialize, Debug)] #[serde(transparent)] -pub struct Headers( - #[serde(skip_serializing_if = "is_default")] Option>, - #[serde(skip)] PhantomData, -); +pub struct Headers(#[serde(skip_serializing_if = "is_default")] Option>); #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] @@ -78,7 +74,7 @@ pub enum Source { #[serde(rename_all = "camelCase")] Curl { src: Location, - headers: Headers, + headers: Headers, #[serde(skip_serializing_if = "Option::is_none")] method: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -195,30 +191,16 @@ impl Location { } } -impl Headers { - pub fn into_btree_map(self) -> Option> { +impl Headers { + pub fn into_btree_map(self) -> Option> { self.0 } - pub fn as_btree_map(&self) -> &Option> { + pub fn as_btree_map(&self) -> &Option> { &self.0 } } -impl Headers { - pub fn resolve(self, reader_context: &ConfigReaderContext) -> Headers { - // Resolve the header values with mustache template. - let resolved_headers = self.0.map(|headers_inner| { - headers_inner - .into_iter() - .map(|(k, v)| (k, v.resolve(reader_context))) - .collect::>() - }); - - Headers(resolved_headers, PhantomData) - } -} - impl Output { pub fn resolve(self, parent_dir: Option<&Path>) -> anyhow::Result> { Ok(Output { @@ -229,19 +211,14 @@ impl Output { } impl Source { - pub fn resolve( - self, - parent_dir: Option<&Path>, - reader_context: &ConfigReaderContext, - ) -> anyhow::Result> { + pub fn resolve(self, parent_dir: Option<&Path>) -> anyhow::Result> { match self { Source::Curl { src, field_name, headers, body, method, is_mutation } => { let resolved_path = src.into_resolved(parent_dir); - let resolved_headers = headers.resolve(reader_context); Ok(Source::Curl { src: resolved_path, field_name, - headers: resolved_headers, + headers, body, method, is_mutation, @@ -260,34 +237,26 @@ impl Source { } impl Input { - pub fn resolve( - self, - parent_dir: Option<&Path>, - reader_context: &ConfigReaderContext, - ) -> anyhow::Result> { - let resolved_source = self.source.resolve(parent_dir, reader_context)?; + pub fn resolve(self, parent_dir: Option<&Path>) -> anyhow::Result> { + let resolved_source = self.source.resolve(parent_dir)?; Ok(Input { source: resolved_source }) } } impl Config { /// Resolves all the relative paths present inside the GeneratorConfig. - pub fn into_resolved( - self, - config_path: &str, - reader_context: ConfigReaderContext, - ) -> anyhow::Result> { + pub fn into_resolved(self, config_path: &str) -> anyhow::Result> { let parent_dir = Some(Path::new(config_path).parent().unwrap_or(Path::new(""))); let inputs = self .inputs .into_iter() - .map(|input| input.resolve(parent_dir, &reader_context)) + .map(|input| input.resolve(parent_dir)) .collect::>>>()?; let output = self.output.resolve(parent_dir)?; let llm = self.llm.map(|llm| { - let secret = llm.secret.map(|s| s.resolve(&reader_context)); + let secret = llm.secret; LLMConfig { model: llm.model, secret } }); @@ -304,46 +273,33 @@ impl Config { #[cfg(test)] mod tests { use std::collections::HashMap; - use std::sync::Arc; use pretty_assertions::assert_eq; use super::*; - use crate::core::tests::TestEnvIO; use crate::core::valid::{ValidateInto, ValidationError, Validator}; fn location>(s: S) -> Location { Location(s.as_ref().to_string(), PhantomData) } - fn to_headers(raw_headers: BTreeMap) -> Headers { - Headers(Some(raw_headers), PhantomData) + fn to_headers(raw_headers: BTreeMap) -> Headers { + Headers(Some(raw_headers)) } #[test] fn test_headers_resolve() { let mut headers = BTreeMap::new(); - headers.insert("Authorization".to_owned(), "Bearer {{.env.TOKEN}}".into()); + let token = "eyJhbGciOiJIUzI1NiIsInR5"; + headers.insert("Authorization".to_owned(), format!("Bearer {token}")); let mut env_vars = HashMap::new(); - let token = "eyJhbGciOiJIUzI1NiIsInR5"; env_vars.insert("TOKEN".to_owned(), token.to_owned()); - let unresolved_headers = to_headers(headers); + let headers = to_headers(headers); - let mut runtime = crate::core::runtime::test::init(None); - runtime.env = Arc::new(TestEnvIO::init(env_vars)); - - let reader_ctx = ConfigReaderContext { - runtime: &runtime, - vars: &Default::default(), - headers: Default::default(), - }; - - let resolved_headers = unresolved_headers.resolve(&reader_ctx); - - let expected = TemplateString::from(format!("Bearer {token}").as_str()); - let actual = resolved_headers + let expected = format!("Bearer {token}"); + let actual = headers .as_btree_map() .as_ref() .unwrap() @@ -510,29 +466,18 @@ mod tests { #[test] fn test_llm_config() { - let mut env_vars = HashMap::new(); let token = "eyJhbGciOiJIUzI1NiIsInR5"; - env_vars.insert("TAILCALL_SECRET".to_owned(), token.to_owned()); - - let mut runtime = crate::core::runtime::test::init(None); - runtime.env = Arc::new(TestEnvIO::init(env_vars)); - - let reader_ctx = ConfigReaderContext { - runtime: &runtime, - vars: &Default::default(), - headers: Default::default(), - }; let config = Config::default().llm(Some(LLMConfig { model: Some("gpt-3.5-turbo".to_string()), - secret: Some(TemplateString::parse("{{.env.TAILCALL_SECRET}}").unwrap()), + secret: Some(token.to_string()), })); - let resolved_config = config.into_resolved("", reader_ctx).unwrap(); + let resolved_config = config.into_resolved("").unwrap(); let actual = resolved_config.llm; let expected = Some(LLMConfig { model: Some("gpt-3.5-turbo".to_string()), - secret: Some(TemplateString::from(token)), + secret: Some(token.to_string()), }); assert_eq!(actual, expected); diff --git a/src/cli/generator/generator.rs b/src/cli/generator/generator.rs index 96333fbbdc..6da3ac426e 100644 --- a/src/cli/generator/generator.rs +++ b/src/cli/generator/generator.rs @@ -16,7 +16,7 @@ use crate::core::proto_reader::ProtoReader; use crate::core::resource_reader::{Resource, ResourceReader}; use crate::core::runtime::TargetRuntime; use crate::core::valid::{ValidateInto, Validator}; -use crate::core::Transform; +use crate::core::{Mustache, Transform}; /// CLI that reads the the config file and generates the required tailcall /// configuration. @@ -75,12 +75,7 @@ impl Generator { pub async fn read(&self) -> anyhow::Result> { let config_path = &self.config_path; let source = ConfigSource::detect(config_path)?; - let config_content = self.runtime.file.read(config_path).await?; - - let config: Config = match source { - ConfigSource::Json => serde_json::from_str(&config_content)?, - ConfigSource::Yml => serde_yaml::from_str(&config_content)?, - }; + let mut config_content = self.runtime.file.read(config_path).await?; // While reading resolve the internal paths and mustache headers of generalized // config. @@ -89,7 +84,14 @@ impl Generator { vars: &Default::default(), headers: Default::default(), }; - config.into_resolved(config_path, reader_context) + config_content = Mustache::parse(&config_content).render(&reader_context); + + let config: Config = match source { + ConfigSource::Json => serde_json::from_str(&config_content)?, + ConfigSource::Yml => serde_yaml::from_str(&config_content)?, + }; + + config.into_resolved(config_path) } /// performs all the i/o's required in the config file and generates diff --git a/src/core/generator/from_json.rs b/src/core/generator/from_json.rs index ce82cbe87d..bb615a771b 100644 --- a/src/core/generator/from_json.rs +++ b/src/core/generator/from_json.rs @@ -10,7 +10,6 @@ use crate::core::config::transformer::RenameTypes; use crate::core::config::{Config, GraphQLOperationType}; use crate::core::http::Method; use crate::core::merge_right::MergeRight; -use crate::core::mustache::TemplateString; use crate::core::transform::{Transform, TransformerOps}; use crate::core::valid::{Valid, Validator}; @@ -21,7 +20,7 @@ pub struct RequestSample { pub res_body: Value, pub field_name: String, pub operation_type: GraphQLOperationType, - pub headers: Option>, + pub headers: Option>, } impl RequestSample { @@ -47,7 +46,7 @@ impl RequestSample { self } - pub fn with_headers(mut self, headers: Option>) -> Self { + pub fn with_headers(mut self, headers: Option>) -> Self { self.headers = headers; self } diff --git a/src/core/generator/generator.rs b/src/core/generator/generator.rs index fee6674d91..ce04350304 100644 --- a/src/core/generator/generator.rs +++ b/src/core/generator/generator.rs @@ -11,7 +11,6 @@ use super::{FromJsonGenerator, NameGenerator, RequestSample}; use crate::core::config::{self, Config, ConfigModule, Link, LinkType}; use crate::core::http::Method; use crate::core::merge_right::MergeRight; -use crate::core::mustache::TemplateString; use crate::core::proto_reader::ProtoMetadata; use crate::core::transform::{Transform, TransformerOps}; use crate::core::valid::Validator; @@ -37,7 +36,7 @@ pub enum Input { res_body: Value, field_name: String, is_mutation: bool, - headers: Option>, + headers: Option>, }, Proto(ProtoMetadata), Config { @@ -175,7 +174,6 @@ pub mod test { use crate::core::config::transformer::Preset; use crate::core::generator::generator::Input; use crate::core::http::Method; - use crate::core::mustache::TemplateString; use crate::core::proto_reader::ProtoMetadata; fn compile_protobuf(files: &[&str]) -> anyhow::Result { @@ -189,7 +187,7 @@ pub mod test { pub method: Method, #[serde(default)] pub body: Option, - pub headers: Option>, + pub headers: Option>, } pub struct JsonFixture { diff --git a/src/core/generator/tests/json_to_config_spec.rs b/src/core/generator/tests/json_to_config_spec.rs index 89b6d0cba8..e5f0edd8fc 100644 --- a/src/core/generator/tests/json_to_config_spec.rs +++ b/src/core/generator/tests/json_to_config_spec.rs @@ -6,7 +6,6 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use tailcall::core::generator::{Generator, Input}; use tailcall::core::http::Method; -use tailcall::core::mustache::TemplateString; use url::Url; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] @@ -15,7 +14,7 @@ pub struct APIRequest { pub method: Method, pub url: Url, #[serde(default)] - pub headers: Option>, + pub headers: Option>, #[serde(default, rename = "body")] pub body: Option, } @@ -32,7 +31,7 @@ pub struct APIResponse { #[serde(default = "default::status")] pub status: u16, #[serde(default)] - pub headers: BTreeMap, + pub headers: BTreeMap, #[serde(default, rename = "body")] pub body: Option, } diff --git a/src/core/mustache/mod.rs b/src/core/mustache/mod.rs index 964a0ee6bd..13f906c339 100644 --- a/src/core/mustache/mod.rs +++ b/src/core/mustache/mod.rs @@ -1,8 +1,5 @@ mod eval; mod model; mod parse; -mod template_string; - pub use eval::Eval; pub use model::*; -pub use template_string::TemplateString; diff --git a/src/core/mustache/template_string.rs b/src/core/mustache/template_string.rs deleted file mode 100644 index fe93072f97..0000000000 --- a/src/core/mustache/template_string.rs +++ /dev/null @@ -1,139 +0,0 @@ -use serde::{Deserialize, Deserializer, Serialize, Serializer}; - -use super::{Mustache, Segment}; -use crate::core::path::PathString; - -/// TemplateString acts as wrapper over mustache but supports serialization and -/// deserialization. It provides utilities for parsing, resolving, and comparing -/// template strings. -#[derive(Debug, derive_more::Display, Default, Clone)] -pub struct TemplateString(Mustache); - -impl PartialEq for TemplateString { - fn eq(&self, other: &Self) -> bool { - self.0 == other.0 - } -} - -impl From<&str> for TemplateString { - fn from(value: &str) -> Self { - Self(Mustache::parse(value)) - } -} - -impl TemplateString { - pub fn is_empty(&self) -> bool { - self.0.to_string().is_empty() - } - - pub fn parse(value: &str) -> anyhow::Result { - Ok(Self(Mustache::parse(value))) - } - - pub fn resolve(&self, ctx: &impl PathString) -> Self { - let resolved_secret = Mustache::from(vec![Segment::Literal(self.0.render(ctx))]); - Self(resolved_secret) - } -} - -impl Serialize for TemplateString { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&self.0.to_string()) - } -} - -impl<'de> Deserialize<'de> for TemplateString { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let template_string = String::deserialize(deserializer)?; - let mustache = Mustache::parse(&template_string); - - Ok(TemplateString(mustache)) - } -} - -#[cfg(test)] -mod tests { - use std::collections::HashMap; - use std::sync::Arc; - - use crate::core::config::ConfigReaderContext; - use crate::core::mustache::TemplateString; - use crate::core::tests::TestEnvIO; - use crate::core::Mustache; - - #[test] - fn test_default() { - let default_template = TemplateString::default(); - assert!(default_template.is_empty()); - } - - #[test] - fn test_from_str() { - let template_str = "Hello, World!"; - let template = TemplateString::from(template_str); - assert_eq!(template.0.to_string(), template_str); - } - - #[test] - fn test_is_empty() { - let empty_template = TemplateString::default(); - assert!(empty_template.is_empty()); - - let non_empty_template = TemplateString::from("Hello"); - assert!(!non_empty_template.is_empty()); - } - - #[test] - fn test_parse() { - let actual = TemplateString::parse("{{.env.TAILCALL_SECRET}}").unwrap(); - let expected = Mustache::parse("{{.env.TAILCALL_SECRET}}"); - assert_eq!(actual.0, expected); - } - - #[test] - fn test_resolve() { - let mut env_vars = HashMap::new(); - let token = "eyJhbGciOiJIUzI1NiIsInR5"; - env_vars.insert("TAILCALL_SECRET".to_owned(), token.to_owned()); - - let mut runtime = crate::core::runtime::test::init(None); - runtime.env = Arc::new(TestEnvIO::init(env_vars)); - - let ctx = ConfigReaderContext { - runtime: &runtime, - vars: &Default::default(), - headers: Default::default(), - }; - - let actual = TemplateString::parse("{{.env.TAILCALL_SECRET}}") - .unwrap() - .resolve(&ctx); - let expected = TemplateString::from("eyJhbGciOiJIUzI1NiIsInR5"); - - assert_eq!(actual, expected); - } - - #[test] - fn test_serialize() { - let template = TemplateString::from("{{.env.TEST}}"); - let serialized = serde_json::to_string(&template).unwrap(); - assert_eq!(serialized, "\"{{env.TEST}}\""); - } - - #[test] - fn test_deserialize() { - let serialized = "\"{{.env.TEST}}\""; - let template: TemplateString = serde_json::from_str(serialized).unwrap(); - - let actual = template.0; - let expected = Mustache::parse("{{.env.TEST}}"); - - assert_eq!(actual, expected); - } -} diff --git a/tests/cli/fixtures/generator/gen_deezer.md b/tests/cli/fixtures/generator/gen_deezer.md index 74cfe25711..2e3a61140a 100644 --- a/tests/cli/fixtures/generator/gen_deezer.md +++ b/tests/cli/fixtures/generator/gen_deezer.md @@ -3,49 +3,49 @@ "inputs": [ { "curl": { - "src": "https://api.deezer.com/track/3135556", + "src": "{{.env.BASE_URL}}/track/3135556", "fieldName": "track" } }, { "curl": { - "src": "https://api.deezer.com/album/302127", + "src": "{{.env.BASE_URL}}/album/302127", "fieldName": "album" } }, { "curl": { - "src": "https://api.deezer.com/artist/27", + "src": "{{.env.BASE_URL}}/artist/27", "fieldName": "artist" } }, { "curl": { - "src": "https://api.deezer.com/playlist/908622995", + "src": "{{.env.BASE_URL}}/playlist/908622995", "fieldName": "playlist" } }, { "curl": { - "src": "https://api.deezer.com/chart", + "src": "{{.env.BASE_URL}}/chart", "fieldName": "chart" } }, { "curl": { - "src": "https://api.deezer.com/editorial", + "src": "{{.env.BASE_URL}}/editorial", "fieldName": "editorial" } }, { "curl": { - "src": "https://api.deezer.com/user/2529", + "src": "{{.env.BASE_URL}}/user/2529", "fieldName": "user" } }, { "curl": { - "src": "https://api.deezer.com/search?q=eminem", + "src": "{{.env.BASE_URL}}/search?q=eminem", "fieldName": "search" } } @@ -65,3 +65,9 @@ } } ``` + +```json @env +{ + "BASE_URL": "https://api.deezer.com" +} +``` diff --git a/tests/cli/fixtures/generator/gen_jsonplaceholder.md b/tests/cli/fixtures/generator/gen_jsonplaceholder.md index e7bfca0aac..2c9487a93f 100644 --- a/tests/cli/fixtures/generator/gen_jsonplaceholder.md +++ b/tests/cli/fixtures/generator/gen_jsonplaceholder.md @@ -3,7 +3,7 @@ "inputs": [ { "curl": { - "src": "https://jsonplaceholder.typicode.com/posts/1", + "src": "{{.env.BASE_URL}}/posts/1", "headers": { "Content-Type": "application/json", "Accept": "application/json" @@ -13,61 +13,61 @@ }, { "curl": { - "src": "https://jsonplaceholder.typicode.com/users/1", + "src": "{{.env.BASE_URL}}/users/1", "fieldName": "user" } }, { "curl": { - "src": "https://jsonplaceholder.typicode.com/users", + "src": "{{.env.BASE_URL}}/users", "fieldName": "users" } }, { "curl": { - "src": "https://jsonplaceholder.typicode.com/posts", + "src": "{{.env.BASE_URL}}/posts", "fieldName": "posts" } }, { "curl": { - "src": "https://jsonplaceholder.typicode.com/comments", + "src": "{{.env.BASE_URL}}/comments", "fieldName": "comments" } }, { "curl": { - "src": "https://jsonplaceholder.typicode.com/comments/1", + "src": "{{.env.BASE_URL}}/comments/1", "fieldName": "comment" } }, { "curl": { - "src": "https://jsonplaceholder.typicode.com/photos", + "src": "{{.env.BASE_URL}}/photos", "fieldName": "photos" } }, { "curl": { - "src": "https://jsonplaceholder.typicode.com/photos/1", + "src": "{{.env.BASE_URL}}/photos/1", "fieldName": "photo" } }, { "curl": { - "src": "https://jsonplaceholder.typicode.com/todos", + "src": "{{.env.BASE_URL}}/todos", "fieldName": "todos" } }, { "curl": { - "src": "https://jsonplaceholder.typicode.com/todos/1", + "src": "{{.env.BASE_URL}}/todos/1", "fieldName": "todo" } }, { "curl": { - "src": "https://jsonplaceholder.typicode.com/comments?postId=1", + "src": "{{.env.BASE_URL}}/comments?postId=1", "fieldName": "postComments" } } @@ -87,3 +87,9 @@ } } ``` + +```json @env +{ + "BASE_URL": "https://jsonplaceholder.typicode.com" +} +``` From bbd6d4db23b2b4796faa32d95360b63742b689c6 Mon Sep 17 00:00:00 2001 From: laststylebender <43403528+laststylebender14@users.noreply.github.com> Date: Fri, 30 Aug 2024 14:33:04 +0530 Subject: [PATCH 10/14] feat(jit): support introspection in JIT (#2769) Co-authored-by: Tushar Mathur --- src/core/jit/builder.rs | 17 +++- src/core/jit/exec_const.rs | 4 +- src/core/jit/graphql_executor.rs | 44 ++++++++-- src/core/jit/input_resolver.rs | 1 + src/core/jit/model.rs | 15 +++- src/core/jit/request.rs | 1 + src/core/jit/response.rs | 82 +++++++++++++++++++ ...l__core__jit__response__test__merging.snap | 30 +++++++ ...it__response__test__merging_of_errors.snap | 21 +++++ ..._response__test__merging_of_responses.snap | 30 +++++++ src/core/lift.rs | 16 +++- .../graphql-conformance-http-013.md_1.snap | 42 ++++++++++ ...uery-with-disabled-introspection.md_0.snap | 13 +++ ...uery-with-disabled-introspection.md_1.snap | 19 +++++ ...with-disabled-introspection.md_client.snap | 53 ++++++++++++ ...with-disabled-introspection.md_merged.snap | 19 +++++ .../execution/graphql-conformance-http-013.md | 41 +++++++++- ...ction-query-with-disabled-introspection.md | 70 ++++++++++++++++ tests/jit_spec.rs | 2 +- 19 files changed, 503 insertions(+), 17 deletions(-) create mode 100644 src/core/jit/snapshots/tailcall__core__jit__response__test__merging.snap create mode 100644 src/core/jit/snapshots/tailcall__core__jit__response__test__merging_of_errors.snap create mode 100644 src/core/jit/snapshots/tailcall__core__jit__response__test__merging_of_responses.snap create mode 100644 tests/core/snapshots/graphql-conformance-http-013.md_1.snap create mode 100644 tests/core/snapshots/introspection-query-with-disabled-introspection.md_0.snap create mode 100644 tests/core/snapshots/introspection-query-with-disabled-introspection.md_1.snap create mode 100644 tests/core/snapshots/introspection-query-with-disabled-introspection.md_client.snap create mode 100644 tests/core/snapshots/introspection-query-with-disabled-introspection.md_merged.snap create mode 100644 tests/execution/introspection-query-with-disabled-introspection.md diff --git a/src/core/jit/builder.rs b/src/core/jit/builder.rs index 0124aa4949..970eef92fb 100644 --- a/src/core/jit/builder.rs +++ b/src/core/jit/builder.rs @@ -344,7 +344,22 @@ impl Builder { // skip the fields depending on variables. fields.retain(|f| !f.skip(variables)); - let plan = OperationPlan::new(fields, operation.ty, self.index.clone()); + let is_introspection_query = operation.selection_set.node.items.iter().any(|f| { + if let Selection::Field(Positioned { node: gql_field, .. }) = &f.node { + let query = gql_field.name.node.as_str(); + query.contains("__schema") || query.contains("__type") + } else { + false + } + }); + + let plan = OperationPlan::new( + fields, + operation.ty, + self.index.clone(), + is_introspection_query, + ); + // TODO: operation from [ExecutableDocument] could contain definitions for // default values of arguments. That info should be passed to // [InputResolver] to resolve defaults properly diff --git a/src/core/jit/exec_const.rs b/src/core/jit/exec_const.rs index df6f90f64f..6e1c74682f 100644 --- a/src/core/jit/exec_const.rs +++ b/src/core/jit/exec_const.rs @@ -13,7 +13,7 @@ use crate::core::jit::synth::Synth; /// A specialized executor that executes with async_graphql::Value pub struct ConstValueExecutor { - plan: OperationPlan, + pub plan: OperationPlan, } impl ConstValueExecutor { @@ -24,7 +24,7 @@ impl ConstValueExecutor { pub async fn execute( self, req_ctx: &RequestContext, - request: Request, + request: &Request, ) -> Response { let exec = ConstValueExec::new(req_ctx); let plan = self.plan; diff --git a/src/core/jit/graphql_executor.rs b/src/core/jit/graphql_executor.rs index a2f454343c..5e582432c8 100644 --- a/src/core/jit/graphql_executor.rs +++ b/src/core/jit/graphql_executor.rs @@ -1,13 +1,15 @@ +use std::collections::BTreeMap; use std::future::Future; use std::sync::Arc; -use async_graphql::{Data, Executor, Response}; +use async_graphql::{Data, Executor, Response, Value}; use futures_util::stream::BoxStream; use crate::core::app_context::AppContext; use crate::core::http::RequestContext; use crate::core::jit; use crate::core::jit::ConstValueExecutor; +use crate::core::merge_right::MergeRight; #[derive(Clone)] pub struct JITExecutor { @@ -21,15 +23,47 @@ impl JITExecutor { } } +impl From> for async_graphql::Request { + fn from(value: jit::Request) -> Self { + let mut request = async_graphql::Request::new(value.query); + request.variables.extend( + value + .variables + .into_hashmap() + .into_iter() + .map(|(k, v)| (async_graphql::Name::new(k), v)) + .collect::>(), + ); + request.extensions = value.extensions; + request.operation_name = value.operation_name; + request + } +} + impl Executor for JITExecutor { fn execute(&self, request: async_graphql::Request) -> impl Future + Send { - let request = jit::Request::from(request); + let jit_request = jit::Request::from(request); async { - match ConstValueExecutor::new(&request, self.app_ctx.clone()) { + match ConstValueExecutor::new(&jit_request, self.app_ctx.clone()) { Ok(exec) => { - let resp = exec.execute(&self.req_ctx, request).await; - resp.into_async_graphql() + let is_introspection_query = + self.app_ctx.blueprint.server.get_enable_introspection() + && exec.plan.is_introspection_query; + + let jit_resp = exec + .execute(&self.req_ctx, &jit_request) + .await + .into_async_graphql(); + + if is_introspection_query { + let async_req = + async_graphql::Request::from(jit_request).only_introspection(); + let async_resp = self.app_ctx.execute(async_req).await; + jit_resp.merge_right(async_resp) + } else { + jit_resp + } } Err(error) => Response::from_errors(vec![error.into()]), } diff --git a/src/core/jit/input_resolver.rs b/src/core/jit/input_resolver.rs index 286070ed39..4e9ccbb776 100644 --- a/src/core/jit/input_resolver.rs +++ b/src/core/jit/input_resolver.rs @@ -63,6 +63,7 @@ where new_fields, self.plan.operation_type(), self.plan.index.clone(), + self.plan.is_introspection_query, )) } } diff --git a/src/core/jit/model.rs b/src/core/jit/model.rs index be2fdff0af..824e02e756 100644 --- a/src/core/jit/model.rs +++ b/src/core/jit/model.rs @@ -29,6 +29,10 @@ impl Variables { pub fn get(&self, key: &str) -> Option<&Value> { self.0.get(key) } + pub fn into_hashmap(self) -> HashMap { + self.0 + } + pub fn insert(&mut self, key: String, value: Value) { self.0.insert(key, value); } @@ -350,6 +354,7 @@ pub struct OperationPlan { nested: Vec, Input>>, // TODO: drop index from here. Embed all the necessary information in each field of the plan. pub index: Arc, + pub is_introspection_query: bool, } impl std::fmt::Debug for OperationPlan { @@ -382,6 +387,7 @@ impl OperationPlan { operation_type: self.operation_type, nested, index: self.index, + is_introspection_query: self.is_introspection_query, }) } } @@ -391,6 +397,7 @@ impl OperationPlan { fields: Vec>, operation_type: OperationType, index: Arc, + is_introspection_query: bool, ) -> Self where Input: Clone, @@ -402,7 +409,13 @@ impl OperationPlan { .map(|f| f.into_nested(&fields)) .collect::>(); - Self { flat: fields, nested, operation_type, index } + Self { + flat: fields, + nested, + operation_type, + index, + is_introspection_query, + } } /// Returns a graphQL operation type diff --git a/src/core/jit/request.rs b/src/core/jit/request.rs index 2d78a0db56..8eec9cb387 100644 --- a/src/core/jit/request.rs +++ b/src/core/jit/request.rs @@ -19,6 +19,7 @@ pub struct Request { pub extensions: HashMap, } +// NOTE: This is hot code and should allocate minimal memory impl From for Request { fn from(mut value: async_graphql::Request) -> Self { let variables = std::mem::take(value.variables.deref_mut()); diff --git a/src/core/jit/response.rs b/src/core/jit/response.rs index 1b4bf12ec3..f489d25fc5 100644 --- a/src/core/jit/response.rs +++ b/src/core/jit/response.rs @@ -1,8 +1,11 @@ +use std::borrow::BorrowMut; + use derive_setters::Setters; use serde::Serialize; use super::Positioned; use crate::core::jit; +use crate::core::merge_right::MergeRight; #[derive(Setters, Serialize)] pub struct Response { @@ -31,6 +34,22 @@ impl Response { } } +impl MergeRight for async_graphql::Response { + fn merge_right(mut self, other: Self) -> Self { + if let async_graphql::Value::Object(mut other_obj) = other.data { + if let async_graphql::Value::Object(self_obj) = std::mem::take(self.data.borrow_mut()) { + other_obj.extend(self_obj); + self.data = async_graphql::Value::Object(other_obj); + } + } + + self.errors.extend(other.errors); + self.extensions.extend(other.extensions); + + self + } +} + impl Response { pub fn into_async_graphql(self) -> async_graphql::Response { let mut resp = async_graphql::Response::new(self.data.unwrap_or_default()); @@ -50,6 +69,7 @@ mod test { use super::Response; use crate::core::jit::{self, Pos, Positioned}; + use crate::core::merge_right::MergeRight; #[test] fn test_with_response() { @@ -117,4 +137,66 @@ mod test { assert_eq!(async_response.errors.len(), 2); insta::assert_debug_snapshot!(async_response); } + + #[test] + pub fn test_merging_of_responses() { + let introspection_response = r#" + { + "__type": { + "name": "User", + "fields": [ + { + "name": "birthday", + "type": { + "name": "Date" + } + }, + { + "name": "id", + "type": { + "name": "String" + } + } + ] + } + } + "#; + let introspection_data = + ConstValue::from_json(serde_json::from_str(introspection_response).unwrap()).unwrap(); + let introspection_response = async_graphql::Response::new(introspection_data); + + let user_response = r#" + { + "me": { + "id": 1, + "name": "John Smith", + "birthday": "2023-03-08T12:45:26-05:00" + } + } + "#; + let user_data = + ConstValue::from_json(serde_json::from_str(user_response).unwrap()).unwrap(); + let query_response = async_graphql::Response::new(user_data); + + let merged_response = introspection_response.merge_right(query_response); + + insta::assert_json_snapshot!(merged_response); + } + + #[test] + pub fn test_merging_of_errors() { + let mut resp1 = async_graphql::Response::new(ConstValue::default()); + let mut err1 = vec![async_graphql::ServerError::new("Error-1", None)]; + resp1.errors.append(&mut err1); + + let mut resp2 = async_graphql::Response::new(ConstValue::default()); + let mut err2 = vec![async_graphql::ServerError::new( + "Error-2", + Some(async_graphql::Pos::default()), + )]; + resp2.errors.append(&mut err2); + + let merged_resp = resp1.merge_right(resp2); + insta::assert_json_snapshot!(merged_resp); + } } diff --git a/src/core/jit/snapshots/tailcall__core__jit__response__test__merging.snap b/src/core/jit/snapshots/tailcall__core__jit__response__test__merging.snap new file mode 100644 index 0000000000..dbed1326e0 --- /dev/null +++ b/src/core/jit/snapshots/tailcall__core__jit__response__test__merging.snap @@ -0,0 +1,30 @@ +--- +source: src/core/jit/response.rs +expression: merged_response +--- +{ + "data": { + "me": { + "id": 1, + "name": "John Smith", + "birthday": "2023-03-08T12:45:26-05:00" + }, + "__type": { + "name": "User", + "fields": [ + { + "name": "birthday", + "type": { + "name": "Date" + } + }, + { + "name": "id", + "type": { + "name": "String" + } + } + ] + } + } +} diff --git a/src/core/jit/snapshots/tailcall__core__jit__response__test__merging_of_errors.snap b/src/core/jit/snapshots/tailcall__core__jit__response__test__merging_of_errors.snap new file mode 100644 index 0000000000..93107dd1d7 --- /dev/null +++ b/src/core/jit/snapshots/tailcall__core__jit__response__test__merging_of_errors.snap @@ -0,0 +1,21 @@ +--- +source: src/core/jit/response.rs +expression: merged_resp +--- +{ + "data": null, + "errors": [ + { + "message": "Error-1" + }, + { + "message": "Error-2", + "locations": [ + { + "line": 0, + "column": 0 + } + ] + } + ] +} diff --git a/src/core/jit/snapshots/tailcall__core__jit__response__test__merging_of_responses.snap b/src/core/jit/snapshots/tailcall__core__jit__response__test__merging_of_responses.snap new file mode 100644 index 0000000000..dbed1326e0 --- /dev/null +++ b/src/core/jit/snapshots/tailcall__core__jit__response__test__merging_of_responses.snap @@ -0,0 +1,30 @@ +--- +source: src/core/jit/response.rs +expression: merged_response +--- +{ + "data": { + "me": { + "id": 1, + "name": "John Smith", + "birthday": "2023-03-08T12:45:26-05:00" + }, + "__type": { + "name": "User", + "fields": [ + { + "name": "birthday", + "type": { + "name": "Date" + } + }, + { + "name": "id", + "type": { + "name": "String" + } + } + ] + } + } +} diff --git a/src/core/lift.rs b/src/core/lift.rs index d29d25d84b..eb8b61fee6 100644 --- a/src/core/lift.rs +++ b/src/core/lift.rs @@ -1,8 +1,9 @@ +#![allow(dead_code)] use std::ops::Deref; /// -/// Just an empty wrapper around a value used to implement `From` for foreign -/// types. +/// Just an empty wrapper around a value used to implement foreign traits for +/// foreign types. pub struct Lift(A); impl Deref for Lift { type Target = A; @@ -12,7 +13,6 @@ impl Deref for Lift { } impl Lift { - #[allow(dead_code)] pub fn take(self) -> A { self.0 } @@ -23,3 +23,13 @@ impl From for Lift { Lift(a) } } + +pub trait CanLift: Sized { + fn lift(self) -> Lift; +} + +impl CanLift for A { + fn lift(self) -> Lift { + Lift::from(self) + } +} diff --git a/tests/core/snapshots/graphql-conformance-http-013.md_1.snap b/tests/core/snapshots/graphql-conformance-http-013.md_1.snap new file mode 100644 index 0000000000..45aae9e823 --- /dev/null +++ b/tests/core/snapshots/graphql-conformance-http-013.md_1.snap @@ -0,0 +1,42 @@ +--- +source: tests/core/spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": { + "__type": { + "name": "User", + "fields": [ + { + "name": "birthday", + "type": { + "name": "Date" + } + }, + { + "name": "id", + "type": { + "name": "String" + } + }, + { + "name": "name", + "type": { + "name": "String" + } + } + ] + }, + "me": { + "id": 1, + "name": "John Smith", + "birthday": "2023-03-08T12:45:26-05:00" + } + } + } +} diff --git a/tests/core/snapshots/introspection-query-with-disabled-introspection.md_0.snap b/tests/core/snapshots/introspection-query-with-disabled-introspection.md_0.snap new file mode 100644 index 0000000000..661accca7b --- /dev/null +++ b/tests/core/snapshots/introspection-query-with-disabled-introspection.md_0.snap @@ -0,0 +1,13 @@ +--- +source: tests/core/spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": {} + } +} diff --git a/tests/core/snapshots/introspection-query-with-disabled-introspection.md_1.snap b/tests/core/snapshots/introspection-query-with-disabled-introspection.md_1.snap new file mode 100644 index 0000000000..537dca8d84 --- /dev/null +++ b/tests/core/snapshots/introspection-query-with-disabled-introspection.md_1.snap @@ -0,0 +1,19 @@ +--- +source: tests/core/spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": { + "me": { + "id": 1, + "name": "John Smith", + "birthday": "2023-03-08T12:45:26-05:00" + } + } + } +} diff --git a/tests/core/snapshots/introspection-query-with-disabled-introspection.md_client.snap b/tests/core/snapshots/introspection-query-with-disabled-introspection.md_client.snap new file mode 100644 index 0000000000..76b73fd3c9 --- /dev/null +++ b/tests/core/snapshots/introspection-query-with-disabled-introspection.md_client.snap @@ -0,0 +1,53 @@ +--- +source: tests/core/spec.rs +expression: formatted +--- +scalar Bytes + +scalar Date + +scalar DateTime + +scalar Email + +scalar Empty + +scalar Int128 + +scalar Int16 + +scalar Int32 + +scalar Int64 + +scalar Int8 + +scalar JSON + +scalar PhoneNumber + +type Query { + me: User! +} + +scalar UInt128 + +scalar UInt16 + +scalar UInt32 + +scalar UInt64 + +scalar UInt8 + +scalar Url + +type User { + birthday: Date + id: String + name: String +} + +schema { + query: Query +} diff --git a/tests/core/snapshots/introspection-query-with-disabled-introspection.md_merged.snap b/tests/core/snapshots/introspection-query-with-disabled-introspection.md_merged.snap new file mode 100644 index 0000000000..9c90bca585 --- /dev/null +++ b/tests/core/snapshots/introspection-query-with-disabled-introspection.md_merged.snap @@ -0,0 +1,19 @@ +--- +source: tests/core/spec.rs +expression: formatter +--- +schema + @server(hostname: "0.0.0.0", introspection: false, port: 8001, queryValidation: false) + @upstream(baseURL: "http://upstream/", httpCache: 42) { + query: Query +} + +type Query { + me: User! @http(path: "/me") +} + +type User { + birthday: Date + id: String + name: String +} diff --git a/tests/execution/graphql-conformance-http-013.md b/tests/execution/graphql-conformance-http-013.md index 04237949ab..1d328fd624 100644 --- a/tests/execution/graphql-conformance-http-013.md +++ b/tests/execution/graphql-conformance-http-013.md @@ -18,6 +18,18 @@ type User { } ``` +```yml @mock +- request: + method: GET + url: http://upstream/me + response: + status: 200 + body: + id: 1 + name: "John Smith" + birthday: "2023-03-08T12:45:26-05:00" +``` + ```yml @test - method: POST url: http://localhost:8080/graphql @@ -25,13 +37,34 @@ type User { query: | { __type(name: "User") { - name - fields { name - type { + fields { name + type { + name + } } - } + } + } + +- method: POST + url: http://localhost:8080/graphql + body: + query: | + { + __type(name: "User") { + name + fields { + name + type { + name + } + } + } + me { + id + name + birthday } } ``` diff --git a/tests/execution/introspection-query-with-disabled-introspection.md b/tests/execution/introspection-query-with-disabled-introspection.md new file mode 100644 index 0000000000..31a85d39c3 --- /dev/null +++ b/tests/execution/introspection-query-with-disabled-introspection.md @@ -0,0 +1,70 @@ +# Test schema inspection with false flag + +```graphql @config +schema + @server(port: 8001, queryValidation: false, hostname: "0.0.0.0", introspection: false) + @upstream(baseURL: "http://upstream/", httpCache: 42) { + query: Query +} + +type Query { + me: User! @http(path: "/me") +} + +type User { + id: String + name: String + birthday: Date +} +``` + +```yml @mock +- request: + method: GET + url: http://upstream/me + response: + status: 200 + body: + id: 1 + name: "John Smith" + birthday: "2023-03-08T12:45:26-05:00" +``` + +```yml @test +- method: POST + url: http://localhost:8080/graphql + body: + query: | + { + __type(name: "User") { + name + fields { + name + type { + name + } + } + } + } + +- method: POST + url: http://localhost:8080/graphql + body: + query: | + { + __type(name: "User") { + name + fields { + name + type { + name + } + } + } + me { + id + name + birthday + } + } +``` diff --git a/tests/jit_spec.rs b/tests/jit_spec.rs index 0cf8263206..cd78055f68 100644 --- a/tests/jit_spec.rs +++ b/tests/jit_spec.rs @@ -35,7 +35,7 @@ mod tests { ) -> anyhow::Result> { let executor = ConstValueExecutor::new(&request, self.app_ctx.clone())?; - Ok(executor.execute(&self.req_ctx, request).await) + Ok(executor.execute(&self.req_ctx, &request).await) } } From 0634073983414db18fbbd30e1169f1a533dd5e52 Mon Sep 17 00:00:00 2001 From: laststylebender <43403528+laststylebender14@users.noreply.github.com> Date: Fri, 30 Aug 2024 18:35:35 +0530 Subject: [PATCH 11/14] fix(jit): error handling for lists in JIT (#2771) --- src/core/jit/synth/synth.rs | 64 +++++++++---------- .../snapshots/test-required-fields.md_13.snap | 5 +- .../snapshots/test-required-fields.md_17.snap | 26 +++----- .../snapshots/test-required-fields.md_21.snap | 26 +++----- .../snapshots/test-required-fields.md_9.snap | 5 +- .../test-required-fields.md_merged.snap | 2 +- tests/execution/test-required-fields.md | 2 +- 7 files changed, 55 insertions(+), 75 deletions(-) diff --git a/src/core/jit/synth/synth.rs b/src/core/jit/synth/synth.rs index e8b430d66b..446fc8f00a 100644 --- a/src/core/jit/synth/synth.rs +++ b/src/core/jit/synth/synth.rs @@ -36,6 +36,7 @@ where #[inline(always)] pub fn synthesize(&'a self) -> Result> { let mut data = Value::JsonObject::new(); + let mut path = Vec::new(); for child in self.plan.as_nested().iter() { if !self.include(child) { @@ -43,22 +44,25 @@ where } // TODO: in case of error set `child.output_name` to null // and append error to response error array - let val = self.iter(child, None, &DataPath::new())?; - + let val = self.iter(child, None, &DataPath::new(), &mut path)?; data.insert_key(&child.output_name, val); } Ok(Value::object(data)) } + #[allow(clippy::too_many_arguments)] #[inline(always)] fn iter( &'a self, node: &'a Field, Value>, value: Option<&'a Value>, data_path: &DataPath, + path: &mut Vec, ) -> Result> { - match self.store.get(&node.id) { + path.push(PathSegment::Field(node.output_name.clone())); + + let result = match self.store.get(&node.id) { Some(val) => { let mut data = val; @@ -76,9 +80,10 @@ where let value = result.as_ref().map_err(Clone::clone)?; if node.type_of.is_list() != value.as_array().is_some() { - return self.node_nullable_guard(node); + self.node_nullable_guard(node, path) + } else { + self.iter_inner(node, value, data_path, path) } - self.iter_inner(node, value, data_path) } _ => { // TODO: should bailout instead of returning Null @@ -87,10 +92,13 @@ where } } None => match value { - Some(result) => self.iter_inner(node, result, data_path), - None => self.node_nullable_guard(node), + Some(result) => self.iter_inner(node, result, data_path, path), + None => self.node_nullable_guard(node, path), }, - } + }; + + path.pop(); + result } /// This guard ensures to return Null value only if node type permits it, in @@ -98,21 +106,25 @@ where fn node_nullable_guard( &'a self, node: &'a Field, Value>, + path: &[PathSegment], ) -> Result> { // according to GraphQL spec https://spec.graphql.org/October2021/#sec-Handling-Field-Errors if node.type_of.is_nullable() { Ok(Value::null()) } else { - Err(ValidationError::ValueRequired.into()).map_err(|e| self.to_location_error(e, node)) + Err(ValidationError::ValueRequired.into()) + .map_err(|e| self.to_location_error(e, node, path)) } } + #[allow(clippy::too_many_arguments)] #[inline(always)] fn iter_inner( &'a self, node: &'a Field, Value>, value: &'a Value, data_path: &DataPath, + path: &mut Vec, ) -> Result> { // skip the field if field is not included in schema if !self.include(node) { @@ -166,13 +178,12 @@ where for child in self.plan.field_iter_only(node, value) { // all checks for skip must occur in `iter_inner` // and include be checked before calling `iter` or recursing. - let include = self.include(child); - if include { + if self.include(child) { let value = if child.name == "__typename" { Value::string(node.value_type(value).into()) } else { let val = obj.get_key(child.name.as_str()); - self.iter(child, val, data_path)? + self.iter(child, val, data_path, path)? }; ans.insert_key(&child.output_name, value); } @@ -183,8 +194,11 @@ where (Some(arr), _) => { let mut ans = vec![]; for (i, val) in arr.iter().enumerate() { - let val = self.iter_inner(node, val, &data_path.clone().with_index(i))?; - ans.push(val) + path.push(PathSegment::Index(i)); + let val = + self.iter_inner(node, val, &data_path.clone().with_index(i), path)?; + path.pop(); + ans.push(val); } Ok(Value::array(ans)) } @@ -192,32 +206,16 @@ where } }; - eval_result.map_err(|e| self.to_location_error(e, node)) + eval_result.map_err(|e| self.to_location_error(e, node, path)) } fn to_location_error( &'a self, error: Error, node: &'a Field, Value>, + path: &[PathSegment], ) -> Positioned { - // create path from the root to the current node in the fields tree - let path = { - let mut path = Vec::new(); - - let mut parent = self.plan.find_field(node.id.clone()); - - while let Some(field) = parent { - path.push(PathSegment::Field(field.output_name.to_string())); - parent = field - .parent() - .and_then(|id| self.plan.find_field(id.clone())); - } - - path.reverse(); - path - }; - - Positioned::new(error, node.pos).with_path(path) + Positioned::new(error, node.pos).with_path(path.to_vec()) } } diff --git a/tests/core/snapshots/test-required-fields.md_13.snap b/tests/core/snapshots/test-required-fields.md_13.snap index 65e3a4af79..5a2e8949c7 100644 --- a/tests/core/snapshots/test-required-fields.md_13.snap +++ b/tests/core/snapshots/test-required-fields.md_13.snap @@ -15,13 +15,12 @@ expression: response "locations": [ { "line": 1, - "column": 29 + "column": 9 } ], "path": [ "innerEntryMissing", - 1, - "id" + 1 ] } ] diff --git a/tests/core/snapshots/test-required-fields.md_17.snap b/tests/core/snapshots/test-required-fields.md_17.snap index b6f7294e44..cabdc83aea 100644 --- a/tests/core/snapshots/test-required-fields.md_17.snap +++ b/tests/core/snapshots/test-required-fields.md_17.snap @@ -8,22 +8,14 @@ expression: response "content-type": "application/json" }, "body": { - "data": null, - "errors": [ - { - "message": "internal: non-null types require a return value", - "locations": [ - { - "line": 1, - "column": 29 - } - ], - "path": [ - "outerEntryMissing", - 1, - "id" - ] - } - ] + "data": { + "outerEntryMissing": [ + { + "id": 1, + "bar": "bar_1" + }, + null + ] + } } } diff --git a/tests/core/snapshots/test-required-fields.md_21.snap b/tests/core/snapshots/test-required-fields.md_21.snap index 0bee8203d9..0a7dffb745 100644 --- a/tests/core/snapshots/test-required-fields.md_21.snap +++ b/tests/core/snapshots/test-required-fields.md_21.snap @@ -8,22 +8,14 @@ expression: response "content-type": "application/json" }, "body": { - "data": null, - "errors": [ - { - "message": "internal: non-null types require a return value", - "locations": [ - { - "line": 1, - "column": 28 - } - ], - "path": [ - "noneEntryMissing", - 1, - "id" - ] - } - ] + "data": { + "noneEntryMissing": [ + { + "id": 1, + "bar": "bar_1" + }, + null + ] + } } } diff --git a/tests/core/snapshots/test-required-fields.md_9.snap b/tests/core/snapshots/test-required-fields.md_9.snap index 392bb18994..046718d6d8 100644 --- a/tests/core/snapshots/test-required-fields.md_9.snap +++ b/tests/core/snapshots/test-required-fields.md_9.snap @@ -15,13 +15,12 @@ expression: response "locations": [ { "line": 1, - "column": 28 + "column": 9 } ], "path": [ "fullEntryMissing", - 1, - "id" + 1 ] } ] diff --git a/tests/core/snapshots/test-required-fields.md_merged.snap b/tests/core/snapshots/test-required-fields.md_merged.snap index 3a4bb400ff..d4c5a67bb7 100644 --- a/tests/core/snapshots/test-required-fields.md_merged.snap +++ b/tests/core/snapshots/test-required-fields.md_merged.snap @@ -2,7 +2,7 @@ source: tests/core/spec.rs expression: formatter --- -schema @server @upstream(baseURL: "http://jsonplaceholder.typicode.com") { +schema @server(enableJIT: true) @upstream(baseURL: "http://jsonplaceholder.typicode.com") { query: Query } diff --git a/tests/execution/test-required-fields.md b/tests/execution/test-required-fields.md index 115ca86075..925051fd58 100644 --- a/tests/execution/test-required-fields.md +++ b/tests/execution/test-required-fields.md @@ -1,7 +1,7 @@ # Test API ```graphql @config -schema @server @upstream(baseURL: "http://jsonplaceholder.typicode.com") { +schema @server(enableJIT: true) @upstream(baseURL: "http://jsonplaceholder.typicode.com") { query: Query } From 3fa1c157628033de6cffd69475810eedf8e91201 Mon Sep 17 00:00:00 2001 From: laststylebender <43403528+laststylebender14@users.noreply.github.com> Date: Fri, 30 Aug 2024 18:52:20 +0530 Subject: [PATCH 12/14] feat: enable JIT by default for all requests (#2772) Co-authored-by: Tushar Mathur --- src/core/config/server.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/config/server.rs b/src/core/config/server.rs index cb4256e6c4..960e8ce40e 100644 --- a/src/core/config/server.rs +++ b/src/core/config/server.rs @@ -31,7 +31,7 @@ use crate::core::merge_right::MergeRight; pub struct Server { // The `enableJIT` option activates Just-In-Time (JIT) compilation. When set to true, it // optimizes execution of each incoming request independently, resulting in significantly - // better performance in most cases. + // better performance in most cases, it's enabled by default. #[serde(default, skip_serializing_if = "is_default", rename = "enableJIT")] pub enable_jit: Option, @@ -229,7 +229,7 @@ impl Server { self.dedupe.unwrap_or(false) } pub fn enable_jit(&self) -> bool { - self.enable_jit.unwrap_or(false) + self.enable_jit.unwrap_or(true) } } From 30a323fab9c815243524adb9c322315fb67ef87a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 30 Aug 2024 13:23:40 +0000 Subject: [PATCH 13/14] chore(deps): update dependency wrangler to v3.73.0 --- tailcall-cloudflare/package-lock.json | 107 +++++++++++--------------- 1 file changed, 44 insertions(+), 63 deletions(-) diff --git a/tailcall-cloudflare/package-lock.json b/tailcall-cloudflare/package-lock.json index 6521dc4ffa..c3e1278a7b 100644 --- a/tailcall-cloudflare/package-lock.json +++ b/tailcall-cloudflare/package-lock.json @@ -130,9 +130,9 @@ } }, "node_modules/@cloudflare/workers-shared": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-shared/-/workers-shared-0.4.0.tgz", - "integrity": "sha512-XAFOldVQsbxQ7mjbqX2q1dNIgcLbKSytk41pwuZTn9e0p7OeTpFTosJef8uwosL6CcOAHqcW1f1HJxyjwmtGxw==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-shared/-/workers-shared-0.4.1.tgz", + "integrity": "sha512-nYh4r8JwOOjYIdH2zub++CmIKlkYFlpxI1nBHimoiHcytJXD/b7ldJ21TtfzUZMCgI78mxVlymMHA/ReaOxKlA==", "dev": true, "engines": { "node": ">=16.7.0" @@ -1063,15 +1063,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/consola": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", - "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", - "dev": true, - "engines": { - "node": "^14.18.0 || >=16.10.0" - } - }, "node_modules/cookie": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", @@ -1521,12 +1512,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/node-fetch-native": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.4.tgz", - "integrity": "sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==", - "dev": true - }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -1572,6 +1557,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ohash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.3.tgz", + "integrity": "sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==", + "dev": true + }, "node_modules/onetime": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", @@ -1983,9 +1974,9 @@ "dev": true }, "node_modules/ufo": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz", - "integrity": "sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", + "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==", "dev": true }, "node_modules/undici": { @@ -2008,17 +1999,15 @@ }, "node_modules/unenv": { "name": "unenv-nightly", - "version": "1.10.0-1717606461.a117952", - "resolved": "https://registry.npmjs.org/unenv-nightly/-/unenv-nightly-1.10.0-1717606461.a117952.tgz", - "integrity": "sha512-u3TfBX02WzbHTpaEfWEKwDijDSFAHcgXkayUZ+MVDrjhLFvgAJzFGTSTmwlEhwWi2exyRQey23ah9wELMM6etg==", + "version": "2.0.0-1724863496.70db6f1", + "resolved": "https://registry.npmjs.org/unenv-nightly/-/unenv-nightly-2.0.0-1724863496.70db6f1.tgz", + "integrity": "sha512-r+VIl1gnsI4WQxluruSQhy8alpAf1AsLRLm4sEKp3otCyTIVD6I6wHEYzeQnwsyWgaD4+3BD4A/eqrgOpdTzhw==", "dev": true, "dependencies": { - "consola": "^3.2.3", "defu": "^6.1.4", - "mime": "^3.0.0", - "node-fetch-native": "^1.6.4", + "ohash": "^1.1.3", "pathe": "^1.1.2", - "ufo": "^1.5.3" + "ufo": "^1.5.4" } }, "node_modules/vite": { @@ -2214,13 +2203,13 @@ } }, "node_modules/wrangler": { - "version": "3.72.3", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.72.3.tgz", - "integrity": "sha512-EBlJGOcwanbzFkiJkRB47WKhvevh1AZK0ty0MyD0gptsgWnAxBfmFGiBuzOuRXbvH45ZrFrTqgi8c67EwcV1nA==", + "version": "3.73.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.73.0.tgz", + "integrity": "sha512-VrdDR2OpvsCQp+r5Of3rDP1W64cNN/LHLVx1roULOlPS8PZiv7rUYgkwhdCQ61+HICAaeSxWYIzkL5+B9+8W3g==", "dev": true, "dependencies": { "@cloudflare/kv-asset-handler": "0.3.4", - "@cloudflare/workers-shared": "0.4.0", + "@cloudflare/workers-shared": "0.4.1", "@esbuild-plugins/node-globals-polyfill": "^0.2.3", "@esbuild-plugins/node-modules-polyfill": "^0.2.2", "blake3-wasm": "^2.1.5", @@ -2234,7 +2223,7 @@ "resolve.exports": "^2.0.2", "selfsigned": "^2.0.1", "source-map": "^0.6.1", - "unenv": "npm:unenv-nightly@1.10.0-1717606461.a117952", + "unenv": "npm:unenv-nightly@2.0.0-1724863496.70db6f1", "workerd": "1.20240821.1", "xxhash-wasm": "^1.0.1" }, @@ -2762,9 +2751,9 @@ "optional": true }, "@cloudflare/workers-shared": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-shared/-/workers-shared-0.4.0.tgz", - "integrity": "sha512-XAFOldVQsbxQ7mjbqX2q1dNIgcLbKSytk41pwuZTn9e0p7OeTpFTosJef8uwosL6CcOAHqcW1f1HJxyjwmtGxw==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-shared/-/workers-shared-0.4.1.tgz", + "integrity": "sha512-nYh4r8JwOOjYIdH2zub++CmIKlkYFlpxI1nBHimoiHcytJXD/b7ldJ21TtfzUZMCgI78mxVlymMHA/ReaOxKlA==", "dev": true }, "@cloudflare/workers-types": { @@ -3318,12 +3307,6 @@ "readdirp": "~3.6.0" } }, - "consola": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", - "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", - "dev": true - }, "cookie": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", @@ -3642,12 +3625,6 @@ "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "dev": true }, - "node-fetch-native": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.4.tgz", - "integrity": "sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==", - "dev": true - }, "node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -3677,6 +3654,12 @@ } } }, + "ohash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.3.tgz", + "integrity": "sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==", + "dev": true + }, "onetime": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", @@ -3985,9 +3968,9 @@ "dev": true }, "ufo": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz", - "integrity": "sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", + "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==", "dev": true }, "undici": { @@ -4006,17 +3989,15 @@ "dev": true }, "unenv": { - "version": "npm:unenv-nightly@1.10.0-1717606461.a117952", - "resolved": "https://registry.npmjs.org/unenv-nightly/-/unenv-nightly-1.10.0-1717606461.a117952.tgz", - "integrity": "sha512-u3TfBX02WzbHTpaEfWEKwDijDSFAHcgXkayUZ+MVDrjhLFvgAJzFGTSTmwlEhwWi2exyRQey23ah9wELMM6etg==", + "version": "npm:unenv-nightly@2.0.0-1724863496.70db6f1", + "resolved": "https://registry.npmjs.org/unenv-nightly/-/unenv-nightly-2.0.0-1724863496.70db6f1.tgz", + "integrity": "sha512-r+VIl1gnsI4WQxluruSQhy8alpAf1AsLRLm4sEKp3otCyTIVD6I6wHEYzeQnwsyWgaD4+3BD4A/eqrgOpdTzhw==", "dev": true, "requires": { - "consola": "^3.2.3", "defu": "^6.1.4", - "mime": "^3.0.0", - "node-fetch-native": "^1.6.4", + "ohash": "^1.1.3", "pathe": "^1.1.2", - "ufo": "^1.5.3" + "ufo": "^1.5.4" } }, "vite": { @@ -4104,13 +4085,13 @@ } }, "wrangler": { - "version": "3.72.3", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.72.3.tgz", - "integrity": "sha512-EBlJGOcwanbzFkiJkRB47WKhvevh1AZK0ty0MyD0gptsgWnAxBfmFGiBuzOuRXbvH45ZrFrTqgi8c67EwcV1nA==", + "version": "3.73.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.73.0.tgz", + "integrity": "sha512-VrdDR2OpvsCQp+r5Of3rDP1W64cNN/LHLVx1roULOlPS8PZiv7rUYgkwhdCQ61+HICAaeSxWYIzkL5+B9+8W3g==", "dev": true, "requires": { "@cloudflare/kv-asset-handler": "0.3.4", - "@cloudflare/workers-shared": "0.4.0", + "@cloudflare/workers-shared": "0.4.1", "@esbuild-plugins/node-globals-polyfill": "^0.2.3", "@esbuild-plugins/node-modules-polyfill": "^0.2.2", "blake3-wasm": "^2.1.5", @@ -4125,7 +4106,7 @@ "resolve.exports": "^2.0.2", "selfsigned": "^2.0.1", "source-map": "^0.6.1", - "unenv": "npm:unenv-nightly@1.10.0-1717606461.a117952", + "unenv": "npm:unenv-nightly@2.0.0-1724863496.70db6f1", "workerd": "1.20240821.1", "xxhash-wasm": "^1.0.1" }, From d473af2b41a2b9522179c45e69eeb45c2405a191 Mon Sep 17 00:00:00 2001 From: neo773 <62795688+neo773@users.noreply.github.com> Date: Sun, 1 Sep 2024 19:05:07 +0530 Subject: [PATCH 14/14] fix(2777): use option_env for trackers (#2778) --- .github/workflows/ci.yml | 2 ++ tailcall-tracker/src/tracker.rs | 12 ++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e6ece73b8..4c0e004af5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -241,6 +241,8 @@ jobs: pull-requests: write env: GITHUB_TOKEN: ${{secrets.GITHUBTOKEN}} + GA_API_SECRET: ${{secrets.GA_API_SECRET}} + GA_MEASUREMENT_ID: ${{secrets.GA_MEASUREMENT_ID}} APP_VERSION: ${{ needs.draft_release.outputs.create_release_name }} steps: diff --git a/tailcall-tracker/src/tracker.rs b/tailcall-tracker/src/tracker.rs index 40de9a5aa7..d30ad0180f 100644 --- a/tailcall-tracker/src/tracker.rs +++ b/tailcall-tracker/src/tracker.rs @@ -4,8 +4,16 @@ use super::Result; use crate::check_tracking::check_tracking; use crate::event::Event; -const API_SECRET: &str = "GVaEzXFeRkCI9YBIylbEjQ"; -const MEASUREMENT_ID: &str = "G-JEP3QDWT0G"; +const API_SECRET: &str = match option_env!("GA_API_SECRET") { + Some(val) => val, + None => "dev", +}; + +const MEASUREMENT_ID: &str = match option_env!("GA_MEASUREMENT_ID") { + Some(val) => val, + None => "dev", +}; + const BASE_URL: &str = "https://www.google-analytics.com"; ///