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<()> {