Introduce SampleIdentifiers to stably track samples
and use these in output filenames
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use std::process::ExitCode;
|
||||
use std::{collections::BTreeSet, fmt::Write, process::ExitCode};
|
||||
|
||||
use anyhow::{Context as _, bail};
|
||||
use camino::Utf8PathBuf;
|
||||
@@ -83,7 +83,7 @@ impl Options {
|
||||
tokio::fs::read_dir(&out_dir).await.with_context(|| {
|
||||
format!("could not read {out_dir} to check it's empty")
|
||||
})?;
|
||||
while let Some(x) = read_dir.next_entry().await? {
|
||||
if read_dir.next_entry().await?.is_some() {
|
||||
bail!("Render directory {out_dir} is not empty, refusing to write.");
|
||||
}
|
||||
} else {
|
||||
@@ -92,19 +92,49 @@ impl Options {
|
||||
.with_context(|| format!("could not create {out_dir}"))?;
|
||||
}
|
||||
|
||||
for (template, template_renders) in &all_renders {
|
||||
let template_filename_base =
|
||||
template.trim_end_matches(".html").replace('/', "_");
|
||||
for (idx, render) in template_renders.iter().enumerate() {
|
||||
let render_path =
|
||||
out_dir.join(format!("{template_filename_base}-sample{idx}.html"));
|
||||
let all_locales: BTreeSet<&str> = all_renders
|
||||
.iter()
|
||||
.filter_map(|((_, sample_identifier), _)| {
|
||||
sample_identifier.locale.as_deref()
|
||||
})
|
||||
.collect();
|
||||
for locale in all_locales {
|
||||
let locale_dir = out_dir.join(locale);
|
||||
tokio::fs::create_dir(&locale_dir)
|
||||
.await
|
||||
.with_context(|| format!("could not create {locale_dir}"))?;
|
||||
}
|
||||
|
||||
tokio::fs::write(&render_path, render.as_bytes())
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("could not write render to {render_path}")
|
||||
})?;
|
||||
}
|
||||
for ((template, sample_identifier), template_render) in &all_renders {
|
||||
let (template_filename_base, template_ext) =
|
||||
template.rsplit_once('.').unwrap_or((template, "txt"));
|
||||
let template_filename_base = template_filename_base.replace('/', "_");
|
||||
|
||||
// Make a string like:
|
||||
// - `-sample1`
|
||||
// - `-session2-sample1`
|
||||
let sample_suffix = {
|
||||
let mut s = String::new();
|
||||
if let Some(session_index) = sample_identifier.session_index {
|
||||
write!(s, "-session{session_index}")?;
|
||||
}
|
||||
write!(s, "-sample{}", sample_identifier.index)?;
|
||||
s
|
||||
};
|
||||
|
||||
let locale_dir = if let Some(locale) = &sample_identifier.locale {
|
||||
out_dir.join(locale)
|
||||
} else {
|
||||
out_dir.clone()
|
||||
};
|
||||
|
||||
let render_path = locale_dir.join(format!(
|
||||
"{template_filename_base}{sample_suffix}.{template_ext}"
|
||||
));
|
||||
|
||||
tokio::fs::write(&render_path, template_render.as_bytes())
|
||||
.await
|
||||
.with_context(|| format!("could not write render to {render_path}"))?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ mod ext;
|
||||
mod features;
|
||||
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
fmt::Formatter,
|
||||
net::{IpAddr, Ipv4Addr},
|
||||
};
|
||||
@@ -105,21 +106,72 @@ pub trait TemplateContext: Serialize {
|
||||
///
|
||||
/// This is then used to check for template validity in unit tests and in
|
||||
/// the CLI (`cargo run -- templates check`)
|
||||
fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, locales: &[DataLocale]) -> Vec<Self>
|
||||
fn sample(
|
||||
now: chrono::DateTime<Utc>,
|
||||
rng: &mut impl Rng,
|
||||
locales: &[DataLocale],
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct SampleIdentifier {
|
||||
/// A stable locale identifier.
|
||||
pub locale: Option<String>,
|
||||
|
||||
/// A stable identifier for the session that was used in this sample.
|
||||
pub session_index: Option<usize>,
|
||||
|
||||
/// A stable positional index of the sample for this context.
|
||||
pub index: usize,
|
||||
}
|
||||
|
||||
impl SampleIdentifier {
|
||||
pub fn with_locale(&self, locale: String) -> Self {
|
||||
SampleIdentifier {
|
||||
locale: Some(locale),
|
||||
session_index: self.session_index,
|
||||
index: self.index,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_session_index(self, session_index: usize) -> Self {
|
||||
SampleIdentifier {
|
||||
locale: self.locale,
|
||||
session_index: Some(session_index),
|
||||
index: self.index,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn sample_list<T: TemplateContext>(samples: Vec<T>) -> BTreeMap<SampleIdentifier, T> {
|
||||
samples
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, sample)| {
|
||||
(
|
||||
SampleIdentifier {
|
||||
locale: None,
|
||||
session_index: None,
|
||||
index,
|
||||
},
|
||||
sample,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl TemplateContext for () {
|
||||
fn sample(
|
||||
_now: chrono::DateTime<Utc>,
|
||||
_rng: &mut impl Rng,
|
||||
_locales: &[DataLocale],
|
||||
) -> Vec<Self>
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Vec::new()
|
||||
BTreeMap::new()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +200,11 @@ impl<T> std::ops::Deref for WithLanguage<T> {
|
||||
}
|
||||
|
||||
impl<T: TemplateContext> TemplateContext for WithLanguage<T> {
|
||||
fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, locales: &[DataLocale]) -> Vec<Self>
|
||||
fn sample(
|
||||
now: chrono::DateTime<Utc>,
|
||||
rng: &mut impl Rng,
|
||||
locales: &[DataLocale],
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -157,9 +213,14 @@ impl<T: TemplateContext> TemplateContext for WithLanguage<T> {
|
||||
.flat_map(|locale| {
|
||||
T::sample(now, rng, locales)
|
||||
.into_iter()
|
||||
.map(move |inner| WithLanguage {
|
||||
lang: locale.to_string(),
|
||||
inner,
|
||||
.map(|(sample_id, sample)| {
|
||||
(
|
||||
sample_id.with_locale(locale.to_string()),
|
||||
WithLanguage {
|
||||
lang: locale.to_string(),
|
||||
inner: sample,
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
@@ -176,15 +237,24 @@ pub struct WithCsrf<T> {
|
||||
}
|
||||
|
||||
impl<T: TemplateContext> TemplateContext for WithCsrf<T> {
|
||||
fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, locales: &[DataLocale]) -> Vec<Self>
|
||||
fn sample(
|
||||
now: chrono::DateTime<Utc>,
|
||||
rng: &mut impl Rng,
|
||||
locales: &[DataLocale],
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
T::sample(now, rng, locales)
|
||||
.into_iter()
|
||||
.map(|inner| WithCsrf {
|
||||
csrf_token: "fake_csrf_token".into(),
|
||||
inner,
|
||||
.map(|(k, inner)| {
|
||||
(
|
||||
k,
|
||||
WithCsrf {
|
||||
csrf_token: "fake_csrf_token".into(),
|
||||
inner,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -200,18 +270,28 @@ pub struct WithSession<T> {
|
||||
}
|
||||
|
||||
impl<T: TemplateContext> TemplateContext for WithSession<T> {
|
||||
fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, locales: &[DataLocale]) -> Vec<Self>
|
||||
fn sample(
|
||||
now: chrono::DateTime<Utc>,
|
||||
rng: &mut impl Rng,
|
||||
locales: &[DataLocale],
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
BrowserSession::samples(now, rng)
|
||||
.into_iter()
|
||||
.flat_map(|session| {
|
||||
.enumerate()
|
||||
.flat_map(|(session_index, session)| {
|
||||
T::sample(now, rng, locales)
|
||||
.into_iter()
|
||||
.map(move |inner| WithSession {
|
||||
current_session: session.clone(),
|
||||
inner,
|
||||
.map(move |(k, inner)| {
|
||||
(
|
||||
k.with_session_index(session_index),
|
||||
WithSession {
|
||||
current_session: session.clone(),
|
||||
inner,
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
@@ -228,7 +308,11 @@ pub struct WithOptionalSession<T> {
|
||||
}
|
||||
|
||||
impl<T: TemplateContext> TemplateContext for WithOptionalSession<T> {
|
||||
fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, locales: &[DataLocale]) -> Vec<Self>
|
||||
fn sample(
|
||||
now: chrono::DateTime<Utc>,
|
||||
rng: &mut impl Rng,
|
||||
locales: &[DataLocale],
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -236,12 +320,22 @@ impl<T: TemplateContext> TemplateContext for WithOptionalSession<T> {
|
||||
.into_iter()
|
||||
.map(Some) // Wrap all samples in an Option
|
||||
.chain(std::iter::once(None)) // Add the "None" option
|
||||
.flat_map(|session| {
|
||||
.enumerate()
|
||||
.flat_map(|(session_index, session)| {
|
||||
T::sample(now, rng, locales)
|
||||
.into_iter()
|
||||
.map(move |inner| WithOptionalSession {
|
||||
current_session: session.clone(),
|
||||
inner,
|
||||
.map(move |(k, inner)| {
|
||||
(
|
||||
if session.is_some() {
|
||||
k.with_session_index(session_index)
|
||||
} else {
|
||||
k
|
||||
},
|
||||
WithOptionalSession {
|
||||
current_session: session.clone(),
|
||||
inner,
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
@@ -269,11 +363,11 @@ impl TemplateContext for EmptyContext {
|
||||
_now: chrono::DateTime<Utc>,
|
||||
_rng: &mut impl Rng,
|
||||
_locales: &[DataLocale],
|
||||
) -> Vec<Self>
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
vec![EmptyContext]
|
||||
sample_list(vec![EmptyContext])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,15 +391,15 @@ impl TemplateContext for IndexContext {
|
||||
_now: chrono::DateTime<Utc>,
|
||||
_rng: &mut impl Rng,
|
||||
_locales: &[DataLocale],
|
||||
) -> Vec<Self>
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
vec![Self {
|
||||
sample_list(vec![Self {
|
||||
discovery_url: "https://example.com/.well-known/openid-configuration"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
}]
|
||||
}])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,12 +437,12 @@ impl TemplateContext for AppContext {
|
||||
_now: chrono::DateTime<Utc>,
|
||||
_rng: &mut impl Rng,
|
||||
_locales: &[DataLocale],
|
||||
) -> Vec<Self>
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let url_builder = UrlBuilder::new("https://example.com/".parse().unwrap(), None, None);
|
||||
vec![Self::from_url_builder(&url_builder)]
|
||||
sample_list(vec![Self::from_url_builder(&url_builder)])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,12 +470,12 @@ impl TemplateContext for ApiDocContext {
|
||||
_now: chrono::DateTime<Utc>,
|
||||
_rng: &mut impl Rng,
|
||||
_locales: &[DataLocale],
|
||||
) -> Vec<Self>
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let url_builder = UrlBuilder::new("https://example.com/".parse().unwrap(), None, None);
|
||||
vec![Self::from_url_builder(&url_builder)]
|
||||
sample_list(vec![Self::from_url_builder(&url_builder)])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -468,12 +562,12 @@ impl TemplateContext for LoginContext {
|
||||
_now: chrono::DateTime<Utc>,
|
||||
_rng: &mut impl Rng,
|
||||
_locales: &[DataLocale],
|
||||
) -> Vec<Self>
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
// TODO: samples with errors
|
||||
vec![
|
||||
sample_list(vec![
|
||||
LoginContext {
|
||||
form: FormState::default(),
|
||||
next: None,
|
||||
@@ -503,7 +597,7 @@ impl TemplateContext for LoginContext {
|
||||
next: None,
|
||||
providers: Vec::new(),
|
||||
},
|
||||
]
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -576,14 +670,14 @@ impl TemplateContext for RegisterContext {
|
||||
_now: chrono::DateTime<Utc>,
|
||||
_rng: &mut impl Rng,
|
||||
_locales: &[DataLocale],
|
||||
) -> Vec<Self>
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
vec![RegisterContext {
|
||||
sample_list(vec![RegisterContext {
|
||||
providers: Vec::new(),
|
||||
next: None,
|
||||
}]
|
||||
}])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -619,15 +713,15 @@ impl TemplateContext for PasswordRegisterContext {
|
||||
_now: chrono::DateTime<Utc>,
|
||||
_rng: &mut impl Rng,
|
||||
_locales: &[DataLocale],
|
||||
) -> Vec<Self>
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
// TODO: samples with errors
|
||||
vec![PasswordRegisterContext {
|
||||
sample_list(vec![PasswordRegisterContext {
|
||||
form: FormState::default(),
|
||||
next: None,
|
||||
}]
|
||||
}])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -657,24 +751,30 @@ pub struct ConsentContext {
|
||||
}
|
||||
|
||||
impl TemplateContext for ConsentContext {
|
||||
fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
|
||||
fn sample(
|
||||
now: chrono::DateTime<Utc>,
|
||||
rng: &mut impl Rng,
|
||||
_locales: &[DataLocale],
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Client::samples(now, rng)
|
||||
.into_iter()
|
||||
.map(|client| {
|
||||
let mut grant = AuthorizationGrant::sample(now, rng);
|
||||
let action = PostAuthAction::continue_grant(grant.id);
|
||||
// XXX
|
||||
grant.client_id = client.id;
|
||||
Self {
|
||||
grant,
|
||||
client,
|
||||
action,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
sample_list(
|
||||
Client::samples(now, rng)
|
||||
.into_iter()
|
||||
.map(|client| {
|
||||
let mut grant = AuthorizationGrant::sample(now, rng);
|
||||
let action = PostAuthAction::continue_grant(grant.id);
|
||||
// XXX
|
||||
grant.client_id = client.id;
|
||||
Self {
|
||||
grant,
|
||||
client,
|
||||
action,
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -709,38 +809,44 @@ pub struct PolicyViolationContext {
|
||||
}
|
||||
|
||||
impl TemplateContext for PolicyViolationContext {
|
||||
fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
|
||||
fn sample(
|
||||
now: chrono::DateTime<Utc>,
|
||||
rng: &mut impl Rng,
|
||||
_locales: &[DataLocale],
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Client::samples(now, rng)
|
||||
.into_iter()
|
||||
.flat_map(|client| {
|
||||
let mut grant = AuthorizationGrant::sample(now, rng);
|
||||
// XXX
|
||||
grant.client_id = client.id;
|
||||
sample_list(
|
||||
Client::samples(now, rng)
|
||||
.into_iter()
|
||||
.flat_map(|client| {
|
||||
let mut grant = AuthorizationGrant::sample(now, rng);
|
||||
// XXX
|
||||
grant.client_id = client.id;
|
||||
|
||||
let authorization_grant =
|
||||
PolicyViolationContext::for_authorization_grant(grant, client.clone());
|
||||
let device_code_grant = PolicyViolationContext::for_device_code_grant(
|
||||
DeviceCodeGrant {
|
||||
id: Ulid::from_datetime_with_source(now.into(), rng),
|
||||
state: mas_data_model::DeviceCodeGrantState::Pending,
|
||||
client_id: client.id,
|
||||
scope: [OPENID].into_iter().collect(),
|
||||
user_code: Alphanumeric.sample_string(rng, 6).to_uppercase(),
|
||||
device_code: Alphanumeric.sample_string(rng, 32),
|
||||
created_at: now - Duration::try_minutes(5).unwrap(),
|
||||
expires_at: now + Duration::try_minutes(25).unwrap(),
|
||||
ip_address: None,
|
||||
user_agent: None,
|
||||
},
|
||||
client,
|
||||
);
|
||||
let authorization_grant =
|
||||
PolicyViolationContext::for_authorization_grant(grant, client.clone());
|
||||
let device_code_grant = PolicyViolationContext::for_device_code_grant(
|
||||
DeviceCodeGrant {
|
||||
id: Ulid::from_datetime_with_source(now.into(), rng),
|
||||
state: mas_data_model::DeviceCodeGrantState::Pending,
|
||||
client_id: client.id,
|
||||
scope: [OPENID].into_iter().collect(),
|
||||
user_code: Alphanumeric.sample_string(rng, 6).to_uppercase(),
|
||||
device_code: Alphanumeric.sample_string(rng, 32),
|
||||
created_at: now - Duration::try_minutes(5).unwrap(),
|
||||
expires_at: now + Duration::try_minutes(25).unwrap(),
|
||||
ip_address: None,
|
||||
user_agent: None,
|
||||
},
|
||||
client,
|
||||
);
|
||||
|
||||
[authorization_grant, device_code_grant]
|
||||
})
|
||||
.collect()
|
||||
[authorization_grant, device_code_grant]
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -778,18 +884,22 @@ pub struct CompatSsoContext {
|
||||
}
|
||||
|
||||
impl TemplateContext for CompatSsoContext {
|
||||
fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
|
||||
fn sample(
|
||||
now: chrono::DateTime<Utc>,
|
||||
rng: &mut impl Rng,
|
||||
_locales: &[DataLocale],
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let id = Ulid::from_datetime_with_source(now.into(), rng);
|
||||
vec![CompatSsoContext::new(CompatSsoLogin {
|
||||
sample_list(vec![CompatSsoContext::new(CompatSsoLogin {
|
||||
id,
|
||||
redirect_uri: Url::parse("https://app.element.io/").unwrap(),
|
||||
login_token: "abcdefghijklmnopqrstuvwxyz012345".into(),
|
||||
created_at: now,
|
||||
state: CompatSsoLoginState::Pending,
|
||||
})]
|
||||
})])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -836,11 +946,15 @@ impl EmailRecoveryContext {
|
||||
}
|
||||
|
||||
impl TemplateContext for EmailRecoveryContext {
|
||||
fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
|
||||
fn sample(
|
||||
now: chrono::DateTime<Utc>,
|
||||
rng: &mut impl Rng,
|
||||
_locales: &[DataLocale],
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
User::samples(now, rng).into_iter().map(|user| {
|
||||
sample_list(User::samples(now, rng).into_iter().map(|user| {
|
||||
let session = UserRecoverySession {
|
||||
id: Ulid::from_datetime_with_source(now.into(), rng),
|
||||
email: "hello@example.com".to_owned(),
|
||||
@@ -854,7 +968,7 @@ impl TemplateContext for EmailRecoveryContext {
|
||||
let link = "https://example.com/recovery/complete?ticket=abcdefghijklmnopqrstuvwxyz0123456789".parse().unwrap();
|
||||
|
||||
Self::new(user, session, link)
|
||||
}).collect()
|
||||
}).collect())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -897,28 +1011,37 @@ impl EmailVerificationContext {
|
||||
}
|
||||
|
||||
impl TemplateContext for EmailVerificationContext {
|
||||
fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
|
||||
fn sample(
|
||||
now: chrono::DateTime<Utc>,
|
||||
rng: &mut impl Rng,
|
||||
_locales: &[DataLocale],
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
BrowserSession::samples(now, rng)
|
||||
.into_iter()
|
||||
.map(|browser_session| {
|
||||
let authentication_code = UserEmailAuthenticationCode {
|
||||
id: Ulid::from_datetime_with_source(now.into(), rng),
|
||||
user_email_authentication_id: Ulid::from_datetime_with_source(now.into(), rng),
|
||||
code: "123456".to_owned(),
|
||||
created_at: now - Duration::try_minutes(5).unwrap(),
|
||||
expires_at: now + Duration::try_minutes(25).unwrap(),
|
||||
};
|
||||
sample_list(
|
||||
BrowserSession::samples(now, rng)
|
||||
.into_iter()
|
||||
.map(|browser_session| {
|
||||
let authentication_code = UserEmailAuthenticationCode {
|
||||
id: Ulid::from_datetime_with_source(now.into(), rng),
|
||||
user_email_authentication_id: Ulid::from_datetime_with_source(
|
||||
now.into(),
|
||||
rng,
|
||||
),
|
||||
code: "123456".to_owned(),
|
||||
created_at: now - Duration::try_minutes(5).unwrap(),
|
||||
expires_at: now + Duration::try_minutes(25).unwrap(),
|
||||
};
|
||||
|
||||
Self {
|
||||
browser_session: Some(browser_session),
|
||||
user_registration: None,
|
||||
authentication_code,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
Self {
|
||||
browser_session: Some(browser_session),
|
||||
user_registration: None,
|
||||
authentication_code,
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -963,7 +1086,11 @@ impl RegisterStepsVerifyEmailContext {
|
||||
}
|
||||
|
||||
impl TemplateContext for RegisterStepsVerifyEmailContext {
|
||||
fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
|
||||
fn sample(
|
||||
now: chrono::DateTime<Utc>,
|
||||
rng: &mut impl Rng,
|
||||
_locales: &[DataLocale],
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -976,10 +1103,10 @@ impl TemplateContext for RegisterStepsVerifyEmailContext {
|
||||
completed_at: None,
|
||||
};
|
||||
|
||||
vec![Self {
|
||||
sample_list(vec![Self {
|
||||
form: FormState::default(),
|
||||
authentication,
|
||||
}]
|
||||
}])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1003,13 +1130,13 @@ impl TemplateContext for RegisterStepsEmailInUseContext {
|
||||
_now: chrono::DateTime<Utc>,
|
||||
_rng: &mut impl Rng,
|
||||
_locales: &[DataLocale],
|
||||
) -> Vec<Self>
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let email = "hello@example.com".to_owned();
|
||||
let action = PostAuthAction::continue_grant(Ulid::nil());
|
||||
vec![Self::new(email, Some(action))]
|
||||
sample_list(vec![Self::new(email, Some(action))])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1058,13 +1185,13 @@ impl TemplateContext for RegisterStepsDisplayNameContext {
|
||||
_now: chrono::DateTime<chrono::Utc>,
|
||||
_rng: &mut impl Rng,
|
||||
_locales: &[DataLocale],
|
||||
) -> Vec<Self>
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
vec![Self {
|
||||
sample_list(vec![Self {
|
||||
form: FormState::default(),
|
||||
}]
|
||||
}])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1113,13 +1240,13 @@ impl TemplateContext for RegisterStepsRegistrationTokenContext {
|
||||
_now: chrono::DateTime<chrono::Utc>,
|
||||
_rng: &mut impl Rng,
|
||||
_locales: &[DataLocale],
|
||||
) -> Vec<Self>
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
vec![Self {
|
||||
sample_list(vec![Self {
|
||||
form: FormState::default(),
|
||||
}]
|
||||
}])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1164,11 +1291,11 @@ impl TemplateContext for RecoveryStartContext {
|
||||
_now: chrono::DateTime<Utc>,
|
||||
_rng: &mut impl Rng,
|
||||
_locales: &[DataLocale],
|
||||
) -> Vec<Self>
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
vec![
|
||||
sample_list(vec![
|
||||
Self::new(),
|
||||
Self::new().with_form_state(
|
||||
FormState::default()
|
||||
@@ -1178,7 +1305,7 @@ impl TemplateContext for RecoveryStartContext {
|
||||
FormState::default()
|
||||
.with_error_on_field(RecoveryStartFormField::Email, FieldError::Invalid),
|
||||
),
|
||||
]
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1202,7 +1329,11 @@ impl RecoveryProgressContext {
|
||||
}
|
||||
|
||||
impl TemplateContext for RecoveryProgressContext {
|
||||
fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
|
||||
fn sample(
|
||||
now: chrono::DateTime<Utc>,
|
||||
rng: &mut impl Rng,
|
||||
_locales: &[DataLocale],
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -1216,7 +1347,7 @@ impl TemplateContext for RecoveryProgressContext {
|
||||
consumed_at: None,
|
||||
};
|
||||
|
||||
vec![
|
||||
sample_list(vec![
|
||||
Self {
|
||||
session: session.clone(),
|
||||
resend_failed_due_to_rate_limit: false,
|
||||
@@ -1225,7 +1356,7 @@ impl TemplateContext for RecoveryProgressContext {
|
||||
session,
|
||||
resend_failed_due_to_rate_limit: true,
|
||||
},
|
||||
]
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1244,7 +1375,11 @@ impl RecoveryExpiredContext {
|
||||
}
|
||||
|
||||
impl TemplateContext for RecoveryExpiredContext {
|
||||
fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
|
||||
fn sample(
|
||||
now: chrono::DateTime<Utc>,
|
||||
rng: &mut impl Rng,
|
||||
_locales: &[DataLocale],
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -1258,10 +1393,9 @@ impl TemplateContext for RecoveryExpiredContext {
|
||||
consumed_at: None,
|
||||
};
|
||||
|
||||
vec![Self { session }]
|
||||
sample_list(vec![Self { session }])
|
||||
}
|
||||
}
|
||||
|
||||
/// Fields of the account recovery finish form
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@@ -1305,30 +1439,36 @@ impl RecoveryFinishContext {
|
||||
}
|
||||
|
||||
impl TemplateContext for RecoveryFinishContext {
|
||||
fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
|
||||
fn sample(
|
||||
now: chrono::DateTime<Utc>,
|
||||
rng: &mut impl Rng,
|
||||
_locales: &[DataLocale],
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
User::samples(now, rng)
|
||||
.into_iter()
|
||||
.flat_map(|user| {
|
||||
vec![
|
||||
Self::new(user.clone()),
|
||||
Self::new(user.clone()).with_form_state(
|
||||
FormState::default().with_error_on_field(
|
||||
RecoveryFinishFormField::NewPassword,
|
||||
FieldError::Invalid,
|
||||
sample_list(
|
||||
User::samples(now, rng)
|
||||
.into_iter()
|
||||
.flat_map(|user| {
|
||||
vec![
|
||||
Self::new(user.clone()),
|
||||
Self::new(user.clone()).with_form_state(
|
||||
FormState::default().with_error_on_field(
|
||||
RecoveryFinishFormField::NewPassword,
|
||||
FieldError::Invalid,
|
||||
),
|
||||
),
|
||||
),
|
||||
Self::new(user.clone()).with_form_state(
|
||||
FormState::default().with_error_on_field(
|
||||
RecoveryFinishFormField::NewPasswordConfirm,
|
||||
FieldError::Invalid,
|
||||
Self::new(user.clone()).with_form_state(
|
||||
FormState::default().with_error_on_field(
|
||||
RecoveryFinishFormField::NewPasswordConfirm,
|
||||
FieldError::Invalid,
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
})
|
||||
.collect()
|
||||
]
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1348,14 +1488,20 @@ impl UpstreamExistingLinkContext {
|
||||
}
|
||||
|
||||
impl TemplateContext for UpstreamExistingLinkContext {
|
||||
fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
|
||||
fn sample(
|
||||
now: chrono::DateTime<Utc>,
|
||||
rng: &mut impl Rng,
|
||||
_locales: &[DataLocale],
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
User::samples(now, rng)
|
||||
.into_iter()
|
||||
.map(|linked_user| Self { linked_user })
|
||||
.collect()
|
||||
sample_list(
|
||||
User::samples(now, rng)
|
||||
.into_iter()
|
||||
.map(|linked_user| Self { linked_user })
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1380,12 +1526,16 @@ impl UpstreamSuggestLink {
|
||||
}
|
||||
|
||||
impl TemplateContext for UpstreamSuggestLink {
|
||||
fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
|
||||
fn sample(
|
||||
now: chrono::DateTime<Utc>,
|
||||
rng: &mut impl Rng,
|
||||
_locales: &[DataLocale],
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let id = Ulid::from_datetime_with_source(now.into(), rng);
|
||||
vec![Self::for_link_id(id)]
|
||||
sample_list(vec![Self::for_link_id(id)])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1505,11 +1655,15 @@ impl UpstreamRegister {
|
||||
}
|
||||
|
||||
impl TemplateContext for UpstreamRegister {
|
||||
fn sample(now: chrono::DateTime<Utc>, _rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
|
||||
fn sample(
|
||||
now: chrono::DateTime<Utc>,
|
||||
_rng: &mut impl Rng,
|
||||
_locales: &[DataLocale],
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
vec![Self::new(
|
||||
sample_list(vec![Self::new(
|
||||
UpstreamOAuthLink {
|
||||
id: Ulid::nil(),
|
||||
provider_id: Ulid::nil(),
|
||||
@@ -1545,7 +1699,7 @@ impl TemplateContext for UpstreamRegister {
|
||||
disabled_at: None,
|
||||
on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing,
|
||||
},
|
||||
)]
|
||||
)])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1591,17 +1745,17 @@ impl TemplateContext for DeviceLinkContext {
|
||||
_now: chrono::DateTime<Utc>,
|
||||
_rng: &mut impl Rng,
|
||||
_locales: &[DataLocale],
|
||||
) -> Vec<Self>
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
vec![
|
||||
sample_list(vec![
|
||||
Self::new(),
|
||||
Self::new().with_form_state(
|
||||
FormState::default()
|
||||
.with_error_on_field(DeviceLinkFormField::Code, FieldError::Required),
|
||||
),
|
||||
]
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1621,13 +1775,17 @@ impl DeviceConsentContext {
|
||||
}
|
||||
|
||||
impl TemplateContext for DeviceConsentContext {
|
||||
fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
|
||||
fn sample(
|
||||
now: chrono::DateTime<Utc>,
|
||||
rng: &mut impl Rng,
|
||||
_locales: &[DataLocale],
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Client::samples(now, rng)
|
||||
sample_list(Client::samples(now, rng)
|
||||
.into_iter()
|
||||
.map(|client| {
|
||||
.map(|client| {
|
||||
let grant = DeviceCodeGrant {
|
||||
id: Ulid::from_datetime_with_source(now.into(), rng),
|
||||
state: mas_data_model::DeviceCodeGrantState::Pending,
|
||||
@@ -1642,7 +1800,7 @@ impl TemplateContext for DeviceConsentContext {
|
||||
};
|
||||
Self { grant, client }
|
||||
})
|
||||
.collect()
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1662,14 +1820,20 @@ impl AccountInactiveContext {
|
||||
}
|
||||
|
||||
impl TemplateContext for AccountInactiveContext {
|
||||
fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
|
||||
fn sample(
|
||||
now: chrono::DateTime<Utc>,
|
||||
rng: &mut impl Rng,
|
||||
_locales: &[DataLocale],
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
User::samples(now, rng)
|
||||
.into_iter()
|
||||
.map(|user| AccountInactiveContext { user })
|
||||
.collect()
|
||||
sample_list(
|
||||
User::samples(now, rng)
|
||||
.into_iter()
|
||||
.map(|user| AccountInactiveContext { user })
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1692,17 +1856,21 @@ impl DeviceNameContext {
|
||||
}
|
||||
|
||||
impl TemplateContext for DeviceNameContext {
|
||||
fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
|
||||
fn sample(
|
||||
now: chrono::DateTime<Utc>,
|
||||
rng: &mut impl Rng,
|
||||
_locales: &[DataLocale],
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Client::samples(now, rng)
|
||||
sample_list(Client::samples(now, rng)
|
||||
.into_iter()
|
||||
.map(|client| DeviceNameContext {
|
||||
client,
|
||||
raw_user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Safari/537.36".to_owned(),
|
||||
})
|
||||
.collect()
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1714,16 +1882,25 @@ pub struct FormPostContext<T> {
|
||||
}
|
||||
|
||||
impl<T: TemplateContext> TemplateContext for FormPostContext<T> {
|
||||
fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, locales: &[DataLocale]) -> Vec<Self>
|
||||
fn sample(
|
||||
now: chrono::DateTime<Utc>,
|
||||
rng: &mut impl Rng,
|
||||
locales: &[DataLocale],
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let sample_params = T::sample(now, rng, locales);
|
||||
sample_params
|
||||
.into_iter()
|
||||
.map(|params| FormPostContext {
|
||||
redirect_uri: "https://example.com/callback".parse().ok(),
|
||||
params,
|
||||
.map(|(k, params)| {
|
||||
(
|
||||
k,
|
||||
FormPostContext {
|
||||
redirect_uri: "https://example.com/callback".parse().ok(),
|
||||
params,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -1791,18 +1968,18 @@ impl TemplateContext for ErrorContext {
|
||||
_now: chrono::DateTime<Utc>,
|
||||
_rng: &mut impl Rng,
|
||||
_locales: &[DataLocale],
|
||||
) -> Vec<Self>
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
vec![
|
||||
sample_list(vec![
|
||||
Self::new()
|
||||
.with_code("sample_error")
|
||||
.with_description("A fancy description".into())
|
||||
.with_details("Something happened".into()),
|
||||
Self::new().with_code("another_error"),
|
||||
Self::new(),
|
||||
]
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1881,11 +2058,15 @@ impl NotFoundContext {
|
||||
}
|
||||
|
||||
impl TemplateContext for NotFoundContext {
|
||||
fn sample(_now: DateTime<Utc>, _rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
|
||||
fn sample(
|
||||
_now: DateTime<Utc>,
|
||||
_rng: &mut impl Rng,
|
||||
_locales: &[DataLocale],
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
vec![
|
||||
sample_list(vec![
|
||||
Self::new(&Method::GET, Version::HTTP_11, &"/".parse().unwrap()),
|
||||
Self::new(&Method::POST, Version::HTTP_2, &"/foo/bar".parse().unwrap()),
|
||||
Self::new(
|
||||
@@ -1893,6 +2074,6 @@ impl TemplateContext for NotFoundContext {
|
||||
Version::HTTP_10,
|
||||
&"/foo?bar=baz".parse().unwrap(),
|
||||
),
|
||||
]
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
|
||||
use mas_i18n::DataLocale;
|
||||
use minijinja::{
|
||||
@@ -13,7 +13,7 @@ use minijinja::{
|
||||
};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::TemplateContext;
|
||||
use crate::{TemplateContext, context::SampleIdentifier};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CaptchaConfig(mas_data_model::CaptchaConfig);
|
||||
@@ -62,14 +62,13 @@ impl<T: TemplateContext> TemplateContext for WithCaptcha<T> {
|
||||
now: chrono::DateTime<chrono::prelude::Utc>,
|
||||
rng: &mut impl rand::prelude::Rng,
|
||||
locales: &[DataLocale],
|
||||
) -> Vec<Self>
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let inner = T::sample(now, rng, locales);
|
||||
inner
|
||||
T::sample(now, rng, locales)
|
||||
.into_iter()
|
||||
.map(|inner| Self::new(None, inner))
|
||||
.map(|(k, inner)| (k, Self::new(None, inner)))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ pub use self::{
|
||||
},
|
||||
forms::{FieldError, FormError, FormField, FormState, ToFormState},
|
||||
};
|
||||
use crate::context::SampleIdentifier;
|
||||
|
||||
/// Escape the given string for use in HTML
|
||||
///
|
||||
@@ -457,7 +458,7 @@ impl Templates {
|
||||
&self,
|
||||
now: chrono::DateTime<chrono::Utc>,
|
||||
rng: &mut impl Rng,
|
||||
) -> anyhow::Result<BTreeMap<&'static str, Vec<String>>> {
|
||||
) -> anyhow::Result<BTreeMap<(&'static str, SampleIdentifier), String>> {
|
||||
check::all(self, now, rng)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,13 +78,14 @@ macro_rules! register_templates {
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if any template fails to render with any of the sample.
|
||||
pub(crate) fn all(templates: &Templates, now: chrono::DateTime<chrono::Utc>, rng: &mut impl rand::Rng) -> anyhow::Result<::std::collections::BTreeMap<&'static str, Vec<String>>> {
|
||||
pub(crate) fn all(templates: &Templates, now: chrono::DateTime<chrono::Utc>, rng: &mut impl rand::Rng) -> anyhow::Result<::std::collections::BTreeMap<(&'static str, SampleIdentifier), String>> {
|
||||
let mut out = ::std::collections::BTreeMap::new();
|
||||
// TODO shouldn't the Rng be independent for each render?
|
||||
$(
|
||||
out.insert(
|
||||
$template,
|
||||
out.extend(
|
||||
$name $(::< $( $generic_default ),* >)? (templates, now, rng)?
|
||||
.into_iter()
|
||||
.map(|(sample_identifier, rendered)| (($template, sample_identifier), rendered))
|
||||
);
|
||||
)*
|
||||
|
||||
@@ -102,18 +103,18 @@ macro_rules! register_templates {
|
||||
pub(crate) fn $name
|
||||
$(< $( $lt $( : $clt $(+ $dlt )* + TemplateContext )? ),+ >)?
|
||||
(templates: &Templates, now: chrono::DateTime<chrono::Utc>, rng: &mut impl rand::Rng)
|
||||
-> anyhow::Result<Vec<String>> {
|
||||
-> anyhow::Result<BTreeMap<SampleIdentifier, String>> {
|
||||
let locales = templates.translator().available_locales();
|
||||
let samples: Vec< $param > = TemplateContext::sample(now, rng, &locales);
|
||||
let samples: BTreeMap<SampleIdentifier, $param > = TemplateContext::sample(now, rng, &locales);
|
||||
|
||||
let name = $template;
|
||||
let mut out = Vec::new();
|
||||
for (idx, sample) in samples.into_iter().enumerate() {
|
||||
let mut out = BTreeMap::new();
|
||||
for (sample_identifier, sample) in samples {
|
||||
let context = serde_json::to_value(&sample)?;
|
||||
::tracing::info!(name, %context, "Rendering template");
|
||||
let rendered = templates. $name (&sample)
|
||||
.with_context(|| format!("Failed to render sample template {name:?}-{idx} with context {context}"))?;
|
||||
out.push(rendered);
|
||||
.with_context(|| format!("Failed to render sample template {name:?}-{sample_identifier:?} with context {context}"))?;
|
||||
out.insert(sample_identifier, rendered);
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
|
||||
Reference in New Issue
Block a user