1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
//! Django-shape custom template tags + filters — issue #383.
//!
//! Django lets app code register custom filters / functions / block
//! tags via the `@register.filter` / `@register.simple_tag` /
//! `@register.tag` decorators, which load automatically when the
//! app is in `INSTALLED_APPS`. This module ships the equivalent for
//! Tera: an inventory-collected registry of filters + functions
//! that the framework's Tera builders apply at template-engine
//! construction time.
//!
//! Tera doesn't expose a block-tag plugin API (no parser-level
//! extension point — see [`crate::cache_fragment`] for the same
//! limitation surface). So Django's `{% mytag %}` block-tag shape
//! stays out of reach; filters + functions are everything app code
//! actually needs in practice.
//!
//! ## Usage
//!
//! ```ignore
//! use std::collections::HashMap;
//! use serde_json::Value;
//!
//! fn shout(value: &Value, _args: &HashMap<String, Value>) -> tera::Result<Value> {
//! Ok(Value::String(
//! value.as_str().unwrap_or("").to_uppercase() + "!"
//! ))
//! }
//! rustango::register_template_filter!("shout", shout);
//!
//! fn build_version(_args: &HashMap<String, Value>) -> tera::Result<Value> {
//! Ok(Value::String(env!("CARGO_PKG_VERSION").to_string()))
//! }
//! rustango::register_template_function!("build_version", build_version);
//! ```
//!
//! Then in a template:
//!
//! ```jinja
//! {{ "hello" | shout }} {# → HELLO! #}
//! {{ build_version() }} {# → 0.42.0 #}
//! ```
//!
//! Filters + functions register globally — every Tera instance the
//! framework builds (admin, template_views, email_templates) picks
//! them up via [`apply_to_tera`].
//!
//! ## Wiring into your own Tera instances
//!
//! Apps building their own `Tera` (outside the framework's CBVs)
//! must call [`apply_to_tera`] explicitly. It's a no-op when no
//! extensions are registered, so call it unconditionally:
//!
//! ```ignore
//! let mut tera = tera::Tera::new("templates/**/*.html")?;
//! rustango::default_filters::register_filters(&mut tera);
//! rustango::template_extensions::apply_to_tera(&mut tera);
//! ```
//!
//! ## Why inventory storage requires `fn` pointers
//!
//! Same reason as [`crate::admin::custom_views::CustomViewHandler`]
//! and [`crate::template_context_processors::ContextProcessorFn`]:
//! `inventory::submit!`'s `static` storage can only hold const-
//! constructible values, so the registration MUST be a plain
//! `fn` pointer rather than `Arc<dyn Fn>`. Both registry macros
//! coerce the user's expression to the typed pointer up front so
//! closure-body type inference still works.
use std::collections::HashMap;
use serde_json::Value;
use tera::Tera;
/// Tera filter signature. Matches the type Tera's
/// `register_filter` expects when given a plain fn.
pub type TeraFilterFn = fn(&Value, &HashMap<String, Value>) -> tera::Result<Value>;
/// Tera function signature. Matches the type Tera's
/// `register_function` expects when given a plain fn.
pub type TeraFunctionFn = fn(&HashMap<String, Value>) -> tera::Result<Value>;
/// One filter registration. Inventory-collected via
/// [`crate::register_template_filter!`].
pub struct TemplateFilter {
/// Name templates use: `{{ value | foo }}`.
pub name: &'static str,
/// The callable.
pub filter: TeraFilterFn,
}
inventory::collect!(TemplateFilter);
/// One function registration. Inventory-collected via
/// [`crate::register_template_function!`].
pub struct TemplateFunction {
/// Name templates use: `{{ foo(arg1=...) }}` or
/// `{% set x = foo() %}`.
pub name: &'static str,
/// The callable.
pub function: TeraFunctionFn,
}
inventory::collect!(TemplateFunction);
/// Apply every inventory-registered filter + function to `tera`.
/// Idempotent — Tera's `register_filter` / `register_function`
/// overwrite the previous binding with the same name, so calling
/// `apply_to_tera` twice is safe.
///
/// Re-registering a built-in name (`length`, `upper`, …) replaces
/// the built-in with the user version. Same trade-off Django
/// makes; document accordingly if you override a stock filter.
pub fn apply_to_tera(tera: &mut Tera) {
for entry in inventory::iter::<TemplateFilter> {
tera.register_filter(entry.name, entry.filter);
}
for entry in inventory::iter::<TemplateFunction> {
tera.register_function(entry.name, entry.function);
}
}
/// Register a custom Tera filter globally. Picked up by
/// [`apply_to_tera`] at template-engine construction time.
///
/// ```ignore
/// use std::collections::HashMap;
/// use serde_json::Value;
///
/// fn shout(v: &Value, _args: &HashMap<String, Value>) -> tera::Result<Value> {
/// Ok(Value::String(v.as_str().unwrap_or("").to_uppercase()))
/// }
/// rustango::register_template_filter!("shout", shout);
/// ```
#[macro_export]
macro_rules! register_template_filter {
($name:expr, $filter:expr $(,)?) => {
$crate::inventory::submit! {
$crate::template_extensions::TemplateFilter {
name: $name,
filter: {
const _FILTER: $crate::template_extensions::TeraFilterFn = $filter;
_FILTER
},
}
}
};
}
/// Register a custom Tera function globally. Picked up by
/// [`apply_to_tera`] at template-engine construction time.
///
/// ```ignore
/// use std::collections::HashMap;
/// use serde_json::Value;
///
/// fn version(_args: &HashMap<String, Value>) -> tera::Result<Value> {
/// Ok(Value::String(env!("CARGO_PKG_VERSION").to_string()))
/// }
/// rustango::register_template_function!("version", version);
/// ```
#[macro_export]
macro_rules! register_template_function {
($name:expr, $function:expr $(,)?) => {
$crate::inventory::submit! {
$crate::template_extensions::TemplateFunction {
name: $name,
function: {
const _FUNCTION: $crate::template_extensions::TeraFunctionFn = $function;
_FUNCTION
},
}
}
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn apply_to_tera_is_a_noop_with_no_registrations() {
// Lib unit-test binary has no `register_template_filter!`
// calls, so the apply walks empty iters and leaves Tera
// untouched. Smoke-checks that the inventory link doesn't
// panic when nothing's submitted.
let mut tera = Tera::default();
apply_to_tera(&mut tera);
// A built-in filter still resolves — sanity that we didn't
// somehow corrupt the Tera registry.
tera.add_raw_template("smoke.html", "{{ [1,2,3] | length }}")
.unwrap();
let rendered = tera
.render("smoke.html", &tera::Context::new())
.expect("built-in filter still works");
assert_eq!(rendered, "3");
}
#[test]
fn apply_to_tera_is_idempotent_when_called_twice() {
// Defensive: registering the same filter twice should be
// safe (Tera::register_filter overwrites).
let mut tera = Tera::default();
apply_to_tera(&mut tera);
apply_to_tera(&mut tera);
// Nothing crashed, that's the test.
}
/// Spot-check the macro shape on a synthetic non-inventory
/// registration — proves the typed fn-pointer coercion works.
/// Real inventory-tying happens in the live test file because
/// `inventory::submit!` can't live inside a function body.
#[test]
fn fn_pointer_coercion_smoke_test() {
fn upper(v: &Value, _args: &HashMap<String, Value>) -> tera::Result<Value> {
Ok(Value::String(v.as_str().unwrap_or("").to_uppercase()))
}
let f: TeraFilterFn = upper;
let mut tera = Tera::default();
tera.register_filter("upper_custom", f);
tera.add_raw_template("t.html", r#"{{ "hi" | upper_custom }}"#)
.unwrap();
let r = tera.render("t.html", &tera::Context::new()).unwrap();
assert_eq!(r, "HI");
}
}