Add proper impl for templating

Signed-off-by: Marcel Müller <neikos@neikos.email>
This commit is contained in:
Marcel Müller 2026-03-06 12:56:16 +01:00
parent 1ee7611981
commit 4470af3926
5 changed files with 85 additions and 27 deletions

View file

@ -1,3 +1,4 @@
use thiserror::Error;
use winnow::Parser; use winnow::Parser;
use winnow::RecoverableParser; use winnow::RecoverableParser;
use winnow::combinator::alt; use winnow::combinator::alt;
@ -113,9 +114,15 @@ impl ParserError<Input<'_>> for AstError {
} }
} }
#[derive(Debug)] #[derive(Debug, Error)]
pub struct AstFailure {} pub struct AstFailure {}
impl std::fmt::Display for AstFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("TODO")
}
}
impl AstFailure { impl AstFailure {
fn from_errors(_errors: Vec<AstError>, _input: &[TemplateToken]) -> AstFailure { fn from_errors(_errors: Vec<AstError>, _input: &[TemplateToken]) -> AstFailure {
AstFailure {} AstFailure {}
@ -217,7 +224,7 @@ mod tests {
fn check_only_content() { fn check_only_content() {
let input = "Hello World"; let input = "Hello World";
let parsed = crate::parser::parse(input).unwrap(); let parsed = crate::parser::parse(input.into()).unwrap();
let ast = parse(parsed.tokens()).unwrap(); let ast = parse(parsed.tokens()).unwrap();
@ -239,7 +246,7 @@ mod tests {
fn check_simple_variable_interpolation() { fn check_simple_variable_interpolation() {
let input = "Hello {{= world }}"; let input = "Hello {{= world }}";
let parsed = crate::parser::parse(input).unwrap(); let parsed = crate::parser::parse(input.into()).unwrap();
let ast = parse(parsed.tokens()).unwrap(); let ast = parse(parsed.tokens()).unwrap();

View file

@ -122,7 +122,7 @@ mod tests {
fn check_simple_variable_interpolation() { fn check_simple_variable_interpolation() {
let input = "Hello {{= world }}"; let input = "Hello {{= world }}";
let parsed = crate::parser::parse(input).unwrap(); let parsed = crate::parser::parse(input.into()).unwrap();
let ast = crate::ast::parse(parsed.tokens()).unwrap(); let ast = crate::ast::parse(parsed.tokens()).unwrap();

View file

@ -8,14 +8,14 @@ use crate::emit::Instruction;
use crate::input::TempleInput; use crate::input::TempleInput;
#[derive(Debug, Error, Display)] #[derive(Debug, Error, Display)]
enum EvalError { pub enum EvaluationError {
/// An unknown variable was encountered: .0 /// An unknown variable was encountered: .0
UnknownVariable(TempleInput), UnknownVariable(TempleInput),
/// An explicit abort was requested /// An explicit abort was requested
ExplicitAbort, ExplicitAbort,
} }
fn execute(instructions: &[Instruction], global_context: &Context) -> Result<String, EvalError> { pub fn execute(instructions: &[Instruction], global_context: &Context) -> Result<String, EvaluationError> {
let mut output = String::new(); let mut output = String::new();
let mut scopes: HashMap<crate::emit::VariableSlot, serde_json::Value> = HashMap::new(); let mut scopes: HashMap<crate::emit::VariableSlot, serde_json::Value> = HashMap::new();
@ -27,7 +27,7 @@ fn execute(instructions: &[Instruction], global_context: &Context) -> Result<Str
let value = global_context let value = global_context
.values .values
.get(name.as_str()) .get(name.as_str())
.ok_or(EvalError::UnknownVariable(name.clone()))?; .ok_or(EvaluationError::UnknownVariable(name.clone()))?;
scopes.insert(*slot, value.clone()); scopes.insert(*slot, value.clone());
} }
@ -36,7 +36,7 @@ fn execute(instructions: &[Instruction], global_context: &Context) -> Result<Str
output.push_str(value); output.push_str(value);
} }
Instruction::PushScope { inherit_parent: _ } => todo!(), Instruction::PushScope { inherit_parent: _ } => todo!(),
Instruction::Abort => return Err(EvalError::ExplicitAbort), Instruction::Abort => return Err(EvaluationError::ExplicitAbort),
} }
} }
@ -52,7 +52,7 @@ mod tests {
fn check_simple_variable_interpolation() { fn check_simple_variable_interpolation() {
let input = "Hello {{= world }}"; let input = "Hello {{= world }}";
let parsed = crate::parser::parse(input).unwrap(); let parsed = crate::parser::parse(input.into()).unwrap();
let ast = crate::ast::parse(parsed.tokens()).unwrap(); let ast = crate::ast::parse(parsed.tokens()).unwrap();

View file

@ -5,16 +5,36 @@ use displaydoc::Display;
use serde::Serialize; use serde::Serialize;
use thiserror::Error; use thiserror::Error;
use crate::emit::Instruction;
use crate::input::TempleInput;
pub mod ast; pub mod ast;
pub mod emit; pub mod emit;
pub mod eval; pub mod eval;
pub mod parser;
mod input; mod input;
pub mod parser;
#[derive(Debug, Error, Display)] #[derive(Debug, Error, Display)]
pub enum TempleError { pub enum TempleError {
/// Could not parse the given template /// Could not parse the given template
ParseError {}, ParseError {
#[from]
source: parser::ParseFailure,
},
/// Invalid Template
AstError {
#[from]
source: ast::AstFailure,
},
/// An error occurred while evaluating
EvaluationError {
#[from]
source: eval::EvaluationError,
},
/// The template '{0}' could not be found
UnknownTemplate(String),
} }
pub struct Temple { pub struct Temple {
@ -37,17 +57,41 @@ impl Temple {
pub fn add_template( pub fn add_template(
&mut self, &mut self,
name: impl Into<String>, name: impl Into<String>,
value: impl AsRef<str>, value: impl Into<TempleInput>,
) -> Result<(), TempleError> { ) -> Result<(), TempleError> {
let source = value.into();
let parse = parser::parse(source.clone())?;
let ast = ast::parse(parse.tokens())?;
let instructions = emit::emit_machine(ast);
self.templates.insert(
name.into(),
Template {
source,
instructions,
},
);
Ok(()) Ok(())
} }
fn render(&self, arg: &str, ctx: &Context) -> Result<String, TempleError> { pub fn render(&self, name: &str, ctx: &Context) -> Result<String, TempleError> {
Ok(String::new()) let template = self
.templates
.get(name)
.ok_or_else(|| TempleError::UnknownTemplate(name.to_string()))?;
let res = eval::execute(&template.instructions, ctx)?;
Ok(res)
} }
} }
struct Template {} struct Template {
source: TempleInput,
instructions: Vec<Instruction>,
}
pub struct Context { pub struct Context {
values: BTreeMap<String, serde_json::Value>, values: BTreeMap<String, serde_json::Value>,
@ -155,6 +199,6 @@ mod tests {
let rendered = temp.render("base", &ctx).unwrap(); let rendered = temp.render("base", &ctx).unwrap();
insta::assert_snapshot!(rendered, @"") insta::assert_snapshot!(rendered, @"Hello World")
} }
} }

View file

@ -4,6 +4,7 @@ use annotate_snippets::AnnotationKind;
use annotate_snippets::Level; use annotate_snippets::Level;
use annotate_snippets::Renderer; use annotate_snippets::Renderer;
use annotate_snippets::Snippet; use annotate_snippets::Snippet;
use thiserror::Error;
use winnow::LocatingSlice; use winnow::LocatingSlice;
use winnow::Parser; use winnow::Parser;
use winnow::RecoverableParser; use winnow::RecoverableParser;
@ -37,16 +38,22 @@ use crate::resume_after_cut;
type Input<'input> = Recoverable<LocatingSlice<TempleInput>, ParseError>; type Input<'input> = Recoverable<LocatingSlice<TempleInput>, ParseError>;
type PResult<'input, T> = Result<T, ParseError>; type PResult<'input, T> = Result<T, ParseError>;
#[derive(Debug)] #[derive(Debug, Error)]
pub struct ParseFailure { pub struct ParseFailure {
source: Arc<str>, input: Arc<str>,
errors: Vec<ParseError>, errors: Vec<ParseError>,
} }
impl std::fmt::Display for ParseFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.to_report())
}
}
impl ParseFailure { impl ParseFailure {
fn from_errors(errors: Vec<ParseError>, input: &str) -> ParseFailure { fn from_errors(errors: Vec<ParseError>, input: TempleInput) -> ParseFailure {
ParseFailure { ParseFailure {
source: Arc::from(input.to_string()), input: Arc::from(input.to_string()),
errors, errors,
} }
} }
@ -64,7 +71,7 @@ impl ParseFailure {
.unwrap_or("An error occurred while parsing"), .unwrap_or("An error occurred while parsing"),
) )
.element( .element(
Snippet::source(self.source.as_ref()).annotation( Snippet::source(self.input.as_ref()).annotation(
AnnotationKind::Primary AnnotationKind::Primary
.span(error.span.clone().map(|s| s.range).unwrap_or_else(|| 0..0)), .span(error.span.clone().map(|s| s.range).unwrap_or_else(|| 0..0)),
), ),
@ -294,9 +301,9 @@ impl TemplateToken {
} }
} }
pub fn parse(input: &str) -> Result<ParsedTemplate, ParseFailure> { pub fn parse(input: TempleInput) -> Result<ParsedTemplate, ParseFailure> {
let (_remaining, val, errors) = let (_remaining, val, errors) =
parse_tokens.recoverable_parse(LocatingSlice::new(TempleInput::from(input))); parse_tokens.recoverable_parse(LocatingSlice::new(input.clone()));
if errors.is_empty() if errors.is_empty()
&& let Some(val) = val && let Some(val) = val
@ -418,7 +425,7 @@ mod tests {
#[test] #[test]
fn parse_simple() { fn parse_simple() {
let input = "Hello There"; let input = "Hello There";
let output = parse(input); let output = parse(input.into());
insta::assert_debug_snapshot!(output, @r#" insta::assert_debug_snapshot!(output, @r#"
Ok( Ok(
@ -437,7 +444,7 @@ mod tests {
#[test] #[test]
fn parse_interpolate() { fn parse_interpolate() {
let input = "Hello {{ there }}"; let input = "Hello {{ there }}";
let output = parse(input); let output = parse(input.into());
insta::assert_debug_snapshot!(output, @r#" insta::assert_debug_snapshot!(output, @r#"
Ok( Ok(
@ -480,12 +487,12 @@ mod tests {
#[test] #[test]
fn parse_interpolate_bad() { fn parse_interpolate_bad() {
let input = "Hello {{ the2re }} {{ the@re }}"; let input = "Hello {{ the2re }} {{ the@re }}";
let output = parse(input); let output = parse(input.into());
insta::assert_debug_snapshot!(output, @r#" insta::assert_debug_snapshot!(output, @r#"
Err( Err(
ParseFailure { ParseFailure {
source: "Hello {{ the2re }} {{ the@re }}", input: "Hello {{ the2re }} {{ the@re }}",
errors: [ errors: [
ParseError { ParseError {
message: Some( message: Some(