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