Skip to content

Commit 0f7d66f

Browse files
committed
feat(experimental:template-language): start an own template language
## CAUTION! This is an experiment The whole reason for this experiment is to get closer to the http plugin template syntax as linked in #5 There the vscode plugin [has this concept of system variables](https://marketplace.visualstudio.com/items?itemName=humao.rest-client#system-variables) that follow the syntax of a mix of expression and function e.g. - {{$randomInt min max}}: Returns a random integer between min (included) and max (excluded) - {{$dotenv [%]variableName}}: Returns the environment value stored in the .env file which exists in the same directory of your .http file. Both examples show that `$<ident>` is similar to a function name and then a variable argument list is passed without any braces like `()`. This experiment focuses on the ability to register functions for this very system variable syntax at compile time in an extensible fashion. done so far: - lexing and parsing of very basic templates with an expression `{{ var }}` - basic runtime to interpret the AST - some tests added - design of a Visitor pattern for the AST yet open - [ ] tests for unhappy path are to less - [ ] runtime is very incomplete yet, `SysVar` hooked functions are missing e.g. `hello {{ $processEnv HOME }}` Signed-off-by: Sven Kanoldt <sven@d34dl0ck.me>
1 parent 9050dec commit 0f7d66f

File tree

4 files changed

+117
-46
lines changed

4 files changed

+117
-46
lines changed

src/curlz/language/ast.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,24 @@ pub enum Stmt<'a> {
6868
EmitRaw(Spanned<EmitRaw<'a>>),
6969
EmitExpr(Spanned<EmitExpr<'a>>),
7070
}
71+
72+
#[cfg(test)]
73+
pub trait IntoSpanned {
74+
fn spanned(self) -> Spanned<Self>
75+
where
76+
Self: Sized,
77+
{
78+
Spanned::new(
79+
self,
80+
Span {
81+
start_line: 1,
82+
start_col: 0,
83+
end_line: 1,
84+
end_col: 1,
85+
},
86+
)
87+
}
88+
}
89+
90+
#[cfg(test)]
91+
impl<T> IntoSpanned for T {}

src/curlz/language/ast_visitor.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*!
2+
this module contains AST related tooling such as the visitor trait and
3+
the double dispatch (impl of [`AstVisitAcceptor`]) for all AST nodes.
4+
*/
5+
use crate::language::ast;
6+
7+
pub trait AstVisitAcceptor<'ast> {
8+
fn accept<V: AstVisit<'ast>>(&self, visitor: &mut V);
9+
}
10+
11+
pub trait AstVisit<'ast> {
12+
fn visit_stmt(&mut self, _stmt: &ast::Stmt<'ast>) {}
13+
fn visit_expr(&mut self, _expr: &ast::Expr<'ast>) {}
14+
fn visit_emit_raw(&mut self, _raw: &ast::EmitRaw<'ast>) {}
15+
}
16+
17+
impl<'ast> AstVisitAcceptor<'ast> for ast::Stmt<'ast> {
18+
fn accept<V: AstVisit<'ast>>(&self, visitor: &mut V) {
19+
visitor.visit_stmt(self);
20+
}
21+
}
22+
23+
impl<'ast> AstVisitAcceptor<'ast> for ast::Expr<'ast> {
24+
fn accept<V: AstVisit<'ast>>(&self, visitor: &mut V) {
25+
visitor.visit_expr(self);
26+
}
27+
}
28+
29+
impl<'ast> AstVisitAcceptor<'ast> for ast::EmitRaw<'ast> {
30+
fn accept<V: AstVisit<'ast>>(&self, visitor: &mut V) {
31+
visitor.visit_emit_raw(self);
32+
}
33+
}

src/curlz/language/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod ast;
2+
mod ast_visitor;
23
mod lexer;
34
mod parser;
45
mod runtime;

src/curlz/language/runtime.rs

Lines changed: 62 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,113 @@
11
use crate::language::ast;
2-
use crate::language::ast::Spanned;
2+
use crate::language::ast_visitor::{AstVisit, AstVisitAcceptor};
3+
use std::borrow::Cow;
34
use std::collections::HashMap;
45

56
#[derive(Default)]
67
struct Runtime<'source> {
78
// todo: maybe an `Ident` should be the key for variables
89
vars: HashMap<&'source str, ast::Value>,
9-
interpreted: Vec<u8>,
10+
output: Vec<u8>,
1011
}
1112

12-
trait Visitable<'source> {
13-
fn accept<V: Visitor<'source>>(&self, visitor: &mut V);
14-
}
15-
16-
trait Visitor<'source> {
17-
fn visit_stmt(&mut self, stmt: &ast::Stmt<'source>);
18-
fn visit_expr(&mut self, expr: &ast::Expr<'source>);
19-
fn visit_emit_raw(&mut self, raw: &ast::EmitRaw<'source>);
20-
}
13+
impl<'source> Runtime<'source> {
14+
/// registers a variable with a given `id` that is the variable identifier
15+
pub fn with_variable(mut self, id: &'source str, var: impl Into<ast::Value>) -> Self {
16+
self.vars.insert(id, var.into());
2117

22-
impl<'source> Visitable<'source> for ast::Stmt<'source> {
23-
fn accept<V: Visitor<'source>>(&self, visitor: &mut V) {
24-
visitor.visit_stmt(self);
18+
self
2519
}
26-
}
2720

28-
impl<'source> Visitable<'source> for ast::Expr<'source> {
29-
fn accept<V: Visitor<'source>>(&self, visitor: &mut V) {
30-
visitor.visit_expr(self);
21+
/// returns the rendered template as a string in form of a `Cow<'_, str>`
22+
pub fn rendered(&mut self) -> Cow<'_, str> {
23+
String::from_utf8_lossy(self.output.as_slice())
3124
}
32-
}
3325

34-
impl<'source> Visitable<'source> for ast::EmitRaw<'source> {
35-
fn accept<V: Visitor<'source>>(&self, visitor: &mut V) {
36-
visitor.visit_emit_raw(self);
26+
#[cfg(test)]
27+
pub fn render(&mut self, source: &'source str) -> Cow<'_, str> {
28+
use crate::language::parser::Parser;
29+
30+
let parsed = Parser::new(source).parse().unwrap();
31+
parsed.accept(self);
32+
33+
self.rendered()
3734
}
3835
}
3936

40-
impl<'source> Visitor<'source> for Runtime<'source> {
37+
impl<'source> AstVisit<'source> for Runtime<'source> {
4138
fn visit_stmt(&mut self, stmt: &ast::Stmt<'source>) {
39+
use ast::Stmt::*;
40+
4241
match stmt {
43-
ast::Stmt::Template(spanned) => {
42+
Template(spanned) => {
4443
for s in spanned.node.children.as_slice() {
4544
s.accept(self);
4645
}
4746
}
48-
ast::Stmt::EmitRaw(spanned) => spanned.node.accept(self),
49-
ast::Stmt::EmitExpr(spanned) => spanned.node.expr.accept(self),
47+
EmitRaw(spanned) => spanned.node.accept(self),
48+
EmitExpr(spanned) => spanned.node.expr.accept(self),
5049
}
5150
}
5251

5352
fn visit_expr(&mut self, expr: &ast::Expr<'source>) {
54-
use ast::Expr;
53+
use ast::Expr::*;
5554

5655
match expr {
57-
Expr::SysVar(var) => todo!(),
58-
Expr::Var(var) => {
56+
SysVar(var) => todo!(),
57+
Var(var) => {
5958
if let Some(var) = self.vars.get(var.node.id) {
60-
self.interpreted
59+
self.output
6160
.extend_from_slice(var.as_str().unwrap().as_bytes());
6261
}
6362
}
64-
Expr::Const(_) => {
63+
Const(_) => {
6564
todo!()
6665
}
67-
Expr::Call(_) => {
66+
Call(_) => {
6867
todo!()
6968
}
7069
}
7170
}
7271

7372
fn visit_emit_raw(&mut self, raw: &ast::EmitRaw<'source>) {
74-
self.interpreted.extend_from_slice(raw.raw.as_bytes());
73+
self.output.extend_from_slice(raw.raw.as_bytes());
7574
}
7675
}
7776

7877
#[cfg(test)]
7978
mod tests {
8079
use super::*;
81-
use crate::language::parser::Parser;
80+
use crate::language::ast::IntoSpanned;
81+
82+
#[test]
83+
fn test_expr_var() {
84+
let mut runtime = Runtime::default().with_variable("foo", "John");
85+
let expr = ast::Expr::Var(ast::Var { id: "foo" }.spanned());
86+
87+
expr.accept(&mut runtime);
88+
assert_eq!(runtime.rendered(), "John");
89+
}
90+
91+
#[test]
92+
fn test_expr_sys_var() {
93+
assert_eq!(
94+
Runtime::default().render("{{ $processEnv HOME }}"),
95+
env!("HOME")
96+
);
97+
}
98+
99+
#[test]
100+
fn test_whole_template() {
101+
assert_eq!(
102+
Runtime::default()
103+
.with_variable("world", "John")
104+
.render("hello {{ world }}"),
105+
"hello John"
106+
);
107+
}
82108

83109
#[test]
84-
fn test_x() {
85-
let mut p = Parser::new("hello {{ world }}");
86-
let stmt = p.parse().unwrap();
87-
let mut runtime = Runtime::default();
88-
runtime
89-
.vars
90-
.insert("world", ast::Value::String("John".to_string()));
91-
92-
stmt.accept(&mut runtime);
93-
94-
let result = String::from_utf8_lossy(runtime.interpreted.as_slice());
95-
assert_eq!(result, "hello John");
110+
fn test_whole_template_unhappy() {
111+
assert_eq!(Runtime::default().render("hello {{ world }}"), "hello ");
96112
}
97113
}

0 commit comments

Comments
 (0)