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    pub function_id: String,
54    pub payload: Value,
55    pub action: Option<TriggerAction>,
56    pub timeout_ms: Option<u64>,
57}
58
59impl TriggerRequest {
60    /// Attach per-invocation metadata without adding a required field to
61    /// [`TriggerRequest`] struct literals.
62    pub fn metadata(self, metadata: Value) -> TriggerRequestWithMetadata {
63        TriggerRequestWithMetadata {
64            request: self,
65            metadata: Some(metadata),
66        }
67    }
68}
69
70/// Trigger request plus optional per-invocation metadata.
71#[derive(Debug, Clone)]
72pub struct TriggerRequestWithMetadata {
73    pub(crate) request: TriggerRequest,
74    pub(crate) metadata: Option<Value>,
75}
76
77impl<T> From<T> for TriggerRequestWithMetadata
78where
79    T: Into<TriggerRequest>,
80{
81    fn from(request: T) -> Self {
82        Self {
83            request: request.into(),
84            metadata: None,
85        }
86    }
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
90#[serde(tag = "type", rename_all = "lowercase")]
91pub enum Message {
92    RegisterTriggerType {
93        id: String,
94        description: String,
95        #[serde(skip_serializing_if = "Option::is_none")]
96        trigger_request_format: Option<Value>,
97        #[serde(skip_serializing_if = "Option::is_none")]
98        call_request_format: Option<Value>,
99    },
100    RegisterTrigger {
101        id: String,
102        trigger_type: String,
103        function_id: String,
104        config: Value,
105        #[serde(skip_serializing_if = "Option::is_none")]
106        metadata: Option<Value>,
107    },
108    TriggerRegistrationResult {
109        id: String,
110        trigger_type: String,
111        function_id: String,
112        #[serde(skip_serializing_if = "Option::is_none")]
113        error: Option<ErrorBody>,
114    },
115    UnregisterTrigger {
116        id: String,
117        trigger_type: String,
118    },
119    UnregisterTriggerType {
120        id: String,
121    },
122    RegisterFunction {
123        id: String,
124        #[serde(skip_serializing_if = "Option::is_none")]
125        description: Option<String>,
126        #[serde(skip_serializing_if = "Option::is_none")]
127        request_format: Option<Value>,
128        #[serde(skip_serializing_if = "Option::is_none")]
129        response_format: Option<Value>,
130        #[serde(skip_serializing_if = "Option::is_none")]
131        metadata: Option<Value>,
132        #[serde(skip_serializing_if = "Option::is_none")]
133        invocation: Option<HttpInvocationConfig>,
134    },
135    UnregisterFunction {
136        id: String,
137    },
138    InvokeFunction {
139        invocation_id: Option<Uuid>,
140        function_id: String,
141        data: Value,
142        #[serde(skip_serializing_if = "Option::is_none")]
143        traceparent: Option<String>,
144        #[serde(skip_serializing_if = "Option::is_none")]
145        baggage: Option<String>,
146        #[serde(skip_serializing_if = "Option::is_none")]
147        action: Option<TriggerAction>,
148        /// Per-invocation metadata sidecar, surfaced to the handler as a
149        /// distinct argument (not folded into `data`). Optional and additive
150        /// for wire compatibility with engines that don't send it.
151        #[serde(default, skip_serializing_if = "Option::is_none")]
152        metadata: Option<Value>,
153    },
154    InvocationResult {
155        invocation_id: Uuid,
156        function_id: String,
157        #[serde(skip_serializing_if = "Option::is_none")]
158        result: Option<Value>,
159        #[serde(skip_serializing_if = "Option::is_none")]
160        error: Option<ErrorBody>,
161        #[serde(skip_serializing_if = "Option::is_none")]
162        traceparent: Option<String>,
163        #[serde(skip_serializing_if = "Option::is_none")]
164        baggage: Option<String>,
165    },
166    Ping,
167    Pong,
168    WorkerRegistered {
169        worker_id: String,
170    },
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct RegisterTriggerTypeMessage {
175    pub id: String,
176    pub description: String,
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub trigger_request_format: Option<Value>,
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub call_request_format: Option<Value>,
181}
182
183impl RegisterTriggerTypeMessage {
184    pub fn to_message(&self) -> Message {
185        Message::RegisterTriggerType {
186            id: self.id.clone(),
187            description: self.description.clone(),
188            trigger_request_format: self.trigger_request_format.clone(),
189            call_request_format: self.call_request_format.clone(),
190        }
191    }
192}
193
194/// Input for [`IIIClient::register_trigger`](crate::IIIClient::register_trigger).
195/// The `id` is auto-generated internally.
196#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct RegisterTriggerInput {
198    pub trigger_type: String,
199    pub function_id: String,
200    pub config: Value,
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub metadata: Option<Value>,
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct RegisterTriggerMessage {
207    pub id: String,
208    pub trigger_type: String,
209    pub function_id: String,
210    pub config: Value,
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub metadata: Option<Value>,
213}
214
215impl RegisterTriggerMessage {
216    pub fn to_message(&self) -> Message {
217        Message::RegisterTrigger {
218            id: self.id.clone(),
219            trigger_type: self.trigger_type.clone(),
220            function_id: self.function_id.clone(),
221            config: self.config.clone(),
222            metadata: self.metadata.clone(),
223        }
224    }
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct UnregisterTriggerMessage {
229    pub id: String,
230    pub trigger_type: String,
231}
232
233impl UnregisterTriggerMessage {
234    pub fn to_message(&self) -> Message {
235        Message::UnregisterTrigger {
236            id: self.id.clone(),
237            trigger_type: self.trigger_type.clone(),
238        }
239    }
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
243pub struct UnregisterTriggerTypeMessage {
244    pub id: String,
245}
246
247impl UnregisterTriggerTypeMessage {
248    pub fn to_message(&self) -> Message {
249        Message::UnregisterTriggerType {
250            id: self.id.clone(),
251        }
252    }
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct RegisterFunctionMessage {
257    pub id: String,
258    #[serde(skip_serializing_if = "Option::is_none")]
259    pub description: Option<String>,
260    #[serde(skip_serializing_if = "Option::is_none")]
261    pub request_format: Option<Value>,
262    #[serde(skip_serializing_if = "Option::is_none")]
263    pub response_format: Option<Value>,
264    #[serde(skip_serializing_if = "Option::is_none")]
265    pub metadata: Option<Value>,
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub invocation: Option<HttpInvocationConfig>,
268}
269
270impl RegisterFunctionMessage {
271    pub fn with_id(name: String) -> Self {
272        RegisterFunctionMessage {
273            id: name,
274            description: None,
275            request_format: None,
276            response_format: None,
277            metadata: None,
278            invocation: None,
279        }
280    }
281    pub fn with_description(mut self, description: String) -> Self {
282        self.description = Some(description);
283        self
284    }
285    pub fn to_message(&self) -> Message {
286        Message::RegisterFunction {
287            id: self.id.clone(),
288            description: self.description.clone(),
289            request_format: self.request_format.clone(),
290            response_format: self.response_format.clone(),
291            metadata: self.metadata.clone(),
292            invocation: self.invocation.clone(),
293        }
294    }
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize)]
298pub struct FunctionMessage {
299    pub function_id: String,
300    #[serde(skip_serializing_if = "Option::is_none")]
301    pub description: Option<String>,
302    #[serde(skip_serializing_if = "Option::is_none")]
303    pub request_format: Option<Value>,
304    #[serde(skip_serializing_if = "Option::is_none")]
305    pub response_format: Option<Value>,
306    #[serde(skip_serializing_if = "Option::is_none")]
307    pub metadata: Option<Value>,
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize)]
311#[non_exhaustive]
312pub struct ErrorBody {
313    pub code: String,
314    pub message: String,
315    #[serde(skip_serializing_if = "Option::is_none")]
316    pub stacktrace: Option<String>,
317}
318
319#[cfg(test)]
320mod tests {
321    use std::collections::HashMap;
322
323    use super::*;
324
325    #[test]
326    fn register_function_to_message_and_serializes_type() {
327        let msg = RegisterFunctionMessage {
328            id: "functions.echo".to_string(),
329            description: Some("Echo function".to_string()),
330            request_format: None,
331            response_format: None,
332            metadata: None,
333            invocation: None,
334        };
335
336        let message = msg.to_message();
337        match &message {
338            Message::RegisterFunction {
339                id, description, ..
340            } => {
341                assert_eq!(id, "functions.echo");
342                assert_eq!(description.as_deref(), Some("Echo function"));
343            }
344            _ => panic!("unexpected message variant"),
345        }
346
347        let serialized = serde_json::to_value(&message).unwrap();
348        assert_eq!(serialized["type"], "registerfunction");
349        assert_eq!(serialized["id"], "functions.echo");
350        assert_eq!(serialized["description"], "Echo function");
351    }
352
353    #[test]
354    fn register_http_function_serializes_invocation() {
355        use iii_helpers::http::{HttpInvocationConfig, HttpMethod};
356
357        let msg = RegisterFunctionMessage {
358            id: "external::my_lambda".to_string(),
359            description: None,
360            request_format: None,
361            response_format: None,
362            metadata: None,
363            invocation: Some(HttpInvocationConfig {
364                url: "https://example.com/invoke".to_string(),
365                method: HttpMethod::Post,
366                timeout_ms: Some(30000),
367                headers: HashMap::new(),
368                auth: None,
369            }),
370        };
371
372        let serialized = serde_json::to_value(msg.to_message()).unwrap();
373        assert_eq!(serialized["type"], "registerfunction");
374        assert_eq!(serialized["id"], "external::my_lambda");
375        assert!(serialized["invocation"].is_object());
376        assert_eq!(
377            serialized["invocation"]["url"],
378            "https://example.com/invoke"
379        );
380        assert_eq!(serialized["invocation"]["method"], "POST");
381    }
382}