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}}/