Add support for template-accessible methods and include leftpad and urlencode

This commit is contained in:
Olivier 'reivilibre' 2024-05-02 21:18:32 +01:00
parent b119bf376c
commit 00e67d34d9
9 changed files with 159 additions and 4 deletions

1
Cargo.lock generated
View File

@ -782,6 +782,7 @@ dependencies = [
"html-escape",
"insta",
"itertools",
"percent-encoding",
"pollster",
"thiserror",
"tracing",

View File

@ -25,6 +25,7 @@ itertools = "0.10.5"
pollster = "0.3.0"
tracing = "0.1.37"
percent-encoding = "2.2.0"
[features]
default = ["fluent"]

View File

@ -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<LS>,
pub(crate) locale: String,
pub(crate) scopes: Vec<Scope<'a>>,
pub(crate) methods: &'a BTreeMap<String, TemplateAccessibleMethod>,
}
#[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<Value> = 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;

View File

@ -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<Value>) -> Result<Value, String>,
}
impl TemplateAccessibleMethod {
pub(crate) fn call(&self, obj: Value, args: Vec<Value>) -> Result<Value, String> {
(self.function)(obj, args)
}
}

View File

@ -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<Value>) -> Result<Value, String>,
)] = &[("leftpad", leftpad), ("urlencode", urlencode)];
/// Return a map of the default suggested template-accessible methods.
pub fn default_template_accessible_methods() -> BTreeMap<String, TemplateAccessibleMethod> {
DEFAULT_TEMPLATE_ACCESSIBLE_METHODS
.iter()
.map(|(name, func)| {
(
(*name).to_owned(),
TemplateAccessibleMethod { function: *func },
)
})
.collect()
}
pub fn leftpad(obj: Value, args: Vec<Value>) -> Result<Value, String> {
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<Value>) -> Result<Value, String> {
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(),
)))
}

View File

@ -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<LS> {
// todo might be tempted to use e.g. ouroboros here, to keep the file source adjacent?
// or do we just staticify?
template_functions: BTreeMap<String, Arc<Vec<Step>>>,
methods: Arc<BTreeMap<String, TemplateAccessibleMethod>>,
localisation: Arc<LS>,
}
@ -65,6 +68,7 @@ impl<'a, LS> LoadedTemplates<LS> {
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<LS> {
variables: params,
localisation: self.localisation.clone(),
locale,
methods: self.methods.clone(),
}
}
}
pub struct PreparedTemplate<LS> {
pub(crate) all_instructions: Arc<BTreeMap<String, Arc<Vec<Step>>>>,
pub(crate) methods: Arc<BTreeMap<String, TemplateAccessibleMethod>>,
pub(crate) entrypoint: String,
pub(crate) variables: Params,
pub(crate) localisation: Arc<LS>,
@ -188,6 +194,7 @@ impl<LS: LocalisationSystem + Sync + Send> PreparedTemplate<LS> {
variables: self.variables.params,
slots: Default::default(),
}],
methods: &self.methods,
};
interpreter.run().await?;
Ok(())

View File

@ -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<LE: Debug + Clone, OE: Debug> {
TemplateFindError(String),
}
pub use functions::defaults::default_template_accessible_methods;
pub use functions::TemplateAccessibleMethod;
pub use interface::{LoadedTemplates, LocalisationSystem, OutputSystem, Params, PreparedTemplate};

View File

@ -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()}"
"#
))
}

View File

@ -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!<br>padded to 15: MMMMmmm CARROT!<br>urlencoded: mmm%20CARROT%21