Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Access tera context from functions code #543

Open
technic opened this issue Aug 1, 2020 · 8 comments
Open

Access tera context from functions code #543

technic opened this issue Aug 1, 2020 · 8 comments

Comments

@technic
Copy link

technic commented Aug 1, 2020

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.

@Keats
Copy link
Owner

Keats commented Aug 2, 2020

It's something that would be nice but is a breaking change. Probably for the next version.

@MightyPork
Copy link

MightyPork commented Feb 7, 2021

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...

@Keats
Copy link
Owner

Keats commented Feb 7, 2021

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

@MightyPork
Copy link

MightyPork commented Feb 7, 2021

@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"] }}.

@Keats
Copy link
Owner

Keats commented Feb 7, 2021

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"]) }}

@MightyPork
Copy link

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
}

@rydrman
Copy link

rydrman commented Jan 14, 2024

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?

@Keats
Copy link
Owner

Keats commented Jan 15, 2024

I would say no

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants