diff --git a/Cargo.lock b/Cargo.lock index 21d0ed4fab..78e41cf401 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4936,9 +4936,9 @@ checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" [[package]] name = "serde" -version = "1.0.214" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ "serde_derive", ] @@ -4986,9 +4986,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.214" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", @@ -5008,9 +5008,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.132" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "indexmap 2.6.0", "itoa", diff --git a/src/cli/generator/generator.rs b/src/cli/generator/generator.rs index 4cdbb1ea50..38d4920a53 100644 --- a/src/cli/generator/generator.rs +++ b/src/cli/generator/generator.rs @@ -78,11 +78,7 @@ impl Generator { // While reading resolve the internal paths and mustache headers of generalized // config. - let reader_context = ConfigReaderContext { - runtime: &self.runtime, - vars: &Default::default(), - headers: Default::default(), - }; + let reader_context = ConfigReaderContext::new(&self.runtime); config_content = Mustache::parse(&config_content).render(&reader_context); let config: Config = match source { diff --git a/src/core/config/reader.rs b/src/core/config/reader.rs index ec24107aba..609cb0eb62 100644 --- a/src/core/config/reader.rs +++ b/src/core/config/reader.rs @@ -40,6 +40,8 @@ impl ConfigReader { config_module: ConfigModule, parent_dir: Option<&'async_recursion Path>, ) -> anyhow::Result { + let reader_ctx = ConfigReaderContext::new(&self.runtime); + let links: Vec = config_module .config() .links @@ -65,7 +67,11 @@ impl ConfigReader { match link.type_of { LinkType::Config => { - let source = self.resource_reader.read_file(path).await?; + let source = self + .resource_reader + .read_file(path) + .await? + .render(&reader_ctx); let content = source.content; let config = Config::from_source(Source::detect(&source.path)?, &content)?; config_module = config_module.and_then(|config_module| { @@ -184,7 +190,16 @@ impl ConfigReader { &self, files: &[T], ) -> anyhow::Result { - let files = self.resource_reader.read_files(files).await?; + let reader_ctx = ConfigReaderContext::new(&self.runtime); + + let files = self + .resource_reader + .read_files(files) + .await? + .into_iter() + .map(|file| file.render(&reader_ctx)) + .collect::>(); + let mut config_module = Valid::succeed(ConfigModule::default()); for file in files.iter() { @@ -214,16 +229,13 @@ impl ConfigReader { parent_dir: Option<&Path>, ) -> anyhow::Result { // Setup telemetry in Config - let reader_ctx = ConfigReaderContext { - runtime: &self.runtime, - vars: &config - .server - .vars - .iter() - .map(|vars| (vars.key.clone(), vars.value.clone())) - .collect(), - headers: Default::default(), - }; + let vars = &config + .server + .vars + .iter() + .map(|vars| (vars.key.clone(), vars.value.clone())) + .collect(); + let reader_ctx = ConfigReaderContext::new(&self.runtime).vars(vars); config.telemetry.render_mustache(&reader_ctx)?; // Create initial config set & extend it with the links diff --git a/src/core/config/reader_context.rs b/src/core/config/reader_context.rs index ef228e3535..696156b365 100644 --- a/src/core/config/reader_context.rs +++ b/src/core/config/reader_context.rs @@ -1,18 +1,27 @@ use std::borrow::Cow; use std::collections::BTreeMap; +use derive_setters::Setters; use http::header::HeaderMap; use crate::core::has_headers::HasHeaders; use crate::core::path::PathString; use crate::core::runtime::TargetRuntime; +#[derive(Setters)] pub struct ConfigReaderContext<'a> { pub runtime: &'a TargetRuntime, - pub vars: &'a BTreeMap, + #[setters(strip_option)] + pub vars: Option<&'a BTreeMap>, pub headers: HeaderMap, } +impl<'a> ConfigReaderContext<'a> { + pub fn new(runtime: &'a TargetRuntime) -> Self { + Self { runtime, vars: None, headers: Default::default() } + } +} + impl PathString for ConfigReaderContext<'_> { fn path_string>(&self, path: &[T]) -> Option> { if path.is_empty() { @@ -21,7 +30,7 @@ impl PathString for ConfigReaderContext<'_> { path.split_first() .and_then(|(head, tail)| match head.as_ref() { - "vars" => self.vars.get(tail[0].as_ref()).map(|v| v.into()), + "vars" => self.vars?.get(tail[0].as_ref()).map(|v| v.into()), "env" => self.runtime.env.get(tail[0].as_ref()), _ => None, }) @@ -49,11 +58,12 @@ mod tests { "ENV_VAL".to_owned(), )])); - let reader_context = ConfigReaderContext { - runtime: &runtime, - vars: &BTreeMap::from_iter([("VAR_1".to_owned(), "VAR_VAL".to_owned())]), - headers: Default::default(), - }; + let vars = &[("VAR_1".to_owned(), "VAR_VAL".to_owned())] + .iter() + .cloned() + .collect(); + + let reader_context = ConfigReaderContext::new(&runtime).vars(vars); assert_eq!( reader_context.path_string(&["env", "ENV_1"]), diff --git a/src/core/mustache/eval.rs b/src/core/mustache/eval.rs index 0726856af0..5fd1a011dc 100644 --- a/src/core/mustache/eval.rs +++ b/src/core/mustache/eval.rs @@ -10,10 +10,37 @@ pub trait Eval<'a> { pub struct PathStringEval(std::marker::PhantomData); +impl Default for PathStringEval { + fn default() -> Self { + Self::new() + } +} + impl PathStringEval { pub fn new() -> Self { Self(std::marker::PhantomData) } + + /// Tries to evaluate the mustache template with the given value. + /// If a path/value is not found, the template will be rendered as is. + pub fn eval_partial(&self, mustache: &Mustache, in_value: &A) -> String + where + A: PathString, + { + mustache + .segments() + .iter() + .map(|segment| match segment { + Segment::Literal(text) => text.clone(), + Segment::Expression(parts) => in_value + .path_string(parts) + .map(|a| a.to_string()) + .unwrap_or( + Mustache::from(vec![Segment::Expression(parts.to_vec())]).to_string(), + ), + }) + .collect() + } } impl Eval<'_> for PathStringEval { @@ -107,7 +134,6 @@ impl Mustache { #[cfg(test)] mod tests { - mod render { use std::borrow::Cow; diff --git a/src/core/mustache/mod.rs b/src/core/mustache/mod.rs index 13f906c339..723765f76d 100644 --- a/src/core/mustache/mod.rs +++ b/src/core/mustache/mod.rs @@ -1,5 +1,5 @@ mod eval; mod model; mod parse; -pub use eval::Eval; +pub use eval::{Eval, PathStringEval}; pub use model::*; diff --git a/src/core/mustache/model.rs b/src/core/mustache/model.rs index c007e7a875..6717533c48 100644 --- a/src/core/mustache/model.rs +++ b/src/core/mustache/model.rs @@ -62,7 +62,7 @@ impl Display for Mustache { .iter() .map(|segment| match segment { Segment::Literal(text) => text.clone(), - Segment::Expression(parts) => format!("{{{{{}}}}}", parts.join(".")), + Segment::Expression(parts) => format!("{{{{.{}}}}}", parts.join(".")), }) .collect::>() .join(""); diff --git a/src/core/mustache/parse.rs b/src/core/mustache/parse.rs index a814e22f53..6035f7895b 100644 --- a/src/core/mustache/parse.rs +++ b/src/core/mustache/parse.rs @@ -88,14 +88,14 @@ mod tests { #[test] fn test_to_string() { let expectations = vec![ - r"/users/{{value.id}}/todos", - r"http://localhost:8090/{{foo.bar}}/api/{{hello.world}}/end", - r"http://localhost:{{args.port}}", - r"/users/{{value.userId}}", - r"/bar?id={{args.id}}&flag={{args.flag}}", - r"/foo?id={{value.id}}", - r"{{value.d}}", - r"/posts/{{args.id}}", + r"/users/{{.value.id}}/todos", + r"http://localhost:8090/{{.foo.bar}}/api/{{.hello.world}}/end", + r"http://localhost:{{.args.port}}", + r"/users/{{.value.userId}}", + r"/bar?id={{.args.id}}&flag={{.args.flag}}", + r"/foo?id={{.value.id}}", + r"{{.value.d}}", + r"/posts/{{.args.id}}", r"http://localhost:8000", ]; diff --git a/src/core/proto_reader/fetch.rs b/src/core/proto_reader/fetch.rs index 7267918fe0..da2a34f5c8 100644 --- a/src/core/proto_reader/fetch.rs +++ b/src/core/proto_reader/fetch.rs @@ -169,11 +169,7 @@ impl GrpcReflection { operation_type: Default::default(), }; - let ctx = ConfigReaderContext { - runtime: &self.target_runtime, - vars: &Default::default(), - headers: Default::default(), - }; + let ctx = ConfigReaderContext::new(&self.target_runtime); let req = req_template.render(&ctx)?.to_request()?; let resp = self.target_runtime.http2_only.execute(req).await?; diff --git a/src/core/resource_reader.rs b/src/core/resource_reader.rs index 3352d1c1c1..5d0aed446c 100644 --- a/src/core/resource_reader.rs +++ b/src/core/resource_reader.rs @@ -7,7 +7,10 @@ use futures_util::TryFutureExt; use tailcall_hasher::TailcallHasher; use url::Url; +use crate::core::mustache::PathStringEval; +use crate::core::path::PathString; use crate::core::runtime::TargetRuntime; +use crate::core::Mustache; /// Response of a file read operation #[derive(Debug)] @@ -16,6 +19,16 @@ pub struct FileRead { pub path: String, } +impl FileRead { + /// Renders the content of the file using the given context + pub fn render(mut self, context: &impl PathString) -> Self { + let mustache = Mustache::parse(&self.content); + let schema = PathStringEval::new().eval_partial(&mustache, context); + self.content = schema; + self + } +} + /// Supported Resources by Resource Reader pub enum Resource { RawPath(String), diff --git a/tests/core/snapshots/env-value.md_merged.snap b/tests/core/snapshots/env-value.md_merged.snap index ae2ea9bd84..0a7f593d45 100644 --- a/tests/core/snapshots/env-value.md_merged.snap +++ b/tests/core/snapshots/env-value.md_merged.snap @@ -14,7 +14,7 @@ type Post { } type Query { - post1: Post @http(url: "http://jsonplaceholder.typicode.com/posts/{{.env.ID}}") - post2: Post @http(url: "http://jsonplaceholder.typicode.com/posts/{{.env.POST_ID}}") - post3: Post @http(url: "http://jsonplaceholder.typicode.com/posts/{{.env.NESTED_POST_ID}}") + post1: Post @http(url: "http://jsonplaceholder.typicode.com/posts/1") + post2: Post @http(url: "http://jsonplaceholder.typicode.com/posts/2") + post3: Post @http(url: "http://jsonplaceholder.typicode.com/posts/3") } diff --git a/tests/core/snapshots/test-eval-partial.md_client.snap b/tests/core/snapshots/test-eval-partial.md_client.snap new file mode 100644 index 0000000000..451a92880a --- /dev/null +++ b/tests/core/snapshots/test-eval-partial.md_client.snap @@ -0,0 +1,22 @@ +--- +source: tests/core/spec.rs +expression: formatted +--- +type Post { + foo: String + id: Int! + user: User + userId: Int! +} + +type Query { + post(id: Int!): [Post] +} + +type User { + id: Int! +} + +schema { + query: Query +} diff --git a/tests/core/snapshots/test-eval-partial.md_merged.snap b/tests/core/snapshots/test-eval-partial.md_merged.snap new file mode 100644 index 0000000000..2d8f99a4cf --- /dev/null +++ b/tests/core/snapshots/test-eval-partial.md_merged.snap @@ -0,0 +1,22 @@ +--- +source: tests/core/spec.rs +expression: formatter +--- +schema @server(port: 8080) @upstream(batch: {delay: 100, headers: []}, httpCache: 42) { + query: Query +} + +type Post { + foo: String @http(url: "http://jsonplaceholder.typicode.com/posts/foo") + id: Int! + user: User @http(url: "http://jsonplaceholder.typicode.com/users/{{.value.userId}}") + userId: Int! +} + +type Query { + post(id: Int!): [Post] @http(url: "http://jsonplaceholder.typicode.com/posts/{{.args.id}}") +} + +type User { + id: Int! +} diff --git a/tests/core/snapshots/yaml-nested-unions.md_merged.snap b/tests/core/snapshots/yaml-nested-unions.md_merged.snap index e9ebca71ad..0d8adaebdc 100644 --- a/tests/core/snapshots/yaml-nested-unions.md_merged.snap +++ b/tests/core/snapshots/yaml-nested-unions.md_merged.snap @@ -34,11 +34,11 @@ union U1 = T1 | T2 | T3 union U2 = T3 | T4 type Query { - testVar0(u: T1Input!): U @http(url: "http://localhost/users/{{args.u}}") - testVar1(u: T2Input!): U @http(url: "http://localhost/users/{{args.u}}") - testVar2(u: T3Input!): U @http(url: "http://localhost/users/{{args.u}}") - testVar3(u: T4Input!): U @http(url: "http://localhost/users/{{args.u}}") - testVar4(u: T5Input!): U @http(url: "http://localhost/users/{{args.u}}") + testVar0(u: T1Input!): U @http(url: "http://localhost/users/{{.args.u}}") + testVar1(u: T2Input!): U @http(url: "http://localhost/users/{{.args.u}}") + testVar2(u: T3Input!): U @http(url: "http://localhost/users/{{.args.u}}") + testVar3(u: T4Input!): U @http(url: "http://localhost/users/{{.args.u}}") + testVar4(u: T5Input!): U @http(url: "http://localhost/users/{{.args.u}}") } type T1 { diff --git a/tests/core/snapshots/yaml-union-in-type.md_merged.snap b/tests/core/snapshots/yaml-union-in-type.md_merged.snap index 6d07e24486..500685a6e2 100644 --- a/tests/core/snapshots/yaml-union-in-type.md_merged.snap +++ b/tests/core/snapshots/yaml-union-in-type.md_merged.snap @@ -66,15 +66,15 @@ type NU { } type Query { - testVar0Var0(nu: NU__u0!, nnu: NNU__nu0): U @http(url: "http://localhost/users/{{args.nu.u}}") - testVar0Var1(nu: NU__u0!, nnu: NNU__nu1): U @http(url: "http://localhost/users/{{args.nu.u}}") - testVar0Var2(nu: NU__u0!, nnu: NNU__nu2): U @http(url: "http://localhost/users/{{args.nu.u}}") - testVar1Var0(nu: NU__u1!, nnu: NNU__nu0): U @http(url: "http://localhost/users/{{args.nu.u}}") - testVar1Var1(nu: NU__u1!, nnu: NNU__nu1): U @http(url: "http://localhost/users/{{args.nu.u}}") - testVar1Var2(nu: NU__u1!, nnu: NNU__nu2): U @http(url: "http://localhost/users/{{args.nu.u}}") - testVar2Var0(nu: NU__u2!, nnu: NNU__nu0): U @http(url: "http://localhost/users/{{args.nu.u}}") - testVar2Var1(nu: NU__u2!, nnu: NNU__nu1): U @http(url: "http://localhost/users/{{args.nu.u}}") - testVar2Var2(nu: NU__u2!, nnu: NNU__nu2): U @http(url: "http://localhost/users/{{args.nu.u}}") + testVar0Var0(nu: NU__u0!, nnu: NNU__nu0): U @http(url: "http://localhost/users/{{.args.nu.u}}") + testVar0Var1(nu: NU__u0!, nnu: NNU__nu1): U @http(url: "http://localhost/users/{{.args.nu.u}}") + testVar0Var2(nu: NU__u0!, nnu: NNU__nu2): U @http(url: "http://localhost/users/{{.args.nu.u}}") + testVar1Var0(nu: NU__u1!, nnu: NNU__nu0): U @http(url: "http://localhost/users/{{.args.nu.u}}") + testVar1Var1(nu: NU__u1!, nnu: NNU__nu1): U @http(url: "http://localhost/users/{{.args.nu.u}}") + testVar1Var2(nu: NU__u1!, nnu: NNU__nu2): U @http(url: "http://localhost/users/{{.args.nu.u}}") + testVar2Var0(nu: NU__u2!, nnu: NNU__nu0): U @http(url: "http://localhost/users/{{.args.nu.u}}") + testVar2Var1(nu: NU__u2!, nnu: NNU__nu1): U @http(url: "http://localhost/users/{{.args.nu.u}}") + testVar2Var2(nu: NU__u2!, nnu: NNU__nu2): U @http(url: "http://localhost/users/{{.args.nu.u}}") } type T1 { diff --git a/tests/core/snapshots/yaml-union.md_merged.snap b/tests/core/snapshots/yaml-union.md_merged.snap index b2288bc17a..f4fa97e00b 100644 --- a/tests/core/snapshots/yaml-union.md_merged.snap +++ b/tests/core/snapshots/yaml-union.md_merged.snap @@ -30,9 +30,9 @@ type NU { } type Query { - testVar0(u: T1Input!): U @http(url: "http://localhost/users/{{args.u}}/") - testVar1(u: T2Input!): U @http(url: "http://localhost/users/{{args.u}}/") - testVar2(u: T3Input!): U @http(url: "http://localhost/users/{{args.u}}/") + testVar0(u: T1Input!): U @http(url: "http://localhost/users/{{.args.u}}/") + testVar1(u: T2Input!): U @http(url: "http://localhost/users/{{.args.u}}/") + testVar2(u: T3Input!): U @http(url: "http://localhost/users/{{.args.u}}/") } type T1 { diff --git a/tests/core/spec.rs b/tests/core/spec.rs index 2866aefa4c..4170bb42bb 100644 --- a/tests/core/spec.rs +++ b/tests/core/spec.rs @@ -16,10 +16,12 @@ use tailcall::core::async_graphql_hyper::{GraphQLBatchRequest, GraphQLRequest}; use tailcall::core::blueprint::{Blueprint, BlueprintError}; use tailcall::core::config::reader::ConfigReader; use tailcall::core::config::transformer::Required; -use tailcall::core::config::{Config, ConfigModule, Source}; +use tailcall::core::config::{Config, ConfigModule, ConfigReaderContext, Source}; use tailcall::core::http::handle_request; +use tailcall::core::mustache::PathStringEval; use tailcall::core::print_schema::print_schema; use tailcall::core::variance::Invariant; +use tailcall::core::Mustache; use tailcall_prettier::Parser; use tailcall_valid::{Cause, Valid, ValidationError, Validator}; @@ -87,7 +89,7 @@ async fn is_sdl_error(spec: &ExecutionSpec, config_module: Valid) { // TODO: we should probably figure out a way to do this for every test // but GraphQL identity checking is very hard, since a lot depends on the code // style the re-serializing check gives us some of the advantages of the @@ -97,7 +99,9 @@ async fn check_identity(spec: &ExecutionSpec) { if spec.check_identity { for (source, content) in spec.server.iter() { if matches!(source, Source::GraphQL) { - let config = Config::from_source(source.to_owned(), content).unwrap(); + let mustache = Mustache::parse(content); + let content = PathStringEval::new().eval_partial(&mustache, reader_ctx); + let config = Config::from_source(source.to_owned(), &content).unwrap(); let actual = config.to_sdl(); // \r is added automatically in windows, it's safe to replace it with \n @@ -190,11 +194,18 @@ async fn test_spec(spec: ExecutionSpec) { let mut runtime = runtime::create_runtime(mock_http_client.clone(), spec.env.clone(), None); runtime.file = Arc::new(File::new(spec.clone())); + + let runtime_clone = runtime.clone(); + let reader_ctx = ConfigReaderContext::new(&runtime_clone); + let reader = ConfigReader::init(runtime); // Resolve all configs let config_modules = join_all(spec.server.iter().map(|(source, content)| async { - let config = Config::from_source(source.to_owned(), content)?; + let mustache = Mustache::parse(content); + let content = PathStringEval::new().eval_partial(&mustache, &reader_ctx); + + let config = Config::from_source(source.to_owned(), &content)?; reader.resolve(config, spec.path.parent()).await })) @@ -237,7 +248,7 @@ async fn test_spec(spec: ExecutionSpec) { .collect::, _>>() .unwrap(); - check_identity(&spec).await; + check_identity(&spec, &reader_ctx).await; // client: Check if client spec matches snapshot if config_modules.len() == 1 { diff --git a/tests/execution/test-eval-partial.md b/tests/execution/test-eval-partial.md new file mode 100644 index 0000000000..9114e22438 --- /dev/null +++ b/tests/execution/test-eval-partial.md @@ -0,0 +1,26 @@ +```graphql @config +schema @server(port: 8080) @upstream(httpCache: 42, batch: {delay: 100}) { + query: Query +} + +type Query { + post(id: Int!): [Post] @http(url: "http://jsonplaceholder.typicode.com/posts/{{.args.id}}") +} + +type User { + id: Int! +} + +type Post { + id: Int! + userId: Int! + foo: String @http(url: "http://jsonplaceholder.typicode.com/posts/{{.env.FOO}}") + user: User @http(url: "http://jsonplaceholder.typicode.com/users/{{.value.userId}}") +} +``` + +```json @env +{ + "FOO": "foo" +} +```