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

add Context-scoped functions #656

Open
moschroe opened this issue Jul 22, 2021 · 12 comments
Open

add Context-scoped functions #656

moschroe opened this issue Jul 22, 2021 · 12 comments

Comments

@moschroe
Copy link
Contributor

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: moschroe@c613ad7

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?

@Keats
Copy link
Owner

Keats commented Jul 22, 2021

Wait the url_for of actix is on a request....?
How would you change the global Tera types to fit that usecase for v2?

@moschroe
Copy link
Contributor Author

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.

@Keats
Copy link
Owner

Keats commented Jul 23, 2021

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.

@moschroe
Copy link
Contributor Author

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.

@Keats
Copy link
Owner

Keats commented Aug 24, 2021

Can you do a PR so I can review the code easily?

@moschroe
Copy link
Contributor Author

moschroe commented Sep 5, 2021

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.

@pjungwir
Copy link

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!

@Keats
Copy link
Owner

Keats commented Oct 28, 2021

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

@moschroe
Copy link
Contributor Author

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

@pjungwir
Copy link

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.

@pjungwir
Copy link

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

@Keats
Copy link
Owner

Keats commented Oct 29, 2021

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

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

No branches or pull requests

3 participants