Skip to content

Commit

Permalink
feat: Support mustache on link directives (#3176)
Browse files Browse the repository at this point in the history
Co-authored-by: Tushar Mathur <[email protected]>
  • Loading branch information
ssddOnTop and tusharmath authored Nov 29, 2024
1 parent 02c7821 commit 9039963
Show file tree
Hide file tree
Showing 18 changed files with 205 additions and 71 deletions.
12 changes: 6 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 1 addition & 5 deletions src/cli/generator/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
36 changes: 24 additions & 12 deletions src/core/config/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ impl ConfigReader {
config_module: ConfigModule,
parent_dir: Option<&'async_recursion Path>,
) -> anyhow::Result<ConfigModule> {
let reader_ctx = ConfigReaderContext::new(&self.runtime);

let links: Vec<Link> = config_module
.config()
.links
Expand All @@ -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| {
Expand Down Expand Up @@ -184,7 +190,16 @@ impl ConfigReader {
&self,
files: &[T],
) -> anyhow::Result<ConfigModule> {
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::<Vec<_>>();

let mut config_module = Valid::succeed(ConfigModule::default());

for file in files.iter() {
Expand Down Expand Up @@ -214,16 +229,13 @@ impl ConfigReader {
parent_dir: Option<&Path>,
) -> anyhow::Result<ConfigModule> {
// 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
Expand Down
24 changes: 17 additions & 7 deletions src/core/config/reader_context.rs
Original file line number Diff line number Diff line change
@@ -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<String, String>,
#[setters(strip_option)]
pub vars: Option<&'a BTreeMap<String, String>>,
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<T: AsRef<str>>(&self, path: &[T]) -> Option<Cow<'_, str>> {
if path.is_empty() {
Expand All @@ -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,
})
Expand Down Expand Up @@ -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"]),
Expand Down
28 changes: 27 additions & 1 deletion src/core/mustache/eval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,37 @@ pub trait Eval<'a> {

pub struct PathStringEval<A>(std::marker::PhantomData<A>);

impl<A> Default for PathStringEval<A> {
fn default() -> Self {
Self::new()
}
}

impl<A> PathStringEval<A> {
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<A: PathString> Eval<'_> for PathStringEval<A> {
Expand Down Expand Up @@ -107,7 +134,6 @@ impl Mustache {

#[cfg(test)]
mod tests {

mod render {
use std::borrow::Cow;

Expand Down
2 changes: 1 addition & 1 deletion src/core/mustache/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
mod eval;
mod model;
mod parse;
pub use eval::Eval;
pub use eval::{Eval, PathStringEval};
pub use model::*;
2 changes: 1 addition & 1 deletion src/core/mustache/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Vec<String>>()
.join("");
Expand Down
16 changes: 8 additions & 8 deletions src/core/mustache/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
];

Expand Down
6 changes: 1 addition & 5 deletions src/core/proto_reader/fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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?;
Expand Down
13 changes: 13 additions & 0 deletions src/core/resource_reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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),
Expand Down
6 changes: 3 additions & 3 deletions tests/core/snapshots/env-value.md_merged.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
22 changes: 22 additions & 0 deletions tests/core/snapshots/test-eval-partial.md_client.snap
Original file line number Diff line number Diff line change
@@ -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
}
22 changes: 22 additions & 0 deletions tests/core/snapshots/test-eval-partial.md_merged.snap
Original file line number Diff line number Diff line change
@@ -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!
}
10 changes: 5 additions & 5 deletions tests/core/snapshots/yaml-nested-unions.md_merged.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
18 changes: 9 additions & 9 deletions tests/core/snapshots/yaml-union-in-type.md_merged.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading

1 comment on commit 9039963

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Running 30s test @ http://localhost:8000/graphql

4 threads and 100 connections

Thread Stats Avg Stdev Max +/- Stdev
Latency 4.02ms 1.89ms 25.95ms 78.41%
Req/Sec 6.37k 848.06 7.41k 95.42%

761065 requests in 30.01s, 3.81GB read

Requests/sec: 25360.23

Transfer/sec: 130.17MB

Please sign in to comment.