tera
tera copied to clipboard
Access tera context from functions code
Hi!
I have a situation, where it would be useful to have a global variable in the functions. I faced it when working with translations / localization using fluent.
Basically on the rust side I have something like this:
let mut ctx = Context::new();
ctx.insert("LANG", "en");
// more inserts
tera.render(template_name, ctx)
And then in the template code I have
<p>{{translate("some text", lang=LANG)}}<p>
<p>{{translate("more text", lang=LANG)}}<p>
e.t.c.
It works, but I have to put lang argument every time, which is a bit verbose for my taste.
The only solution that we have for now is to modify translate function in the rust code, so it will capture a lang variable.
The problem is that Function trait is required to be Send + Sync.
So, we need to use an external variable and the synchronization for this. Synchronization will mean, that we can not render templates on web server in parallel. Because each client needs different value of the global variable. (for example different languages).
Can't we pass reference to Context inside each function? This mean that lambdas that we use in register_function in addition to HashMap with arguments also accepts &Context reference. Then I can know value of "LANG" inside translate implementation. This value will be specific to each render() call and can work in parallel.
It's something that would be nice but is a breaking change. Probably for the next version.
A similar issue comes up with URLs in actix-web.
There is a ResourceMap struct with URL templates and a URL building method url_for, but it cannot be serialized.
- One poor man's solution would be to extract the URL templates to something serializable, inject that into Context, and then use them in a Tera function - like the LANG string above.
- A better way would be to have some way to pass arbitrary references to the Tera context that can be only accessed from functions and filters. In C this would be a void*, but I'm not really sure how to go about that in rust...
- Or context could allow defining ad-hoc closure functions that can be called by a given name from the template. This may be the best option, if it's possible...
I don't know about ResourceMap but if it's created outside of Tera, you can just impl the TeraFn trait on it. That's how I pass things in Zola: https://github.com/getzola/zola/blob/master/components/templates/src/global_fns/mod.rs#L24-L46
@Keats If I read that right, you still have to pass the lang argument to the function, right?
Here's one way to do what I want right now, with pre-generating the URLs and passing them inside the template as strings.
(ResourceMap is part of HttpRequest, so it's accessed through the request)
#[get("/show-page")]
pub(crate) async fn show_page(request : HttpRequest, session : Session) -> actix_web::Result<impl Responder> {
let mut context = tera::Context::new();
let delete_url = request.url_for("record", &["123", "delete"])?;
context.insert("delete_url", &delete_url.to_string());
let html = TERA.render("page", &context).map_err(|e| {
actix_web::error::ErrorInternalServerError(e)
})?;
Ok(HttpResponse::Ok().body(html))
}
then I can use {{ delete_url }}.
Ideally, I could use something like this in the template: {{ url_for("record", ["123", "delete"] }}.
If I read that right, you still have to pass the lang argument to the function, right?
Yes. Passing the context automatically is going to be a feature of Tera 2.0, but it hasn't started yet.
request.url_for("record", &["123", "delete"])?;
That sounds like this URL resolver doesn't need to be at the request level and it could exist when you're declaring the actix routes right? How would you generate the routes in a test for example? If the URL mapping is available in the actix main then you should be able to create a Tera function that works like {{ url_for("name=record", args=["123", "delete"]) }}
I'm still exploring actix-web, so I may have missed something. If the routes were known ahead, then yeah your solution works fine
Here's how the app is started:
use once_cell::sync::Lazy;
pub(crate) static TERA : Lazy<Tera> = Lazy::new(|| {
let mut tera = Tera::default();
tera.add_include_dir_templates(&TEMPLATES); // my extension to add files from include_dir!
tera
});
#[actix_web::main]
async fn main() -> std::io::Result<()> {
simple_logging::log_to_stderr(LevelFilter::Debug);
// Ensure the lazy ref is initialized early (to catch template bugs at startup)
let _ = TERA.deref();
let yopa_store: YopaStoreWrapper = init_yopa();
let mut session_key = [0u8; 32];
rand::thread_rng().fill(&mut session_key);
HttpServer::new(move || {
// If I understand this right, the closure runs for every started worker thread.
// Tera could be initialized here for every thread, but we still don't know the URLs
let static_files = StaticFiles::new("/static", included_static_files())
.do_not_resolve_defaults();
// This creates a "Service Factory". The URLs can't be read from it at this point
App::new()
/* Bind shared objects */
.app_data(yopa_store.clone())
/* Routes */
.service(routes::index) // URL is read from the attribute macro by actix procgen (?)
.service(static_files)
.default_service(web::to(|| HttpResponse::NotFound().body("Not found")))
})
.bind("127.0.0.1:8080")?
.run().await
}
I have a use case for modifying the context during a global function. Is there any willingness to support a function call structure that takes a &mut tera::Context rather than simply being able to access it?
I would say no