-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 <[email protected]>
- Loading branch information
Showing
4 changed files
with
117 additions
and
46 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
/*! | ||
this module contains AST related tooling such as the visitor trait and | ||
the double dispatch (impl of [`AstVisitAcceptor`]) for all AST nodes. | ||
*/ | ||
use crate::language::ast; | ||
|
||
pub trait AstVisitAcceptor<'ast> { | ||
fn accept<V: AstVisit<'ast>>(&self, visitor: &mut V); | ||
} | ||
|
||
pub trait AstVisit<'ast> { | ||
fn visit_stmt(&mut self, _stmt: &ast::Stmt<'ast>) {} | ||
fn visit_expr(&mut self, _expr: &ast::Expr<'ast>) {} | ||
fn visit_emit_raw(&mut self, _raw: &ast::EmitRaw<'ast>) {} | ||
} | ||
|
||
impl<'ast> AstVisitAcceptor<'ast> for ast::Stmt<'ast> { | ||
fn accept<V: AstVisit<'ast>>(&self, visitor: &mut V) { | ||
visitor.visit_stmt(self); | ||
} | ||
} | ||
|
||
impl<'ast> AstVisitAcceptor<'ast> for ast::Expr<'ast> { | ||
fn accept<V: AstVisit<'ast>>(&self, visitor: &mut V) { | ||
visitor.visit_expr(self); | ||
} | ||
} | ||
|
||
impl<'ast> AstVisitAcceptor<'ast> for ast::EmitRaw<'ast> { | ||
fn accept<V: AstVisit<'ast>>(&self, visitor: &mut V) { | ||
visitor.visit_emit_raw(self); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
mod ast; | ||
mod ast_visitor; | ||
mod lexer; | ||
mod parser; | ||
mod runtime; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,97 +1,113 @@ | ||
use crate::language::ast; | ||
use crate::language::ast::Spanned; | ||
use crate::language::ast_visitor::{AstVisit, AstVisitAcceptor}; | ||
use std::borrow::Cow; | ||
use std::collections::HashMap; | ||
|
||
#[derive(Default)] | ||
struct Runtime<'source> { | ||
// todo: maybe an `Ident` should be the key for variables | ||
vars: HashMap<&'source str, ast::Value>, | ||
interpreted: Vec<u8>, | ||
output: Vec<u8>, | ||
} | ||
|
||
trait Visitable<'source> { | ||
fn accept<V: Visitor<'source>>(&self, visitor: &mut V); | ||
} | ||
|
||
trait Visitor<'source> { | ||
fn visit_stmt(&mut self, stmt: &ast::Stmt<'source>); | ||
fn visit_expr(&mut self, expr: &ast::Expr<'source>); | ||
fn visit_emit_raw(&mut self, raw: &ast::EmitRaw<'source>); | ||
} | ||
impl<'source> Runtime<'source> { | ||
/// registers a variable with a given `id` that is the variable identifier | ||
pub fn with_variable(mut self, id: &'source str, var: impl Into<ast::Value>) -> Self { | ||
self.vars.insert(id, var.into()); | ||
|
||
impl<'source> Visitable<'source> for ast::Stmt<'source> { | ||
fn accept<V: Visitor<'source>>(&self, visitor: &mut V) { | ||
visitor.visit_stmt(self); | ||
self | ||
} | ||
} | ||
|
||
impl<'source> Visitable<'source> for ast::Expr<'source> { | ||
fn accept<V: Visitor<'source>>(&self, visitor: &mut V) { | ||
visitor.visit_expr(self); | ||
/// returns the rendered template as a string in form of a `Cow<'_, str>` | ||
pub fn rendered(&mut self) -> Cow<'_, str> { | ||
String::from_utf8_lossy(self.output.as_slice()) | ||
} | ||
} | ||
|
||
impl<'source> Visitable<'source> for ast::EmitRaw<'source> { | ||
fn accept<V: Visitor<'source>>(&self, visitor: &mut V) { | ||
visitor.visit_emit_raw(self); | ||
#[cfg(test)] | ||
pub fn render(&mut self, source: &'source str) -> Cow<'_, str> { | ||
use crate::language::parser::Parser; | ||
|
||
let parsed = Parser::new(source).parse().unwrap(); | ||
parsed.accept(self); | ||
|
||
self.rendered() | ||
} | ||
} | ||
|
||
impl<'source> Visitor<'source> for Runtime<'source> { | ||
impl<'source> AstVisit<'source> for Runtime<'source> { | ||
fn visit_stmt(&mut self, stmt: &ast::Stmt<'source>) { | ||
use ast::Stmt::*; | ||
|
||
match stmt { | ||
ast::Stmt::Template(spanned) => { | ||
Template(spanned) => { | ||
for s in spanned.node.children.as_slice() { | ||
s.accept(self); | ||
} | ||
} | ||
ast::Stmt::EmitRaw(spanned) => spanned.node.accept(self), | ||
ast::Stmt::EmitExpr(spanned) => spanned.node.expr.accept(self), | ||
EmitRaw(spanned) => spanned.node.accept(self), | ||
EmitExpr(spanned) => spanned.node.expr.accept(self), | ||
} | ||
} | ||
|
||
fn visit_expr(&mut self, expr: &ast::Expr<'source>) { | ||
use ast::Expr; | ||
use ast::Expr::*; | ||
|
||
match expr { | ||
Expr::SysVar(var) => todo!(), | ||
Expr::Var(var) => { | ||
SysVar(var) => todo!(), | ||
Var(var) => { | ||
if let Some(var) = self.vars.get(var.node.id) { | ||
self.interpreted | ||
self.output | ||
.extend_from_slice(var.as_str().unwrap().as_bytes()); | ||
} | ||
} | ||
Expr::Const(_) => { | ||
Const(_) => { | ||
todo!() | ||
} | ||
Expr::Call(_) => { | ||
Call(_) => { | ||
todo!() | ||
} | ||
} | ||
} | ||
|
||
fn visit_emit_raw(&mut self, raw: &ast::EmitRaw<'source>) { | ||
self.interpreted.extend_from_slice(raw.raw.as_bytes()); | ||
self.output.extend_from_slice(raw.raw.as_bytes()); | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
use crate::language::parser::Parser; | ||
use crate::language::ast::IntoSpanned; | ||
|
||
#[test] | ||
fn test_expr_var() { | ||
let mut runtime = Runtime::default().with_variable("foo", "John"); | ||
let expr = ast::Expr::Var(ast::Var { id: "foo" }.spanned()); | ||
|
||
expr.accept(&mut runtime); | ||
assert_eq!(runtime.rendered(), "John"); | ||
} | ||
|
||
#[test] | ||
fn test_expr_sys_var() { | ||
assert_eq!( | ||
Runtime::default().render("{{ $processEnv HOME }}"), | ||
env!("HOME") | ||
); | ||
} | ||
|
||
#[test] | ||
fn test_whole_template() { | ||
assert_eq!( | ||
Runtime::default() | ||
.with_variable("world", "John") | ||
.render("hello {{ world }}"), | ||
"hello John" | ||
); | ||
} | ||
|
||
#[test] | ||
fn test_x() { | ||
let mut p = Parser::new("hello {{ world }}"); | ||
let stmt = p.parse().unwrap(); | ||
let mut runtime = Runtime::default(); | ||
runtime | ||
.vars | ||
.insert("world", ast::Value::String("John".to_string())); | ||
|
||
stmt.accept(&mut runtime); | ||
|
||
let result = String::from_utf8_lossy(runtime.interpreted.as_slice()); | ||
assert_eq!(result, "hello John"); | ||
fn test_whole_template_unhappy() { | ||
assert_eq!(Runtime::default().render("hello {{ world }}"), "hello "); | ||
} | ||
} |