diff --git a/generated/.tailcallrc.schema.json b/generated/.tailcallrc.schema.json index 70ca3e9258..3939019104 100644 --- a/generated/.tailcallrc.schema.json +++ b/generated/.tailcallrc.schema.json @@ -2,9 +2,6 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "RuntimeConfig", "type": "object", - "required": [ - "links" - ], "properties": { "links": { "description": "A list of all links in the schema.", @@ -15,7 +12,6 @@ }, "server": { "description": "Dictates how the server behaves and helps tune tailcall for all ingress requests. Features such as request batching, SSL, HTTP2 etc. can be configured here.", - "default": {}, "allOf": [ { "$ref": "#/definitions/Server" @@ -32,7 +28,6 @@ }, "upstream": { "description": "Dictates how tailcall should handle upstream requests/responses. Tuning upstream can improve performance and reliability for connections.", - "default": {}, "allOf": [ { "$ref": "#/definitions/Upstream" diff --git a/src/core/config/config.rs b/src/core/config/config.rs index 759ee0a9aa..3cf91aa6f6 100644 --- a/src/core/config/config.rs +++ b/src/core/config/config.rs @@ -43,17 +43,18 @@ pub struct RuntimeConfig { /// Dictates how the server behaves and helps tune tailcall for all ingress /// requests. Features such as request batching, SSL, HTTP2 etc. can be /// configured here. - #[serde(default)] + #[serde(default, skip_serializing_if = "is_default")] pub server: Server, /// /// Dictates how tailcall should handle upstream requests/responses. /// Tuning upstream can improve performance and reliability for connections. - #[serde(default)] + #[serde(default, skip_serializing_if = "is_default")] pub upstream: Upstream, /// /// A list of all links in the schema. + #[serde(default, skip_serializing_if = "is_default")] pub links: Vec, /// Enable [opentelemetry](https://opentelemetry.io) support diff --git a/src/core/config/transformer/subgraph.rs b/src/core/config/transformer/subgraph.rs index 0d336d544f..c43e727bef 100644 --- a/src/core/config/transformer/subgraph.rs +++ b/src/core/config/transformer/subgraph.rs @@ -59,7 +59,12 @@ impl Transform for Subgraph { let key = Key { fields }; to_directive(key.to_directive()).map(|directive| { - ty.directives.push(directive); + // Prevent transformer to push the same directive multiple times + if !ty.directives.iter().any(|d| { + d.name == directive.name && d.arguments == directive.arguments + }) { + ty.directives.push(directive); + } }) } None => Valid::succeed(()), diff --git a/tests/core/parse.rs b/tests/core/parse.rs index 6246c40008..f7af845d81 100644 --- a/tests/core/parse.rs +++ b/tests/core/parse.rs @@ -15,7 +15,8 @@ use tailcall::cli::javascript; use tailcall::core::app_context::AppContext; use tailcall::core::blueprint::Blueprint; use tailcall::core::cache::InMemoryCache; -use tailcall::core::config::{ConfigModule, Source}; +use tailcall::core::config::{ConfigModule, Link, RuntimeConfig, Source}; +use tailcall::core::merge_right::MergeRight; use tailcall::core::runtime::TargetRuntime; use tailcall::core::worker::{Command, Event}; use tailcall::core::{EnvIO, WorkerIO}; @@ -51,7 +52,7 @@ impl ExecutionSpec { .peekable(); let mut name: Option = None; - let mut server: Vec<(Source, String)> = Vec::with_capacity(2); + let mut config = RuntimeConfig::default(); let mut mock: Option> = None; let mut env: Option> = None; let mut files: BTreeMap = BTreeMap::new(); @@ -59,6 +60,7 @@ impl ExecutionSpec { let mut runner: Option = None; let mut check_identity = false; let mut sdl_error = false; + let mut links_counter = 0; while let Some(node) = children.next() { match node { @@ -172,8 +174,16 @@ impl ExecutionSpec { match name { "config" => { - // Server configs are only parsed if the test isn't skipped. - server.push((source, content)); + config = config.merge_right( + RuntimeConfig::from_source(source, &content).unwrap(), + ); + } + "schema" => { + // Schemas configs are only parsed if the test isn't skipped. + let name = format!("schema_{}.graphql", links_counter); + files.insert(name.clone(), content); + config.links.push(Link { src: name, ..Default::default() }); + links_counter += 1; } "mock" => { if mock.is_none() { @@ -240,9 +250,9 @@ impl ExecutionSpec { } } - if server.is_empty() { + if links_counter == 0 { return Err(anyhow!( - "Unexpected blocks in {:?}: You must define a GraphQL Config in an execution test.", + "Unexpected blocks in {:?}: You must define a GraphQL Schema in an execution test.", path, )); } @@ -252,7 +262,7 @@ impl ExecutionSpec { name: name.unwrap_or_else(|| path.file_name().unwrap().to_str().unwrap().to_string()), safe_name: path.file_name().unwrap().to_str().unwrap().to_string(), - server, + config, mock, env, test, diff --git a/tests/core/runtime.rs b/tests/core/runtime.rs index 0961593cc3..22ad9748af 100644 --- a/tests/core/runtime.rs +++ b/tests/core/runtime.rs @@ -10,7 +10,7 @@ use derive_setters::Setters; use tailcall::cli::javascript::init_worker_io; use tailcall::core::blueprint::Script; use tailcall::core::cache::InMemoryCache; -use tailcall::core::config::Source; +use tailcall::core::config::RuntimeConfig; use tailcall::core::runtime::TargetRuntime; use tailcall::core::worker::{Command, Event}; @@ -25,7 +25,7 @@ pub struct ExecutionSpec { pub name: String, pub safe_name: String, - pub server: Vec<(Source, String)>, + pub config: RuntimeConfig, pub mock: Option>, pub env: Option>, pub test: Option>, diff --git a/tests/core/spec.rs b/tests/core/spec.rs index 56a569da2b..1892aaf959 100644 --- a/tests/core/spec.rs +++ b/tests/core/spec.rs @@ -16,11 +16,10 @@ use tailcall::core::async_graphql_hyper::{GraphQLBatchRequest, GraphQLRequest}; use tailcall::core::blueprint::{Blueprint, BlueprintError}; use tailcall::core::config::reader::ConfigReader; use tailcall::core::config::transformer::Required; -use tailcall::core::config::{Config, ConfigModule, ConfigReaderContext, Source}; +use tailcall::core::config::{Config, ConfigModule, ConfigReaderContext, LinkType, Source}; use tailcall::core::http::handle_request; use tailcall::core::mustache::PathStringEval; use tailcall::core::print_schema::print_schema; -use tailcall::core::variance::Invariant; use tailcall::core::Mustache; use tailcall_prettier::Parser; use tailcall_valid::{Cause, Valid, ValidationError, Validator}; @@ -97,54 +96,53 @@ async fn check_identity(spec: &ExecutionSpec, reader_ctx: &ConfigReaderContext<' // enabled for either new tests that request it or old graphql_spec // tests that were explicitly written with it in mind if spec.check_identity { - for (source, content) in spec.server.iter() { - if matches!(source, Source::GraphQL) { - let mustache = Mustache::parse(content); - let content = PathStringEval::new().eval_partial(&mustache, reader_ctx); - let config = Config::from_source(source.to_owned(), &content).unwrap(); - let actual = config.to_sdl(); - - // \r is added automatically in windows, it's safe to replace it with \n - let content = content.replace("\r\n", "\n"); - - let path_str = spec.path.display().to_string(); - let context = format!("path: {}", path_str); - - let actual = tailcall_prettier::format(actual, &tailcall_prettier::Parser::Gql) - .await - .map_err(|e| e.with_context(context.clone())) - .unwrap(); - - let expected = tailcall_prettier::format(content, &tailcall_prettier::Parser::Gql) - .await - .map_err(|e| e.with_context(context.clone())) - .unwrap(); - - pretty_assertions::assert_eq!( - actual, - expected, - "Identity check failed for {:#?}", - spec.path, - ); - } else { - panic!( - "Spec {:#?} has \"check identity\" enabled, but its config isn't in GraphQL.", - spec.path - ); - } + for link in spec + .config + .links + .iter() + .filter(|link| link.type_of == LinkType::Config) + { + let content = reader_ctx.runtime.file.read(&link.src).await.unwrap(); + let mustache = Mustache::parse(&content); + let content = PathStringEval::new().eval_partial(&mustache, reader_ctx); + let config = Config::from_source(Source::GraphQL, &content).unwrap(); + let actual = config.to_sdl(); + + // \r is added automatically in windows, it's safe to replace it with \n + let content = content.replace("\r\n", "\n"); + + let path_str = spec.path.display().to_string(); + let context = format!("path: {}", path_str); + + let actual = tailcall_prettier::format(actual, &tailcall_prettier::Parser::Gql) + .await + .map_err(|e| e.with_context(context.clone())) + .unwrap(); + + let expected = tailcall_prettier::format(content, &tailcall_prettier::Parser::Gql) + .await + .map_err(|e| e.with_context(context.clone())) + .unwrap(); + + pretty_assertions::assert_eq!( + actual, + expected, + "Identity check failed for {:#?}", + spec.path, + ); } } } async fn run_query_tests_on_spec( spec: ExecutionSpec, - server: Vec, + config_module: &ConfigModule, mock_http_client: Arc, ) { if let Some(tests) = spec.test.as_ref() { let app_ctx = spec .app_context( - server.first().unwrap(), + config_module, spec.env.clone().unwrap_or_default(), mock_http_client.clone(), ) @@ -200,42 +198,27 @@ async fn test_spec(spec: ExecutionSpec) { let reader = ConfigReader::init(runtime); - // Resolve all configs - let config_modules = join_all(spec.server.iter().map(|(source, content)| async { - let mustache = Mustache::parse(content); - let content = PathStringEval::new().eval_partial(&mustache, &reader_ctx); + let config = Config::from(spec.config.clone()); - let config = Config::from_source(source.to_owned(), &content)?; + let config_module = reader.resolve(config, spec.path.parent()).await; - reader.resolve(config, spec.path.parent()).await - })) - .await; - - let config_module = Valid::from_iter(config_modules.iter(), |config_module| { - Valid::from(config_module.as_ref().map_err(|e| { - match e.downcast_ref::>() { + let config_module = + Valid::from( + config_module.map_err(|e| match e.downcast_ref::>() { Some(err) => err.clone(), None => ValidationError::new(e.to_string()), - } - })) - }) - .and_then(|cfgs| { - let mut cfgs = cfgs.into_iter(); - let config_module = cfgs.next().expect("At least one config should be defined"); - - cfgs.fold(Valid::succeed(config_module.clone()), |acc, c| { - acc.and_then(|acc| acc.unify(c.clone())) - }) - }) - // Apply required transformers to the configuration - .and_then(|cfg| cfg.transform(Required)); + }), + ) + // Apply required transformers to the configuration + .and_then(|cfg| cfg.transform(Required)); // check sdl error if any if is_sdl_error(&spec, config_module.clone()).await { return; } - let merged = config_module.to_result().unwrap().to_sdl(); + let config_module = config_module.to_result().unwrap(); + let merged = config_module.to_sdl(); let formatter = tailcall_prettier::format(merged, &Parser::Gql) .await @@ -245,34 +228,25 @@ async fn test_spec(spec: ExecutionSpec) { insta::assert_snapshot!(snapshot_name, formatter); - let config_modules = config_modules - .into_iter() - .collect::, _>>() - .unwrap(); - check_identity(&spec, &reader_ctx).await; // client: Check if client spec matches snapshot - if config_modules.len() == 1 { - let config = &config_modules[0]; - - let client = print_schema( - (Blueprint::try_from(config) - .context(format!("file: {}", spec.path.to_str().unwrap())) - .unwrap()) - .to_schema(), - ); - - let formatted = tailcall_prettier::format(client, &Parser::Gql) - .await - .unwrap(); - let snapshot_name = format!("{}_client", spec.safe_name); - - insta::assert_snapshot!(snapshot_name, formatted); - } + let client = print_schema( + (Blueprint::try_from(&config_module) + .context(format!("file: {}", spec.path.to_str().unwrap())) + .unwrap()) + .to_schema(), + ); + + let formatted = tailcall_prettier::format(client, &Parser::Gql) + .await + .unwrap(); + let snapshot_name = format!("{}_client", spec.safe_name); + + insta::assert_snapshot!(snapshot_name, formatted); // run query tests - run_query_tests_on_spec(spec, config_modules, mock_http_client).await; + run_query_tests_on_spec(spec, &config_module, mock_http_client).await; } pub async fn load_and_test_execution_spec(path: &Path) -> anyhow::Result<()> {