diff --git a/benches/handle_request_bench.rs b/benches/handle_request_bench.rs index f33c52a895..efff2dee86 100644 --- a/benches/handle_request_bench.rs +++ b/benches/handle_request_bench.rs @@ -22,7 +22,7 @@ pub fn benchmark_handle_request(c: &mut Criterion) { let endpoints = config_module.extensions().endpoint_set.clone(); let endpoints_clone = endpoints.clone(); - blueprint.server.enable_jit = false; + blueprint.config.server.enable_jit = false; let server_config = tokio_runtime .block_on(ServerConfig::new(blueprint.clone(), endpoints.clone())) .unwrap(); @@ -47,7 +47,7 @@ pub fn benchmark_handle_request(c: &mut Criterion) { }) }); - blueprint_clone.server.enable_jit = true; + blueprint_clone.config.server.enable_jit = true; let server_config = tokio_runtime .block_on(ServerConfig::new(blueprint_clone, endpoints_clone)) .unwrap(); diff --git a/benches/http_execute_bench.rs b/benches/http_execute_bench.rs index c9a96edee6..71fbcd6da0 100644 --- a/benches/http_execute_bench.rs +++ b/benches/http_execute_bench.rs @@ -8,8 +8,8 @@ pub fn benchmark_http_execute_method(c: &mut Criterion) { let tokio_runtime = tokio::runtime::Runtime::new().unwrap(); let mut blueprint = Blueprint::default(); - blueprint.upstream.http_cache = 42; // allow http caching for bench test. - let native_http = NativeHttp::init(&blueprint.upstream, &blueprint.telemetry); + blueprint.config.upstream.http_cache = 42; // allow http caching for bench test. + let native_http = NativeHttp::init(&blueprint.config.upstream, &blueprint.config.telemetry); let request_url = String::from("http://jsonplaceholder.typicode.com/users"); tokio_runtime.block_on(async { diff --git a/benches/impl_path_string_for_evaluation_context.rs b/benches/impl_path_string_for_evaluation_context.rs index e4eb603f67..711d1fd5eb 100644 --- a/benches/impl_path_string_for_evaluation_context.rs +++ b/benches/impl_path_string_for_evaluation_context.rs @@ -13,8 +13,8 @@ use indexmap::IndexMap; use once_cell::sync::Lazy; use reqwest::{Client, Request}; use reqwest_middleware::{ClientBuilder, ClientWithMiddleware}; -use tailcall::core::blueprint::{Server, Upstream}; use tailcall::core::cache::InMemoryCache; +use tailcall::core::config::{ServerRuntime, UpstreamRuntime}; use tailcall::core::http::{RequestContext, Response}; use tailcall::core::ir::{EvalContext, ResolverContextLike, SelectionField}; use tailcall::core::path::PathString; @@ -28,7 +28,7 @@ struct Http { } impl Http { - fn init(upstream: &Upstream) -> Self { + fn init(upstream: &UpstreamRuntime) -> Self { let mut builder = Client::builder() .tcp_keepalive(Some(Duration::from_secs(upstream.tcp_keep_alive))) .timeout(Duration::from_secs(upstream.timeout)) @@ -238,8 +238,8 @@ fn request_context() -> RequestContext { let config_module = tailcall::core::config::ConfigModule::default(); //TODO: default is used only in tests. Drop default and move it to test. - let upstream = Upstream::try_from(&config_module).unwrap(); - let server = Server::try_from(config_module).unwrap(); + let upstream = UpstreamRuntime::try_from(&config_module).unwrap(); + let server = ServerRuntime::try_from(config_module).unwrap(); let http = Arc::new(Http::init(&upstream)); let http2 = Arc::new(Http::init(&upstream.clone().http2_only(true))); let runtime = TargetRuntime { diff --git a/generated/.tailcallrc.graphql b/generated/.tailcallrc.graphql index 9dc5ed9dc1..b2e6a2be3d 100644 --- a/generated/.tailcallrc.graphql +++ b/generated/.tailcallrc.graphql @@ -373,7 +373,7 @@ directive, developers gain access to valuable insights into the performance and of their applications. """ directive @telemetry( - export: TelemetryExporter + export: TelemetryExporterConfig """ The list of headers that will be sent as additional attributes to telemetry exporters Be careful about **leaking sensitive information** from requests when enabling the @@ -628,43 +628,43 @@ input Schema { """ Type to configure Cross-Origin Resource Sharing (CORS) for a server. """ -input Cors { +input CorsStatic { """ Indicates whether the server allows credentials (e.g., cookies, authorization headers) to be sent in cross-origin requests. """ - allowCredentials: Boolean + allow_credentials: Boolean """ A list of allowed headers in cross-origin requests. This can be used to specify custom headers that are allowed to be included in cross-origin requests. """ - allowHeaders: [String!] + allow_headers: [String!] """ A list of allowed HTTP methods in cross-origin requests. These methods specify the actions that are permitted in cross-origin requests. """ - allowMethods: [Method] + allow_methods: [Method] """ A list of origins that are allowed to access the server's resources in cross-origin requests. An origin can be a domain, a subdomain, or even 'null' for local file schemes. """ - allowOrigins: [String!] + allow_origins: [String!] """ Indicates whether requests from private network addresses are allowed in cross-origin requests. Private network addresses typically include IP addresses reserved for internal networks. """ - allowPrivateNetwork: Boolean + allow_private_network: Boolean """ A list of headers that the server exposes to the browser in cross-origin responses. Exposing certain headers allows the client-side code to access them in the response. """ - exposeHeaders: [String!] + expose_headers: [String!] """ The maximum time (in seconds) that the client should cache preflight OPTIONS requests in order to avoid sending excessive requests to the server. """ - maxAge: Int + max_age: Int """ A list of header names that indicate the values of which might cause the server's response to vary, potentially affecting caching. @@ -674,16 +674,16 @@ input Cors { input Headers { """ - `cacheControl` sends `Cache-Control` headers in responses when activated. The `max-age` + `cache_control` sends `Cache-Control` headers in responses when activated. The `max-age` value is the least of the values received from upstream services. @default `false`. """ - cacheControl: Boolean + cache_control: Boolean """ `cors` allows Cross-Origin Resource Sharing (CORS) for a server. """ - cors: Cors + cors: CorsStatic """ - `headers` are key-value pairs included in every server response. Useful for setting + `custom` are key-value pairs included in every server response. Useful for setting headers like `Access-Control-Allow-Origin` for cross-origin requests or additional headers for downstream services. """ @@ -694,14 +694,23 @@ input Headers { """ experimental: [String!] """ - `setCookies` when enabled stores `set-cookie` headers and all the response will be - sent with the headers. + `set_cookies` when enabled stores `set-cookie` headers and all the response will + be sent with the headers. """ - setCookies: Boolean + set_cookies: Boolean } +""" +Used to configure the default routes of the server. +""" input Routes { - graphQL: String! + """ + The path for the GraphQL endpoint. Defaults to `/graphql`. + """ + graphql: String! + """ + The path for the status endpoint. Defaults to `/status`. + """ status: String! } @@ -709,23 +718,23 @@ input ScriptOptions { timeout: Int } -input Apollo { +input ApolloTelemetry { """ - Setting `apiKey` for Apollo. + Setting `api_key` for Apollo. """ - apiKey: String! + api_key: String! """ - Setting `graphRef` for Apollo in the format @. + Setting `graph_ref` for Apollo in the format @. """ - graphRef: String! + graph_ref: String! """ Setting `platform` for Apollo. """ platform: String """ - Setting `userVersion` for Apollo. + Setting `user_version` for Apollo. """ - userVersion: String + user_version: String """ Setting `version` for Apollo. """ @@ -735,7 +744,7 @@ input Apollo { """ Output the opentelemetry data to otlp collector """ -input OtlpExporter { +input OtlpExporterConfig { headers: [KeyValue] url: String! } @@ -758,17 +767,17 @@ input StdoutExporter { pretty: Boolean! } -input TelemetryExporter { - stdout: StdoutExporter - otlp: OtlpExporter - prometheus: PrometheusExporter - apollo: Apollo +input TelemetryExporterConfig { + Stdout: StdoutExporter + Otlp: OtlpExporterConfig + Prometheus: PrometheusExporter + Apollo: ApolloTelemetry } input Batch { delay: Int! headers: [String!] - maxSize: Int + max_size: Int } input Proxy { @@ -984,7 +993,7 @@ directive, developers gain access to valuable insights into the performance and of their applications. """ input Telemetry { - export: TelemetryExporter + export: TelemetryExporterConfig """ The list of headers that will be sent as additional attributes to telemetry exporters Be careful about **leaking sensitive information** from requests when enabling the @@ -1010,6 +1019,9 @@ enum Method { TRACE } +""" +The acceptable types of linked files that can be loaded on bootstrap. +""" enum LinkType { Config Protobuf @@ -1031,6 +1043,6 @@ enum HttpVersion { Output format for prometheus data """ enum PrometheusFormat { - text - protobuf + Text + Protobuf } \ No newline at end of file diff --git a/generated/.tailcallrc.schema.json b/generated/.tailcallrc.schema.json index e32e9b91f8..641494ac61 100644 --- a/generated/.tailcallrc.schema.json +++ b/generated/.tailcallrc.schema.json @@ -2,16 +2,14 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "Config", "type": "object", - "required": [ - "schema" - ], "properties": { - "enums": { - "description": "A map of all the enum types in the schema", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Enum" - } + "blueprintBuilder": { + "description": "A builder for generating the blueprint.", + "allOf": [ + { + "$ref": "#/definitions/BlueprintBuilder" + } + ] }, "links": { "description": "A list of all links in the schema.", @@ -20,14 +18,6 @@ "$ref": "#/definitions/Link" } }, - "schema": { - "description": "Specifies the entry points for query and mutation in the generated GraphQL schema.", - "allOf": [ - { - "$ref": "#/definitions/RootSchema" - } - ] - }, "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": {}, @@ -45,21 +35,6 @@ } ] }, - "types": { - "description": "A map of all the types in the schema.", - "default": {}, - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Type" - } - }, - "unions": { - "description": "A map of all the union types in the schema.", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Union" - } - }, "upstream": { "description": "Dictates how tailcall should handle upstream requests/responses. Tuning upstream can improve performance and reliability for connections.", "default": {}, @@ -109,19 +84,19 @@ } } }, - "Apollo": { + "ApolloTelemetry": { "type": "object", "required": [ - "apiKey", - "graphRef" + "api_key", + "graph_ref" ], "properties": { - "apiKey": { - "description": "Setting `apiKey` for Apollo.", + "api_key": { + "description": "Setting `api_key` for Apollo.", "type": "string" }, - "graphRef": { - "description": "Setting `graphRef` for Apollo in the format @.", + "graph_ref": { + "description": "Setting `graph_ref` for Apollo in the format @.", "type": "string" }, "platform": { @@ -131,8 +106,8 @@ "null" ] }, - "userVersion": { - "description": "Setting `userVersion` for Apollo.", + "user_version": { + "description": "Setting `user_version` for Apollo.", "type": [ "string", "null" @@ -177,22 +152,24 @@ }, "Batch": { "type": "object", + "required": [ + "delay", + "headers" + ], "properties": { "delay": { - "default": 0, "type": "integer", "format": "uint", "minimum": 0.0 }, "headers": { - "default": [], "type": "array", "items": { "type": "string" }, "uniqueItems": true }, - "maxSize": { + "max_size": { "type": [ "integer", "null" @@ -202,6 +179,44 @@ } } }, + "BlueprintBuilder": { + "type": "object", + "required": [ + "schema" + ], + "properties": { + "enums": { + "description": "A map of all the enum types in the schema", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Enum" + } + }, + "schema": { + "description": "Specifies the entry points for query and mutation in the generated GraphQL schema.", + "allOf": [ + { + "$ref": "#/definitions/RootSchema" + } + ] + }, + "types": { + "description": "A map of all the types in the schema.", + "default": {}, + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Type" + } + }, + "unions": { + "description": "A map of all the union types in the schema.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Union" + } + } + } + }, "Bytes": { "title": "Bytes", "description": "Field whose value is a sequence of bytes." @@ -245,53 +260,53 @@ } } }, - "Cors": { + "CorsStatic": { "description": "Type to configure Cross-Origin Resource Sharing (CORS) for a server.", "type": "object", "properties": { - "allowCredentials": { + "allow_credentials": { "description": "Indicates whether the server allows credentials (e.g., cookies, authorization headers) to be sent in cross-origin requests.", "type": [ "boolean", "null" ] }, - "allowHeaders": { + "allow_headers": { "description": "A list of allowed headers in cross-origin requests. This can be used to specify custom headers that are allowed to be included in cross-origin requests.", "type": "array", "items": { "type": "string" } }, - "allowMethods": { + "allow_methods": { "description": "A list of allowed HTTP methods in cross-origin requests. These methods specify the actions that are permitted in cross-origin requests.", "type": "array", "items": { "$ref": "#/definitions/Method" } }, - "allowOrigins": { + "allow_origins": { "description": "A list of origins that are allowed to access the server's resources in cross-origin requests. An origin can be a domain, a subdomain, or even 'null' for local file schemes.", "type": "array", "items": { "type": "string" } }, - "allowPrivateNetwork": { + "allow_private_network": { "description": "Indicates whether requests from private network addresses are allowed in cross-origin requests. Private network addresses typically include IP addresses reserved for internal networks.", "type": [ "boolean", "null" ] }, - "exposeHeaders": { + "expose_headers": { "description": "A list of headers that the server exposes to the browser in cross-origin responses. Exposing certain headers allows the client-side code to access them in the response.", "type": "array", "items": { "type": "string" } }, - "maxAge": { + "max_age": { "description": "The maximum time (in seconds) that the client should cache preflight OPTIONS requests in order to avoid sending excessive requests to the server.", "type": [ "integer", @@ -662,8 +677,8 @@ "Headers": { "type": "object", "properties": { - "cacheControl": { - "description": "`cacheControl` sends `Cache-Control` headers in responses when activated. The `max-age` value is the least of the values received from upstream services. @default `false`.", + "cache_control": { + "description": "`cache_control` sends `Cache-Control` headers in responses when activated. The `max-age` value is the least of the values received from upstream services. @default `false`.", "type": [ "boolean", "null" @@ -673,7 +688,7 @@ "description": "`cors` allows Cross-Origin Resource Sharing (CORS) for a server.", "anyOf": [ { - "$ref": "#/definitions/Cors" + "$ref": "#/definitions/CorsStatic" }, { "type": "null" @@ -681,7 +696,7 @@ ] }, "custom": { - "description": "`headers` are key-value pairs included in every server response. Useful for setting headers like `Access-Control-Allow-Origin` for cross-origin requests or additional headers for downstream services.", + "description": "`custom` are key-value pairs included in every server response. Useful for setting headers like `Access-Control-Allow-Origin` for cross-origin requests or additional headers for downstream services.", "type": "array", "items": { "$ref": "#/definitions/KeyValue" @@ -698,8 +713,8 @@ }, "uniqueItems": true }, - "setCookies": { - "description": "`setCookies` when enabled stores `set-cookie` headers and all the response will be sent with the headers.", + "set_cookies": { + "description": "`set_cookies` when enabled stores `set-cookie` headers and all the response will be sent with the headers.", "type": [ "boolean", "null" @@ -901,6 +916,7 @@ "additionalProperties": false }, "LinkType": { + "description": "The acceptable types of linked files that can be loaded on bootstrap.", "type": "string", "enum": [ "Config", @@ -951,7 +967,7 @@ "type": "object", "additionalProperties": false }, - "OtlpExporter": { + "OtlpExporterConfig": { "description": "Output the opentelemetry data to otlp collector", "type": "object", "required": [ @@ -990,8 +1006,8 @@ "description": "Output format for prometheus data", "type": "string", "enum": [ - "text", - "protobuf" + "Text", + "Protobuf" ] }, "Protected": { @@ -1032,14 +1048,19 @@ } }, "Routes": { + "description": "Used to configure the default routes of the server.", "type": "object", + "required": [ + "graphql", + "status" + ], "properties": { - "graphQL": { - "default": "/graphql", + "graphql": { + "description": "The path for the GraphQL endpoint. Defaults to `/graphql`.", "type": "string" }, "status": { - "default": "/status", + "description": "The path for the status endpoint. Defaults to `/status`.", "type": "string" } } @@ -1252,7 +1273,7 @@ "export": { "anyOf": [ { - "$ref": "#/definitions/TelemetryExporter" + "$ref": "#/definitions/TelemetryExporterConfig" }, { "type": "null" @@ -1269,15 +1290,15 @@ }, "additionalProperties": false }, - "TelemetryExporter": { + "TelemetryExporterConfig": { "oneOf": [ { "type": "object", "required": [ - "stdout" + "Stdout" ], "properties": { - "stdout": { + "Stdout": { "$ref": "#/definitions/StdoutExporter" } }, @@ -1286,11 +1307,11 @@ { "type": "object", "required": [ - "otlp" + "Otlp" ], "properties": { - "otlp": { - "$ref": "#/definitions/OtlpExporter" + "Otlp": { + "$ref": "#/definitions/OtlpExporterConfig" } }, "additionalProperties": false @@ -1298,10 +1319,10 @@ { "type": "object", "required": [ - "prometheus" + "Prometheus" ], "properties": { - "prometheus": { + "Prometheus": { "$ref": "#/definitions/PrometheusExporter" } }, @@ -1310,11 +1331,11 @@ { "type": "object", "required": [ - "apollo" + "Apollo" ], "properties": { - "apollo": { - "$ref": "#/definitions/Apollo" + "Apollo": { + "$ref": "#/definitions/ApolloTelemetry" } }, "additionalProperties": false diff --git a/src/cli/javascript/mod.rs b/src/cli/javascript/mod.rs index 173928a153..4e4a350110 100644 --- a/src/cli/javascript/mod.rs +++ b/src/cli/javascript/mod.rs @@ -9,9 +9,10 @@ mod runtime; pub use runtime::Runtime; -use crate::core::{blueprint, WorkerIO}; +use crate::core::config::ScriptRuntime; +use crate::core::WorkerIO; -pub fn init_worker_io(script: blueprint::Script) -> Arc + Send + Sync> +pub fn init_worker_io(script: ScriptRuntime) -> Arc + Send + Sync> where Runtime: WorkerIO, { diff --git a/src/cli/javascript/runtime.rs b/src/cli/javascript/runtime.rs index a3c2fc6933..264856b0bc 100644 --- a/src/cli/javascript/runtime.rs +++ b/src/cli/javascript/runtime.rs @@ -5,8 +5,9 @@ use std::thread; use async_graphql_value::ConstValue; use rquickjs::{Context, Ctx, FromJs, Function, IntoJs, Value}; +use crate::core::config::ScriptRuntime; use crate::core::worker::{Command, Event, WorkerRequest}; -use crate::core::{blueprint, worker, WorkerIO}; +use crate::core::{worker, WorkerIO}; struct LocalRuntime(Context); @@ -33,10 +34,10 @@ fn setup_builtins(ctx: &Ctx<'_>) -> rquickjs::Result<()> { Ok(()) } -impl TryFrom for LocalRuntime { +impl TryFrom for LocalRuntime { type Error = anyhow::Error; - fn try_from(script: blueprint::Script) -> Result { + fn try_from(script: ScriptRuntime) -> Result { let source = script.source; let js_runtime = rquickjs::Runtime::new()?; let context = Context::full(&js_runtime)?; @@ -51,7 +52,7 @@ impl TryFrom for LocalRuntime { } pub struct Runtime { - script: blueprint::Script, + script: ScriptRuntime, // Single threaded JS runtime, that's shared across all tokio workers. tokio_runtime: Option, } @@ -63,7 +64,7 @@ impl Debug for Runtime { } impl Runtime { - pub fn new(script: blueprint::Script) -> Self { + pub fn new(script: ScriptRuntime) -> Self { let tokio_runtime = tokio::runtime::Builder::new_multi_thread() .worker_threads(1) .build() @@ -125,7 +126,7 @@ impl WorkerIO for Runtime { } } -fn init_rt(script: blueprint::Script) -> anyhow::Result<()> { +fn init_rt(script: ScriptRuntime) -> anyhow::Result<()> { // initialize runtime if this is the first call // exit if failed to initialize LOCAL_RUNTIME.with(move |cell| { diff --git a/src/cli/llm/infer_type_name.rs b/src/cli/llm/infer_type_name.rs index 7183eacbde..5f4ce84d0d 100644 --- a/src/cli/llm/infer_type_name.rs +++ b/src/cli/llm/infer_type_name.rs @@ -97,6 +97,7 @@ impl InferTypeName { let mut new_name_mappings: HashMap = HashMap::new(); // Filter out root operation types and types with non-auto-generated names let types_to_be_processed = config + .blueprint_builder .types .iter() .filter(|(type_name, _)| { @@ -105,6 +106,7 @@ impl InferTypeName { .collect::>(); let mut used_type_names = config + .blueprint_builder .types .iter() .filter(|(ty_name, _)| !Self::is_auto_generated(ty_name)) @@ -130,7 +132,9 @@ impl InferTypeName { Ok(answer) => { let name = &answer.suggestions.join(", "); for name in answer.suggestions { - if config.types.contains_key(&name) || used_type_names.contains(&name) { + if config.blueprint_builder.types.contains_key(&name) + || used_type_names.contains(&name) + { continue; } used_type_names.insert(name.clone()); diff --git a/src/cli/runtime/http.rs b/src/cli/runtime/http.rs index 487febe544..50a045ff4b 100644 --- a/src/cli/runtime/http.rs +++ b/src/cli/runtime/http.rs @@ -17,8 +17,7 @@ use tailcall_http_cache::HttpCacheManager; use tracing_opentelemetry::OpenTelemetrySpanExt; use super::HttpIO; -use crate::core::blueprint::telemetry::Telemetry; -use crate::core::blueprint::Upstream; +use crate::core::config::{TelemetryRuntime, UpstreamRuntime}; use crate::core::http::Response; static HTTP_CLIENT_REQUEST_COUNT: Lazy> = Lazy::new(|| { @@ -85,7 +84,7 @@ impl Default for NativeHttp { } impl NativeHttp { - pub fn init(upstream: &Upstream, telemetry: &Telemetry) -> Self { + pub fn init(upstream: &UpstreamRuntime, telemetry: &TelemetryRuntime) -> Self { let mut builder = Client::builder() .tcp_keepalive(Some(Duration::from_secs(upstream.tcp_keep_alive))) .timeout(Duration::from_secs(upstream.timeout)) @@ -253,7 +252,7 @@ mod tests { then.status(200).body("Hello"); }); - let upstream = Upstream { http_cache: 2, ..Default::default() }; + let upstream = UpstreamRuntime { http_cache: 2, ..Default::default() }; let native_http = NativeHttp::init(&upstream, &Default::default()); let port = server.port(); diff --git a/src/cli/runtime/mod.rs b/src/cli/runtime/mod.rs index d57bf1be31..307263f8a7 100644 --- a/src/cli/runtime/mod.rs +++ b/src/cli/runtime/mod.rs @@ -11,9 +11,10 @@ use inquire::{Confirm, Select}; use crate::core::blueprint::Blueprint; use crate::core::cache::InMemoryCache; +use crate::core::config::ScriptRuntime; use crate::core::runtime::TargetRuntime; use crate::core::worker::{Command, Event}; -use crate::core::{blueprint, EnvIO, FileIO, HttpIO, WorkerIO}; +use crate::core::{EnvIO, FileIO, HttpIO, WorkerIO}; // Provides access to env in native rust environment fn init_env() -> Arc { @@ -25,9 +26,7 @@ fn init_file() -> Arc { Arc::new(file::NativeFileIO::init()) } -fn init_http_worker_io( - script: Option, -) -> Option>> { +fn init_http_worker_io(script: Option) -> Option>> { #[cfg(feature = "js")] return Some(super::javascript::init_worker_io(script?)); #[cfg(not(feature = "js"))] @@ -38,7 +37,7 @@ fn init_http_worker_io( } fn init_resolver_worker_io( - script: Option, + script: Option, ) -> Option>> { #[cfg(feature = "js")] return Some(super::javascript::init_worker_io(script?)); @@ -52,16 +51,16 @@ fn init_resolver_worker_io( // Provides access to http in native rust environment fn init_http(blueprint: &Blueprint) -> Arc { Arc::new(http::NativeHttp::init( - &blueprint.upstream, - &blueprint.telemetry, + &blueprint.config.upstream, + &blueprint.config.telemetry, )) } // Provides access to http in native rust environment fn init_http2_only(blueprint: &Blueprint) -> Arc { Arc::new(http::NativeHttp::init( - &blueprint.upstream.clone().http2_only(true), - &blueprint.telemetry, + &blueprint.config.upstream.clone().http2_only(true), + &blueprint.config.telemetry, )) } @@ -80,8 +79,8 @@ pub fn init(blueprint: &Blueprint) -> TargetRuntime { file: init_file(), cache: Arc::new(init_in_memory_cache()), extensions: Arc::new(vec![]), - cmd_worker: init_http_worker_io(blueprint.server.script.clone()), - worker: init_resolver_worker_io(blueprint.server.script.clone()), + cmd_worker: init_http_worker_io(blueprint.config.server.script.clone()), + worker: init_resolver_worker_io(blueprint.config.server.script.clone()), } } diff --git a/src/cli/server/http_1.rs b/src/cli/server/http_1.rs index 76360e860a..cde2e4403d 100644 --- a/src/cli/server/http_1.rs +++ b/src/cli/server/http_1.rs @@ -32,7 +32,7 @@ pub async fn start_http_1( }); let builder = hyper::Server::try_bind(&addr) .map_err(Errata::from)? - .http1_pipeline_flush(sc.app_ctx.blueprint.server.pipeline_flush); + .http1_pipeline_flush(sc.app_ctx.blueprint.config.server.pipeline_flush); super::log_launch(sc.as_ref()); if let Some(sender) = server_up_sender { @@ -42,7 +42,7 @@ pub async fn start_http_1( } let server: std::prelude::v1::Result<(), hyper::Error> = - if sc.blueprint.server.enable_batch_requests { + if sc.blueprint.config.server.enable_batch_requests { builder.serve(make_svc_batch_req).await } else { builder.serve(make_svc_single_req).await diff --git a/src/cli/server/http_2.rs b/src/cli/server/http_2.rs index 7a38c74119..649001dadb 100644 --- a/src/cli/server/http_2.rs +++ b/src/cli/server/http_2.rs @@ -55,7 +55,7 @@ pub async fn start_http_2( } let server: std::prelude::v1::Result<(), hyper::Error> = - if sc.blueprint.server.enable_batch_requests { + if sc.blueprint.config.server.enable_batch_requests { builder.serve(make_svc_batch_req).await } else { builder.serve(make_svc_single_req).await diff --git a/src/cli/server/http_server.rs b/src/cli/server/http_server.rs index 3661c9f5f7..11f117187c 100644 --- a/src/cli/server/http_server.rs +++ b/src/cli/server/http_server.rs @@ -8,8 +8,8 @@ use super::http_1::start_http_1; use super::http_2::start_http_2; use super::server_config::ServerConfig; use crate::cli::telemetry::init_opentelemetry; -use crate::core::blueprint::{Blueprint, Http}; -use crate::core::config::ConfigModule; +use crate::core::blueprint::Blueprint; +use crate::core::config::{ConfigModule, HttpVersionRuntime}; use crate::core::Errata; pub struct Server { @@ -36,13 +36,16 @@ impl Server { let endpoints = self.config_module.extensions().endpoint_set.clone(); let server_config = Arc::new(ServerConfig::new(blueprint.clone(), endpoints).await?); - init_opentelemetry(blueprint.telemetry.clone(), &server_config.app_ctx.runtime)?; + init_opentelemetry( + blueprint.config.telemetry.clone(), + &server_config.app_ctx.runtime, + )?; - match blueprint.server.http.clone() { - Http::HTTP2 { cert, key } => { + match blueprint.config.server.http.clone() { + HttpVersionRuntime::HTTP2 { cert, key } => { start_http_2(server_config, cert, key, self.server_up_sender).await } - Http::HTTP1 => start_http_1(server_config, self.server_up_sender).await, + HttpVersionRuntime::HTTP1 => start_http_1(server_config, self.server_up_sender).await, } } diff --git a/src/cli/server/mod.rs b/src/cli/server/mod.rs index 3cd410e27b..79515989cd 100644 --- a/src/cli/server/mod.rs +++ b/src/cli/server/mod.rs @@ -16,7 +16,7 @@ fn log_launch(sc: &ServerConfig) { sc.http_version() ); - let gql_slug = sc.app_ctx.blueprint.server.routes.graphql(); + let gql_slug = sc.app_ctx.blueprint.config.server.routes.graphql(); let graphiql_url = sc.graphiql_url() + gql_slug; let url = playground::build_url(&graphiql_url); diff --git a/src/cli/server/server_config.rs b/src/cli/server/server_config.rs index b6fd3899c0..982e8b8682 100644 --- a/src/cli/server/server_config.rs +++ b/src/cli/server/server_config.rs @@ -5,8 +5,8 @@ use async_graphql_extension_apollo_tracing::ApolloTracing; use crate::cli::runtime::init; use crate::core::app_context::AppContext; -use crate::core::blueprint::telemetry::TelemetryExporter; -use crate::core::blueprint::{Blueprint, Http}; +use crate::core::blueprint::Blueprint; +use crate::core::config::{HttpVersionRuntime, TelemetryExporterRuntime}; use crate::core::rest::{EndpointSet, Unchecked}; use crate::core::schema_extension::SchemaExtension; @@ -24,7 +24,9 @@ impl ServerConfig { let mut extensions = vec![]; - if let Some(TelemetryExporter::Apollo(apollo)) = blueprint.telemetry.export.as_ref() { + if let Some(TelemetryExporterRuntime::Apollo(apollo)) = + blueprint.config.telemetry.export.as_ref() + { let (graph_id, variant) = apollo.graph_ref.split_once('@').unwrap(); extensions.push(SchemaExtension::new(ApolloTracing::new( apollo.api_key.clone(), @@ -43,12 +45,16 @@ impl ServerConfig { } pub fn addr(&self) -> SocketAddr { - (self.blueprint.server.hostname, self.blueprint.server.port).into() + ( + self.blueprint.config.server.hostname, + self.blueprint.config.server.port, + ) + .into() } pub fn http_version(&self) -> String { - match self.blueprint.server.http { - Http::HTTP2 { cert: _, key: _ } => "HTTP/2".to_string(), + match self.blueprint.config.server.http { + HttpVersionRuntime::HTTP2 { cert: _, key: _ } => "HTTP/2".to_string(), _ => "HTTP/1.1".to_string(), } } diff --git a/src/cli/tc/init.rs b/src/cli/tc/init.rs index 99be5b3296..eaceb589a8 100644 --- a/src/cli/tc/init.rs +++ b/src/cli/tc/init.rs @@ -5,6 +5,7 @@ use anyhow::Result; use super::helpers::{GRAPHQL_RC, TAILCALL_RC, TAILCALL_RC_SCHEMA}; use crate::cli::runtime::{confirm_and_write, create_directory, select_prompt}; +use crate::core::blueprint::BlueprintBuilder; use crate::core::config::{Config, Expr, Field, Resolver, RootSchema, Source}; use crate::core::merge_right::MergeRight; use crate::core::runtime::TargetRuntime; @@ -102,9 +103,13 @@ fn main_config() -> Config { Config { server: Default::default(), upstream: Default::default(), - schema: RootSchema { query: Some("Query".to_string()), ..Default::default() }, - types: BTreeMap::from([("Query".into(), query_type)]), - ..Default::default() + blueprint_builder: BlueprintBuilder { + schema: RootSchema { query: Some("Query".to_string()), ..Default::default() }, + types: BTreeMap::from([("Query".into(), query_type)]), + ..Default::default() + }, + links: Default::default(), + telemetry: Default::default(), } } diff --git a/src/cli/telemetry.rs b/src/cli/telemetry.rs index 50ea364d41..bd282f0a82 100644 --- a/src/cli/telemetry.rs +++ b/src/cli/telemetry.rs @@ -24,7 +24,7 @@ use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::{Layer, Registry}; use super::metrics::init_metrics; -use crate::core::blueprint::telemetry::{OtlpExporter, Telemetry, TelemetryExporter}; +use crate::core::config::{OtlpExporterRuntime, TelemetryExporterRuntime, TelemetryRuntime}; use crate::core::runtime::TargetRuntime; use crate::core::tracing::{ default_tracing, default_tracing_tailcall, get_log_level, tailcall_filter_target, @@ -53,7 +53,7 @@ fn pretty_encoder(writer: &mut dyn Write, data: T) -> Result<()> { } // TODO: add more options for otlp exporter if needed -fn otlp_exporter(config: &OtlpExporter) -> TonicExporterBuilder { +fn otlp_exporter(config: &OtlpExporterRuntime) -> TonicExporterBuilder { opentelemetry_otlp::new_exporter() .tonic() .with_endpoint(config.url.as_str()) @@ -61,10 +61,10 @@ fn otlp_exporter(config: &OtlpExporter) -> TonicExporterBuilder { } fn set_trace_provider( - exporter: &TelemetryExporter, + exporter: &TelemetryExporterRuntime, ) -> TraceResult>> { let provider = match exporter { - TelemetryExporter::Stdout(config) => TracerProvider::builder() + TelemetryExporterRuntime::Stdout(config) => TracerProvider::builder() .with_batch_exporter( { let mut builder = opentelemetry_stdout::SpanExporterBuilder::default(); @@ -82,7 +82,7 @@ fn set_trace_provider( ) .with_config(opentelemetry_sdk::trace::config().with_resource(RESOURCE.clone())) .build(), - TelemetryExporter::Otlp(config) => opentelemetry_otlp::new_pipeline() + TelemetryExporterRuntime::Otlp(config) => opentelemetry_otlp::new_pipeline() .tracing() .with_exporter(otlp_exporter(config)) .with_trace_config(opentelemetry_sdk::trace::config().with_resource(RESOURCE.clone())) @@ -92,8 +92,8 @@ fn set_trace_provider( anyhow!("Failed to instantiate OTLP provider").into(), ))?, // Prometheus works only with metrics - TelemetryExporter::Prometheus(_) => return Ok(None), - TelemetryExporter::Apollo(_) => return Ok(None), + TelemetryExporterRuntime::Prometheus(_) => return Ok(None), + TelemetryExporterRuntime::Apollo(_) => return Ok(None), }; let tracer = provider.tracer("tracing"); let telemetry = tracing_opentelemetry::layer() @@ -107,10 +107,10 @@ fn set_trace_provider( } fn set_logger_provider( - exporter: &TelemetryExporter, + exporter: &TelemetryExporterRuntime, ) -> LogResult>> { let provider = match exporter { - TelemetryExporter::Stdout(config) => LoggerProvider::builder() + TelemetryExporterRuntime::Stdout(config) => LoggerProvider::builder() .with_batch_exporter( { let mut builder = opentelemetry_stdout::LogExporterBuilder::default(); @@ -127,15 +127,15 @@ fn set_logger_provider( ) .with_config(opentelemetry_sdk::logs::config().with_resource(RESOURCE.clone())) .build(), - TelemetryExporter::Otlp(config) => opentelemetry_otlp::new_pipeline() + TelemetryExporterRuntime::Otlp(config) => opentelemetry_otlp::new_pipeline() .logging() .with_exporter(otlp_exporter(config)) .with_log_config(opentelemetry_sdk::logs::config().with_resource(RESOURCE.clone())) .install_batch(runtime::Tokio)? , // Prometheus works only with metrics - TelemetryExporter::Prometheus(_) => return Ok(None), - TelemetryExporter::Apollo(_) => return Ok(None), + TelemetryExporterRuntime::Prometheus(_) => return Ok(None), + TelemetryExporterRuntime::Apollo(_) => return Ok(None), }; let otel_tracing_appender = OpenTelemetryTracingBridge::new(&provider); @@ -143,9 +143,9 @@ fn set_logger_provider( Ok(Some(otel_tracing_appender)) } -fn set_meter_provider(exporter: &TelemetryExporter) -> MetricsResult<()> { +fn set_meter_provider(exporter: &TelemetryExporterRuntime) -> MetricsResult<()> { let provider = match exporter { - TelemetryExporter::Stdout(config) => { + TelemetryExporterRuntime::Stdout(config) => { let mut builder = opentelemetry_stdout::MetricsExporterBuilder::default(); if config.pretty { @@ -162,12 +162,12 @@ fn set_meter_provider(exporter: &TelemetryExporter) -> MetricsResult<()> { .with_resource(RESOURCE.clone()) .build() } - TelemetryExporter::Otlp(config) => opentelemetry_otlp::new_pipeline() + TelemetryExporterRuntime::Otlp(config) => opentelemetry_otlp::new_pipeline() .metrics(Tokio) .with_resource(RESOURCE.clone()) .with_exporter(otlp_exporter(config)) .build()?, - TelemetryExporter::Prometheus(_) => { + TelemetryExporterRuntime::Prometheus(_) => { let exporter = opentelemetry_prometheus::exporter() .with_registry(prometheus::default_registry().clone()) .build()?; @@ -192,7 +192,7 @@ fn set_tracing_subscriber(subscriber: impl Subscriber + Send + Sync) { let _ = tracing::subscriber::set_global_default(subscriber); } -pub fn init_opentelemetry(config: Telemetry, runtime: &TargetRuntime) -> anyhow::Result<()> { +pub fn init_opentelemetry(config: TelemetryRuntime, runtime: &TargetRuntime) -> anyhow::Result<()> { if let Some(export) = &config.export { global::set_error_handler(|error| { if !matches!( diff --git a/src/core/app_context.rs b/src/core/app_context.rs index af1963868e..8a3e764ca7 100644 --- a/src/core/app_context.rs +++ b/src/core/app_context.rs @@ -47,7 +47,7 @@ impl AppContext { for def in blueprint.definitions.iter_mut() { if let Definition::Object(def) = def { for field in &mut def.fields { - let upstream_batch = &blueprint.upstream.batch; + let upstream_batch = &blueprint.config.upstream.batch; field.map_expr(|expr| { expr.modify(&mut |expr| match expr { IR::IO(io) => match io { @@ -138,7 +138,7 @@ impl AppContext { let schema = blueprint .to_schema_with(SchemaModifiers::default().extensions(runtime.extensions.clone())); - let auth = blueprint.server.auth.clone(); + let auth = blueprint.config.server.auth.clone(); let auth_ctx = GlobalAuthContext::new(auth); AppContext { diff --git a/src/core/auth/basic.rs b/src/core/auth/basic.rs index 1806d63205..ca3d596099 100644 --- a/src/core/auth/basic.rs +++ b/src/core/auth/basic.rs @@ -5,7 +5,7 @@ use htpasswd_verify::Htpasswd; use super::error::Error; use super::verification::Verification; use super::verify::Verify; -use crate::core::blueprint; +use crate::core::config::BasicRuntime; use crate::core::http::RequestContext; pub struct BasicVerifier { @@ -36,7 +36,7 @@ impl Verify for BasicVerifier { } impl BasicVerifier { - pub fn new(options: blueprint::Basic) -> Self { + pub fn new(options: BasicRuntime) -> Self { Self { verifier: Htpasswd::new_owned(&options.htpasswd) } } } @@ -46,21 +46,7 @@ pub mod tests { use http::header::HeaderValue; use super::*; - - // testuser1:password123 - // testuser2:mypassword - // testuser3:abc123 - pub static HTPASSWD_TEST: &str = " -testuser1:$apr1$e3dp9qh2$fFIfHU9bilvVZBl8TxKzL/ -testuser2:$2y$10$wJ/mZDURcAOBIrswCAKFsO0Nk7BpHmWl/XuhF7lNm3gBAFH3ofsuu -testuser3:{SHA}Y2fEjdGT1W6nsLqtJbGUVeUp9e4= -"; - - impl blueprint::Basic { - pub fn test_value() -> Self { - Self { htpasswd: HTPASSWD_TEST.to_owned() } - } - } + use crate::core::config::HTPASSWD_TEST; pub fn create_basic_auth_request(username: &str, password: &str) -> RequestContext { let mut req_context = RequestContext::default(); @@ -129,6 +115,6 @@ testuser3:{SHA}Y2fEjdGT1W6nsLqtJbGUVeUp9e4= // Helper function for setting up the provider fn setup_provider() -> BasicVerifier { - BasicVerifier::new(blueprint::Basic { htpasswd: HTPASSWD_TEST.to_owned() }) + BasicVerifier::new(BasicRuntime { htpasswd: HTPASSWD_TEST.to_owned() }) } } diff --git a/src/core/auth/context.rs b/src/core/auth/context.rs index 40ab641853..a25201d487 100644 --- a/src/core/auth/context.rs +++ b/src/core/auth/context.rs @@ -2,7 +2,7 @@ use std::sync::{Arc, RwLock}; use super::verification::Verification; use super::verify::{AuthVerifier, Verify}; -use crate::core::blueprint::Auth; +use crate::core::config::AuthRuntime; use crate::core::http::RequestContext; #[derive(Default)] @@ -31,7 +31,7 @@ impl GlobalAuthContext { } impl GlobalAuthContext { - pub fn new(auth: Option) -> Self { + pub fn new(auth: Option) -> Self { Self { verifier: auth.map(AuthVerifier::from) } } } @@ -62,7 +62,7 @@ impl From<&Arc> for AuthContext { #[cfg(test)] mod tests { use super::*; - use crate::core::auth::basic::tests::{create_basic_auth_request, HTPASSWD_TEST}; + use crate::core::auth::basic::tests::create_basic_auth_request; use crate::core::auth::basic::BasicVerifier; use crate::core::auth::error::Error; use crate::core::auth::jwt::jwt_verify::tests::{ @@ -70,7 +70,7 @@ mod tests { }; use crate::core::auth::jwt::jwt_verify::JwtVerifier; use crate::core::auth::verify::Verifier; - use crate::core::blueprint; + use crate::core::config::{BasicRuntime, JwtRuntime, HTPASSWD_TEST}; #[tokio::test] async fn validate_request_missing_credentials() { @@ -109,8 +109,8 @@ mod tests { // Helper function for setting up the auth context async fn setup_auth_context() -> GlobalAuthContext { let basic_provider = - BasicVerifier::new(blueprint::Basic { htpasswd: HTPASSWD_TEST.to_owned() }); - let jwt_options = blueprint::Jwt::test_value(); + BasicVerifier::new(BasicRuntime { htpasswd: HTPASSWD_TEST.to_owned() }); + let jwt_options = JwtRuntime::test_value(); let jwt_provider = JwtVerifier::new(jwt_options); GlobalAuthContext { diff --git a/src/core/auth/jwt/jwt_verify.rs b/src/core/auth/jwt/jwt_verify.rs index 5238b4364e..b6ff94830f 100644 --- a/src/core/auth/jwt/jwt_verify.rs +++ b/src/core/auth/jwt/jwt_verify.rs @@ -6,7 +6,7 @@ use super::jwks::Jwks; use crate::core::auth::error::Error; use crate::core::auth::verification::Verification; use crate::core::auth::verify::Verify; -use crate::core::blueprint; +use crate::core::config::JwtRuntime; use crate::core::http::RequestContext; #[derive(Debug, Deserialize)] @@ -23,12 +23,12 @@ pub struct JwtClaim { } pub struct JwtVerifier { - options: blueprint::Jwt, + options: JwtRuntime, decoder: Jwks, } impl JwtVerifier { - pub fn new(options: blueprint::Jwt) -> Self { + pub fn new(options: JwtRuntime) -> Self { Self { decoder: Jwks { set: options.jwks.clone(), @@ -78,7 +78,7 @@ impl Verify for JwtVerifier { } } -pub fn validate_iss(options: &blueprint::Jwt, claims: &JwtClaim) -> bool { +pub fn validate_iss(options: &JwtRuntime, claims: &JwtClaim) -> bool { options .issuer .as_ref() @@ -92,7 +92,7 @@ pub fn validate_iss(options: &blueprint::Jwt, claims: &JwtClaim) -> bool { .unwrap_or(true) } -pub fn validate_aud(options: &blueprint::Jwt, claims: &JwtClaim) -> bool { +pub fn validate_aud(options: &JwtRuntime, claims: &JwtClaim) -> bool { let audiences = &options.audiences; if audiences.is_empty() { @@ -152,7 +152,7 @@ pub mod tests { serde_json::from_value(value).unwrap() }); - impl blueprint::Jwt { + impl JwtRuntime { pub fn test_value() -> Self { Self { issuer: Default::default(), @@ -175,7 +175,7 @@ pub mod tests { #[tokio::test] async fn validate_token_iss() { - let jwt_options = blueprint::Jwt::test_value(); + let jwt_options = JwtRuntime::test_value(); let jwt_provider = JwtVerifier::new(jwt_options); let valid = jwt_provider @@ -184,10 +184,7 @@ pub mod tests { assert_eq!(valid, Verification::succeed()); - let jwt_options = blueprint::Jwt { - issuer: Some("me".to_owned()), - ..blueprint::Jwt::test_value() - }; + let jwt_options = JwtRuntime { issuer: Some("me".to_owned()), ..JwtRuntime::test_value() }; let jwt_provider = JwtVerifier::new(jwt_options); let valid = jwt_provider @@ -196,9 +193,9 @@ pub mod tests { assert_eq!(valid, Verification::succeed()); - let jwt_options = blueprint::Jwt { + let jwt_options = JwtRuntime { issuer: Some("another".to_owned()), - ..blueprint::Jwt::test_value() + ..JwtRuntime::test_value() }; let jwt_provider = JwtVerifier::new(jwt_options); @@ -211,7 +208,7 @@ pub mod tests { #[tokio::test] async fn validate_token_aud() { - let jwt_options = blueprint::Jwt::test_value(); + let jwt_options = JwtRuntime::test_value(); let jwt_provider = JwtVerifier::new(jwt_options); let valid = jwt_provider @@ -220,9 +217,9 @@ pub mod tests { assert_eq!(valid, Verification::succeed()); - let jwt_options = blueprint::Jwt { + let jwt_options = JwtRuntime { audiences: HashSet::from_iter(["them".to_string()]), - ..blueprint::Jwt::test_value() + ..JwtRuntime::test_value() }; let jwt_provider = JwtVerifier::new(jwt_options); @@ -232,9 +229,9 @@ pub mod tests { assert_eq!(valid, Verification::succeed()); - let jwt_options = blueprint::Jwt { + let jwt_options = JwtRuntime { audiences: HashSet::from_iter(["anothem".to_string()]), - ..blueprint::Jwt::test_value() + ..JwtRuntime::test_value() }; let jwt_provider = JwtVerifier::new(jwt_options); @@ -247,11 +244,10 @@ pub mod tests { mod iss { use super::*; - use crate::core::blueprint::Jwt; #[test] fn validate_iss_not_defined() { - let options = Jwt::test_value(); + let options = JwtRuntime::test_value(); let mut claims = JwtClaim::default(); assert!(validate_iss(&options, &claims)); @@ -263,7 +259,7 @@ pub mod tests { #[test] fn validate_iss_defined() { - let options = Jwt { issuer: Some("iss".to_owned()), ..Jwt::test_value() }; + let options = JwtRuntime { issuer: Some("iss".to_owned()), ..JwtRuntime::test_value() }; let mut claims = JwtClaim::default(); assert!(!validate_iss(&options, &claims)); @@ -282,11 +278,10 @@ pub mod tests { use std::collections::HashSet; use super::*; - use crate::core::blueprint::Jwt; #[test] fn validate_aud_not_defined() { - let options = Jwt::test_value(); + let options = JwtRuntime::test_value(); let mut claims = JwtClaim::default(); assert!(validate_aud(&options, &claims)); @@ -299,9 +294,9 @@ pub mod tests { #[test] fn validate_aud_defined() { - let options = Jwt { + let options = JwtRuntime { audiences: HashSet::from_iter(["aud1".to_owned(), "aud2".to_owned()]), - ..Jwt::test_value() + ..JwtRuntime::test_value() }; let mut claims = JwtClaim::default(); assert!(!validate_aud(&options, &claims)); diff --git a/src/core/auth/verify.rs b/src/core/auth/verify.rs index 4c6934f939..da503b46e0 100644 --- a/src/core/auth/verify.rs +++ b/src/core/auth/verify.rs @@ -3,7 +3,7 @@ use futures_util::join; use super::basic::BasicVerifier; use super::jwt::jwt_verify::JwtVerifier; use super::verification::Verification; -use crate::core::blueprint; +use crate::core::config::{AuthProviderRuntime, AuthRuntime}; use crate::core::http::RequestContext; #[async_trait::async_trait] @@ -22,23 +22,23 @@ pub enum AuthVerifier { Or(Box, Box), } -impl From for Verifier { - fn from(provider: blueprint::Provider) -> Self { +impl From for Verifier { + fn from(provider: AuthProviderRuntime) -> Self { match provider { - blueprint::Provider::Basic(options) => Verifier::Basic(BasicVerifier::new(options)), - blueprint::Provider::Jwt(options) => Verifier::Jwt(JwtVerifier::new(options)), + AuthProviderRuntime::Basic(options) => Verifier::Basic(BasicVerifier::new(options)), + AuthProviderRuntime::Jwt(options) => Verifier::Jwt(JwtVerifier::new(options)), } } } -impl From for AuthVerifier { - fn from(provider: blueprint::Auth) -> Self { +impl From for AuthVerifier { + fn from(provider: AuthRuntime) -> Self { match provider { - blueprint::Auth::Provider(provider) => AuthVerifier::Single(provider.into()), - blueprint::Auth::And(left, right) => { + AuthRuntime::Provider(provider) => AuthVerifier::Single(provider.into()), + AuthRuntime::And(left, right) => { AuthVerifier::And(Box::new((*left).into()), Box::new((*right).into())) } - blueprint::Auth::Or(left, right) => { + AuthRuntime::Or(left, right) => { AuthVerifier::Or(Box::new((*left).into()), Box::new((*right).into())) } } @@ -81,7 +81,7 @@ mod tests { }; use crate::core::auth::verification::Verification; use crate::core::auth::verify::Verify; - use crate::core::blueprint::{Auth, Basic, Jwt, Provider}; + use crate::core::config::{AuthProviderRuntime, AuthRuntime, BasicRuntime, JwtRuntime}; use crate::core::http::RequestContext; #[tokio::test] @@ -143,20 +143,22 @@ mod tests { } fn setup_basic_verifier() -> AuthVerifier { - AuthVerifier::from(Auth::Provider(Provider::Basic(Basic::test_value()))) + AuthVerifier::from(AuthRuntime::Provider(AuthProviderRuntime::Basic( + BasicRuntime::test_value(), + ))) } fn setup_and_verifier() -> AuthVerifier { - AuthVerifier::from(Auth::And( - Auth::Provider(Provider::Basic(Basic::test_value())).into(), - Auth::Provider(Provider::Basic(Basic::test_value())).into(), + AuthVerifier::from(AuthRuntime::And( + AuthRuntime::Provider(AuthProviderRuntime::Basic(BasicRuntime::test_value())).into(), + AuthRuntime::Provider(AuthProviderRuntime::Basic(BasicRuntime::test_value())).into(), )) } fn setup_or_verifier() -> AuthVerifier { - AuthVerifier::from(Auth::Or( - Auth::Provider(Provider::Basic(Basic::test_value())).into(), - Auth::Provider(Provider::Jwt(Jwt::test_value())).into(), + AuthVerifier::from(AuthRuntime::Or( + AuthRuntime::Provider(AuthProviderRuntime::Basic(BasicRuntime::test_value())).into(), + AuthRuntime::Provider(AuthProviderRuntime::Jwt(JwtRuntime::test_value())).into(), )) } } diff --git a/src/core/blueprint/auth.rs b/src/core/blueprint/auth.rs deleted file mode 100644 index 55ab644626..0000000000 --- a/src/core/blueprint/auth.rs +++ /dev/null @@ -1,133 +0,0 @@ -use std::collections::HashSet; -use std::fmt::Debug; - -use jsonwebtoken::jwk::JwkSet; -use tailcall_valid::Valid; - -use crate::core::config::ConfigModule; - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Basic { - pub htpasswd: String, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Jwt { - pub issuer: Option, - pub audiences: HashSet, - pub optional_kid: bool, - pub jwks: JwkSet, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Provider { - Basic(Basic), - Jwt(Jwt), -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Auth { - Provider(Provider), - And(Box, Box), - Or(Box, Box), -} - -impl Auth { - pub fn make(config_module: &ConfigModule) -> Valid, String> { - let htpasswd = config_module.extensions().htpasswd.iter().map(|htpasswd| { - Auth::Provider(Provider::Basic(Basic { - htpasswd: htpasswd.content.clone(), - })) - }); - - let jwks = config_module.extensions().jwks.iter().map(|jwks| { - Auth::Provider(Provider::Jwt(Jwt { - jwks: jwks.content.clone(), - // TODO: read those options from link instead of using defaults - issuer: Default::default(), - audiences: Default::default(), - optional_kid: Default::default(), - })) - }); - - let auth = htpasswd.chain(jwks).reduce(|left, right| left.or(right)); - - Valid::succeed(auth) - } - - pub fn and(self, other: Self) -> Self { - Auth::And(Box::new(self), Box::new(other)) - } - - pub fn or(self, other: Self) -> Self { - Auth::Or(Box::new(self), Box::new(other)) - } -} - -#[cfg(test)] -mod tests { - use super::{Auth, Basic, Jwt, Provider}; - - fn test_basic_provider_1() -> Provider { - Provider::Basic(Basic { htpasswd: "1".into() }) - } - - fn test_basic_provider_2() -> Provider { - Provider::Basic(Basic { htpasswd: "2".into() }) - } - - fn test_jwt_provider() -> Provider { - Provider::Jwt(Jwt::test_value()) - } - - #[test] - fn and_basic_with_basic() { - let basic_provider_1 = test_basic_provider_1(); - let basic_provider_2 = test_basic_provider_2(); - - assert_eq!( - Auth::Provider(basic_provider_1.clone()).and(Auth::Provider(basic_provider_2.clone())), - Auth::And( - Auth::Provider(basic_provider_1).into(), - Auth::Provider(basic_provider_2).into() - ) - ); - } - - #[test] - fn and_basic_with_jwt() { - let basic_provider = test_basic_provider_1(); - let jwt_provider = test_jwt_provider(); - - assert_eq!( - Auth::Provider(basic_provider.clone()).and(Auth::Provider(jwt_provider.clone())), - Auth::And( - Auth::Provider(basic_provider).into(), - Auth::Provider(jwt_provider).into() - ) - ); - } - - #[test] - fn and_nested_and_with_jwt() { - let basic_provider_1 = test_basic_provider_1(); - let basic_provider_2 = test_basic_provider_2(); - let jwt_provider = test_jwt_provider(); - - assert_eq!( - Auth::And( - Auth::Provider(basic_provider_1.clone()).into(), - Auth::Provider(basic_provider_2.clone()).into() - ) - .and(Auth::Provider(jwt_provider.clone())), - Auth::And( - Auth::And( - Auth::Provider(basic_provider_1).into(), - Auth::Provider(basic_provider_2).into() - ) - .into(), - Auth::Provider(jwt_provider).into() - ) - ); - } -} diff --git a/src/core/blueprint/blueprint.rs b/src/core/blueprint/blueprint.rs index decd19d822..aeac0315d4 100644 --- a/src/core/blueprint/blueprint.rs +++ b/src/core/blueprint/blueprint.rs @@ -7,9 +7,8 @@ use async_graphql::ValidationMode; use derive_setters::Setters; use super::directive::Directive; -use super::telemetry::Telemetry; use super::{GlobalTimeout, Index}; -use crate::core::blueprint::{Server, Upstream}; +use crate::core::config::RuntimeConfig; use crate::core::ir::model::IR; use crate::core::schema_extension::SchemaExtension; use crate::core::{scalar, Type}; @@ -22,9 +21,7 @@ use crate::core::{scalar, Type}; pub struct Blueprint { pub definitions: Vec, pub schema: SchemaDefinition, - pub server: Server, - pub upstream: Upstream, - pub telemetry: Telemetry, + pub config: RuntimeConfig, } #[derive(Clone, Debug)] @@ -210,7 +207,7 @@ impl Blueprint { self.clone() }; - let server = &blueprint.server; + let server = &blueprint.config.server; let mut schema = SchemaBuilder::from(&blueprint); if server.enable_apollo_tracing { diff --git a/src/core/blueprint/blueprint_builder/blueprint_builder.rs b/src/core/blueprint/blueprint_builder/blueprint_builder.rs new file mode 100644 index 0000000000..77d836adfa --- /dev/null +++ b/src/core/blueprint/blueprint_builder/blueprint_builder.rs @@ -0,0 +1,43 @@ +use std::collections::BTreeMap; + +use derive_setters::Setters; +use serde::{Deserialize, Serialize}; +use tailcall_macros::MergeRight; + +use super::{Enum, RootSchema, Type, Union}; +use crate::core::is_default; + +#[derive( + Serialize, + Deserialize, + Clone, + Debug, + Default, + Setters, + PartialEq, + Eq, + schemars::JsonSchema, + MergeRight, +)] +pub struct BlueprintBuilder { + /// + /// Specifies the entry points for query and mutation in the generated + /// GraphQL schema. + pub schema: RootSchema, + + /// + /// A map of all the types in the schema. + #[serde(default)] + #[setters(skip)] + pub types: BTreeMap, + + /// + /// A map of all the union types in the schema. + #[serde(default, skip_serializing_if = "is_default")] + pub unions: BTreeMap, + + /// + /// A map of all the enum types in the schema + #[serde(default, skip_serializing_if = "is_default")] + pub enums: BTreeMap, +} diff --git a/src/core/blueprint/blueprint_builder/blueprint_builder_types.rs b/src/core/blueprint/blueprint_builder/blueprint_builder_types.rs new file mode 100644 index 0000000000..b768b0cd39 --- /dev/null +++ b/src/core/blueprint/blueprint_builder/blueprint_builder_types.rs @@ -0,0 +1,280 @@ +use std::collections::{BTreeMap, BTreeSet, HashMap}; +use std::fmt::{self, Display}; + +use derive_setters::Setters; +use indexmap::IndexMap; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use tailcall_macros::MergeRight; + +use crate::core::config::{ + AddField, Alias, Cache, Directive, Discriminate, Modify, Omit, Protected, Resolver, +}; +use crate::core::is_default; +use crate::core::merge_right::MergeRight; + +/// +/// Represents a GraphQL type. +/// A type can be an object, interface, enum or scalar. +#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, JsonSchema, MergeRight)] +pub struct Type { + /// + /// A map of field name and its definition. + pub fields: BTreeMap, + #[serde(default, skip_serializing_if = "is_default")] + /// + /// Additional fields to be added to the type + pub added_fields: Vec, + #[serde(default, skip_serializing_if = "is_default")] + /// + /// Documentation for the type that is publicly visible. + pub doc: Option, + #[serde(default, skip_serializing_if = "is_default")] + /// + /// Interfaces that the type implements. + pub implements: BTreeSet, + #[serde(default, skip_serializing_if = "is_default")] + /// + /// Setting to indicate if the type can be cached. + pub cache: Option, + /// + /// Marks field as protected by auth providers + #[serde(default)] + pub protected: Option, + /// + /// Apollo federation entity resolver. + #[serde(flatten, default, skip_serializing_if = "is_default")] + pub resolver: Option, + /// + /// Any additional directives + #[serde(default, skip_serializing_if = "is_default")] + pub directives: Vec, +} + +impl Display for Type { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "{{")?; + + for (field_name, field) in &self.fields { + writeln!(f, " {}: {:?},", field_name, field.type_of)?; + } + writeln!(f, "}}") + } +} + +impl Type { + pub fn fields(mut self, fields: Vec<(&str, Field)>) -> Self { + let mut graphql_fields = BTreeMap::new(); + for (name, field) in fields { + graphql_fields.insert(name.to_string(), field); + } + self.fields = graphql_fields; + self + } + + pub fn scalar(&self) -> bool { + self.fields.is_empty() + } +} + +#[derive( + Serialize, + Deserialize, + Clone, + Debug, + Default, + Setters, + PartialEq, + Eq, + schemars::JsonSchema, + MergeRight, +)] +#[setters(strip_option)] +pub struct RootSchema { + pub query: Option, + #[serde(default, skip_serializing_if = "is_default")] + pub mutation: Option, + #[serde(default, skip_serializing_if = "is_default")] + pub subscription: Option, +} + +/// +/// A field definition containing all the metadata information about resolving a +/// field. +#[derive( + Serialize, Deserialize, Clone, Debug, Default, Setters, PartialEq, Eq, schemars::JsonSchema, +)] +#[setters(strip_option)] +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: crate::core::Type, + + /// + /// Map of argument name and its definition. + #[serde(default, skip_serializing_if = "is_default")] + #[schemars(with = "HashMap::")] + pub args: IndexMap, + + /// + /// Publicly visible documentation for the field. + #[serde(default, skip_serializing_if = "is_default")] + pub doc: Option, + + /// + /// Allows modifying existing fields. + #[serde(default, skip_serializing_if = "is_default")] + pub modify: Option, + + /// + /// Omits a field from public consumption. + #[serde(default, skip_serializing_if = "is_default")] + pub omit: Option, + + /// + /// Sets the cache configuration for a field + pub cache: Option, + + /// + /// Stores the default value for the field + #[serde(default, skip_serializing_if = "is_default")] + pub default_value: Option, + + /// + /// Marks field as protected by auth provider + #[serde(default)] + pub protected: Option, + + /// + /// Used to overwrite the default discrimination strategy + pub discriminate: Option, + + /// + /// Resolver for the field + #[serde(flatten, default, skip_serializing_if = "is_default")] + pub resolver: Option, + + /// + /// Any additional directives + #[serde(default, skip_serializing_if = "is_default")] + pub directives: Vec, +} + +// It's a terminal implementation of MergeRight +impl MergeRight for Field { + fn merge_right(self, other: Self) -> Self { + other + } +} + +impl Field { + pub fn has_resolver(&self) -> bool { + self.resolver.is_some() + } + + pub fn has_batched_resolver(&self) -> bool { + self.resolver + .as_ref() + .map(Resolver::is_batched) + .unwrap_or(false) + } + + pub fn int() -> Self { + Self { type_of: "Int".to_string().into(), ..Default::default() } + } + + pub fn string() -> Self { + Self { type_of: "String".to_string().into(), ..Default::default() } + } + + pub fn float() -> Self { + Self { type_of: "Float".to_string().into(), ..Default::default() } + } + + pub fn boolean() -> Self { + Self { type_of: "Boolean".to_string().into(), ..Default::default() } + } + + pub fn id() -> Self { + Self { type_of: "ID".to_string().into(), ..Default::default() } + } + + pub fn is_omitted(&self) -> bool { + self.omit.is_some() + || self + .modify + .as_ref() + .and_then(|m| m.omit) + .unwrap_or_default() + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct Inline { + pub path: Vec, +} + +#[derive(Default, Serialize, Deserialize, Clone, Debug, PartialEq, Eq, schemars::JsonSchema)] +pub struct Arg { + #[serde(rename = "type")] + pub type_of: crate::core::Type, + #[serde(default, skip_serializing_if = "is_default")] + pub doc: Option, + #[serde(default, skip_serializing_if = "is_default")] + pub modify: Option, + #[serde(default, skip_serializing_if = "is_default")] + pub default_value: Option, +} + +#[derive( + Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, schemars::JsonSchema, MergeRight, +)] +pub struct Union { + pub types: BTreeSet, + pub doc: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, schemars::JsonSchema, MergeRight)] +/// Definition of GraphQL enum type +pub struct Enum { + pub variants: BTreeSet, + pub doc: Option, +} + +/// Definition of GraphQL value +#[derive( + Serialize, + Deserialize, + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + schemars::JsonSchema, + MergeRight, +)] +pub struct Variant { + pub name: String, + // directive: alias + pub alias: Option, +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum GraphQLOperationType { + #[default] + Query, + Mutation, +} + +impl Display for GraphQLOperationType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(match self { + Self::Query => "query", + Self::Mutation => "mutation", + }) + } +} diff --git a/src/core/blueprint/blueprint_builder/mod.rs b/src/core/blueprint/blueprint_builder/mod.rs new file mode 100644 index 0000000000..66c4c94f51 --- /dev/null +++ b/src/core/blueprint/blueprint_builder/mod.rs @@ -0,0 +1,4 @@ +pub mod blueprint_builder; +pub use blueprint_builder::*; +pub mod blueprint_builder_types; +pub use blueprint_builder_types::*; diff --git a/src/core/blueprint/blueprint_runtime/mod.rs b/src/core/blueprint/blueprint_runtime/mod.rs new file mode 100644 index 0000000000..03b96367b1 --- /dev/null +++ b/src/core/blueprint/blueprint_runtime/mod.rs @@ -0,0 +1 @@ +pub struct BlueprintRuntime {} diff --git a/src/core/blueprint/definitions.rs b/src/core/blueprint/definitions.rs index 15dc40bb41..f331e46e01 100644 --- a/src/core/blueprint/definitions.rs +++ b/src/core/blueprint/definitions.rs @@ -335,7 +335,13 @@ pub fn fix_dangling_resolvers<'a>( move |(config, field, _, name), mut b_field| { let mut set = HashSet::new(); if !field.has_resolver() - && validate_field_has_resolver(name, field, &config.types, &mut set).is_succeed() + && validate_field_has_resolver( + name, + field, + &config.blueprint_builder.types, + &mut set, + ) + .is_succeed() { b_field = b_field.resolver(Some(IR::Dynamic(DynamicValue::Value( ConstValue::Object(Default::default()), @@ -378,6 +384,7 @@ fn to_fields( config_module: &ConfigModule, ) -> Valid, String> { let operation_type = if config_module + .blueprint_builder .schema .mutation .as_deref() @@ -520,32 +527,42 @@ pub fn to_field_definition( pub fn to_definitions<'a>() -> TryFold<'a, ConfigModule, Vec, String> { TryFold::, String>::new(|config_module, _| { - Valid::from_iter(config_module.types.iter(), |(name, type_)| { - if type_.scalar() { - to_scalar_type_definition(name).trace(name) - } else { - to_object_type_definition(name, type_, config_module) - .trace(name) - .and_then(|definition| match definition.clone() { - Definition::Object(object_type_definition) => { - if config_module.input_types().contains(name) { - to_input_object_type_definition(object_type_definition).trace(name) - } else if config_module.interfaces_types_map().contains_key(name) { - to_interface_type_definition(object_type_definition).trace(name) - } else { - Valid::succeed(definition) + Valid::from_iter( + config_module.blueprint_builder.types.iter(), + |(name, type_)| { + if type_.scalar() { + to_scalar_type_definition(name).trace(name) + } else { + to_object_type_definition(name, type_, config_module) + .trace(name) + .and_then(|definition| match definition.clone() { + Definition::Object(object_type_definition) => { + if config_module.input_types().contains(name) { + to_input_object_type_definition(object_type_definition) + .trace(name) + } else if config_module.interfaces_types_map().contains_key(name) { + to_interface_type_definition(object_type_definition).trace(name) + } else { + Valid::succeed(definition) + } } - } - _ => Valid::succeed(definition), - }) - } - }) + _ => Valid::succeed(definition), + }) + } + }, + ) .map(|mut types| { - types.extend(config_module.unions.iter().map(to_union_type_definition)); + types.extend( + config_module + .blueprint_builder + .unions + .iter() + .map(to_union_type_definition), + ); types }) .fuse(Valid::from_iter( - config_module.enums.iter(), + config_module.blueprint_builder.enums.iter(), |(name, type_)| { if type_.variants.is_empty() { Valid::fail("No variants found for enum".to_string()) diff --git a/src/core/blueprint/from_config.rs b/src/core/blueprint/from_config.rs index e9a8d094b3..f89f5c4049 100644 --- a/src/core/blueprint/from_config.rs +++ b/src/core/blueprint/from_config.rs @@ -4,20 +4,23 @@ use async_graphql::dynamic::SchemaBuilder; use indexmap::IndexMap; use tailcall_valid::{Valid, ValidationError, Validator}; -use self::telemetry::to_opentelemetry; -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}; +use crate::core::config::{ + to_opentelemetry, Batch, Config, ConfigModule, ServerRuntime, UpstreamRuntime, +}; use crate::core::ir::model::{IO, IR}; use crate::core::json::JsonSchema; use crate::core::try_fold::TryFold; use crate::core::Type; pub fn config_blueprint<'a>() -> TryFold<'a, ConfigModule, Blueprint, String> { - let server = TryFoldConfig::::new(|config_module, blueprint| { - Valid::from(Server::try_from(config_module.clone())).map(|server| blueprint.server(server)) + let server = TryFoldConfig::::new(|config_module, mut blueprint| { + Valid::from(ServerRuntime::try_from(config_module.clone())).map(|server| { + blueprint.config = blueprint.config.server(server); + blueprint + }) }); let schema = to_schema().transform::( @@ -30,8 +33,11 @@ pub fn config_blueprint<'a>() -> TryFold<'a, ConfigModule, Blueprint, String> { |blueprint| blueprint.definitions, ); - let upstream = TryFoldConfig::::new(|config_module, blueprint| { - Valid::from(Upstream::try_from(config_module)).map(|upstream| blueprint.upstream(upstream)) + let upstream = TryFoldConfig::::new(|config_module, mut blueprint| { + Valid::from(UpstreamRuntime::try_from(config_module)).map(|upstream| { + blueprint.config = blueprint.config.upstream(upstream); + blueprint + }) }); let links = TryFoldConfig::::new(|config_module, blueprint| { @@ -39,8 +45,11 @@ pub fn config_blueprint<'a>() -> TryFold<'a, ConfigModule, Blueprint, String> { }); let opentelemetry = to_opentelemetry().transform::( - |opentelemetry, blueprint| blueprint.telemetry(opentelemetry), - |blueprint| blueprint.telemetry, + |opentelemetry, mut blueprint| { + blueprint.config = blueprint.config.telemetry(opentelemetry); + blueprint + }, + |blueprint| blueprint.config.telemetry, ); server @@ -63,7 +72,8 @@ pub fn apply_batching(mut blueprint: Blueprint) -> Blueprint { if let Definition::Object(object_type_definition) = def { for field in object_type_definition.fields.iter() { if let Some(IR::IO(IO::Http { group_by: Some(_), .. })) = field.resolver.clone() { - blueprint.upstream.batch = blueprint.upstream.batch.or(Some(Batch::default())); + blueprint.config.upstream.batch = + blueprint.config.upstream.batch.or(Some(Batch::default())); return blueprint; } } diff --git a/src/core/blueprint/mod.rs b/src/core/blueprint/mod.rs index 93c97302d0..1bd2c8d547 100644 --- a/src/core/blueprint/mod.rs +++ b/src/core/blueprint/mod.rs @@ -1,7 +1,7 @@ -mod auth; mod blueprint; +pub mod blueprint_builder; +mod blueprint_runtime; mod compress; -mod cors; mod definitions; mod directive; mod dynamic_value; @@ -14,15 +14,12 @@ mod links; mod mustache; mod operators; mod schema; -mod server; -pub mod telemetry; mod timeout; mod union_resolver; -mod upstream; -pub use auth::*; pub use blueprint::*; -pub use cors::*; +pub use blueprint_builder::*; +pub use blueprint_runtime::*; pub use definitions::*; pub use dynamic_value::*; pub use from_config::*; @@ -30,9 +27,7 @@ pub use index::*; pub use links::*; pub use operators::*; pub use schema::*; -pub use server::*; pub use timeout::GlobalTimeout; -pub use upstream::*; use crate::core::config::ConfigModule; use crate::core::try_fold::TryFold; diff --git a/src/core/blueprint/mustache.rs b/src/core/blueprint/mustache.rs index 8845aaa5ac..3067a17ff0 100644 --- a/src/core/blueprint/mustache.rs +++ b/src/core/blueprint/mustache.rs @@ -198,7 +198,10 @@ mod test { ..Default::default() }, ); - config.types.insert("T1".to_string(), t1_type); + config + .blueprint_builder + .types + .insert("T1".to_string(), t1_type); let type_ = Type::List { of_type: Box::new(Type::Named { name: "Int".to_string(), non_null: false }), @@ -227,8 +230,11 @@ mod test { fn test_allow_list_arguments_for_query_type() { let (config, field_def) = initialize_test_config_and_field(); - let parts_validator = - MustachePartsValidator::new(config.types.get("T1").unwrap(), &config, &field_def); + let parts_validator = MustachePartsValidator::new( + config.blueprint_builder.types.get("T1").unwrap(), + &config, + &field_def, + ); let validation_result = parts_validator.validate(&["args".to_string(), "q".to_string()], true); @@ -239,8 +245,11 @@ mod test { fn test_should_not_allow_list_arguments_for_path_variable() { let (config, field_def) = initialize_test_config_and_field(); - let parts_validator = - MustachePartsValidator::new(config.types.get("T1").unwrap(), &config, &field_def); + let parts_validator = MustachePartsValidator::new( + config.blueprint_builder.types.get("T1").unwrap(), + &config, + &field_def, + ); let validation_result = parts_validator.validate(&["args".to_string(), "q".to_string()], false); diff --git a/src/core/blueprint/operators/apollo_federation.rs b/src/core/blueprint/operators/apollo_federation.rs index 46c4a8e770..b238e35f01 100644 --- a/src/core/blueprint/operators/apollo_federation.rs +++ b/src/core/blueprint/operators/apollo_federation.rs @@ -114,7 +114,7 @@ pub fn update_federation<'a>() -> TryFoldConfig<'a, Blueprint> { return Valid::fail("Query type is not an object inside the blueprint".to_string()); }; - let Some(config_type) = config_module.types.get(&query_name) else { + let Some(config_type) = config_module.blueprint_builder.types.get(&query_name) else { return Valid::fail(format!("Cannot find type {query_name} in the config")); }; diff --git a/src/core/blueprint/operators/enum_alias.rs b/src/core/blueprint/operators/enum_alias.rs index 22eebd4965..c3532973ad 100644 --- a/src/core/blueprint/operators/enum_alias.rs +++ b/src/core/blueprint/operators/enum_alias.rs @@ -13,7 +13,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.name()); + let enum_type = config.blueprint_builder.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/schema.rs b/src/core/blueprint/schema.rs index 8ecaf24651..a2c3d83b8f 100644 --- a/src/core/blueprint/schema.rs +++ b/src/core/blueprint/schema.rs @@ -9,7 +9,7 @@ use crate::core::directive::DirectiveCodec; fn validate_query(config: &Config) -> Valid<(), String> { Valid::from_option( - config.schema.query.clone(), + config.blueprint_builder.schema.query.clone(), "Query root is missing".to_owned(), ) .and_then(|ref query_type_name| { @@ -17,7 +17,12 @@ fn validate_query(config: &Config) -> Valid<(), String> { return Valid::fail("Query type is not defined".to_owned()).trace(query_type_name); }; let mut set = HashSet::new(); - validate_type_has_resolvers(query_type_name, query, &config.types, &mut set) + validate_type_has_resolvers( + query_type_name, + query, + &config.blueprint_builder.types, + &mut set, + ) }) .unit() } @@ -66,7 +71,7 @@ pub fn validate_field_has_resolver( } fn validate_mutation(config: &Config) -> Valid<(), String> { - let mutation_type_name = config.schema.mutation.as_ref(); + let mutation_type_name = config.blueprint_builder.schema.mutation.as_ref(); if let Some(mutation_type_name) = mutation_type_name { let Some(mutation) = config.find_type(mutation_type_name) else { @@ -74,7 +79,12 @@ fn validate_mutation(config: &Config) -> Valid<(), String> { .trace(mutation_type_name); }; let mut set = HashSet::new(); - validate_type_has_resolvers(mutation_type_name, mutation, &config.types, &mut set) + validate_type_has_resolvers( + mutation_type_name, + mutation, + &config.blueprint_builder.types, + &mut set, + ) } else { Valid::succeed(()) } @@ -85,13 +95,13 @@ pub fn to_schema<'a>() -> TryFoldConfig<'a, SchemaDefinition> { validate_query(config) .and(validate_mutation(config)) .and(Valid::from_option( - config.schema.query.as_ref(), + config.blueprint_builder.schema.query.as_ref(), "Query root is missing".to_owned(), )) .zip(to_directive(config.server.to_directive())) .map(|(query_type_name, directive)| SchemaDefinition { query: query_type_name.to_owned(), - mutation: config.schema.mutation.clone(), + mutation: config.blueprint_builder.schema.mutation.clone(), directives: vec![directive], }) }) diff --git a/src/core/config/config.rs b/src/core/config/config.rs index 070ec7499b..15969f6320 100644 --- a/src/core/config/config.rs +++ b/src/core/config/config.rs @@ -1,23 +1,23 @@ -use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; -use std::fmt::{self, Display}; +use std::collections::{BTreeMap, BTreeSet, HashSet}; use anyhow::Result; use async_graphql::parser::types::ServiceDocument; use derive_setters::Setters; -use indexmap::IndexMap; use serde::{Deserialize, Serialize}; -use serde_json::Value; use strum::IntoEnumIterator; use tailcall_typedefs_common::directive_definition::DirectiveDefinition; use tailcall_typedefs_common::input_definition::InputDefinition; use tailcall_typedefs_common::ServiceDocumentBuilder; use tailcall_valid::{Valid, Validator}; -use super::directive::Directive; use super::from_document::from_document; use super::{ AddField, Alias, Cache, Call, Discriminate, Expr, GraphQL, Grpc, Http, Link, Modify, Omit, - Protected, Resolver, Server, Telemetry, Upstream, JS, + Protected, Server, Telemetry, Upstream, JS, +}; +use crate::core::blueprint::BlueprintBuilder; +pub use crate::core::blueprint::{ + Arg, Enum, Field, GraphQLOperationType, RootSchema, Type, Union, Variant, }; use crate::core::config::npo::QueryPath; use crate::core::config::source::Source; @@ -53,27 +53,6 @@ pub struct Config { #[serde(default)] pub upstream: Upstream, - /// - /// Specifies the entry points for query and mutation in the generated - /// GraphQL schema. - pub schema: RootSchema, - - /// - /// A map of all the types in the schema. - #[serde(default)] - #[setters(skip)] - pub types: BTreeMap, - - /// - /// A map of all the union types in the schema. - #[serde(default, skip_serializing_if = "is_default")] - pub unions: BTreeMap, - - /// - /// A map of all the enum types in the schema - #[serde(default, skip_serializing_if = "is_default")] - pub enums: BTreeMap, - /// /// A list of all links in the schema. #[serde(default, skip_serializing_if = "is_default")] @@ -82,273 +61,11 @@ pub struct Config { /// Enable [opentelemetry](https://opentelemetry.io) support #[serde(default, skip_serializing_if = "is_default")] pub telemetry: Telemetry, -} -/// -/// Represents a GraphQL type. -/// A type can be an object, interface, enum or scalar. -#[derive( - Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, schemars::JsonSchema, MergeRight, -)] -pub struct Type { - /// - /// A map of field name and its definition. - pub fields: BTreeMap, - #[serde(default, skip_serializing_if = "is_default")] - /// - /// Additional fields to be added to the type - pub added_fields: Vec, - #[serde(default, skip_serializing_if = "is_default")] - /// - /// Documentation for the type that is publicly visible. - pub doc: Option, - #[serde(default, skip_serializing_if = "is_default")] - /// - /// Interfaces that the type implements. - pub implements: BTreeSet, - #[serde(default, skip_serializing_if = "is_default")] - /// - /// Setting to indicate if the type can be cached. - pub cache: Option, - /// - /// Marks field as protected by auth providers - #[serde(default)] - pub protected: Option, - /// - /// Apollo federation entity resolver. - #[serde(flatten, default, skip_serializing_if = "is_default")] - pub resolver: Option, /// - /// Any additional directives - #[serde(default, skip_serializing_if = "is_default")] - pub directives: Vec, -} - -impl Display for Type { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - writeln!(f, "{{")?; - - for (field_name, field) in &self.fields { - writeln!(f, " {}: {:?},", field_name, field.type_of)?; - } - writeln!(f, "}}") - } -} - -impl Type { - pub fn fields(mut self, fields: Vec<(&str, Field)>) -> Self { - let mut graphql_fields = BTreeMap::new(); - for (name, field) in fields { - graphql_fields.insert(name.to_string(), field); - } - self.fields = graphql_fields; - self - } - - pub fn scalar(&self) -> bool { - self.fields.is_empty() - } -} - -#[derive( - Serialize, - Deserialize, - Clone, - Debug, - Default, - Setters, - PartialEq, - Eq, - schemars::JsonSchema, - MergeRight, -)] -#[setters(strip_option)] -pub struct RootSchema { - pub query: Option, + /// A builder for generating the blueprint. #[serde(default, skip_serializing_if = "is_default")] - pub mutation: Option, - #[serde(default, skip_serializing_if = "is_default")] - pub subscription: Option, -} - -/// -/// A field definition containing all the metadata information about resolving a -/// field. -#[derive( - Serialize, Deserialize, Clone, Debug, Default, Setters, PartialEq, Eq, schemars::JsonSchema, -)] -#[setters(strip_option)] -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: crate::core::Type, - - /// - /// Map of argument name and its definition. - #[serde(default, skip_serializing_if = "is_default")] - #[schemars(with = "HashMap::")] - pub args: IndexMap, - - /// - /// Publicly visible documentation for the field. - #[serde(default, skip_serializing_if = "is_default")] - pub doc: Option, - - /// - /// Allows modifying existing fields. - #[serde(default, skip_serializing_if = "is_default")] - pub modify: Option, - - /// - /// Omits a field from public consumption. - #[serde(default, skip_serializing_if = "is_default")] - pub omit: Option, - - /// - /// Sets the cache configuration for a field - pub cache: Option, - - /// - /// Stores the default value for the field - #[serde(default, skip_serializing_if = "is_default")] - pub default_value: Option, - - /// - /// Marks field as protected by auth provider - #[serde(default)] - pub protected: Option, - - /// - /// Used to overwrite the default discrimination strategy - pub discriminate: Option, - - /// - /// Resolver for the field - #[serde(flatten, default, skip_serializing_if = "is_default")] - pub resolver: Option, - - /// - /// Any additional directives - #[serde(default, skip_serializing_if = "is_default")] - pub directives: Vec, -} - -// It's a terminal implementation of MergeRight -impl MergeRight for Field { - fn merge_right(self, other: Self) -> Self { - other - } -} - -impl Field { - pub fn has_resolver(&self) -> bool { - self.resolver.is_some() - } - - pub fn has_batched_resolver(&self) -> bool { - self.resolver - .as_ref() - .map(Resolver::is_batched) - .unwrap_or(false) - } - - pub fn int() -> Self { - Self { type_of: "Int".to_string().into(), ..Default::default() } - } - - pub fn string() -> Self { - Self { type_of: "String".to_string().into(), ..Default::default() } - } - - pub fn float() -> Self { - Self { type_of: "Float".to_string().into(), ..Default::default() } - } - - pub fn boolean() -> Self { - Self { type_of: "Boolean".to_string().into(), ..Default::default() } - } - - pub fn id() -> Self { - Self { type_of: "ID".to_string().into(), ..Default::default() } - } - - pub fn is_omitted(&self) -> bool { - self.omit.is_some() - || self - .modify - .as_ref() - .and_then(|m| m.omit) - .unwrap_or_default() - } -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] -pub struct Inline { - pub path: Vec, -} - -#[derive(Default, Serialize, Deserialize, Clone, Debug, PartialEq, Eq, schemars::JsonSchema)] -pub struct Arg { - #[serde(rename = "type")] - pub type_of: crate::core::Type, - #[serde(default, skip_serializing_if = "is_default")] - pub doc: Option, - #[serde(default, skip_serializing_if = "is_default")] - pub modify: Option, - #[serde(default, skip_serializing_if = "is_default")] - pub default_value: Option, -} - -#[derive( - Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, schemars::JsonSchema, MergeRight, -)] -pub struct Union { - pub types: BTreeSet, - pub doc: Option, -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, schemars::JsonSchema, MergeRight)] -/// Definition of GraphQL enum type -pub struct Enum { - pub variants: BTreeSet, - pub doc: Option, -} - -/// Definition of GraphQL value -#[derive( - Serialize, - Deserialize, - Clone, - Debug, - PartialEq, - Eq, - PartialOrd, - Ord, - schemars::JsonSchema, - MergeRight, -)] -pub struct Variant { - pub name: String, - // directive: alias - pub alias: Option, -} - -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "lowercase")] -pub enum GraphQLOperationType { - #[default] - Query, - Mutation, -} - -impl Display for GraphQLOperationType { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str(match self { - Self::Query => "query", - Self::Mutation => "mutation", - }) - } + pub blueprint_builder: BlueprintBuilder, } impl Config { @@ -356,9 +73,9 @@ impl Config { let type_name = type_name.to_lowercase(); [ - &self.schema.query, - &self.schema.mutation, - &self.schema.subscription, + &self.blueprint_builder.schema.query, + &self.blueprint_builder.schema.mutation, + &self.blueprint_builder.schema.subscription, ] .iter() .filter_map(|&root_name| root_name.as_ref()) @@ -370,15 +87,15 @@ impl Config { } pub fn find_type(&self, name: &str) -> Option<&Type> { - self.types.get(name) + self.blueprint_builder.types.get(name) } pub fn find_union(&self, name: &str) -> Option<&Union> { - self.unions.get(name) + self.blueprint_builder.unions.get(name) } pub fn find_enum(&self, name: &str) -> Option<&Enum> { - self.enums.get(name) + self.blueprint_builder.enums.get(name) } pub fn to_yaml(&self) -> Result { @@ -399,7 +116,7 @@ impl Config { } pub fn query(mut self, query: &str) -> Self { - self.schema.query = Some(query.to_string()); + self.blueprint_builder.schema.query = Some(query.to_string()); self } @@ -408,14 +125,14 @@ impl Config { for (name, type_) in types { graphql_types.insert(name.to_string(), type_); } - self.types = graphql_types; + self.blueprint_builder.types = graphql_types; self } pub fn contains(&self, name: &str) -> bool { - self.types.contains_key(name) - || self.unions.contains_key(name) - || self.enums.contains_key(name) + self.blueprint_builder.types.contains_key(name) + || self.blueprint_builder.unions.contains_key(name) + || self.blueprint_builder.enums.contains_key(name) } pub fn from_json(json: &str) -> Result { @@ -472,7 +189,8 @@ impl Config { /// /// Checks if a type is a scalar or not. pub fn is_scalar(&self, type_name: &str) -> bool { - self.types + self.blueprint_builder + .types .get(type_name) .map_or(Scalar::is_predefined(type_name), |ty| ty.scalar()) } @@ -492,7 +210,8 @@ impl Config { /// finds the all types which are present in union. pub fn union_types(&self) -> HashSet { - self.unions + self.blueprint_builder + .unions .values() .flat_map(|union| union.types.iter().cloned()) .collect() @@ -502,11 +221,11 @@ impl Config { pub fn output_types(&self) -> HashSet { let mut types = HashSet::new(); - if let Some(ref query) = &self.schema.query { + if let Some(ref query) = &self.blueprint_builder.schema.query { types = self.find_connections(query, types); } - if let Some(ref mutation) = &self.schema.mutation { + if let Some(ref mutation) = &self.blueprint_builder.schema.mutation { types = self.find_connections(mutation, types); } @@ -516,7 +235,7 @@ impl Config { pub fn interfaces_types_map(&self) -> BTreeMap> { let mut interfaces_types: BTreeMap> = BTreeMap::new(); - for (type_name, type_definition) in self.types.iter() { + for (type_name, type_definition) in self.blueprint_builder.types.iter() { for implement_name in type_definition.implements.clone() { interfaces_types .entry(implement_name) @@ -574,7 +293,8 @@ impl Config { /// Returns a list of all the arguments in the configuration fn arguments(&self) -> Vec<(&String, &Arg)> { - self.types + self.blueprint_builder + .types .iter() .flat_map(|(_, type_of)| type_of.fields.iter()) .flat_map(|(_, field)| field.args.iter()) @@ -583,8 +303,8 @@ impl Config { /// Removes all types that are passed in the set pub fn remove_types(mut self, types: HashSet) -> Self { for unused_type in types { - self.types.remove(&unused_type); - self.unions.remove(&unused_type); + self.blueprint_builder.types.remove(&unused_type); + self.blueprint_builder.unions.remove(&unused_type); } self @@ -593,9 +313,10 @@ impl Config { pub fn unused_types(&self) -> HashSet { let used_types = self.get_all_used_type_names(); let all_types: HashSet = self + .blueprint_builder .types .keys() - .chain(self.unions.keys()) + .chain(self.blueprint_builder.unions.keys()) .cloned() .collect(); all_types.difference(&used_types).cloned().collect() @@ -605,22 +326,22 @@ impl Config { pub fn get_all_used_type_names(&self) -> HashSet { let mut set = HashSet::new(); let mut stack = Vec::new(); - if let Some(query) = &self.schema.query { + if let Some(query) = &self.blueprint_builder.schema.query { stack.push(query.clone()); } - if let Some(mutation) = &self.schema.mutation { + if let Some(mutation) = &self.blueprint_builder.schema.mutation { stack.push(mutation.clone()); } while let Some(type_name) = stack.pop() { if set.contains(&type_name) { continue; } - if let Some(union_) = self.unions.get(&type_name) { + if let Some(union_) = self.blueprint_builder.unions.get(&type_name) { set.insert(type_name); for type_ in &union_.types { stack.push(type_.clone()); } - } else if let Some(typ) = self.types.get(&type_name) { + } else if let Some(typ) = self.blueprint_builder.types.get(&type_name) { set.insert(type_name); for field in typ.fields.values() { stack.extend(field.args.values().map(|arg| arg.type_of.name().to_owned())); @@ -679,20 +400,12 @@ impl Config { } } -#[derive( - Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Default, schemars::JsonSchema, -)] -pub enum Encoding { - #[default] - ApplicationJson, - ApplicationXWwwFormUrlencoded, -} - #[cfg(test)] mod tests { use pretty_assertions::assert_eq; use super::*; + use crate::core::config::Resolver; use crate::core::directive::DirectiveCodec; #[test] @@ -765,7 +478,7 @@ mod tests { #[test] fn test_is_root_operation_type_with_query() { let mut config = Config::default(); - config.schema.query = Some("Query".to_string()); + config.blueprint_builder.schema.query = Some("Query".to_string()); assert!(config.is_root_operation_type("Query")); assert!(!config.is_root_operation_type("Mutation")); @@ -775,7 +488,7 @@ mod tests { #[test] fn test_is_root_operation_type_with_mutation() { let mut config = Config::default(); - config.schema.mutation = Some("Mutation".to_string()); + config.blueprint_builder.schema.mutation = Some("Mutation".to_string()); assert!(!config.is_root_operation_type("Query")); assert!(config.is_root_operation_type("Mutation")); @@ -785,7 +498,7 @@ mod tests { #[test] fn test_is_root_operation_type_with_subscription() { let mut config = Config::default(); - config.schema.subscription = Some("Subscription".to_string()); + config.blueprint_builder.schema.subscription = Some("Subscription".to_string()); assert!(!config.is_root_operation_type("Query")); assert!(!config.is_root_operation_type("Mutation")); diff --git a/src/core/config/config_module.rs b/src/core/config/config_module.rs index b7c617ca84..d0dcd0bf78 100644 --- a/src/core/config/config_module.rs +++ b/src/core/config/config_module.rs @@ -3,9 +3,10 @@ use std::ops::Deref; use jsonwebtoken::jwk::JwkSet; use prost_reflect::prost_types::{FileDescriptorProto, FileDescriptorSet}; -use rustls_pki_types::{CertificateDer, PrivateKeyDer}; +use rustls_pki_types::CertificateDer; use tailcall_valid::{Valid, Validator}; +use super::PrivateKey; use crate::core::config::Config; use crate::core::macros::MergeRight; use crate::core::merge_right::MergeRight; @@ -103,27 +104,6 @@ impl Deref for Content { } } -#[derive(Debug)] -pub struct PrivateKey(PrivateKeyDer<'static>); - -impl Clone for PrivateKey { - fn clone(&self) -> Self { - Self(self.0.clone_key()) - } -} - -impl From> for PrivateKey { - fn from(value: PrivateKeyDer<'static>) -> Self { - Self(value) - } -} - -impl PrivateKey { - pub fn into_inner(self) -> PrivateKeyDer<'static> { - self.0 - } -} - /// Extensions are meta-information required before we can generate the /// blueprint. Typically, this information cannot be inferred without performing /// an IO operation, i.e., reading a file, making an HTTP call, etc. diff --git a/src/core/config/config_module/merge.rs b/src/core/config/config_module/merge.rs index 0c145234d2..cee14ae5af 100644 --- a/src/core/config/config_module/merge.rs +++ b/src/core/config/config_module/merge.rs @@ -5,6 +5,7 @@ use tailcall_valid::{Valid, Validator}; use super::{Cache, ConfigModule}; use crate::core; +use crate::core::blueprint::BlueprintBuilder; use crate::core::config::{Arg, Config, Enum, Field, Type}; use crate::core::merge_right::MergeRight; use crate::core::variance::{Contravariant, Covariant, Invariant}; @@ -185,10 +186,10 @@ impl Covariant for Enum { impl Invariant for Cache { fn unify(self, other: Self) -> Valid { - let mut types = self.config.types; - let mut enums = self.config.enums; + let mut types = self.config.blueprint_builder.types; + let mut enums = self.config.blueprint_builder.enums; - Valid::from_iter(other.config.types, |(type_name, other_type)| { + Valid::from_iter(other.config.blueprint_builder.types, |(type_name, other_type)| { let trace_name = type_name.clone(); match types.remove(&type_name) { Some(ty) => { @@ -229,7 +230,7 @@ impl Invariant for Cache { .map(|ty| (type_name, ty)) .trace(&trace_name) }) - .fuse(Valid::from_iter(other.config.enums, |(name, other_enum)| { + .fuse(Valid::from_iter(other.config.blueprint_builder.enums, |(name, other_enum)| { let trace_name = name.clone(); match enums.remove(&name) { @@ -275,7 +276,17 @@ impl Invariant for Cache { enums.extend(merged_enums); let config = Config { - types, enums, unions: self.config.unions.merge_right(other.config.unions), server: self.config.server.merge_right(other.config.server), upstream: self.config.upstream.merge_right(other.config.upstream), schema: self.config.schema.merge_right(other.config.schema), links: self.config.links.merge_right(other.config.links), telemetry: self.config.telemetry.merge_right(other.config.telemetry) }; + blueprint_builder: BlueprintBuilder { + types, + enums, + unions: self.config.blueprint_builder.unions.merge_right(other.config.blueprint_builder.unions), + schema: self.config.blueprint_builder.schema.merge_right(other.config.blueprint_builder.schema), + }, + server: self.config.server.merge_right(other.config.server), + upstream: self.config.upstream.merge_right(other.config.upstream), + links: self.config.links.merge_right(other.config.links), + telemetry: self.config.telemetry.merge_right(other.config.telemetry) + }; Cache { config, diff --git a/src/core/config/directives/link.rs b/src/core/config/directives/link.rs index c9f2a372e0..650c1a08ad 100644 --- a/src/core/config/directives/link.rs +++ b/src/core/config/directives/link.rs @@ -1,33 +1,9 @@ use serde::{Deserialize, Serialize}; use tailcall_macros::DirectiveDefinition; -use crate::core::config::KeyValue; +use crate::core::config::{KeyValue, LinkType}; use crate::core::is_default; -#[derive( - Default, - Serialize, - Deserialize, - PartialEq, - Eq, - Debug, - Clone, - schemars::JsonSchema, - strum_macros::Display, -)] -pub enum LinkType { - #[default] - Config, - Protobuf, - Script, - Cert, - Key, - Operation, - Htpasswd, - Jwks, - Grpc, -} - /// The @link directive allows you to import external resources, such as /// configuration – which will be merged into the config importing it –, /// or a .proto file – which will be later used by `@grpc` directive –. diff --git a/src/core/config/directives/server.rs b/src/core/config/directives/server.rs index ec006e2023..97da76c551 100644 --- a/src/core/config/directives/server.rs +++ b/src/core/config/directives/server.rs @@ -1,12 +1,11 @@ use std::collections::{BTreeMap, BTreeSet}; -use derive_getters::Getters; -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use tailcall_macros::DirectiveDefinition; -use crate::core::config::headers::Headers; -use crate::core::config::{merge_key_value_vecs, KeyValue}; +use crate::core::config::{ + merge_key_value_vecs, Headers, HttpVersion, KeyValue, Routes, ScriptOptions, +}; use crate::core::is_default; use crate::core::macros::MergeRight; @@ -127,58 +126,11 @@ pub struct Server { pub routes: Option, } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, MergeRight, JsonSchema, Getters)] -pub struct Routes { - #[serde(default = "default_status")] - status: String, - #[serde(rename = "graphQL", default = "default_graphql")] - graphql: String, -} - -fn default_status() -> String { - "/status".into() -} - -fn default_graphql() -> String { - "/graphql".into() -} - -impl Default for Routes { - fn default() -> Self { - Self { status: "/status".into(), graphql: "/graphql".into() } - } -} - -impl Routes { - pub fn with_status>(self, status: T) -> Self { - Self { graphql: self.graphql, status: status.into() } - } - - pub fn with_graphql>(self, graphql: T) -> Self { - Self { status: self.status, graphql: graphql.into() } - } -} - fn merge_right_vars(mut left: Vec, right: Vec) -> Vec { left = merge_key_value_vecs(&left, &right); left } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, schemars::JsonSchema, MergeRight)] -#[serde(rename_all = "camelCase")] -pub struct ScriptOptions { - pub timeout: Option, -} - -#[derive( - Deserialize, Serialize, Debug, PartialEq, Eq, Clone, Default, schemars::JsonSchema, MergeRight, -)] -pub enum HttpVersion { - #[default] - HTTP1, - HTTP2, -} - impl Server { pub fn enable_apollo_tracing(&self) -> bool { self.apollo_tracing.unwrap_or(false) diff --git a/src/core/config/directives/telemetry.rs b/src/core/config/directives/telemetry.rs index 108ff2aab3..d2be665813 100644 --- a/src/core/config/directives/telemetry.rs +++ b/src/core/config/directives/telemetry.rs @@ -3,70 +3,12 @@ use serde::{Deserialize, Serialize}; use tailcall_macros::{DirectiveDefinition, InputDefinition}; use tailcall_valid::Validator; -use crate::core::config::{Apollo, ConfigReaderContext, KeyValue}; +use crate::core::config::{ConfigReaderContext, KeyValue, TelemetryExporterConfig}; use crate::core::helpers::headers::to_mustache_headers; use crate::core::is_default; -use crate::core::macros::MergeRight; use crate::core::merge_right::MergeRight; use crate::core::mustache::Mustache; -mod defaults { - pub mod prometheus { - pub fn path() -> String { - "/metrics".to_owned() - } - } -} - -/// Output the opentelemetry data to the stdout. Mostly used for debug purposes -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema, MergeRight)] -#[serde(rename_all = "camelCase")] -pub struct StdoutExporter { - /// Output to stdout in pretty human-readable format - #[serde(default, skip_serializing_if = "is_default")] - pub pretty: bool, -} - -/// Output the opentelemetry data to otlp collector -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema, MergeRight)] -#[serde(rename_all = "camelCase")] -pub struct OtlpExporter { - pub url: String, - #[serde(default, skip_serializing_if = "is_default")] - pub headers: Vec, -} - -/// Output format for prometheus data -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema)] -#[serde(rename_all = "camelCase")] -pub enum PrometheusFormat { - #[default] - Text, - Protobuf, -} - -/// Output the telemetry metrics data to prometheus server -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct PrometheusExporter { - #[serde( - default = "defaults::prometheus::path", - skip_serializing_if = "is_default" - )] - pub path: String, - #[serde(default, skip_serializing_if = "is_default")] - pub format: PrometheusFormat, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema, MergeRight)] -#[serde(rename_all = "camelCase")] -pub enum TelemetryExporter { - Stdout(StdoutExporter), - Otlp(OtlpExporter), - Prometheus(PrometheusExporter), - Apollo(Apollo), -} - #[derive( Debug, Default, @@ -87,7 +29,7 @@ pub enum TelemetryExporter { /// by Tailcall. By leveraging this directive, developers gain access to /// valuable insights into the performance and behavior of their applications. pub struct Telemetry { - pub export: Option, + pub export: Option, /// The list of headers that will be sent as additional attributes to /// telemetry exporters Be careful about **leaking sensitive /// information** from requests when enabling the headers that may @@ -111,7 +53,7 @@ impl Telemetry { pub fn render_mustache(&mut self, reader_ctx: &ConfigReaderContext) -> Result<()> { match &mut self.export { - Some(TelemetryExporter::Otlp(otlp)) => { + Some(TelemetryExporterConfig::Otlp(otlp)) => { let url_tmpl = Mustache::parse(&otlp.url); otlp.url = url_tmpl.render(reader_ctx); @@ -122,7 +64,7 @@ impl Telemetry { .map(|(key, value)| KeyValue { key, value }) .collect(); } - Some(TelemetryExporter::Apollo(apollo)) => apollo.render_mustache(reader_ctx)?, + Some(TelemetryExporterConfig::Apollo(apollo)) => apollo.render_mustache(reader_ctx)?, _ => {} } @@ -133,37 +75,42 @@ impl Telemetry { #[cfg(test)] mod tests { use super::*; + use crate::core::config::{ + OtlpExporterConfig, PrometheusExporter, PrometheusFormat, StdoutExporter, + }; #[test] fn merge_right() { let exporter_none = Telemetry { export: None, ..Default::default() }; let exporter_stdout = Telemetry { - export: Some(TelemetryExporter::Stdout(StdoutExporter { pretty: true })), + export: Some(TelemetryExporterConfig::Stdout(StdoutExporter { + pretty: true, + })), ..Default::default() }; let exporter_otlp_1 = Telemetry { - export: Some(TelemetryExporter::Otlp(OtlpExporter { + export: Some(TelemetryExporterConfig::Otlp(OtlpExporterConfig { url: "test-url".to_owned(), headers: vec![KeyValue { key: "header_a".to_owned(), value: "a".to_owned() }], })), request_headers: vec!["Api-Key-A".to_owned()], }; let exporter_otlp_2 = Telemetry { - export: Some(TelemetryExporter::Otlp(OtlpExporter { + export: Some(TelemetryExporterConfig::Otlp(OtlpExporterConfig { url: "test-url-2".to_owned(), headers: vec![KeyValue { key: "header_b".to_owned(), value: "b".to_owned() }], })), request_headers: vec!["Api-Key-B".to_owned()], }; let exporter_prometheus_1 = Telemetry { - export: Some(TelemetryExporter::Prometheus(PrometheusExporter { + export: Some(TelemetryExporterConfig::Prometheus(PrometheusExporter { path: "/metrics".to_owned(), format: PrometheusFormat::Text, })), ..Default::default() }; let exporter_prometheus_2 = Telemetry { - export: Some(TelemetryExporter::Prometheus(PrometheusExporter { + export: Some(TelemetryExporterConfig::Prometheus(PrometheusExporter { path: "/prom".to_owned(), format: PrometheusFormat::Protobuf, })), @@ -198,7 +145,7 @@ mod tests { assert_eq!( exporter_otlp_1.clone().merge_right(exporter_otlp_2.clone()), Telemetry { - export: Some(TelemetryExporter::Otlp(OtlpExporter { + export: Some(TelemetryExporterConfig::Otlp(OtlpExporterConfig { url: "test-url-2".to_owned(), headers: vec![KeyValue { key: "header_b".to_owned(), value: "b".to_owned() }] })), diff --git a/src/core/config/directives/upstream.rs b/src/core/config/directives/upstream.rs index 0beeb99d42..883632447d 100644 --- a/src/core/config/directives/upstream.rs +++ b/src/core/config/directives/upstream.rs @@ -4,36 +4,10 @@ use derive_setters::Setters; use serde::{Deserialize, Serialize}; use tailcall_macros::{DirectiveDefinition, InputDefinition}; +use crate::core::config::{Batch, Proxy, DEFAULT_MAX_SIZE}; use crate::core::macros::MergeRight; use crate::core::{default_verify_ssl, is_default, verify_ssl_is_default}; -const DEFAULT_MAX_SIZE: usize = 100; - -#[derive( - Serialize, Deserialize, PartialEq, Eq, Clone, Debug, Setters, schemars::JsonSchema, MergeRight, -)] -#[serde(rename_all = "camelCase", default)] -pub struct Batch { - pub delay: usize, - pub headers: BTreeSet, - #[serde(default, skip_serializing_if = "is_default")] - pub max_size: Option, -} -impl Default for Batch { - fn default() -> Self { - Batch { - max_size: Some(DEFAULT_MAX_SIZE), - delay: 0, - headers: BTreeSet::new(), - } - } -} - -#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug, schemars::JsonSchema, MergeRight)] -pub struct Proxy { - pub url: String, -} - #[derive( Serialize, Deserialize, diff --git a/src/core/config/from_document.rs b/src/core/config/from_document.rs index 3ccd7b72b6..379b6961cb 100644 --- a/src/core/config/from_document.rs +++ b/src/core/config/from_document.rs @@ -13,6 +13,7 @@ use tailcall_valid::{Valid, ValidationError, Validator}; use super::directive::{to_directive, Directive}; use super::{Alias, Discriminate, Resolver, Telemetry, FEDERATION_DIRECTIVES}; +use crate::core::blueprint::BlueprintBuilder; use crate::core::config::{ self, Cache, Config, Enum, Link, Modify, Omit, Protected, RootSchema, Server, Union, Upstream, Variant, @@ -54,12 +55,9 @@ pub fn from_document(doc: ServiceDocument) -> Valid { |(server, upstream, types, unions, enums, schema, links, telemetry)| Config { server, upstream, - types, - unions, - enums, - schema, links, telemetry, + blueprint_builder: BlueprintBuilder { types, unions, enums, schema }, }, ) }) diff --git a/src/core/config/into_document.rs b/src/core/config/into_document.rs index b06ff84311..561b39d613 100644 --- a/src/core/config/into_document.rs +++ b/src/core/config/into_document.rs @@ -43,13 +43,20 @@ fn config_document(config: &Config) -> ServiceDocument { let schema_definition = SchemaDefinition { extend: false, directives, - query: config.schema.query.clone().map(|name| pos(Name::new(name))), + query: config + .blueprint_builder + .schema + .query + .clone() + .map(|name| pos(Name::new(name))), mutation: config + .blueprint_builder .schema .mutation .clone() .map(|name| pos(Name::new(name))), subscription: config + .blueprint_builder .schema .subscription .clone() @@ -58,7 +65,7 @@ fn config_document(config: &Config) -> ServiceDocument { definitions.push(TypeSystemDefinition::Schema(pos(schema_definition))); let interface_types = config.interfaces_types_map(); let input_types = config.input_types(); - for (type_name, type_def) in config.types.iter() { + for (type_name, type_def) in config.blueprint_builder.types.iter() { let kind = if interface_types.contains_key(type_name) { TypeKind::Interface(InterfaceType { implements: type_def @@ -159,7 +166,7 @@ fn config_document(config: &Config) -> ServiceDocument { kind, }))); } - for (name, union) in config.unions.iter() { + for (name, union) in config.blueprint_builder.unions.iter() { definitions.push(TypeSystemDefinition::Type(pos(TypeDefinition { extend: false, description: None, @@ -175,7 +182,7 @@ fn config_document(config: &Config) -> ServiceDocument { }))); } - for (name, values) in config.enums.iter() { + for (name, values) in config.blueprint_builder.enums.iter() { definitions.push(TypeSystemDefinition::Type(pos(TypeDefinition { extend: false, description: values.doc.clone().map(pos), diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index 8236e783ce..72b1384cfd 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -1,29 +1,25 @@ -pub use apollo::*; pub use config::*; pub use config_module::*; pub use directive::Directive; pub use directives::*; -pub use key_values::*; pub use npo::QueryPath; -pub use reader_context::*; pub use resolver::*; -pub use source::*; pub use url_query::*; -mod apollo; mod config; mod config_module; -pub mod cors; mod directive; pub mod directives; mod from_document; pub mod group_by; -mod headers; mod into_document; -mod key_values; mod npo; pub mod reader; -pub mod reader_context; mod resolver; -mod source; +pub mod runtime_config; +pub mod static_config; pub mod transformer; mod url_query; +pub mod utils; +pub use runtime_config::*; +pub use static_config::*; +pub use utils::*; diff --git a/src/core/config/npo/tracker.rs b/src/core/config/npo/tracker.rs index 80d5c0f800..412e556c55 100644 --- a/src/core/config/npo/tracker.rs +++ b/src/core/config/npo/tracker.rs @@ -153,7 +153,7 @@ impl<'a> PathTracker<'a> { } fn find_chunks(&mut self) -> Chunk>> { - match &self.config.schema.query { + match &self.config.blueprint_builder.schema.query { None => Chunk::new(), Some(query) => self.iter( Chunk::new(), diff --git a/src/core/config/reader.rs b/src/core/config/reader.rs index fc6c6c9ebc..1556848df5 100644 --- a/src/core/config/reader.rs +++ b/src/core/config/reader.rs @@ -265,7 +265,7 @@ mod reader_tests { let runtime = crate::core::runtime::test::init(None); let mut cfg = Config::default(); - cfg.schema.query = Some("Test".to_string()); + cfg.blueprint_builder.schema.query = Some("Test".to_string()); cfg = cfg.types([("Test", Type::default())].to_vec()); let server = start_mock_server(); @@ -301,7 +301,8 @@ mod reader_tests { .iter() .map(|i| i.to_string()) .collect::>(), - c.types + c.blueprint_builder + .types .keys() .map(|i| i.to_string()) .collect::>() @@ -329,7 +330,8 @@ mod reader_tests { .iter() .map(|i| i.to_string()) .collect::>(), - c.types + c.blueprint_builder + .types .keys() .map(|i| i.to_string()) .collect::>() diff --git a/src/core/config/runtime_config/mod.rs b/src/core/config/runtime_config/mod.rs new file mode 100644 index 0000000000..904b5e8aca --- /dev/null +++ b/src/core/config/runtime_config/mod.rs @@ -0,0 +1,8 @@ +pub mod server_runtime; +pub use server_runtime::*; +pub mod telemetry_runtime; +pub use telemetry_runtime::*; +pub mod upstream_runtime; +pub use upstream_runtime::*; +pub mod runtime_config; +pub use runtime_config::*; diff --git a/src/core/config/runtime_config/runtime_config.rs b/src/core/config/runtime_config/runtime_config.rs new file mode 100644 index 0000000000..32af28f704 --- /dev/null +++ b/src/core/config/runtime_config/runtime_config.rs @@ -0,0 +1,10 @@ +use derive_setters::Setters; + +use super::{ServerRuntime, TelemetryRuntime, UpstreamRuntime}; + +#[derive(Clone, Debug, Default, Setters)] +pub struct RuntimeConfig { + pub server: ServerRuntime, + pub upstream: UpstreamRuntime, + pub telemetry: TelemetryRuntime, +} diff --git a/src/core/blueprint/server.rs b/src/core/config/runtime_config/server_runtime.rs similarity index 87% rename from src/core/blueprint/server.rs rename to src/core/config/runtime_config/server_runtime.rs index e8e0f54acd..03b744e1b7 100644 --- a/src/core/blueprint/server.rs +++ b/src/core/config/runtime_config/server_runtime.rs @@ -8,12 +8,16 @@ use http::header::{HeaderMap, HeaderName, HeaderValue}; use rustls_pki_types::CertificateDer; use tailcall_valid::{Valid, ValidationError, Validator}; -use super::Auth; -use crate::core::blueprint::Cors; +use crate::core::config::cors_static::CorsStatic; use crate::core::config::{self, ConfigModule, HttpVersion, PrivateKey, Routes}; +pub mod auth_runtime; +pub use auth_runtime::*; +pub mod cors_runtime; +pub use cors_runtime::*; + #[derive(Clone, Debug, Setters)] -pub struct Server { +pub struct ServerRuntime { pub enable_jit: bool, pub enable_apollo_tracing: bool, pub enable_cache_control_header: bool, @@ -29,24 +33,24 @@ pub struct Server { pub hostname: IpAddr, pub vars: BTreeMap, pub response_headers: HeaderMap, - pub http: Http, + pub http: HttpVersionRuntime, pub pipeline_flush: bool, - pub script: Option