use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct Recipient {
#[serde(rename = "subscriberId")]
pub subscriber_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub phone: Option<String>,
pub locale: String,
}
impl Recipient {
pub fn new(subscriber_id: impl Into<String>, locale: impl Into<String>) -> Self {
Self {
subscriber_id: subscriber_id.into(),
email: None,
phone: None,
locale: locale.into(),
}
}
pub fn email(
subscriber_id: impl Into<String>,
email: impl Into<String>,
locale: impl Into<String>,
) -> Self {
Self {
email: Some(email.into()),
..Self::new(subscriber_id, locale)
}
}
pub fn phone(
subscriber_id: impl Into<String>,
phone: impl Into<String>,
locale: impl Into<String>,
) -> Self {
Self {
phone: Some(phone.into()),
..Self::new(subscriber_id, locale)
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct TopicRecipient {
#[serde(rename = "type")]
kind: &'static str,
#[serde(rename = "topicKey")]
pub topic_key: String,
}
impl TopicRecipient {
pub fn new(topic_key: impl Into<String>) -> Self {
Self {
kind: "Topic",
topic_key: topic_key.into(),
}
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(untagged)]
pub enum TriggerTarget {
Subscriber(Recipient),
Topic(TopicRecipient),
}
impl From<Recipient> for TriggerTarget {
fn from(recipient: Recipient) -> Self {
Self::Subscriber(recipient)
}
}
impl From<TopicRecipient> for TriggerTarget {
fn from(topic: TopicRecipient) -> Self {
Self::Topic(topic)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
High,
#[default]
Normal,
Low,
}
#[derive(Debug, Clone, Serialize)]
pub struct TriggerRequest {
#[serde(rename = "name")]
pub workflow: String,
pub to: TriggerTarget,
pub payload: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub overrides: Option<serde_json::Value>,
#[serde(rename = "transactionId", skip_serializing_if = "Option::is_none")]
pub transaction_id: Option<String>,
}
impl TriggerRequest {
pub fn new(
workflow: impl Into<String>,
to: impl Into<TriggerTarget>,
payload: serde_json::Value,
) -> Self {
Self {
workflow: workflow.into(),
to: to.into(),
payload,
overrides: None,
transaction_id: None,
}
}
pub fn with_severity(mut self, severity: Severity) -> Self {
let severity = serde_json::to_value(severity).expect("severity serializes");
match self.overrides {
Some(serde_json::Value::Object(ref mut map)) => {
map.insert("severity".to_string(), severity);
}
_ => {
self.overrides = Some(serde_json::json!({ "severity": severity }));
}
}
self
}
pub fn with_transaction_id(mut self, id: impl Into<String>) -> Self {
self.transaction_id = Some(id.into());
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::notification::workflows;
#[test]
fn serializes_to_the_novu_trigger_shape() {
let request = TriggerRequest::new(
workflows::ACTION_CONFIRMATION,
Recipient::email("sub-1", "alice@example.com", "tr"),
serde_json::json!({ "otp": "123456" }),
);
let value = serde_json::to_value(&request).unwrap();
assert_eq!(value["name"], "action-confirmation");
assert_eq!(value["to"]["subscriberId"], "sub-1");
assert_eq!(value["to"]["email"], "alice@example.com");
assert!(value["to"].get("phone").is_none());
assert_eq!(value["to"]["locale"], "tr");
assert_eq!(value["payload"]["otp"], "123456");
assert!(value.get("overrides").is_none());
assert!(value.get("transactionId").is_none());
}
#[test]
fn severity_and_transaction_id_ride_the_wire_when_set() {
let request = TriggerRequest::new(
workflows::REGISTER_EMAIL_VERIFICATION,
Recipient::email("sub-9", "b@x.com", "tr"),
serde_json::json!({}),
)
.with_severity(Severity::High)
.with_transaction_id("txn-1");
let value = serde_json::to_value(&request).unwrap();
assert_eq!(value["overrides"]["severity"], "high");
assert_eq!(value["transactionId"], "txn-1");
}
#[test]
fn phone_recipient_omits_email() {
let value = serde_json::to_value(Recipient::phone("sub-2", "+905551234567", "tr")).unwrap();
assert_eq!(value["phone"], "+905551234567");
assert!(value.get("email").is_none());
}
#[test]
fn a_contactless_recipient_sends_neither_channel() {
let value = serde_json::to_value(Recipient::new("sub-3", "en")).unwrap();
assert_eq!(value["subscriberId"], "sub-3");
assert_eq!(value["locale"], "en");
assert!(value.get("email").is_none());
assert!(value.get("phone").is_none());
}
}