restate_email/options.rs
1//! Restate-specific per-send options carried through `SendOptions::transport_options`.
2
3use std::time::Duration;
4
5use email_transport::TransportOption;
6
7/// Restate-specific options for a single send attempt.
8///
9/// This is the one typed value inserted into
10/// [`email_transport::SendOptions::transport_options`] for Restate. It travels
11/// under the provider key `"restate"` and overrides the defaults configured on
12/// the caller-side `RestateTransport` for one send.
13///
14/// The slot is forwarded to the worker in the queued payload like every other
15/// provider slice. Workers ignore it unless their transport registry itself
16/// contains a Restate-backed transport, in which case the same mode and delay
17/// apply to the second hop as well.
18#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
19#[non_exhaustive]
20pub struct RestateSendOptions {
21 /// Override the transport's configured [`InvocationMode`] for this send.
22 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub invocation_mode: Option<InvocationMode>,
24 /// Delay before Restate starts the worker invocation.
25 ///
26 /// Sent as the ingress `delay` query parameter, encoded as whole
27 /// milliseconds rounded up. Only valid with [`InvocationMode::Queued`];
28 /// combining it with [`InvocationMode::Sent`] fails the send with
29 /// `ErrorKind::Validation` before any request is made.
30 ///
31 /// This is provider scheduling behavior, not a delivery constraint: a
32 /// transport that does not recognize the `restate` slot delivers now
33 /// rather than later.
34 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub delay: Option<Duration>,
36}
37
38impl TransportOption for RestateSendOptions {
39 fn provider_key() -> &'static str {
40 "restate"
41 }
42}
43
44impl RestateSendOptions {
45 /// Create empty Restate-specific send options.
46 #[must_use]
47 pub fn new() -> Self {
48 Self::default()
49 }
50
51 /// Override the invocation mode for this send.
52 #[must_use]
53 pub const fn with_invocation_mode(mut self, invocation_mode: InvocationMode) -> Self {
54 self.invocation_mode = Some(invocation_mode);
55 self
56 }
57
58 /// Delay the worker invocation by `delay`.
59 #[must_use]
60 pub const fn with_delay(mut self, delay: Duration) -> Self {
61 self.delay = Some(delay);
62 self
63 }
64
65 /// Return whether no override is configured.
66 #[must_use]
67 pub const fn is_empty(&self) -> bool {
68 self.invocation_mode.is_none() && self.delay.is_none()
69 }
70}
71
72/// How far a Restate-backed send is followed before it returns.
73///
74/// Both modes invoke the same `Email.send` handler; they differ in the state
75/// the send has reached when the future resolves and in what the resulting
76/// `SendReport` describes.
77///
78/// The enum is deliberately exhaustive: a transport must implement every
79/// mode, so a new mode is a breaking change rather than a silently ignored
80/// wildcard.
81#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
82#[serde(rename_all = "snake_case")]
83pub enum InvocationMode {
84 /// Return once Restate has durably accepted the invocation.
85 ///
86 /// The report carries `"restate"` as the provider and the Restate
87 /// invocation id as the message id; the worker has not yet run.
88 #[default]
89 Queued,
90 /// Return once the worker has handed the message to its provider.
91 ///
92 /// The report is the worker's own provider report.
93 Sent,
94}
95
96#[cfg(test)]
97mod tests {
98 use serde_json::json;
99
100 use super::*;
101
102 #[test]
103 fn default_options_serialize_to_empty_object() {
104 let options = RestateSendOptions::default();
105
106 assert!(options.is_empty());
107 assert_eq!(
108 serde_json::to_value(&options).expect("options serialize"),
109 json!({})
110 );
111 }
112
113 #[test]
114 fn options_round_trip_through_json() {
115 let options = RestateSendOptions::new()
116 .with_invocation_mode(InvocationMode::Sent)
117 .with_delay(Duration::new(1, 500));
118
119 let value = serde_json::to_value(&options).expect("options serialize");
120 assert_eq!(
121 value,
122 json!({
123 "invocation_mode": "sent",
124 "delay": {"secs": 1, "nanos": 500}
125 })
126 );
127 assert_eq!(
128 serde_json::from_value::<RestateSendOptions>(value).expect("options deserialize"),
129 options
130 );
131 }
132
133 #[test]
134 fn invocation_mode_defaults_to_queued() {
135 assert_eq!(InvocationMode::default(), InvocationMode::Queued);
136 assert_eq!(
137 serde_json::to_value(InvocationMode::Queued).expect("mode serializes"),
138 json!("queued")
139 );
140 }
141}