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",
|
"html-escape",
|
||||||
"insta",
|
"insta",
|
||||||
"itertools",
|
"itertools",
|
||||||
|
"percent-encoding",
|
||||||
"pollster",
|
"pollster",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
@ -25,6 +25,7 @@ itertools = "0.10.5"
|
|||||||
pollster = "0.3.0"
|
pollster = "0.3.0"
|
||||||
|
|
||||||
tracing = "0.1.37"
|
tracing = "0.1.37"
|
||||||
|
percent-encoding = "2.2.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["fluent"]
|
default = ["fluent"]
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use crate::functions::TemplateAccessibleMethod;
|
||||||
use crate::interface::{LocalisationSystem, OutputSystem};
|
use crate::interface::{LocalisationSystem, OutputSystem};
|
||||||
use crate::InterpreterError;
|
use crate::InterpreterError;
|
||||||
use async_recursion::async_recursion;
|
use async_recursion::async_recursion;
|
||||||
@ -32,6 +33,7 @@ pub(crate) struct Interpreter<'a, O, LS> {
|
|||||||
pub(crate) localisation: Arc<LS>,
|
pub(crate) localisation: Arc<LS>,
|
||||||
pub(crate) locale: String,
|
pub(crate) locale: String,
|
||||||
pub(crate) scopes: Vec<Scope<'a>>,
|
pub(crate) scopes: Vec<Scope<'a>>,
|
||||||
|
pub(crate) methods: &'a BTreeMap<String, TemplateAccessibleMethod>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -727,8 +729,34 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expression::MethodCall { .. } => {
|
Expression::MethodCall {
|
||||||
unimplemented!()
|
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 } => {
|
Expression::Variable { name, loc } => {
|
||||||
let locals = &self.scopes[scope_idx].variables;
|
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::engine::{Interpreter, Scope};
|
||||||
|
use crate::functions::TemplateAccessibleMethod;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use bevy_reflect::Reflect;
|
use bevy_reflect::Reflect;
|
||||||
use hornbeam_grammar::parse_template;
|
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...
|
// Value is currently used in the localisation system. We might pull it away later on...
|
||||||
pub use crate::engine::Value;
|
pub use crate::engine::Value;
|
||||||
use crate::InterpreterError;
|
use crate::{default_template_accessible_methods, InterpreterError};
|
||||||
|
|
||||||
pub struct LoadedTemplates<LS> {
|
pub struct LoadedTemplates<LS> {
|
||||||
// todo might be tempted to use e.g. ouroboros here, to keep the file source adjacent?
|
// todo might be tempted to use e.g. ouroboros here, to keep the file source adjacent?
|
||||||
// or do we just staticify?
|
// or do we just staticify?
|
||||||
template_functions: BTreeMap<String, Arc<Vec<Step>>>,
|
template_functions: BTreeMap<String, Arc<Vec<Step>>>,
|
||||||
|
|
||||||
|
methods: Arc<BTreeMap<String, TemplateAccessibleMethod>>,
|
||||||
|
|
||||||
localisation: Arc<LS>,
|
localisation: Arc<LS>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,6 +68,7 @@ impl<'a, LS> LoadedTemplates<LS> {
|
|||||||
pub fn new(localisation_system: LS) -> Self {
|
pub fn new(localisation_system: LS) -> Self {
|
||||||
LoadedTemplates {
|
LoadedTemplates {
|
||||||
template_functions: Default::default(),
|
template_functions: Default::default(),
|
||||||
|
methods: Arc::new(default_template_accessible_methods()),
|
||||||
localisation: Arc::new(localisation_system),
|
localisation: Arc::new(localisation_system),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -161,12 +165,14 @@ impl<'a, LS> LoadedTemplates<LS> {
|
|||||||
variables: params,
|
variables: params,
|
||||||
localisation: self.localisation.clone(),
|
localisation: self.localisation.clone(),
|
||||||
locale,
|
locale,
|
||||||
|
methods: self.methods.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PreparedTemplate<LS> {
|
pub struct PreparedTemplate<LS> {
|
||||||
pub(crate) all_instructions: Arc<BTreeMap<String, Arc<Vec<Step>>>>,
|
pub(crate) all_instructions: Arc<BTreeMap<String, Arc<Vec<Step>>>>,
|
||||||
|
pub(crate) methods: Arc<BTreeMap<String, TemplateAccessibleMethod>>,
|
||||||
pub(crate) entrypoint: String,
|
pub(crate) entrypoint: String,
|
||||||
pub(crate) variables: Params,
|
pub(crate) variables: Params,
|
||||||
pub(crate) localisation: Arc<LS>,
|
pub(crate) localisation: Arc<LS>,
|
||||||
@ -188,6 +194,7 @@ impl<LS: LocalisationSystem + Sync + Send> PreparedTemplate<LS> {
|
|||||||
variables: self.variables.params,
|
variables: self.variables.params,
|
||||||
slots: Default::default(),
|
slots: Default::default(),
|
||||||
}],
|
}],
|
||||||
|
methods: &self.methods,
|
||||||
};
|
};
|
||||||
interpreter.run().await?;
|
interpreter.run().await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
||||||
mod engine;
|
mod engine;
|
||||||
|
mod functions;
|
||||||
pub(crate) mod interface;
|
pub(crate) mod interface;
|
||||||
|
|
||||||
pub mod localisation;
|
pub mod localisation;
|
||||||
@ -36,4 +37,6 @@ pub enum InterpreterError<LE: Debug + Clone, OE: Debug> {
|
|||||||
TemplateFindError(String),
|
TemplateFindError(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub use functions::defaults::default_template_accessible_methods;
|
||||||
|
pub use functions::TemplateAccessibleMethod;
|
||||||
pub use interface::{LoadedTemplates, LocalisationSystem, OutputSystem, Params, PreparedTemplate};
|
pub use interface::{LoadedTemplates, LocalisationSystem, OutputSystem, Params, PreparedTemplate};
|
||||||
|
@ -15,7 +15,7 @@ fn simple_test_struct() -> SimpleTestStruct {
|
|||||||
wombat: 42,
|
wombat: 42,
|
||||||
apple: 78,
|
apple: 78,
|
||||||
banana: "banana!!!".to_owned(),
|
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