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::*;