bestool 1.6.0

BES Deployment tooling
Documentation
use std::fmt::Display;

use folktime::duration::{Duration as Folktime, Style as FolkStyle};

use miette::{Context as _, IntoDiagnostic, Result};

use sysinfo::System;
use tera::{Context as TeraCtx, Tera};
use tracing::{instrument, warn};

use super::{
	definition::AlertDefinition,
	targets::{
		SendTarget,
		zendesk::{TargetZendesk, ZendeskMethod},
	},
};

const DEFAULT_SUBJECT_TEMPLATE: &str = "[Tamanu Alert] {{ filename }} ({{ hostname }})";

#[derive(serde::Deserialize, Clone, Copy, Debug)]
#[serde(rename_all = "snake_case")]
pub enum TemplateField {
	Filename,
	Subject,
	Body,
	Hostname,
	Requester,
	Interval,
}

impl TemplateField {
	pub fn as_str(self) -> &'static str {
		match self {
			Self::Filename => "filename",
			Self::Subject => "subject",
			Self::Body => "body",
			Self::Hostname => "hostname",
			Self::Requester => "requester",
			Self::Interval => "interval",
		}
	}
}

impl Display for TemplateField {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		write!(f, "{}", self.as_str())
	}
}

#[instrument]
pub fn load_templates(target: &SendTarget) -> Result<Tera> {
	let mut tera = tera::Tera::default();

	match target {
		SendTarget::Email {
			subject, template, ..
		}
		| SendTarget::Zendesk {
			subject, template, ..
		}
		| SendTarget::Slack {
			subject, template, ..
		}
		| SendTarget::External {
			subject, template, ..
		} => {
			tera.add_raw_template(
				TemplateField::Subject.as_str(),
				subject.as_deref().unwrap_or(DEFAULT_SUBJECT_TEMPLATE),
			)
			.into_diagnostic()
			.wrap_err("compiling subject template")?;
			tera.add_raw_template(TemplateField::Body.as_str(), template)
				.into_diagnostic()
				.wrap_err("compiling body template")?;
		}
	}

	if let SendTarget::Zendesk {
		conn: TargetZendesk {
			method: ZendeskMethod::Anonymous { requester },
			..
		},
		..
	} = target
	{
		tera.add_raw_template(TemplateField::Requester.as_str(), requester)
			.into_diagnostic()
			.wrap_err("compiling requester template")?;
	}
	Ok(tera)
}

#[instrument(skip(alert, now))]
pub fn build_context(alert: &AlertDefinition, now: chrono::DateTime<chrono::Utc>) -> TeraCtx {
	let mut context = TeraCtx::new();
	context.insert(
		TemplateField::Interval.as_str(),
		&format!(
			"{}",
			Folktime::new(alert.interval).with_style(FolkStyle::OneUnitWhole)
		),
	);
	context.insert(
		TemplateField::Hostname.as_str(),
		System::host_name().as_deref().unwrap_or("unknown"),
	);
	context.insert(
		TemplateField::Filename.as_str(),
		&alert.file.file_name().unwrap().to_string_lossy(),
	);
	context.insert("now", &now.to_string());

	context
}

#[instrument(skip(tera, context))]
pub fn render_alert(
	tera: &Tera,
	context: &mut TeraCtx,
) -> Result<(String, String, Option<String>)> {
	let subject = tera
		.render(TemplateField::Subject.as_str(), context)
		.into_diagnostic()
		.wrap_err("rendering subject template")?;

	context.insert(TemplateField::Subject.as_str(), &subject.to_string());

	let body = tera
		.render(TemplateField::Body.as_str(), context)
		.into_diagnostic()
		.wrap_err("rendering email template")?;

	let requester = tera
		.render(TemplateField::Requester.as_str(), context)
		.map(Some)
		.or_else(|err| match err.kind {
			tera::ErrorKind::TemplateNotFound(_) => Ok(None),
			_ => Err(err),
		})
		.into_diagnostic()
		.wrap_err("rendering requester template")?;

	Ok((subject, body, requester))
}