add Context-scoped functions
I use tera in an actix-web project and wanted to expose the url_for method of HttpRequest to the template.
The template engine is initialised globally, and so are its registered functions. url_for is specific to the request instance. And the latter is neither Serialisable nor Send nor Sync.
I managed to introduce functions local to a Context. I had to create a separate trait that does not require Send+Sync (in my use case, the HttpRequest contains an Rc, which is neither). I had to add plumbing to the Callstack. The Processor was modified so that a Context-local function will shadow a global one.
For all the changes, see: https://github.com/moschroe/tera/commit/c613ad7dc81eddaff0000ecca76d4693b3b9ac2f
An example using these modifications might look like this:
pub fn register_url_for(context: &mut Context, req: HttpRequest) {
let closure = move |args: &HashMap<String, Value>| -> tera::Result<Value> {
return args
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("missing 'name'"))
.and_then(|key| -> tera::Result<Value> {
let elements = args
.get("elements")
.and_then(Value::as_array)
// TODO: remove Vec allocation by using iterator directly
.map(|arr| arr.iter().map(Value::to_string).collect::<Vec<_>>())
.unwrap_or(vec![]);
req.url_for(key, elements)
.map_err(|err_url| {
tera::Error::msg(format!("unable to create URL: {:?}", err_url))
})
.map(|url| Value::from(url.as_str()))
});
};
context.register_function("url_for", FunctionRelaxedSafe::from(closure));
}
Would this be feasible as a stop-gap solution until 2.0 allows other work-arounds? Shall I open a PR?
Wait the url_for of actix is on a request....?
How would you change the global Tera types to fit that usecase for v2?
TBH, I have no idea about any possible internals of 2.0?
But seeing the Sync+Send on Function, for example, that enables lazy_static! to work and also obviates pervasive lifetime tracking, I'm not sure how feasible it would be to drop those entirely.
If a Context had a non-static lifetime, one could even have non-static values in Context-local functions (or data, provided the serde can be sorted out/avoided), allowing for more freedom whenever the rendering would never outlast temporary data. Ergonomically, I feel it ought to be similar to crossbeam scoped threads.
Another possibility might be to take a leaf out of Lua's book and extend tera::Value by more "not-plain-data" variants like functions or userdata.
TBH, I have no idea about any possible internals of 2.0?
The only part written so far is the parser and the rest might get rewritten from scratch, especially if it uses valuable.
It's kind of hard to cover all the bases, so far 'static seem to satisfy most (?) people.
I was not aware of the mlua approach that seems pretty nice.
Would it be possible to include this addition? It might be able to tide people over until there is a next major version. (And maybe relieve some pressure there?) And it seems to be the minimally viable solution, anything more sophisticated needs proper lifetimes everywhere.
I'd be happy to do any necessary changes/cleanup/etc.
Can you do a PR so I can review the code easily?
Implemented a scoped Context for good measure, see at https://github.com/moschroe/tera/tree/feature_context-lifetime. This allows a closure to capture simple references, enabling the use of truly ephemeral data in templates without reference counting, wrappers, etc.
I came here looking for this feature too, but I'm surprised by the implementation. It sounds like you must call context.register_function within every request (assuming something like rocket), rather than just once on app startup? And that creates a new closure every time? I would have expected something like register_context_aware_function you can call once, and it generates a function that takes both a HashMap and a Context. Is that unreasonable? It seems like it would run faster and be more straightforward to use. Would be interested in a PR doing that?
Thank you for your work on this template library btw!
For @moschroe case, it seems the url mapping is on the request for some reason so doing it once at app init time wouldn't work afaik
@pjungwir Firstly, if the thing I want to use within a template does not exist when initializing tera, there is logically no way to bind it. As keats already pointed out, with actix-web, creating a URL can depend on the request, so there is no global/static way to do it. Thus, only when I handle a request and want to render the template, the struct I need is available (and neither Send nor Sync, which started this whole odyssey).
Secondly, remember in rust a closure is sugar for static code (maybe specialized for generics) and the only dynamic thing: A struct with the captured values. So as long as preparing this struct is cheap (and cloning an Rc or a reference is), then "creating" the closure is pretty cheap as well. A microbenchmark I did showed that all the HTTP and socket stuff takes an order of magnitude more time than the body of the handler function, including creating the closure and doing tera's string wrangling.
Thank you both for taking the time to answer.
I understand that the request is not available up front. I'm saying just as the existing PR adds a new kind of function, so we could also/alternately add a new kind of function that takes a Context object as a second parameter (besides the HashMap that custom functions already receive). So it's just a different way to pass the context when the function is called, but without using a closure (and also without needing to register the function repeatedly in each request that might use it).
It's good to know that building the closures is relatively inexpensive. I still think just passing the Context to custom functions would be even cheaper, and also less complicated for people defining those functions.
FWIW: My own concrete use case also depends on request-specific data: I'm writing a function that takes a localization key and zero or more extra parameters, and it uses the current user's preferred language to look up the localized message. I can pass the language each time I call the function, but that gets tedious & noisy, so I'd rather have the function look it up implicitly. If Tera passed me the context I wouldn't need to write lang=... all over my template. (I understand this new function-on-context also solves my problem, I'm just saying there may be a faster & simpler way.)
If Tera passed me the context I wouldn't need to write lang=... all over my template
This is annoying me as well in Zola, I'll probably pass the context in some way to functions & filters for V2