Skip to main content

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}