1use serde::{Deserialize, Serialize};
17
18use super::{InternalVerb, VerbCategory};
19
20pub struct MessagingEmailSend;
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
28pub struct MessagingEmailSendInput {
29 pub from: String,
31 pub to: Vec<String>,
33 pub subject: String,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub html: Option<String>,
38 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub text: Option<String>,
41 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub reply_to: Option<String>,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
49pub struct MessagingEmailSendOutput {
50 pub id: String,
52}
53
54impl InternalVerb for MessagingEmailSend {
55 type Input = MessagingEmailSendInput;
56 type Output = MessagingEmailSendOutput;
57 const ID: &'static str = "messaging.email.send";
58 const CATEGORY: VerbCategory = VerbCategory::Messaging;
59}
60
61pub struct MessagingSmsSend;
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
69pub struct MessagingSmsSendInput {
70 pub from: String,
73 pub to: String,
75 pub body: String,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
83pub struct MessagingSmsSendOutput {
84 pub id: String,
86 pub status: String,
89}
90
91impl InternalVerb for MessagingSmsSend {
92 type Input = MessagingSmsSendInput;
93 type Output = MessagingSmsSendOutput;
94 const ID: &'static str = "messaging.sms.send";
95 const CATEGORY: VerbCategory = VerbCategory::Messaging;
96}
97
98pub struct MessagingWebhookDispatch;
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
106pub struct MessagingWebhookDispatchInput {
107 pub url: String,
109 pub payload: serde_json::Value,
111 #[serde(default, skip_serializing_if = "Option::is_none")]
114 pub headers: Option<std::collections::BTreeMap<String, String>>,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
119#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
120pub struct MessagingWebhookDispatchOutput {
121 pub status_code: u16,
123 pub ok: bool,
125}
126
127impl InternalVerb for MessagingWebhookDispatch {
128 type Input = MessagingWebhookDispatchInput;
129 type Output = MessagingWebhookDispatchOutput;
130 const ID: &'static str = "messaging.webhook.dispatch";
131 const CATEGORY: VerbCategory = VerbCategory::Messaging;
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn verb_ids_match_canonical_namespace() {
140 for id in [
141 MessagingEmailSend::ID,
142 MessagingSmsSend::ID,
143 MessagingWebhookDispatch::ID,
144 ] {
145 assert!(id.starts_with("messaging."), "{id}");
146 }
147 }
148
149 #[test]
150 fn verbs_are_under_messaging_category() {
151 assert_eq!(MessagingEmailSend::CATEGORY, VerbCategory::Messaging);
152 assert_eq!(MessagingSmsSend::CATEGORY, VerbCategory::Messaging);
153 assert_eq!(MessagingWebhookDispatch::CATEGORY, VerbCategory::Messaging);
154 }
155
156 #[test]
157 fn email_send_optional_fields_omitted() {
158 let input = MessagingEmailSendInput {
159 from: "noreply@yah.dev".into(),
160 to: vec!["user@example.com".into()],
161 subject: "Hello".into(),
162 html: Some("<p>Hi</p>".into()),
163 text: None,
164 reply_to: None,
165 };
166 let wire = serde_json::to_value(&input).unwrap();
167 assert!(!wire.as_object().unwrap().contains_key("text"));
168 assert!(!wire.as_object().unwrap().contains_key("reply_to"));
169 assert_eq!(wire["html"], "<p>Hi</p>");
170 }
171
172 #[test]
173 fn email_send_to_is_vec() {
174 let wire = r#"{"from":"a@b.com","to":["x@y.com","z@w.com"],"subject":"S"}"#;
175 let parsed: MessagingEmailSendInput = serde_json::from_str(wire).unwrap();
176 assert_eq!(parsed.to.len(), 2);
177 }
178
179 #[test]
180 fn sms_send_round_trips() {
181 let input = MessagingSmsSendInput {
182 from: "+15550100".into(),
183 to: "+15550101".into(),
184 body: "Hello".into(),
185 };
186 let wire = serde_json::to_string(&input).unwrap();
187 let back: MessagingSmsSendInput = serde_json::from_str(&wire).unwrap();
188 assert_eq!(back.to, "+15550101");
189 }
190
191 #[test]
192 fn webhook_dispatch_payload_accepts_arbitrary_json() {
193 let wire = r#"{"url":"https://hook.example.com","payload":{"event":"deploy","sha":"abc"}}"#;
194 let parsed: MessagingWebhookDispatchInput = serde_json::from_str(wire).unwrap();
195 assert_eq!(parsed.payload["event"], "deploy");
196 assert!(parsed.headers.is_none());
197 }
198
199 #[test]
200 fn webhook_dispatch_output_ok_reflects_status() {
201 let ok = MessagingWebhookDispatchOutput {
202 status_code: 200,
203 ok: true,
204 };
205 let err = MessagingWebhookDispatchOutput {
206 status_code: 503,
207 ok: false,
208 };
209 assert_eq!(serde_json::to_value(&ok).unwrap()["ok"], true);
210 assert_eq!(serde_json::to_value(&err).unwrap()["status_code"], 503);
211 }
212
213 #[cfg(feature = "json-schema")]
214 #[test]
215 fn verbs_emit_schemas_via_for_verb() {
216 use super::super::VerbDescriptor;
217
218 let email = VerbDescriptor::for_verb::<MessagingEmailSend>();
219 assert_eq!(email.id, "messaging.email.send");
220 assert!(email.input_schema.to_string().contains("subject"));
221
222 let sms = VerbDescriptor::for_verb::<MessagingSmsSend>();
223 assert_eq!(sms.id, "messaging.sms.send");
224
225 let webhook = VerbDescriptor::for_verb::<MessagingWebhookDispatch>();
226 assert_eq!(webhook.id, "messaging.webhook.dispatch");
227 assert!(webhook.output_schema.to_string().contains("status_code"));
228 }
229}