Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support mustache on link directives #3176

Merged
merged 18 commits into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 struct PathStringEval<A>(std::marker::PhantomData<A>);

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

Check warning on line 16 in src/core/mustache/eval.rs

View check run for this annotation

Codecov / codecov/patch

src/core/mustache/eval.rs#L14-L16

Added lines #L14 - L16 were not covered by tests
}

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 @@

#[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
Loading