Add support for template-accessible methods and include leftpad and urlencode
This commit is contained in:
parent
b119bf376c
commit
00e67d34d9
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -782,6 +782,7 @@ dependencies = [
|
||||
"html-escape",
|
||||
"insta",
|
||||
"itertools",
|
||||
"percent-encoding",
|
||||
"pollster",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
|
@ -25,6 +25,7 @@ itertools = "0.10.5"
|
||||
pollster = "0.3.0"
|
||||
|
||||
tracing = "0.1.37"
|
||||
percent-encoding = "2.2.0"
|
||||
|
||||
[features]
|
||||
default = ["fluent"]
|
||||
|
@ -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;
|
||||
|
23
hornbeam_interpreter/src/functions.rs
Normal file
23
hornbeam_interpreter/src/functions.rs
Normal 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)
|
||||
}
|
||||
}
|
74
hornbeam_interpreter/src/functions/defaults.rs
Normal file
74
hornbeam_interpreter/src/functions/defaults.rs
Normal 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(),
|
||||
)))
|
||||
}
|
@ -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(())
|
||||
|
@ -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};
|
||||
|
@ -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()}"
|
||||
"#
|
||||
))
|
||||
}
|
||||
|
@ -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
|
Loading…
Reference in New Issue
Block a user