bestool_alertd/
targets.rs

1use std::collections::HashMap;
2
3use miette::Result;
4
5use crate::{
6	EmailConfig,
7	alert::AlertDefinition,
8	templates::{load_templates, render_alert},
9};
10
11mod default;
12mod email;
13
14pub use default::determine_default_target;
15pub use email::TargetEmail;
16
17#[derive(serde::Deserialize, Debug, Clone)]
18#[serde(rename_all = "snake_case")]
19#[serde(untagged)]
20pub enum SendTarget {
21	// New format: just id, subject, template
22	Simple {
23		id: String,
24		subject: Option<String>,
25		template: String,
26	},
27	// Old format: target: external, id, subject, template
28	External {
29		target: String, // Should be "external" but we ignore the value
30		id: String,
31		subject: Option<String>,
32		template: String,
33	},
34}
35
36impl SendTarget {
37	pub fn id(&self) -> &str {
38		match self {
39			Self::Simple { id, .. } => id,
40			Self::External { id, .. } => id,
41		}
42	}
43
44	pub fn subject(&self) -> &Option<String> {
45		match self {
46			Self::Simple { subject, .. } => subject,
47			Self::External { subject, .. } => subject,
48		}
49	}
50
51	pub fn template(&self) -> &str {
52		match self {
53			Self::Simple { template, .. } => template,
54			Self::External { template, .. } => template,
55		}
56	}
57
58	pub fn resolve_external(
59		&self,
60		external_targets: &HashMap<String, Vec<ExternalTarget>>,
61	) -> Vec<ResolvedTarget> {
62		external_targets
63			.get(self.id())
64			.map(|exts| {
65				exts.iter()
66					.map(|ext| ResolvedTarget {
67						subject: self.subject().clone(),
68						template: self.template().to_string(),
69						conn: ext.conn.clone(),
70					})
71					.collect()
72			})
73			.unwrap_or_default()
74	}
75}
76
77#[derive(Debug, Clone)]
78pub struct ResolvedTarget {
79	pub subject: Option<String>,
80	pub template: String,
81	pub conn: TargetEmail,
82}
83
84impl ResolvedTarget {
85	pub async fn send(
86		&self,
87		alert: &AlertDefinition,
88		tera_ctx: &mut tera::Context,
89		email: Option<&EmailConfig>,
90		dry_run: bool,
91	) -> Result<()> {
92		let tera = load_templates(&self.subject, &self.template)?;
93		let (subject, body) = render_alert(&tera, tera_ctx)?;
94
95		self.conn.send(alert, email, &subject, &body, dry_run).await
96	}
97}
98
99#[derive(serde::Deserialize, facet::Facet, Debug)]
100pub struct AlertTargets {
101	pub targets: Vec<ExternalTarget>,
102}
103
104#[derive(serde::Deserialize, serde::Serialize, facet::Facet, Clone, Debug)]
105#[facet(rename_all = "snake_case")]
106#[serde(rename_all = "snake_case")]
107pub struct ExternalTarget {
108	pub id: String,
109	#[serde(flatten)]
110	pub conn: TargetEmail,
111}
112
113#[cfg(test)]
114mod tests {
115	use super::*;
116
117	#[test]
118	fn test_send_target_simple_format() {
119		let yaml = r#"
120id: test-target
121subject: Test Subject
122template: Test template
123"#;
124		let target: SendTarget = serde_yaml::from_str(yaml).unwrap();
125		assert_eq!(target.id(), "test-target");
126		assert_eq!(target.subject(), &Some("Test Subject".to_string()));
127		assert_eq!(target.template(), "Test template");
128	}
129
130	#[test]
131	fn test_send_target_external_format() {
132		let yaml = r#"
133target: external
134id: test-target
135subject: Test Subject
136template: Test template
137"#;
138		let target: SendTarget = serde_yaml::from_str(yaml).unwrap();
139		assert_eq!(target.id(), "test-target");
140		assert_eq!(target.subject(), &Some("Test Subject".to_string()));
141		assert_eq!(target.template(), "Test template");
142	}
143
144	#[test]
145	fn test_send_target_without_subject() {
146		let yaml = r#"
147id: test-target
148template: Test template
149"#;
150		let target: SendTarget = serde_yaml::from_str(yaml).unwrap();
151		assert_eq!(target.id(), "test-target");
152		assert_eq!(target.subject(), &None);
153		assert_eq!(target.template(), "Test template");
154	}
155}