diff --git a/examples/jsonplaceholder.json b/examples/jsonplaceholder.json index c3b1d96ae6..cce3f0d6b4 100644 --- a/examples/jsonplaceholder.json +++ b/examples/jsonplaceholder.json @@ -1,5 +1,5 @@ { - "$schema": "./.tailcallrc.schema.json", + "$schema": "../generated/.tailcallrc.schema.json", "server": { "hostname": "0.0.0.0", "port": 8000 diff --git a/generated/.tailcallrc.schema.json b/generated/.tailcallrc.schema.json index 6f57e27171..468b392e0e 100644 --- a/generated/.tailcallrc.schema.json +++ b/generated/.tailcallrc.schema.json @@ -372,6 +372,80 @@ "Field": { "description": "A field definition containing all the metadata information about resolving a field.", "type": "object", + "oneOf": [ + { + "type": "object", + "required": [ + "http" + ], + "properties": { + "http": { + "$ref": "#/definitions/Http" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "grpc" + ], + "properties": { + "grpc": { + "$ref": "#/definitions/Grpc" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "graphql" + ], + "properties": { + "graphql": { + "$ref": "#/definitions/GraphQL" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "call" + ], + "properties": { + "call": { + "$ref": "#/definitions/Call" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "js" + ], + "properties": { + "js": { + "$ref": "#/definitions/JS" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "expr" + ], + "properties": { + "expr": { + "$ref": "#/definitions/Expr" + } + }, + "additionalProperties": false + } + ], "properties": { "args": { "description": "Map of argument name and its definition.", @@ -391,17 +465,6 @@ } ] }, - "call": { - "description": "Inserts a call resolver for the field.", - "anyOf": [ - { - "$ref": "#/definitions/Call" - }, - { - "type": "null" - } - ] - }, "default_value": { "description": "Stores the default value for the field" }, @@ -412,50 +475,6 @@ "null" ] }, - "expr": { - "description": "Inserts a constant resolver for the field.", - "anyOf": [ - { - "$ref": "#/definitions/Expr" - }, - { - "type": "null" - } - ] - }, - "graphql": { - "description": "Inserts a GraphQL resolver for the field.", - "anyOf": [ - { - "$ref": "#/definitions/GraphQL" - }, - { - "type": "null" - } - ] - }, - "grpc": { - "description": "Inserts a GRPC resolver for the field.", - "anyOf": [ - { - "$ref": "#/definitions/Grpc" - }, - { - "type": "null" - } - ] - }, - "http": { - "description": "Inserts an HTTP resolver for the field.", - "anyOf": [ - { - "$ref": "#/definitions/Http" - }, - { - "type": "null" - } - ] - }, "list": { "description": "Flag to indicate the type is a list.", "type": "boolean" @@ -502,17 +521,6 @@ "description": "Flag to indicate the type is required.", "type": "boolean" }, - "script": { - "description": "Inserts a Javascript resolver for the field.", - "anyOf": [ - { - "$ref": "#/definitions/JS" - }, - { - "type": "null" - } - ] - }, "type": { "description": "Refers to the type of the value the field can be resolved to.", "type": "string" diff --git a/src/cli/tc/init.rs b/src/cli/tc/init.rs index 0413ed65e5..e1a2b7672a 100644 --- a/src/cli/tc/init.rs +++ b/src/cli/tc/init.rs @@ -5,7 +5,7 @@ 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, RootSchema, Source, Type}; +use crate::core::config::{Config, Expr, Field, Resolver, RootSchema, Source, Type}; use crate::core::merge_right::MergeRight; use crate::core::runtime::TargetRuntime; @@ -75,7 +75,7 @@ fn main_config() -> Config { let field = Field { type_of: "String".to_string(), required: true, - const_field: Some(Expr { body: "Hello, World!".into() }), + resolver: Some(Resolver::Expr(Expr { body: "Hello, World!".into() })), ..Default::default() }; diff --git a/src/core/blueprint/definitions.rs b/src/core/blueprint/definitions.rs index b2e729a29c..d8df056f1f 100644 --- a/src/core/blueprint/definitions.rs +++ b/src/core/blueprint/definitions.rs @@ -102,20 +102,9 @@ fn process_field_within_type(context: ProcessFieldWithinTypeContext) -> Valid add_field.path[1..] .iter() .map(|s| s.to_owned()) @@ -497,15 +486,6 @@ pub fn to_field_definition( type_of: &config::Type, name: &String, ) -> Valid { - let directives = field.resolvable_directives(); - - if directives.len() > 1 { - return Valid::fail(format!( - "Multiple resolvers detected [{}]", - directives.join(", ") - )); - } - update_args() .and(update_http().trace(config::Http::trace_name().as_str())) .and(update_grpc(operation_type).trace(config::Grpc::trace_name().as_str())) diff --git a/src/core/blueprint/from_config.rs b/src/core/blueprint/from_config.rs index 81f3b238c3..fa350ba542 100644 --- a/src/core/blueprint/from_config.rs +++ b/src/core/blueprint/from_config.rs @@ -89,7 +89,7 @@ where let schema = if let Some(type_) = type_ { let mut schema_fields = HashMap::new(); for (name, field) in type_.fields.iter() { - if field.script.is_none() && field.http.is_none() { + if field.resolver.is_none() { schema_fields.insert(name.clone(), to_json_schema_for_field(field, config)); } } diff --git a/src/core/blueprint/operators/call.rs b/src/core/blueprint/operators/call.rs index d820eb3e19..5ea3cf31f3 100644 --- a/src/core/blueprint/operators/call.rs +++ b/src/core/blueprint/operators/call.rs @@ -2,7 +2,7 @@ use serde_json::Value; use crate::core::blueprint::*; use crate::core::config; -use crate::core::config::{Field, GraphQLOperationType}; +use crate::core::config::{Field, GraphQLOperationType, Resolver}; use crate::core::ir::model::IR; use crate::core::try_fold::TryFold; use crate::core::valid::{Valid, ValidationError, Validator}; @@ -14,11 +14,11 @@ pub fn update_call<'a>( { TryFold::<(&ConfigModule, &Field, &config::Type, &str), FieldDefinition, String>::new( move |(config, field, _, name), b_field| { - let Some(ref calls) = field.call else { + let Some(Resolver::Call(call)) = &field.resolver else { return Valid::succeed(b_field); }; - compile_call(config, calls, operation_type, object_name) + compile_call(config, call, operation_type, object_name) .map(|field| b_field.resolver(field.resolver).name(name.to_string())) }, ) diff --git a/src/core/blueprint/operators/expr.rs b/src/core/blueprint/operators/expr.rs index 30c771ed79..a429bf9e3b 100644 --- a/src/core/blueprint/operators/expr.rs +++ b/src/core/blueprint/operators/expr.rs @@ -2,7 +2,7 @@ use async_graphql_value::ConstValue; use crate::core::blueprint::*; use crate::core::config; -use crate::core::config::Field; +use crate::core::config::{Field, Resolver}; use crate::core::ir::model::IR; use crate::core::ir::model::IR::Dynamic; use crate::core::try_fold::TryFold; @@ -64,17 +64,12 @@ pub fn update_const_field<'a>( { TryFold::<(&ConfigModule, &Field, &config::Type, &str), FieldDefinition, String>::new( |(config_module, field, _, _), b_field| { - let Some(const_field) = &field.const_field else { + let Some(Resolver::Expr(expr)) = &field.resolver else { return Valid::succeed(b_field); }; - compile_expr(CompileExpr { - config_module, - field, - value: &const_field.body, - validate: true, - }) - .map(|resolver| b_field.resolver(Some(resolver))) + compile_expr(CompileExpr { config_module, field, value: &expr.body, validate: true }) + .map(|resolver| b_field.resolver(Some(resolver))) }, ) } diff --git a/src/core/blueprint/operators/graphql.rs b/src/core/blueprint/operators/graphql.rs index 74f8282644..bf5f42e9c4 100644 --- a/src/core/blueprint/operators/graphql.rs +++ b/src/core/blueprint/operators/graphql.rs @@ -1,7 +1,9 @@ use std::collections::{HashMap, HashSet}; use crate::core::blueprint::FieldDefinition; -use crate::core::config::{Config, ConfigModule, Field, GraphQL, GraphQLOperationType, Type}; +use crate::core::config::{ + Config, ConfigModule, Field, GraphQL, GraphQLOperationType, Resolver, Type, +}; use crate::core::graphql::RequestTemplate; use crate::core::helpers; use crate::core::ir::model::{IO, IR}; @@ -78,7 +80,7 @@ pub fn update_graphql<'a>( ) -> TryFold<'a, (&'a ConfigModule, &'a Field, &'a Type, &'a str), FieldDefinition, String> { TryFold::<(&ConfigModule, &Field, &Type, &'a str), FieldDefinition, String>::new( |(config, field, type_of, _), b_field| { - let Some(graphql) = &field.graphql else { + let Some(Resolver::Graphql(graphql)) = &field.resolver else { return Valid::succeed(b_field); }; diff --git a/src/core/blueprint/operators/grpc.rs b/src/core/blueprint/operators/grpc.rs index 3ab9bf7334..282a3be12e 100644 --- a/src/core/blueprint/operators/grpc.rs +++ b/src/core/blueprint/operators/grpc.rs @@ -5,7 +5,7 @@ use prost_reflect::FieldDescriptor; use crate::core::blueprint::{FieldDefinition, TypeLike}; use crate::core::config::group_by::GroupBy; -use crate::core::config::{Config, ConfigModule, Field, GraphQLOperationType, Grpc}; +use crate::core::config::{Config, ConfigModule, Field, GraphQLOperationType, Grpc, Resolver}; use crate::core::grpc::protobuf::{ProtobufOperation, ProtobufSet}; use crate::core::grpc::request_template::RequestTemplate; use crate::core::ir::model::{IO, IR}; @@ -216,7 +216,7 @@ pub fn update_grpc<'a>( { TryFold::<(&ConfigModule, &Field, &config::Type, &'a str), FieldDefinition, String>::new( |(config_module, field, type_of, _name), b_field| { - let Some(grpc) = &field.grpc else { + let Some(Resolver::Grpc(grpc)) = &field.resolver else { return Valid::succeed(b_field); }; diff --git a/src/core/blueprint/operators/http.rs b/src/core/blueprint/operators/http.rs index 9ceb1e2769..71e8f28e57 100644 --- a/src/core/blueprint/operators/http.rs +++ b/src/core/blueprint/operators/http.rs @@ -1,6 +1,6 @@ use crate::core::blueprint::*; use crate::core::config::group_by::GroupBy; -use crate::core::config::Field; +use crate::core::config::{Field, Resolver}; use crate::core::endpoint::Endpoint; use crate::core::http::{HttpFilter, Method, RequestTemplate}; use crate::core::ir::model::{IO, IR}; @@ -93,7 +93,7 @@ pub fn update_http<'a>( { TryFold::<(&ConfigModule, &Field, &config::Type, &'a str), FieldDefinition, String>::new( |(config_module, field, type_of, _), b_field| { - let Some(http) = &field.http else { + let Some(Resolver::Http(http)) = &field.resolver else { return Valid::succeed(b_field); }; diff --git a/src/core/blueprint/operators/js.rs b/src/core/blueprint/operators/js.rs index 0687f15b42..cc6b212c60 100644 --- a/src/core/blueprint/operators/js.rs +++ b/src/core/blueprint/operators/js.rs @@ -1,6 +1,6 @@ use crate::core::blueprint::FieldDefinition; use crate::core::config; -use crate::core::config::{ConfigModule, Field}; +use crate::core::config::{ConfigModule, Field, Resolver}; use crate::core::ir::model::{IO, IR}; use crate::core::try_fold::TryFold; use crate::core::valid::{Valid, Validator}; @@ -21,7 +21,7 @@ pub fn update_js_field<'a>( { TryFold::<(&ConfigModule, &Field, &config::Type, &str), FieldDefinition, String>::new( |(module, field, _, _), b_field| { - let Some(js) = &field.script else { + let Some(Resolver::Js(js)) = &field.resolver else { return Valid::succeed(b_field); }; diff --git a/src/core/config/config.rs b/src/core/config/config.rs index 6699b78895..8c25dc08be 100644 --- a/src/core/config/config.rs +++ b/src/core/config/config.rs @@ -3,7 +3,8 @@ use std::fmt::{self, Display}; use std::num::NonZeroU64; use anyhow::Result; -use async_graphql::parser::types::ServiceDocument; +use async_graphql::parser::types::{ConstDirective, ServiceDocument}; +use async_graphql::Positioned; use derive_setters::Setters; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -209,6 +210,85 @@ pub struct RootSchema { /// Used to omit a field from public consumption. pub struct Omit {} +// generate Resolver with macro in order to autogenerate conversion code +// from the underlying directives. +// TODO: replace with derive macro +macro_rules! create_resolver { + ($($var:ident($ty:ty)),+$(,)?) => { + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, schemars::JsonSchema)] + #[serde(rename_all = "camelCase")] + pub enum Resolver { + // just specify the same variants + $($var($ty),)+ + } + + impl Resolver { + pub fn from_directives( + directives: &[Positioned], + ) -> Valid, String> { + let mut result = None; + let mut resolvable_directives = Vec::new(); + let mut valid = Valid::succeed(()); + + $( + // try to parse directive from the Resolver variant + valid = valid.and(<$ty>::from_directives(directives.iter()).map(|resolver| { + if let Some(resolver) = resolver { + // on success store it as a result and remember parsed directives + result = Some(Self::$var(resolver)); + resolvable_directives.push(<$ty>::trace_name()); + } + })); + )+ + + valid.and_then(|_| { + if resolvable_directives.len() > 1 { + Valid::fail(format!( + "Multiple resolvers detected [{}]", + resolvable_directives.join(", ") + )) + } else { + Valid::succeed(result) + } + }) + } + } + }; +} + +create_resolver! { + Http(Http), + Grpc(Grpc), + Graphql(GraphQL), + Call(Call), + Js(JS), + Expr(Expr), +} + +impl Resolver { + pub fn to_directive(&self) -> ConstDirective { + match self { + Resolver::Http(d) => d.to_directive(), + Resolver::Grpc(d) => d.to_directive(), + Resolver::Graphql(d) => d.to_directive(), + Resolver::Call(d) => d.to_directive(), + Resolver::Js(d) => d.to_directive(), + Resolver::Expr(d) => d.to_directive(), + } + } + + pub fn directive_name(&self) -> String { + match self { + Resolver::Http(_) => Http::directive_name(), + Resolver::Grpc(_) => Grpc::directive_name(), + Resolver::Graphql(_) => GraphQL::directive_name(), + Resolver::Call(_) => Call::directive_name(), + Resolver::Js(_) => JS::directive_name(), + Resolver::Expr(_) => Expr::directive_name(), + } + } +} + /// /// A field definition containing all the metadata information about resolving a /// field. @@ -258,38 +338,13 @@ pub struct Field { pub omit: Option, /// - /// Inserts an HTTP resolver for the field. - #[serde(default, skip_serializing_if = "is_default")] - pub http: Option, - - /// - /// Inserts a call resolver for the field. - #[serde(default, skip_serializing_if = "is_default")] - pub call: Option, - - /// - /// Inserts a GRPC resolver for the field. - #[serde(default, skip_serializing_if = "is_default")] - pub grpc: Option, - - /// - /// Inserts a Javascript resolver for the field. - #[serde(default, skip_serializing_if = "is_default")] - pub script: Option, - - /// - /// Inserts a constant resolver for the field. - #[serde(rename = "expr", default, skip_serializing_if = "is_default")] - pub const_field: Option, + /// Sets the cache configuration for a field + pub cache: Option, /// - /// Inserts a GraphQL resolver for the field. + /// Stores the default value for the field #[serde(default, skip_serializing_if = "is_default")] - pub graphql: Option, - - /// - /// Sets the cache configuration for a field - pub cache: Option, + pub default_value: Option, /// /// Marks field as protected by auth provider @@ -297,9 +352,9 @@ pub struct Field { pub protected: Option, /// - /// Stores the default value for the field - #[serde(default, skip_serializing_if = "is_default")] - pub default_value: Option, + /// Resolver for the field + #[serde(flatten, default, skip_serializing_if = "is_default")] + pub resolver: Option, } // It's a terminal implementation of MergeRight @@ -311,46 +366,21 @@ impl MergeRight for Field { impl Field { pub fn has_resolver(&self) -> bool { - self.http.is_some() - || self.script.is_some() - || self.const_field.is_some() - || self.graphql.is_some() - || self.grpc.is_some() - || self.call.is_some() - } - - /// Returns a list of resolvable directives for the field. - pub fn resolvable_directives(&self) -> Vec { - let mut directives = Vec::new(); - if self.http.is_some() { - directives.push(Http::trace_name()); - } - if self.graphql.is_some() { - directives.push(GraphQL::trace_name()); - } - if self.script.is_some() { - directives.push(JS::trace_name()); - } - if self.const_field.is_some() { - directives.push(Expr::trace_name()); - } - if self.grpc.is_some() { - directives.push(Grpc::trace_name()); - } - if self.call.is_some() { - directives.push(Call::trace_name()); - } - directives + self.resolver.is_some() } pub fn has_batched_resolver(&self) -> bool { - self.http - .as_ref() - .is_some_and(|http| !http.batch_key.is_empty()) - || self.graphql.as_ref().is_some_and(|graphql| graphql.batch) - || self - .grpc - .as_ref() - .is_some_and(|grpc| !grpc.batch_key.is_empty()) + if let Some(resolver) = &self.resolver { + match resolver { + Resolver::Http(http) => !http.batch_key.is_empty(), + Resolver::Grpc(grpc) => !grpc.batch_key.is_empty(), + Resolver::Graphql(graphql) => graphql.batch, + Resolver::Call(_) => false, + Resolver::Js(_) => false, + Resolver::Expr(_) => false, + } + } else { + false + } } pub fn into_list(mut self) -> Self { self.list = true; @@ -1077,12 +1107,18 @@ mod tests { let f1 = Field { ..Default::default() }; let f2 = Field { - http: Some(Http { batch_key: vec!["id".to_string()], ..Default::default() }), + resolver: Some(Resolver::Http(Http { + batch_key: vec!["id".to_string()], + ..Default::default() + })), ..Default::default() }; let f3 = Field { - http: Some(Http { batch_key: vec![], ..Default::default() }), + resolver: Some(Resolver::Http(Http { + batch_key: vec![], + ..Default::default() + })), ..Default::default() }; diff --git a/src/core/config/from_document.rs b/src/core/config/from_document.rs index 0443b5d444..da6a94847e 100644 --- a/src/core/config/from_document.rs +++ b/src/core/config/from_document.rs @@ -10,10 +10,10 @@ use async_graphql::Name; use async_graphql_value::ConstValue; use super::telemetry::Telemetry; -use super::{Alias, JS}; +use super::Alias; use crate::core::config::{ - self, Cache, Call, Config, Enum, GraphQL, Grpc, Link, Modify, Omit, Protected, RootSchema, - Server, Union, Upstream, Variant, + self, Cache, Config, Enum, Link, Modify, Omit, Protected, RootSchema, Server, Union, Upstream, + Variant, }; use crate::core::directive::DirectiveCodec; use crate::core::valid::{Valid, ValidationError, Validator}; @@ -74,7 +74,7 @@ fn schema_definition(doc: &ServiceDocument) -> Valid<&SchemaDefinition, String> .map_or_else(|| Valid::succeed(DEFAULT_SCHEMA_DEFINITION), Valid::succeed) } -fn process_schema_directives + Default>( +fn process_schema_directives( schema_definition: &SchemaDefinition, directive_name: &str, ) -> Valid { @@ -87,7 +87,7 @@ fn process_schema_directives + Default>( res } -fn process_schema_multiple_directives + Default>( +fn process_schema_multiple_directives( schema_definition: &SchemaDefinition, directive_name: &str, ) -> Valid, String> { @@ -152,12 +152,14 @@ fn to_types( &type_definition.node.description, &type_definition.node.directives, ) + .trace(&type_name) .some(), TypeKind::Interface(interface_type) => to_object_type( &interface_type, &type_definition.node.description, &type_definition.node.directives, ) + .trace(&type_name) .some(), TypeKind::Enum(_) => Valid::none(), TypeKind::InputObject(input_object_type) => to_input_object( @@ -165,6 +167,7 @@ fn to_types( &type_definition.node.description, &type_definition.node.directives, ) + .trace(&type_name) .some(), TypeKind::Union(_) => Valid::none(), TypeKind::Scalar => Valid::succeed(Some(to_scalar_type())), @@ -227,7 +230,6 @@ fn to_enum_types( .map(|values| values.into_iter().flatten().collect()) } -#[allow(clippy::too_many_arguments)] fn to_object_type( object: &T, description: &Option>, @@ -305,7 +307,7 @@ fn to_common_field( default_value: Option, ) -> Valid where - F: FieldLike, + F: FieldLike + HasName, { let type_of = field.type_of(); let base = &type_of.base; @@ -322,40 +324,30 @@ where 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::Http::from_directives(directives.iter()) - .fuse(GraphQL::from_directives(directives.iter())) + + config::Resolver::from_directives(directives) .fuse(Cache::from_directives(directives.iter())) - .fuse(Grpc::from_directives(directives.iter())) .fuse(Omit::from_directives(directives.iter())) .fuse(Modify::from_directives(directives.iter())) - .fuse(JS::from_directives(directives.iter())) - .fuse(Call::from_directives(directives.iter())) .fuse(Protected::from_directives(directives.iter())) .fuse(default_value) .map( - |(http, graphql, cache, grpc, omit, modify, script, call, protected, default_value)| { - let const_field = to_const_field(directives); - config::Field { - type_of, - list, - required: !nullable, - list_type_required, - args, - doc, - modify, - omit, - http, - grpc, - script, - const_field, - graphql, - cache, - call, - protected, - default_value, - } + |(resolver, cache, omit, modify, protected, default_value)| config::Field { + type_of, + list, + required: !nullable, + list_type_required, + args, + doc, + modify, + omit, + cache, + protected, + default_value, + resolver, }, ) + .trace(pos_name_to_string(field.name()).as_str()) } fn to_type_of(type_: &Type) -> String { @@ -421,17 +413,6 @@ fn to_enum(enum_type: EnumType, doc: Option) -> Valid { }); variants.map(|v| Enum { variants: v.into_iter().collect::>(), doc }) } -fn to_const_field(directives: &[Positioned]) -> Option { - directives.iter().find_map(|directive| { - if directive.node.name.node == config::Expr::directive_name() { - config::Expr::from_directive(&directive.node) - .to_result() - .ok() - } else { - None - } - }) -} fn to_add_fields_from_directives( directives: &[Positioned], diff --git a/src/core/config/into_document.rs b/src/core/config/into_document.rs index e3e9b1fcf4..a33fe457f3 100644 --- a/src/core/config/into_document.rs +++ b/src/core/config/into_document.rs @@ -265,15 +265,10 @@ fn config_document(config: &Config) -> ServiceDocument { fn get_directives(field: &crate::core::config::Field) -> Vec> { let directives = vec![ - field.http.as_ref().map(|d| pos(d.to_directive())), - field.script.as_ref().map(|d| pos(d.to_directive())), - field.const_field.as_ref().map(|d| pos(d.to_directive())), + field.resolver.as_ref().map(|d| pos(d.to_directive())), field.modify.as_ref().map(|d| pos(d.to_directive())), field.omit.as_ref().map(|d| pos(d.to_directive())), - field.graphql.as_ref().map(|d| pos(d.to_directive())), - field.grpc.as_ref().map(|d| pos(d.to_directive())), field.cache.as_ref().map(|d| pos(d.to_directive())), - field.call.as_ref().map(|d| pos(d.to_directive())), field.protected.as_ref().map(|d| pos(d.to_directive())), ]; diff --git a/src/core/config/transformer/consolidate_url/consolidate_url.rs b/src/core/config/transformer/consolidate_url/consolidate_url.rs index b619c0a4e0..71d90ebb9f 100644 --- a/src/core/config/transformer/consolidate_url/consolidate_url.rs +++ b/src/core/config/transformer/consolidate_url/consolidate_url.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; use super::max_value_map::MaxValueMap; -use crate::core::config::Config; +use crate::core::config::{Config, Resolver}; use crate::core::transform::Transform; use crate::core::valid::Valid; @@ -23,9 +23,9 @@ impl UrlTypeMapping { /// Populates the URL type mapping based on the given configuration. fn populate_url_frequency_map(&mut self, config: &Config) { for (type_name, type_) in config.types.iter() { - for field_ in type_.fields.values() { - if let Some(http_directive) = &field_.http { - if let Some(base_url) = &http_directive.base_url { + for field in type_.fields.values() { + if let Some(Resolver::Http(http)) = &field.resolver { + if let Some(base_url) = &http.base_url { self.url_to_frequency_map.increment(base_url.to_owned(), 1); self.visited_type_set.insert(type_name.to_owned()); } @@ -63,11 +63,11 @@ impl ConsolidateURL { for type_name in url_type_mapping.visited_type_set { if let Some(type_) = config.types.get_mut(&type_name) { - for field_ in type_.fields.values_mut() { - if let Some(htto_directive) = &mut field_.http { - if let Some(base_url) = &htto_directive.base_url { - if *base_url == common_url { - htto_directive.base_url = None; + for field in type_.fields.values_mut() { + if let Some(Resolver::Http(http)) = &mut field.resolver { + if let Some(base_url) = &http.base_url { + if base_url == &common_url { + http.base_url = None; } } } diff --git a/src/core/directive.rs b/src/core/directive.rs index bd5a5d3e3d..e10dd8833e 100644 --- a/src/core/directive.rs +++ b/src/core/directive.rs @@ -1,5 +1,3 @@ -use std::slice::Iter; - use async_graphql::parser::types::ConstDirective; use async_graphql::{Name, Pos, Positioned}; use serde::{Deserialize, Serialize}; @@ -25,20 +23,16 @@ fn to_const_directive(directive: &blueprint::Directive) -> Valid { +pub trait DirectiveCodec: Sized { fn directive_name() -> String; - fn from_directive(directive: &ConstDirective) -> Valid; - #[allow(dead_code)] - fn from_blueprint_directive(directive: &blueprint::Directive) -> Valid { - to_const_directive(directive).and_then(|a| Self::from_directive(&a)) - } + fn from_directive(directive: &ConstDirective) -> Valid; fn to_directive(&self) -> ConstDirective; fn trace_name() -> String { format!("@{}", Self::directive_name()) } - fn from_directives( - directives: Iter<'_, Positioned>, - ) -> Valid, String> { + fn from_directives<'a>( + directives: impl Iterator>, + ) -> Valid, String> { for directive in directives { if directive.node.name.node == Self::directive_name() { return Self::from_directive(&directive.node).map(Some); @@ -47,7 +41,7 @@ pub trait DirectiveCodec { Valid::succeed(None) } } -fn lower_case_first_letter(s: String) -> String { +fn lower_case_first_letter(s: &str) -> String { if s.len() <= 2 { s.to_lowercase() } else if let Some(first_char) = s.chars().next() { @@ -57,14 +51,13 @@ fn lower_case_first_letter(s: String) -> String { } } -impl<'a, A: Deserialize<'a> + Serialize + 'a> DirectiveCodec for A { +impl<'a, A: Deserialize<'a> + Serialize + 'a> DirectiveCodec for A { fn directive_name() -> String { lower_case_first_letter( std::any::type_name::() .split("::") .last() - .unwrap_or_default() - .to_string(), + .unwrap_or_default(), ) } diff --git a/src/core/generator/from_proto.rs b/src/core/generator/from_proto.rs index 292fd5b0e4..dabf42b7ae 100644 --- a/src/core/generator/from_proto.rs +++ b/src/core/generator/from_proto.rs @@ -13,7 +13,7 @@ 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, Type, Union, Variant}; +use crate::core::config::{Arg, Config, Enum, Field, Grpc, Resolver, Type, Union, Variant}; use crate::core::transform::{Transform, TransformerOps}; use crate::core::valid::Validator; @@ -364,13 +364,13 @@ impl Context { cfg_field.type_of = output_ty; cfg_field.required = true; - cfg_field.grpc = Some(Grpc { + cfg_field.resolver = Some(Resolver::Grpc(Grpc { base_url: None, body, batch_key: vec![], headers: vec![], method: field_name.id(), - }); + })); let method_path = PathBuilder::new(&path).extend(PathField::Method, method_index as i32); diff --git a/src/core/generator/json/field_base_url_generator.rs b/src/core/generator/json/field_base_url_generator.rs index 4357a9bde5..e30f19f449 100644 --- a/src/core/generator/json/field_base_url_generator.rs +++ b/src/core/generator/json/field_base_url_generator.rs @@ -2,7 +2,7 @@ use convert_case::{Case, Casing}; use url::Url; use super::url_utils::extract_base_url; -use crate::core::config::{Config, GraphQLOperationType}; +use crate::core::config::{Config, GraphQLOperationType, Resolver}; use crate::core::transform::Transform; use crate::core::valid::Valid; @@ -19,14 +19,10 @@ impl<'a> FieldBaseUrlGenerator<'a> { fn update_base_urls(&self, config: &mut Config, operation_name: &str, base_url: &str) { if let Some(query_type) = config.types.get_mut(operation_name) { for field in query_type.fields.values_mut() { - field.http = match field.http.clone() { - Some(mut http) => { - if http.base_url.is_none() { - http.base_url = Some(base_url.to_owned()); - } - Some(http) + if let Some(Resolver::Http(http)) = &mut field.resolver { + if http.base_url.is_none() { + http.base_url = Some(base_url.to_owned()) } - None => None, } } } @@ -55,7 +51,7 @@ mod test { use url::Url; use super::FieldBaseUrlGenerator; - use crate::core::config::{Config, Field, GraphQLOperationType, Http, Type}; + use crate::core::config::{Config, Field, GraphQLOperationType, Http, Resolver, Type}; use crate::core::transform::Transform; use crate::core::valid::Validator; @@ -70,7 +66,10 @@ mod test { "f1".to_string(), Field { type_of: "Int".to_string(), - http: Some(Http { path: "/day".to_string(), ..Default::default() }), + resolver: Some(Resolver::Http(Http { + path: "/day".to_string(), + ..Default::default() + })), ..Default::default() }, ); @@ -78,7 +77,10 @@ mod test { "f2".to_string(), Field { type_of: "String".to_string(), - http: Some(Http { path: "/month".to_string(), ..Default::default() }), + resolver: Some(Resolver::Http(Http { + path: "/month".to_string(), + ..Default::default() + })), ..Default::default() }, ); @@ -86,7 +88,10 @@ mod test { "f3".to_string(), Field { type_of: "String".to_string(), - http: Some(Http { path: "/status".to_string(), ..Default::default() }), + resolver: Some(Resolver::Http(Http { + path: "/status".to_string(), + ..Default::default() + })), ..Default::default() }, ); @@ -108,11 +113,11 @@ mod test { "f1".to_string(), Field { type_of: "Int".to_string(), - http: Some(Http { + resolver: Some(Resolver::Http(Http { base_url: Some("https://calender.com/api/v1/".to_string()), path: "/day".to_string(), ..Default::default() - }), + })), ..Default::default() }, ); @@ -120,7 +125,10 @@ mod test { "f2".to_string(), Field { type_of: "String".to_string(), - http: Some(Http { path: "/month".to_string(), ..Default::default() }), + resolver: Some(Resolver::Http(Http { + path: "/month".to_string(), + ..Default::default() + })), ..Default::default() }, ); @@ -128,7 +136,7 @@ mod test { "f3".to_string(), Field { type_of: "String".to_string(), - http: None, + resolver: None, ..Default::default() }, ); diff --git a/src/core/generator/json/operation_generator.rs b/src/core/generator/json/operation_generator.rs index b060154d7a..dad9302686 100644 --- a/src/core/generator/json/operation_generator.rs +++ b/src/core/generator/json/operation_generator.rs @@ -1,7 +1,7 @@ use convert_case::{Case, Casing}; use super::http_directive_generator::HttpDirectiveGenerator; -use crate::core::config::{Arg, Config, Field, GraphQLOperationType, Type}; +use crate::core::config::{Arg, Config, Field, GraphQLOperationType, Resolver, Type}; use crate::core::generator::json::types_generator::TypeGenerator; use crate::core::generator::{NameGenerator, RequestSample}; use crate::core::valid::Valid; @@ -25,7 +25,9 @@ impl OperationTypeGenerator { // generate required http directive. let http_directive_gen = HttpDirectiveGenerator::new(&request_sample.url); - field.http = Some(http_directive_gen.generate_http_directive(&mut field)); + field.resolver = Some(Resolver::Http( + http_directive_gen.generate_http_directive(&mut field), + )); if let GraphQLOperationType::Mutation = request_sample.operation_type { // generate the input type. @@ -33,9 +35,9 @@ impl OperationTypeGenerator { .generate_types(&request_sample.req_body, &mut config); // add input type to field. let arg_name = format!("{}Input", request_sample.field_name).to_case(Case::Camel); - if let Some(http_) = &mut field.http { - http_.body = Some(format!("{{{{.args.{}}}}}", arg_name.clone())); - http_.method = request_sample.method.to_owned(); + if let Some(Resolver::Http(http)) = &mut field.resolver { + http.body = Some(format!("{{{{.args.{}}}}}", arg_name)); + http.method = request_sample.method.to_owned(); } field .args diff --git a/src/core/grpc/data_loader_request.rs b/src/core/grpc/data_loader_request.rs index 39547f3e1a..7179a19005 100644 --- a/src/core/grpc/data_loader_request.rs +++ b/src/core/grpc/data_loader_request.rs @@ -63,7 +63,7 @@ mod tests { use super::DataLoaderRequest; use crate::core::blueprint::GrpcMethod; use crate::core::config::reader::ConfigReader; - use crate::core::config::{Config, Field, Grpc, Link, LinkType, Type}; + use crate::core::config::{Config, Field, Grpc, Link, LinkType, Resolver, Type}; use crate::core::grpc::protobuf::{ProtobufOperation, ProtobufSet}; use crate::core::grpc::request_template::RenderedRequestTemplate; @@ -82,7 +82,10 @@ mod tests { let grpc = Grpc { method: method.to_string(), ..Default::default() }; config.types.insert( "foo".to_string(), - Type::default().fields(vec![("bar", Field::default().grpc(grpc))]), + Type::default().fields(vec![( + "bar", + Field::default().resolver(Resolver::Grpc(grpc)), + )]), ); let runtime = crate::core::runtime::test::init(None); diff --git a/src/core/grpc/protobuf.rs b/src/core/grpc/protobuf.rs index 4e3ffa7d97..adcea67407 100644 --- a/src/core/grpc/protobuf.rs +++ b/src/core/grpc/protobuf.rs @@ -251,7 +251,7 @@ pub mod tests { use super::*; use crate::core::blueprint::GrpcMethod; use crate::core::config::reader::ConfigReader; - use crate::core::config::{Config, Field, Grpc, Link, LinkType, Type}; + use crate::core::config::{Config, Field, Grpc, Link, LinkType, Resolver, Type}; pub async fn get_proto_file(path: &str) -> Result { let runtime = crate::core::runtime::test::init(None); @@ -272,7 +272,10 @@ pub mod tests { let grpc = Grpc { method: method.to_string(), ..Default::default() }; config.types.insert( "foo".to_string(), - Type::default().fields(vec![("bar", Field::default().grpc(grpc))]), + Type::default().fields(vec![( + "bar", + Field::default().resolver(Resolver::Grpc(grpc)), + )]), ); Ok(reader .resolve(config, None) diff --git a/src/core/grpc/request_template.rs b/src/core/grpc/request_template.rs index aff43e137b..53547414ba 100644 --- a/src/core/grpc/request_template.rs +++ b/src/core/grpc/request_template.rs @@ -142,7 +142,9 @@ mod tests { use super::{RequestBody, RequestTemplate}; use crate::core::blueprint::GrpcMethod; use crate::core::config::reader::ConfigReader; - use crate::core::config::{Config, Field, GraphQLOperationType, Grpc, Link, LinkType, Type}; + use crate::core::config::{ + Config, Field, GraphQLOperationType, Grpc, Link, LinkType, Resolver, Type, + }; use crate::core::grpc::protobuf::{ProtobufOperation, ProtobufSet}; use crate::core::ir::model::CacheKey; use crate::core::mustache::Mustache; @@ -167,7 +169,10 @@ mod tests { let grpc = Grpc { method: method.to_string(), ..Default::default() }; config.types.insert( "foo".to_string(), - Type::default().fields(vec![("bar", Field::default().grpc(grpc))]), + Type::default().fields(vec![( + "bar", + Field::default().resolver(Resolver::Grpc(grpc)), + )]), ); let protobuf_set = ProtobufSet::from_proto_file( diff --git a/tailcall-typedefs/src/main.rs b/tailcall-typedefs/src/main.rs index 9643e887f7..4023a02089 100644 --- a/tailcall-typedefs/src/main.rs +++ b/tailcall-typedefs/src/main.rs @@ -20,7 +20,7 @@ static GRAPHQL_SCHEMA_FILE: &str = "../generated/.tailcallrc.graphql"; #[tokio::main] async fn main() { - tracing::subscriber::set_global_default(default_tracing_for_name("typedefs")).unwrap(); + tracing::subscriber::set_global_default(default_tracing_for_name("tailcall_typedefs")).unwrap(); let args: Vec = env::args().collect(); let arg = args.get(1); diff --git a/tests/core/snapshots/test-graphql-with-add-field.md_error.snap b/tests/core/snapshots/test-graphql-with-add-field.md_error.snap new file mode 100644 index 0000000000..af649e47b8 --- /dev/null +++ b/tests/core/snapshots/test-graphql-with-add-field.md_error.snap @@ -0,0 +1,14 @@ +--- +source: tests/core/spec.rs +expression: errors +--- +[ + { + "message": "Cannot add field", + "trace": [ + "Query", + "@addField" + ], + "description": "Path: [post, user, name] contains resolver graphQL at [Post.user]" + } +] diff --git a/tests/core/snapshots/test-invalid-query-in-http.md_error.snap b/tests/core/snapshots/test-invalid-query-in-http.md_error.snap index 0f574af714..7d4320c23c 100644 --- a/tests/core/snapshots/test-invalid-query-in-http.md_error.snap +++ b/tests/core/snapshots/test-invalid-query-in-http.md_error.snap @@ -6,6 +6,8 @@ expression: errors { "message": "Parsing failed because of invalid type: map, expected a sequence", "trace": [ + "Query", + "user", "@http", "query" ], diff --git a/tests/execution/test-all-blueprint-errors.md b/tests/execution/test-all-blueprint-errors.md index 40bcbe1a2d..433c1ebadd 100644 --- a/tests/execution/test-all-blueprint-errors.md +++ b/tests/execution/test-all-blueprint-errors.md @@ -14,7 +14,7 @@ type Mutation { } type Query { foo(inp: B): Foo - bar: String @expr @expr(body: {name: "John"}) + bar: String @expr(body: {name: "John"}) } type Foo { a: String @expr(body: "1") diff --git a/tests/execution/test-graphql-with-add-field.md b/tests/execution/test-graphql-with-add-field.md new file mode 100644 index 0000000000..9c2ec25490 --- /dev/null +++ b/tests/execution/test-graphql-with-add-field.md @@ -0,0 +1,28 @@ +--- +error: true +--- + +# test-graphql-with-add-field + +```graphql @config +schema @server @upstream(baseURL: "http://jsonplaceholder.typicode.com") { + query: Query +} + +type Query @addField(name: "name", path: ["post", "user", "name"]) { + post: Post @graphQL(name: "posts") +} + +type Post { + id: Int + title: String + body: String + userId: Int! + user: User @graphQL(name: "user") +} + +type User { + id: Int + name: String +} +```