mas_templates/macros.rs
1// Copyright 2025, 2026 Element Creations Ltd.
2// Copyright 2024, 2025 New Vector Ltd.
3// Copyright 2021-2024 The Matrix.org Foundation C.I.C.
4//
5// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
6// Please see LICENSE files in the repository root for full details.
7
8/// Count the number of tokens. Used to have a fixed-sized array for the
9/// templates list.
10macro_rules! count {
11 () => (0_usize);
12 ( $x:tt $($xs:tt)* ) => (1_usize + count!($($xs)*));
13}
14
15/// Macro that helps generating helper function that renders a specific template
16/// with a strongly-typed context. It also register the template in a static
17/// array to help detecting missing templates at startup time.
18///
19/// The syntax looks almost like a function to confuse syntax highlighter as
20/// little as possible.
21#[macro_export]
22macro_rules! register_templates {
23 {
24 $(
25 extra = { $( $extra_template:expr ),* $(,)? };
26 )?
27
28 $(
29 // Match any attribute on the function, such as #[doc], #[expect(dead_code)], etc.
30 $( #[ $attr:meta ] )*
31 // The function name
32 pub fn $name:ident
33 // Optional list of generics. Taken from
34 // https://newbedev.com/rust-macro-accepting-type-with-generic-parameters
35 // For sample rendering, we also require a 'sample' generic parameter to be provided,
36 // using #[sample(Type)] attribute syntax
37 $(< $( #[sample( $generic_default:tt )] $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+ >)?
38 // Type of context taken by the template
39 ( $param:ty )
40 {
41 // The name of the template file
42 $template:expr
43 }
44 )*
45 } => {
46 /// List of registered templates
47 static TEMPLATES: [&'static str; count!( $( $template )* )] = [ $( $template, )* ];
48
49 impl Templates {
50 $(
51 $(#[$attr])?
52 ///
53 /// # Errors
54 ///
55 /// Returns an error if the template fails to render.
56 pub fn $name
57 $(< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)?
58 (&self, context: &$param)
59 -> Result<String, TemplateError> {
60 let ctx = ::minijinja::value::Value::from_serialize(context);
61
62 let env = self.environment.load();
63 let tmpl = env.get_template($template)
64 .map_err(|source| TemplateError::Missing { template: $template, source })?;
65 tmpl.render(ctx)
66 .map_err(|source| TemplateError::Render { template: $template, source })
67 }
68 )*
69 }
70
71 /// Helps rendering each template with sample data
72 pub mod check {
73 use super::*;
74
75 /// Check and render all templates with all samples.
76 ///
77 /// Returns the sample renders. The keys in the map are the template names.
78 ///
79 /// # Errors
80 ///
81 /// Returns an error if any template fails to render with any of the sample.
82 pub(crate) fn all<R: RngExt + Clone>(templates: &Templates, now: chrono::DateTime<chrono::Utc>, rng: &R) -> anyhow::Result<::std::collections::BTreeMap<(&'static str, SampleIdentifier), String>> {
83 let mut out = ::std::collections::BTreeMap::new();
84 // TODO shouldn't the RngExt be independent for each render?
85 $(
86 {
87 let mut rng = rng.clone();
88 out.extend(
89 $name $(::< _ $( , $generic_default ),* >)? (templates, now, &mut rng)?
90 .into_iter()
91 .map(|(sample_identifier, rendered)| (($template, sample_identifier), rendered))
92 );
93 }
94 )*
95
96 Ok(out)
97 }
98
99 $(
100 #[doc = concat!("Render the `", $template, "` template with sample contexts")]
101 ///
102 /// Returns the sample renders.
103 ///
104 /// # Errors
105 ///
106 /// Returns an error if the template fails to render with any of the sample.
107 pub(crate) fn $name
108 < __R: RngExt + Clone $( , $( $lt $( : $clt $(+ $dlt )* + TemplateContext )? ),+ )? >
109 (templates: &Templates, now: chrono::DateTime<chrono::Utc>, rng: &mut __R)
110 -> anyhow::Result<BTreeMap<SampleIdentifier, String>> {
111 let locales = templates.translator().available_locales();
112 let samples: BTreeMap<SampleIdentifier, $param > = TemplateContext::sample(now, rng, &locales);
113
114 let name = $template;
115 let mut out = BTreeMap::new();
116 for (sample_identifier, sample) in samples {
117 let context = serde_json::to_value(&sample)?;
118 ::tracing::info!(name, %context, "Rendering template");
119 let rendered = templates. $name (&sample)
120 .with_context(|| format!("Failed to render sample template {name:?}-{sample_identifier:?} with context {context}"))?;
121 out.insert(sample_identifier, rendered);
122 }
123
124 Ok(out)
125 }
126 )*
127 }
128 };
129}