Skip to main content

iii_sdk/
protocol.rs

1use iii_helpers::http::HttpInvocationConfig;
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use uuid::Uuid;
6
7/// Routing action for [`TriggerRequest`]. Determines how the engine handles
8/// the invocation.
9///
10/// - `Enqueue`: Routes through a named queue for async processing.
11/// - `Void`: Fire-and-forget, no response.
12#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
13#[serde(tag = "type", rename_all = "lowercase")]
14pub enum TriggerAction {
15    /// Routes the invocation through a named queue.
16    Enqueue { queue: String },
17    /// Fire-and-forget routing.
18    Void,
19}
20
21/// Request object for `trigger()`.
22///
23/// ```rust
24/// # use iii_sdk::protocol::{TriggerRequest, TriggerAction};
25/// # use serde_json::json;
26/// // Simple call
27/// TriggerRequest {
28///     function_id: "my::function".to_string(),
29///     payload: json!({ "key": "value" }),
30///     action: None,
31///     timeout_ms: None,
32/// };
33///
34/// // With action
35/// TriggerRequest {
36///     function_id: "my::function".to_string(),
37///     payload: json!({}),
38///     action: Some(TriggerAction::Enqueue { queue: "payments".to_string() }),
39///     timeout_ms: None,
40/// };
41///
42/// // With metadata
43/// TriggerRequest {
44///     function_id: "my::function".to_string(),
45///     payload: json!({}),
46///     action: None,
47///     timeout_ms: None,
48/// }
49/// .metadata(json!({ "tenant": "acme" }));
50/// ```
51#[derive(Debug, Clone)]
52pub struct TriggerRequest {
53    /// ID of the function to invoke.
54    pub function_id: String,
55    /// Input data passed to the function.
56    pub payload: Value,
57    /// Sets how the trigger is routed. `None` for a synchronous request/response.
58    /// Set a routing scheme otherwise (e.g. `TriggerAction::Enqueue { .. }`, `TriggerAction::Void`).
59    pub action: Option<TriggerAction>,
60    /// Override the default invocation timeout, in milliseconds.
61    pub timeout_ms: Option<u64>,
62}
63
64impl TriggerRequest {
65    /// Attach per-invocation metadata without adding a required field to
66    /// [`TriggerRequest`] struct literals.
67    pub fn metadata(self, metadata: Value) -> TriggerRequestWithMetadata {
68        TriggerRequestWithMetadata {
69            request: self,
70            metadata: Some(metadata),
71        }
72    }
73}
74
75/// Trigger request plus optional per-invocation metadata.
76#[derive(Debug, Clone)]
77pub struct TriggerRequestWithMetadata {
78    pub(crate) request: TriggerRequest,
79    pub(crate) metadata: Option<Value>,
80}
81
82impl<T> From<T> for TriggerRequestWithMetadata
83where
84    T: Into<TriggerRequest>,
85{
86    fn from(request: T) -> Self {
87        Self {
88            request: request.into(),
89            metadata: None,
90        }
91    }
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
95#[serde(tag = "type", rename_all = "lowercase")]
96pub enum Message {
97    RegisterTriggerType {
98        id: String,
99        description: String,
100        #[serde(skip_serializing_if = "Option::is_none")]
101        trigger_request_format: Option<Value>,
102        #[serde(skip_serializing_if = "Option::is_none")]
103        call_request_format: Option<Value>,
104    },
105    RegisterTrigger {
106        id: String,
107        trigger_type: String,
108        function_id: String,
109        config: Value,
110        #[serde(skip_serializing_if = "Option::is_none")]
111        metadata: Option<Value>,
112    },
113    TriggerRegistrationResult {
114        id: String,
115        trigger_type: String,
116        function_id: String,
117        #[serde(skip_serializing_if = "Option::is_none")]
118        error: Option<ErrorBody>,
119    },
120    UnregisterTrigger {
121        id: String,
122        trigger_type: String,
123    },
124    UnregisterTriggerType {
125        id: String,
126    },
127    RegisterFunction {
128        id: String,
129        #[serde(skip_serializing_if = "Option::is_none")]
130        description: Option<String>,
131        #[serde(skip_serializing_if = "Option::is_none")]
132        request_format: Option<Value>,
133        #[serde(skip_serializing_if = "Option::is_none")]
134        response_format: Option<Value>,
135        #[serde(skip_serializing_if = "Option::is_none")]
136        metadata: Option<Value>,
137        #[serde(skip_serializing_if = "Option::is_none")]
138        invocation: Option<HttpInvocationConfig>,
139    },
140    UnregisterFunction {
141        id: String,
142    },
143    InvokeFunction {
144        invocation_id: Option<Uuid>,
145        function_id: String,
146        data: Value,
147        #[serde(skip_serializing_if = "Option::is_none")]
148        traceparent: Option<String>,
149        #[serde(skip_serializing_if = "Option::is_none")]
150        baggage: Option<String>,
151        #[serde(skip_serializing_if = "Option::is_none")]
152        action: Option<TriggerAction>,
153        /// Per-invocation metadata sidecar, surfaced to the handler as a
154        /// distinct argument alongside `data`. Optional and additive
155        /// for wire compatibility with engines that don't send it.
156        #[serde(default, skip_serializing_if = "Option::is_none")]
157        metadata: Option<Value>,
158    },
159    InvocationResult {
160        invocation_id: Uuid,
161        function_id: String,
162        #[serde(skip_serializing_if = "Option::is_none")]
163        result: Option<Value>,
164        #[serde(skip_serializing_if = "Option::is_none")]
165        error: Option<ErrorBody>,
166        #[serde(skip_serializing_if = "Option::is_none")]
167        traceparent: Option<String>,
168        #[serde(skip_serializing_if = "Option::is_none")]
169        baggage: Option<String>,
170    },
171    Ping,
172    Pong,
173    /// Sent to the engine as the first message of a reconnect, before the
174    /// registration replay: `previous_worker_id` and `reattach_token` are
175    /// the values the engine assigned via `WorkerRegistered` on the previous
176    /// connection. The engine retires that connection so the replay lands on
177    /// a clean slate; the token is required because worker ids alone are
178    /// publicly discoverable.
179    Reattach {
180        previous_worker_id: String,
181        #[serde(default, skip_serializing_if = "Option::is_none")]
182        reattach_token: Option<String>,
183    },
184    WorkerRegistered {
185        worker_id: String,
186        /// Secret to present in `Reattach` on reconnect; absent on older
187        /// engines.
188        #[serde(default, skip_serializing_if = "Option::is_none")]
189        reattach_token: Option<String>,
190    },
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct RegisterTriggerTypeMessage {
195    /// Unique identifier for the trigger type (e.g. `state`, `durable:subscriber`).
196    pub id: String,
197    /// Human-readable description of what this trigger type does.
198    pub description: String,
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub trigger_request_format: Option<Value>,
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub call_request_format: Option<Value>,
203}
204
205impl RegisterTriggerTypeMessage {
206    pub fn to_message(&self) -> Message {
207        Message::RegisterTriggerType {
208            id: self.id.clone(),
209            description: self.description.clone(),
210            trigger_request_format: self.trigger_request_format.clone(),
211            call_request_format: self.call_request_format.clone(),
212        }
213    }
214}
215
216/// Input for [`IIIClient::register_trigger`](crate::IIIClient::register_trigger).
217/// The `id` is auto-generated internally.
218#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct RegisterTriggerInput {
220    /// Identifier of the registered trigger type this trigger uses (e.g. `storage::object-created`, `http`).
221    pub trigger_type: String,
222    /// ID of the function this trigger invokes when it fires.
223    pub function_id: String,
224    /// Trigger-type-specific configuration, matching the shape the trigger type expects.
225    pub config: Value,
226    /// Arbitrary user-specifiable metadata supplied to the triggered handler function on every invocation.
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub metadata: Option<Value>,
229}
230
231#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct RegisterTriggerMessage {
233    pub id: String,
234    pub trigger_type: String,
235    pub function_id: String,
236    pub config: Value,
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub metadata: Option<Value>,
239}
240
241impl RegisterTriggerMessage {
242    pub fn to_message(&self) -> Message {
243        Message::RegisterTrigger {
244            id: self.id.clone(),
245            trigger_type: self.trigger_type.clone(),
246            function_id: self.function_id.clone(),
247            config: self.config.clone(),
248            metadata: self.metadata.clone(),
249        }
250    }
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
254pub struct UnregisterTriggerMessage {
255    pub id: String,
256    pub trigger_type: String,
257}
258
259impl UnregisterTriggerMessage {
260    pub fn to_message(&self) -> Message {
261        Message::UnregisterTrigger {
262            id: self.id.clone(),
263            trigger_type: self.trigger_type.clone(),
264        }
265    }
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
269pub struct UnregisterTriggerTypeMessage {
270    pub id: String,
271}
272
273impl UnregisterTriggerTypeMessage {
274    pub fn to_message(&self) -> Message {
275        Message::UnregisterTriggerType {
276            id: self.id.clone(),
277        }
278    }
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
282pub struct RegisterFunctionMessage {
283    pub id: String,
284    #[serde(skip_serializing_if = "Option::is_none")]
285    pub description: Option<String>,
286    #[serde(skip_serializing_if = "Option::is_none")]
287    pub request_format: Option<Value>,
288    #[serde(skip_serializing_if = "Option::is_none")]
289    pub response_format: Option<Value>,
290    #[serde(skip_serializing_if = "Option::is_none")]
291    pub metadata: Option<Value>,
292    #[serde(skip_serializing_if = "Option::is_none")]
293    pub invocation: Option<HttpInvocationConfig>,
294}
295
296impl RegisterFunctionMessage {
297    pub fn with_id(name: String) -> Self {
298        RegisterFunctionMessage {
299            id: name,
300            description: None,
301            request_format: None,
302            response_format: None,
303            metadata: None,
304            invocation: None,
305        }
306    }
307    pub fn with_description(mut self, description: String) -> Self {
308        self.description = Some(description);
309        self
310    }
311    pub fn to_message(&self) -> Message {
312        Message::RegisterFunction {
313            id: self.id.clone(),
314            description: self.description.clone(),
315            request_format: self.request_format.clone(),
316            response_format: self.response_format.clone(),
317            metadata: self.metadata.clone(),
318            invocation: self.invocation.clone(),
319        }
320    }
321}
322
323#[derive(Debug, Clone, Serialize, Deserialize)]
324pub struct FunctionMessage {
325    pub function_id: String,
326    #[serde(skip_serializing_if = "Option::is_none")]
327    pub description: Option<String>,
328    #[serde(skip_serializing_if = "Option::is_none")]
329    pub request_format: Option<Value>,
330    #[serde(skip_serializing_if = "Option::is_none")]
331    pub response_format: Option<Value>,
332    #[serde(skip_serializing_if = "Option::is_none")]
333    pub metadata: Option<Value>,
334}
335
336#[derive(Debug, Clone, Serialize, Deserialize)]
337#[non_exhaustive]
338pub struct ErrorBody {
339    pub code: String,
340    pub message: String,
341    #[serde(skip_serializing_if = "Option::is_none")]
342    pub stacktrace: Option<String>,
343}
344
345#[cfg(test)]
346mod tests {
347    use std::collections::HashMap;
348
349    use super::*;
350
351    #[test]
352    fn register_function_to_message_and_serializes_type() {
353        let msg = RegisterFunctionMessage {
354            id: "functions.echo".to_string(),
355            description: Some("Echo function".to_string()),
356            request_format: None,
357            response_format: None,
358            metadata: None,
359            invocation: None,
360        };
361
362        let message = msg.to_message();
363        match &message {
364            Message::RegisterFunction {
365                id, description, ..
366            } => {
367                assert_eq!(id, "functions.echo");
368                assert_eq!(description.as_deref(), Some("Echo function"));
369            }
370            _ => panic!("unexpected message variant"),
371        }
372
373        let serialized = serde_json::to_value(&message).unwrap();
374        assert_eq!(serialized["type"], "registerfunction");
375        assert_eq!(serialized["id"], "functions.echo");
376        assert_eq!(serialized["description"], "Echo function");
377    }
378
379    #[test]
380    fn register_http_function_serializes_invocation() {
381        use iii_helpers::http::{HttpInvocationConfig, HttpMethod};
382
383        let msg = RegisterFunctionMessage {
384            id: "external::my_lambda".to_string(),
385            description: None,
386            request_format: None,
387            response_format: None,
388            metadata: None,
389            invocation: Some(HttpInvocationConfig {
390                url: "https://example.com/invoke".to_string(),
391                method: HttpMethod::Post,
392                timeout_ms: Some(30000),
393                headers: HashMap::new(),
394                auth: None,
395            }),
396        };
397
398        let serialized = serde_json::to_value(msg.to_message()).unwrap();
399        assert_eq!(serialized["type"], "registerfunction");
400        assert_eq!(serialized["id"], "external::my_lambda");
401        assert!(serialized["invocation"].is_object());
402        assert_eq!(
403            serialized["invocation"]["url"],
404            "https://example.com/invoke"
405        );
406        assert_eq!(serialized["invocation"]["method"], "POST");
407    }
408}