From 00e67d34d9b6ce77f287a5336764b0a811e33c03 Mon Sep 17 00:00:00 2001 From: Olivier Date: Thu, 2 May 2024 21:18:32 +0100 Subject: [PATCH] Add support for template-accessible methods and include leftpad and urlencode --- Cargo.lock | 1 + hornbeam_interpreter/Cargo.toml | 1 + hornbeam_interpreter/src/engine.rs | 32 +++++++- hornbeam_interpreter/src/functions.rs | 23 ++++++ .../src/functions/defaults.rs | 74 +++++++++++++++++++ hornbeam_interpreter/src/interface.rs | 9 ++- hornbeam_interpreter/src/lib.rs | 3 + hornbeam_interpreter/tests/snapshots.rs | 15 +++- .../snapshots/snapshots__snapshot_003.snap | 5 ++ 9 files changed, 159 insertions(+), 4 deletions(-) create mode 100644 hornbeam_interpreter/src/functions.rs create mode 100644 hornbeam_interpreter/src/functions/defaults.rs create mode 100644 hornbeam_interpreter/tests/snapshots/snapshots__snapshot_003.snap diff --git a/Cargo.lock b/Cargo.lock index 053f3bb..d35ec52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -782,6 +782,7 @@ dependencies = [ "html-escape", "insta", "itertools", + "percent-encoding", "pollster", "thiserror", "tracing", diff --git a/hornbeam_interpreter/Cargo.toml b/hornbeam_interpreter/Cargo.toml index f481c97..68be66b 100644 --- a/hornbeam_interpreter/Cargo.toml +++ b/hornbeam_interpreter/Cargo.toml @@ -25,6 +25,7 @@ itertools = "0.10.5" pollster = "0.3.0" tracing = "0.1.37" +percent-encoding = "2.2.0" [features] default = ["fluent"] diff --git a/hornbeam_interpreter/src/engine.rs b/hornbeam_interpreter/src/engine.rs index 6b93e38..2f41014 100644 --- a/hornbeam_interpreter/src/engine.rs +++ b/hornbeam_interpreter/src/engine.rs @@ -1,3 +1,4 @@ +use crate::functions::TemplateAccessibleMethod; use crate::interface::{LocalisationSystem, OutputSystem}; use crate::InterpreterError; use async_recursion::async_recursion; @@ -32,6 +33,7 @@ pub(crate) struct Interpreter<'a, O, LS> { pub(crate) localisation: Arc, pub(crate) locale: String, pub(crate) scopes: Vec>, + pub(crate) methods: &'a BTreeMap, } #[derive(Debug)] @@ -727,8 +729,34 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret }), } } - Expression::MethodCall { .. } => { - unimplemented!() + Expression::MethodCall { + obj, + ident, + args, + loc, + } => { + let Some(method) = self.methods.get(ident.as_str()) else { + return Err(InterpreterError::TypeError { + context: format!("method call to {ident:?}"), + conflict: format!("No method by that name!"), + location: loc.clone(), + }); + }; + + let obj_value = self.evaluate_expression(scope_idx, obj, loc)?; + let mut arg_values: Vec = Vec::with_capacity(args.len()); + for arg in args { + arg_values.push(self.evaluate_expression(scope_idx, arg, loc)?); + } + + match method.call(obj_value, arg_values) { + Ok(result_val) => Ok(result_val), + Err(method_err) => Err(InterpreterError::TypeError { + context: format!("method call to {ident:?}"), + conflict: format!("method error: {method_err}"), + location: loc.clone(), + }), + } } Expression::Variable { name, loc } => { let locals = &self.scopes[scope_idx].variables; diff --git a/hornbeam_interpreter/src/functions.rs b/hornbeam_interpreter/src/functions.rs new file mode 100644 index 0000000..d9659c8 --- /dev/null +++ b/hornbeam_interpreter/src/functions.rs @@ -0,0 +1,23 @@ +use crate::interface::Value; + +pub(crate) mod defaults; + +/// A method that can be accessed (called) by templates. +/// There is no dynamic dispatch for methods: the name of the method is the only thing that determines which one to call, +/// not the type of the self-parameter. +#[derive(Clone)] +pub struct TemplateAccessibleMethod { + /// Function pointer that implements the method. + /// Arguments are: + /// - the 'self' parameter + /// - list of any parameters + /// + /// TODO Extend this to expose interpreter state. + function: fn(Value, Vec) -> Result, +} + +impl TemplateAccessibleMethod { + pub(crate) fn call(&self, obj: Value, args: Vec) -> Result { + (self.function)(obj, args) + } +} diff --git a/hornbeam_interpreter/src/functions/defaults.rs b/hornbeam_interpreter/src/functions/defaults.rs new file mode 100644 index 0000000..023ccd9 --- /dev/null +++ b/hornbeam_interpreter/src/functions/defaults.rs @@ -0,0 +1,74 @@ +use std::{collections::BTreeMap, sync::Arc}; + +use percent_encoding::NON_ALPHANUMERIC; + +use crate::interface::Value; + +use super::TemplateAccessibleMethod; + +const DEFAULT_TEMPLATE_ACCESSIBLE_METHODS: &'static [( + &'static str, + fn(Value, Vec) -> Result, +)] = &[("leftpad", leftpad), ("urlencode", urlencode)]; + +/// Return a map of the default suggested template-accessible methods. +pub fn default_template_accessible_methods() -> BTreeMap { + DEFAULT_TEMPLATE_ACCESSIBLE_METHODS + .iter() + .map(|(name, func)| { + ( + (*name).to_owned(), + TemplateAccessibleMethod { function: *func }, + ) + }) + .collect() +} + +pub fn leftpad(obj: Value, args: Vec) -> Result { + let Value::Str(string_to_pad) = obj else { + return Err(format!("{obj:?} is not a string: can't leftpad!")); + }; + if args.len() != 2 { + return Err(format!( + "leftpad takes 2 args (length, padding character), not {}", + args.len() + )); + } + let Value::Int(pad_length) = &args[0] else { + return Err(format!("leftpad's first arg should be an integer")); + }; + let Value::Str(padding_character) = &args[1] else { + return Err(format!( + "leftpad's second arg should be a string (usually a single character)" + )); + }; + + if string_to_pad.len() as i64 >= *pad_length { + // zero-clone shortcut + // also required to prevent underflow! + return Ok(Value::Str(string_to_pad)); + } + + let repetitions = pad_length - string_to_pad.len() as i64; + + let mut result = String::new(); + for _ in 0..repetitions { + result.push_str(&padding_character); + } + result.push_str(&string_to_pad); + + Ok(Value::Str(Arc::new(result))) +} + +pub fn urlencode(obj: Value, args: Vec) -> Result { + let Value::Str(string_to_encode) = obj else { + return Err(format!("{obj:?} is not a string: can't urlencode!")); + }; + if args.len() != 0 { + return Err(format!("urlencode takes 0 args, not {}", args.len())); + } + + Ok(Value::Str(Arc::new( + percent_encoding::utf8_percent_encode(&string_to_encode, NON_ALPHANUMERIC).to_string(), + ))) +} diff --git a/hornbeam_interpreter/src/interface.rs b/hornbeam_interpreter/src/interface.rs index 9190d43..96f397e 100644 --- a/hornbeam_interpreter/src/interface.rs +++ b/hornbeam_interpreter/src/interface.rs @@ -1,4 +1,5 @@ use crate::engine::{Interpreter, Scope}; +use crate::functions::TemplateAccessibleMethod; use async_trait::async_trait; use bevy_reflect::Reflect; use hornbeam_grammar::parse_template; @@ -38,13 +39,15 @@ pub trait OutputSystem { // Value is currently used in the localisation system. We might pull it away later on... pub use crate::engine::Value; -use crate::InterpreterError; +use crate::{default_template_accessible_methods, InterpreterError}; pub struct LoadedTemplates { // todo might be tempted to use e.g. ouroboros here, to keep the file source adjacent? // or do we just staticify? template_functions: BTreeMap>>, + methods: Arc>, + localisation: Arc, } @@ -65,6 +68,7 @@ impl<'a, LS> LoadedTemplates { pub fn new(localisation_system: LS) -> Self { LoadedTemplates { template_functions: Default::default(), + methods: Arc::new(default_template_accessible_methods()), localisation: Arc::new(localisation_system), } } @@ -161,12 +165,14 @@ impl<'a, LS> LoadedTemplates { variables: params, localisation: self.localisation.clone(), locale, + methods: self.methods.clone(), } } } pub struct PreparedTemplate { pub(crate) all_instructions: Arc>>>, + pub(crate) methods: Arc>, pub(crate) entrypoint: String, pub(crate) variables: Params, pub(crate) localisation: Arc, @@ -188,6 +194,7 @@ impl PreparedTemplate { variables: self.variables.params, slots: Default::default(), }], + methods: &self.methods, }; interpreter.run().await?; Ok(()) diff --git a/hornbeam_interpreter/src/lib.rs b/hornbeam_interpreter/src/lib.rs index 08df625..73890e1 100644 --- a/hornbeam_interpreter/src/lib.rs +++ b/hornbeam_interpreter/src/lib.rs @@ -1,6 +1,7 @@ use std::fmt::Debug; mod engine; +mod functions; pub(crate) mod interface; pub mod localisation; @@ -36,4 +37,6 @@ pub enum InterpreterError { TemplateFindError(String), } +pub use functions::defaults::default_template_accessible_methods; +pub use functions::TemplateAccessibleMethod; pub use interface::{LoadedTemplates, LocalisationSystem, OutputSystem, Params, PreparedTemplate}; diff --git a/hornbeam_interpreter/tests/snapshots.rs b/hornbeam_interpreter/tests/snapshots.rs index c05a7b1..804ba5e 100644 --- a/hornbeam_interpreter/tests/snapshots.rs +++ b/hornbeam_interpreter/tests/snapshots.rs @@ -15,7 +15,7 @@ fn simple_test_struct() -> SimpleTestStruct { wombat: 42, apple: 78, banana: "banana!!!".to_owned(), - carrot: "mmm CARROT".to_owned(), + carrot: "mmm CARROT!".to_owned(), } } @@ -65,3 +65,16 @@ br "# )) } + +#[test] +fn snapshot_003() { + assert_snapshot!(simple_render( + r#" +"unpadded: ${$sts.carrot}" +br +"padded to 15: ${$sts.carrot.leftpad(15, 'M')}" +br +"urlencoded: ${$sts.carrot.urlencode()}" + "# + )) +} diff --git a/hornbeam_interpreter/tests/snapshots/snapshots__snapshot_003.snap b/hornbeam_interpreter/tests/snapshots/snapshots__snapshot_003.snap new file mode 100644 index 0000000..cab5f81 --- /dev/null +++ b/hornbeam_interpreter/tests/snapshots/snapshots__snapshot_003.snap @@ -0,0 +1,5 @@ +--- +source: hornbeam_interpreter/tests/snapshots.rs +expression: "simple_render(r#\"\n\"unpadded: ${$sts.carrot}\"\nbr\n\"padded to 15: ${$sts.carrot.leftpad(15, 'M')}\"\nbr\n\"urlencoded: ${$sts.carrot.urlencode()}\"\n \"#)" +--- +unpadded: mmm CARROT!
padded to 15: MMMMmmm CARROT!
urlencoded: mmm%20CARROT%21