Skip to content

Commit

Permalink
feat(experimental:template-language): start an own template language
Browse files Browse the repository at this point in the history
## 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
sassman committed Feb 1, 2023
1 parent e44cf43 commit 696a6a9
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 46 deletions.
21 changes: 21 additions & 0 deletions src/curlz/language/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,24 @@ pub enum Stmt<'a> {
EmitRaw(Spanned<EmitRaw<'a>>),
EmitExpr(Spanned<EmitExpr<'a>>),
}

#[cfg(test)]
pub trait IntoSpanned {
fn spanned(self) -> Spanned<Self>
where
Self: Sized,
{
Spanned::new(
self,
Span {
start_line: 1,
start_col: 0,
end_line: 1,
end_col: 1,
},
)
}
}

#[cfg(test)]
impl<T> IntoSpanned for T {}
33 changes: 33 additions & 0 deletions src/curlz/language/ast_visitor.rs
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);
}
}
1 change: 1 addition & 0 deletions src/curlz/language/mod.rs
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;
Expand Down
108 changes: 62 additions & 46 deletions src/curlz/language/runtime.rs
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 ");
}
}

0 comments on commit 696a6a9

Please sign in to comment.