From 696a6a90b7a909c2fb973d05f94294722e55def1 Mon Sep 17 00:00:00 2001 From: Sven Kanoldt Date: Wed, 1 Feb 2023 01:02:23 +0100 Subject: [PATCH] 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 `$` 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 --- src/curlz/language/ast.rs | 21 ++++++ src/curlz/language/ast_visitor.rs | 33 +++++++++ src/curlz/language/mod.rs | 1 + src/curlz/language/runtime.rs | 108 +++++++++++++++++------------- 4 files changed, 117 insertions(+), 46 deletions(-) create mode 100644 src/curlz/language/ast_visitor.rs diff --git a/src/curlz/language/ast.rs b/src/curlz/language/ast.rs index bf82bc0..1a682ae 100644 --- a/src/curlz/language/ast.rs +++ b/src/curlz/language/ast.rs @@ -68,3 +68,24 @@ pub enum Stmt<'a> { EmitRaw(Spanned>), EmitExpr(Spanned>), } + +#[cfg(test)] +pub trait IntoSpanned { + fn spanned(self) -> Spanned + where + Self: Sized, + { + Spanned::new( + self, + Span { + start_line: 1, + start_col: 0, + end_line: 1, + end_col: 1, + }, + ) + } +} + +#[cfg(test)] +impl IntoSpanned for T {} diff --git a/src/curlz/language/ast_visitor.rs b/src/curlz/language/ast_visitor.rs new file mode 100644 index 0000000..b562d55 --- /dev/null +++ b/src/curlz/language/ast_visitor.rs @@ -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>(&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>(&self, visitor: &mut V) { + visitor.visit_stmt(self); + } +} + +impl<'ast> AstVisitAcceptor<'ast> for ast::Expr<'ast> { + fn accept>(&self, visitor: &mut V) { + visitor.visit_expr(self); + } +} + +impl<'ast> AstVisitAcceptor<'ast> for ast::EmitRaw<'ast> { + fn accept>(&self, visitor: &mut V) { + visitor.visit_emit_raw(self); + } +} diff --git a/src/curlz/language/mod.rs b/src/curlz/language/mod.rs index 98a2922..0a72d6a 100644 --- a/src/curlz/language/mod.rs +++ b/src/curlz/language/mod.rs @@ -1,4 +1,5 @@ mod ast; +mod ast_visitor; mod lexer; mod parser; mod runtime; diff --git a/src/curlz/language/runtime.rs b/src/curlz/language/runtime.rs index 5a9c976..795a020 100644 --- a/src/curlz/language/runtime.rs +++ b/src/curlz/language/runtime.rs @@ -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, + output: Vec, } -trait Visitable<'source> { - fn accept>(&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) -> Self { + self.vars.insert(id, var.into()); -impl<'source> Visitable<'source> for ast::Stmt<'source> { - fn accept>(&self, visitor: &mut V) { - visitor.visit_stmt(self); + self } -} -impl<'source> Visitable<'source> for ast::Expr<'source> { - fn accept>(&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>(&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 "); } }