minijinja/functions.rs
1//! Global functions and abstractions.
2//!
3//! This module provides the abstractions for functions that can registered as
4//! global functions to the environment via
5//! [`add_function`](crate::Environment::add_function).
6//!
7//! # Using Functions
8//!
9//! Functions can be called in any place where an expression is valid. They
10//! are useful to retrieve data. Some functions are special and provided
11//! by the engine (like `super`) within certain context, others are global.
12//!
13//! The following is a motivating example:
14//!
15//! ```jinja
16//! <pre>{{ debug() }}</pre>
17//! ```
18//!
19//! # Custom Functions
20//!
21//! A custom global function is just a simple rust function which accepts optional
22//! arguments and then returns a result. Global functions are typically used to
23//! perform a data loading operation. For instance these functions can be used
24//! to expose data to the template that hasn't been provided by the individual
25//! render invocation.
26//!
27//! ```rust
28//! # use minijinja::Environment;
29//! # let mut env = Environment::new();
30//! use minijinja::{Error, ErrorKind};
31//!
32//! fn include_file(name: String) -> Result<String, Error> {
33//! std::fs::read_to_string(&name)
34//! .map_err(|e| Error::new(
35//! ErrorKind::InvalidOperation,
36//! "cannot load file"
37//! ).with_source(e))
38//! }
39//!
40//! env.add_function("include_file", include_file);
41//! ```
42//!
43#![cfg_attr(
44 feature = "deserialization",
45 doc = r#"
46# Arguments in Custom Functions
47
48All arguments in custom functions must implement the [`ArgType`] trait.
49Standard types, such as `String`, `i32`, `bool`, `f64`, etc, already implement this trait.
50There are also helper types that will make it easier to extract an arguments with custom types.
51The [`ViaDeserialize<T>`](crate::value::ViaDeserialize) type, for instance, can accept any
52type `T` that implements the `Deserialize` trait from `serde`.
53
54```rust
55# use minijinja::Environment;
56# use serde::Deserialize;
57# let mut env = Environment::new();
58use minijinja::value::ViaDeserialize;
59
60#[derive(Deserialize)]
61struct Person {
62 name: String,
63 age: i32,
64}
65
66fn is_adult(person: ViaDeserialize<Person>) -> bool {
67 person.age >= 18
68}
69
70env.add_function("is_adult", is_adult);
71```
72"#
73)]
74//!
75//! # Note on Keyword Arguments
76//!
77//! MiniJinja inherits a lot of the runtime model from Jinja2. That includes support for
78//! keyword arguments. These however are a concept not native to Rust which makes them
79//! somewhat uncomfortable to work with. In MiniJinja keyword arguments are implemented by
80//! converting them into an extra parameter represented by a map. That means if you call
81//! a function as `foo(1, 2, three=3, four=4)` the function gets three arguments:
82//!
83//! ```json
84//! [1, 2, {"three": 3, "four": 4}]
85//! ```
86//!
87//! If a function wants to disambiguate between a value passed as keyword argument or not,
88//! the [`Value::is_kwargs`] can be used which returns `true` if a value represents
89//! keyword arguments as opposed to just a map. A more convenient way to work with keyword
90//! arguments is the [`Kwargs`](crate::value::Kwargs) type.
91//!
92//! # Built-in Functions
93//!
94//! When the `builtins` feature is enabled a range of built-in functions are
95//! automatically added to the environment. These are also all provided in
96//! this module. Note though that these functions are not to be
97//! called from Rust code as their exact interface (arguments and return types)
98//! might change from one MiniJinja version to another.
99use std::fmt;
100use std::sync::Arc;
101
102use crate::error::Error;
103use crate::utils::SealedMarker;
104use crate::value::{ArgType, FunctionArgs, FunctionResult, Object, ObjectRepr, Value};
105use crate::vm::State;
106
107type FuncFunc = dyn Fn(&State, &[Value]) -> Result<Value, Error> + Sync + Send + 'static;
108
109/// A boxed function.
110#[derive(Clone)]
111pub(crate) struct BoxedFunction(Arc<FuncFunc>, #[cfg(feature = "debug")] &'static str);
112
113/// A utility trait that represents global functions.
114///
115/// This trait is used by the [`add_function`](crate::Environment::add_function)
116/// method to abstract over different types of functions.
117///
118/// Functions which at the very least accept the [`State`] by reference as first
119/// parameter and additionally up to 4 further parameters. They share much of
120/// their interface with [`filters`](crate::filters).
121///
122/// A function can return any of the following types:
123///
124/// * `Rv` where `Rv` implements `Into<Value>`
125/// * `Result<Rv, Error>` where `Rv` implements `Into<Value>`
126///
127/// The parameters can be marked optional by using `Option<T>`. The last
128/// argument can also use [`Rest<T>`](crate::value::Rest) to capture the
129/// remaining arguments. All types are supported for which
130/// [`ArgType`] is implemented.
131///
132/// For a list of built-in functions see [`functions`](crate::functions).
133///
134/// **Note:** this trait cannot be implemented and only exists drive the
135/// functionality of [`add_function`](crate::Environment::add_function)
136/// and [`from_function`](crate::value::Value::from_function). If you want
137/// to implement a custom callable, you can directly implement
138/// [`Object::call`] which is what the engine actually uses internally.
139///
140/// This trait is also used for [`filters`](crate::filters) and
141/// [`tests`](crate::tests).
142///
143/// # Basic Example
144///
145/// ```rust
146/// # use minijinja::Environment;
147/// # let mut env = Environment::new();
148/// use minijinja::{Error, ErrorKind};
149///
150/// fn include_file(name: String) -> Result<String, Error> {
151/// std::fs::read_to_string(&name)
152/// .map_err(|e| Error::new(
153/// ErrorKind::InvalidOperation,
154/// "cannot load file"
155/// ).with_source(e))
156/// }
157///
158/// env.add_function("include_file", include_file);
159/// ```
160///
161/// ```jinja
162/// {{ include_file("filename.txt") }}
163/// ```
164///
165/// # Variadic
166///
167/// ```
168/// # use minijinja::Environment;
169/// # let mut env = Environment::new();
170/// use minijinja::value::Rest;
171///
172/// fn sum(values: Rest<i64>) -> i64 {
173/// values.iter().sum()
174/// }
175///
176/// env.add_function("sum", sum);
177/// ```
178///
179/// ```jinja
180/// {{ sum(1, 2, 3) }} -> 6
181/// ```
182///
183/// # Optional Arguments
184///
185/// ```
186/// # use minijinja::Environment;
187/// # let mut env = Environment::new();
188/// fn substr(value: String, start: u32, end: Option<u32>) -> String {
189/// let end = end.unwrap_or(value.len() as _);
190/// value.get(start as usize..end as usize).unwrap_or_default().into()
191/// }
192///
193/// env.add_filter("substr", substr);
194/// ```
195///
196/// ```jinja
197/// {{ "Foo Bar Baz"|substr(4) }} -> Bar Baz
198/// {{ "Foo Bar Baz"|substr(4, 7) }} -> Bar
199/// ```
200pub trait Function<Rv, Args: for<'a> FunctionArgs<'a>>: Send + Sync + 'static {
201 /// Calls a function with the given arguments.
202 #[doc(hidden)]
203 fn invoke(&self, args: <Args as FunctionArgs<'_>>::Output, _: SealedMarker) -> Rv;
204}
205
206// This is necessary to avoid a bug in the trait solver. See
207// https://github.com/mitsuhiko/minijinja/pull/787 for more details.
208trait FunctionHelper<Rv, Args> {
209 fn invoke_nested(&self, args: Args) -> Rv;
210}
211
212macro_rules! tuple_impls {
213 ( $( $name:ident )* ) => {
214 impl<Func, Rv, $($name),*> FunctionHelper<Rv, ($($name,)*)> for Func
215 where
216 Func: Fn($($name),*) -> Rv
217 {
218 fn invoke_nested(&self, args: ($($name,)*)) -> Rv {
219 #[allow(non_snake_case)]
220 let ($($name,)*) = args;
221 (self)($($name,)*)
222 }
223 }
224
225 impl<Func, Rv, $($name),*> Function<Rv, ($($name,)*)> for Func
226 where
227 Func: Send + Sync + 'static,
228 // the crazy bounds here exist to enable borrowing in closures
229 Func: Fn($($name),*) -> Rv + for<'a> FunctionHelper<Rv, ($(<$name as ArgType<'a>>::Output,)*)>,
230 Rv: FunctionResult,
231 $($name: for<'a> ArgType<'a>,)*
232 {
233 // Need to allow this lint for the one-element tuple case.
234 #[allow(clippy::needless_lifetimes)]
235 fn invoke<'a>(&self, args: ($(<$name as ArgType<'a>>::Output,)*), _: SealedMarker) -> Rv {
236 self.invoke_nested(args)
237 }
238 }
239 };
240}
241
242tuple_impls! {}
243tuple_impls! { A }
244tuple_impls! { A B }
245tuple_impls! { A B C }
246tuple_impls! { A B C D }
247tuple_impls! { A B C D E }
248
249impl BoxedFunction {
250 /// Creates a new boxed filter.
251 pub fn new<F, Rv, Args>(f: F) -> BoxedFunction
252 where
253 F: Function<Rv, Args>,
254 Rv: FunctionResult,
255 Args: for<'a> FunctionArgs<'a>,
256 {
257 BoxedFunction(
258 Arc::new(move |state, args| -> Result<Value, Error> {
259 f.invoke(ok!(Args::from_values(Some(state), args)), SealedMarker)
260 .into_result()
261 }),
262 #[cfg(feature = "debug")]
263 std::any::type_name::<F>(),
264 )
265 }
266
267 /// Invokes the function.
268 pub fn invoke(&self, state: &State, args: &[Value]) -> Result<Value, Error> {
269 (self.0)(state, args)
270 }
271
272 /// Creates a value from a boxed function.
273 pub fn to_value(&self) -> Value {
274 Value::from_object(self.clone())
275 }
276}
277
278impl fmt::Debug for BoxedFunction {
279 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
280 #[cfg(feature = "debug")]
281 {
282 if !self.1.is_empty() {
283 return f.write_str(self.1);
284 }
285 }
286 f.write_str("function")
287 }
288}
289
290impl Object for BoxedFunction {
291 fn repr(self: &Arc<Self>) -> ObjectRepr {
292 ObjectRepr::Plain
293 }
294
295 fn call(self: &Arc<Self>, state: &State, args: &[Value]) -> Result<Value, Error> {
296 self.invoke(state, args)
297 }
298}
299
300#[cfg(feature = "builtins")]
301mod builtins {
302 use super::*;
303
304 use crate::error::ErrorKind;
305 use crate::value::{Rest, ValueMap, ValueRepr};
306
307 /// Returns a range.
308 ///
309 /// Return a list containing an arithmetic progression of integers. `range(i,
310 /// j)` returns `[i, i+1, i+2, ..., j-1]`. `lower` defaults to 0. When `step` is
311 /// given, it specifies the increment (or decrement). For example, `range(4)`
312 /// and `range(0, 4, 1)` return `[0, 1, 2, 3]`. The end point is omitted.
313 ///
314 /// ```jinja
315 /// <ul>
316 /// {% for num in range(1, 11) %}
317 /// <li>{{ num }}
318 /// {% endfor %}
319 /// </ul>
320 /// ```
321 ///
322 /// This function will refuse to create ranges over 10.000 items.
323 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
324 pub fn range(lower: u32, upper: Option<u32>, step: Option<u32>) -> Result<Value, Error> {
325 fn to_result<I: ExactSizeIterator<Item = u32> + Send + Sync + Clone + 'static>(
326 i: I,
327 ) -> Result<Value, Error> {
328 if i.len() > 100000 {
329 Err(Error::new(
330 ErrorKind::InvalidOperation,
331 "range has too many elements",
332 ))
333 } else {
334 Ok(Value::make_iterable(move || i.clone()))
335 }
336 }
337
338 let rng = match upper {
339 Some(upper) => lower..upper,
340 None => 0..lower,
341 };
342 if let Some(step) = step {
343 if step == 0 {
344 Err(Error::new(
345 ErrorKind::InvalidOperation,
346 "cannot create range with step of 0",
347 ))
348 } else {
349 to_result(rng.step_by(step as usize))
350 }
351 } else {
352 to_result(rng)
353 }
354 }
355
356 /// Creates a dictionary.
357 ///
358 /// This is a convenient alternative for a dictionary literal.
359 /// `{"foo": "bar"}` is the same as `dict(foo="bar")`.
360 ///
361 /// ```jinja
362 /// <script>const CONFIG = {{ dict(
363 /// DEBUG=true,
364 /// API_URL_PREFIX="/api"
365 /// )|tojson }};</script>
366 /// ```
367 ///
368 /// Additionally this can be used to merge objects by passing extra keyword
369 /// arguments:
370 ///
371 /// ```jinja
372 /// {% set new_dict = dict(old_dict, extra_value=2) %}
373 /// ```
374 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
375 pub fn dict(value: Option<Value>, update_with: crate::value::Kwargs) -> Result<Value, Error> {
376 let mut rv = match value {
377 None => ValueMap::default(),
378 Some(value) => match value.0 {
379 ValueRepr::Undefined(_) => ValueMap::default(),
380 ValueRepr::Object(obj) if obj.repr() == ObjectRepr::Map => {
381 obj.try_iter_pairs().into_iter().flatten().collect()
382 }
383 _ => return Err(Error::from(ErrorKind::InvalidOperation)),
384 },
385 };
386
387 if update_with.values.is_true() {
388 rv.extend(
389 update_with
390 .values
391 .iter()
392 .map(|(k, v)| (k.clone(), v.clone())),
393 );
394 }
395
396 Ok(Value::from_object(rv))
397 }
398
399 /// Outputs the current context or the arguments stringified.
400 ///
401 /// This is a useful function to quickly figure out the state of affairs
402 /// in a template. It emits a stringified debug dump of the current
403 /// engine state including the layers of the context, the current block
404 /// and auto escaping setting. The exact output is not defined and might
405 /// change from one version of Jinja2 to the next.
406 ///
407 /// ```jinja
408 /// <pre>{{ debug() }}</pre>
409 /// <pre>{{ debug(variable1, variable2) }}</pre>
410 /// ```
411 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
412 pub fn debug(state: &State, args: Rest<Value>) -> String {
413 if args.is_empty() {
414 format!("{state:#?}")
415 } else if args.len() == 1 {
416 format!("{:#?}", args.0[0])
417 } else {
418 format!("{:#?}", &args.0[..])
419 }
420 }
421
422 /// Creates a new container that allows attribute assignment using the `{% set %}` tag.
423 ///
424 /// ```jinja
425 /// {% set ns = namespace() %}
426 /// {% set ns.foo = 'bar' %}
427 /// ```
428 ///
429 /// The main purpose of this is to allow carrying a value from within a loop body
430 /// to an outer scope. Initial values can be provided as a dict, as keyword arguments,
431 /// or both (same behavior as [`dict`]).
432 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
433 pub fn namespace(defaults: Option<Value>) -> Result<Value, Error> {
434 let ns = crate::value::namespace_object::Namespace::default();
435 if let Some(defaults) = defaults {
436 if let Some(pairs) = defaults
437 .as_object()
438 .filter(|x| matches!(x.repr(), ObjectRepr::Map))
439 .and_then(|x| x.try_iter_pairs())
440 {
441 for (key, value) in pairs {
442 if let Some(key) = key.as_str() {
443 ns.set_value(key, value);
444 }
445 }
446 } else {
447 return Err(Error::new(
448 ErrorKind::InvalidOperation,
449 format!(
450 "expected object or keyword arguments, got {}",
451 defaults.kind()
452 ),
453 ));
454 }
455 }
456 Ok(Value::from_object(ns))
457 }
458}
459
460#[cfg(feature = "builtins")]
461pub use self::builtins::*;