Skip to main content

iii_sdk/
protocol.rs

1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use uuid::Uuid;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8#[serde(rename_all = "UPPERCASE")]
9pub enum HttpMethod {
10    Get,
11    Post,
12    Put,
13    Patch,
14    Delete,
15}
16
17/// Authentication configuration for HTTP-invoked functions.
18///
19/// - `Hmac` -- HMAC signature verification using a shared secret.
20/// - `Bearer` -- Bearer token authentication.
21/// - `ApiKey` -- API key sent via a custom header.
22#[derive(Debug, Clone, Serialize, Deserialize)]
23#[serde(tag = "type", rename_all = "lowercase")]
24pub enum HttpAuthConfig {
25    Hmac {
26        secret_key: String,
27    },
28    Bearer {
29        token_key: String,
30    },
31    #[serde(rename = "api_key")]
32    ApiKey {
33        header: String,
34        value_key: String,
35    },
36}
37
38/// Configuration for registering an HTTP-invoked function (Lambda, Cloudflare
39/// Workers, etc.) instead of a local handler.
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct HttpInvocationConfig {
42    pub url: String,
43    #[serde(default = "default_http_method")]
44    pub method: HttpMethod,
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub timeout_ms: Option<u64>,
47    #[serde(default)]
48    pub headers: HashMap<String, String>,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub auth: Option<HttpAuthConfig>,
51}
52
53fn default_http_method() -> HttpMethod {
54    HttpMethod::Post
55}
56
57/// Routing action for [`TriggerRequest`]. Determines how the engine handles
58/// the invocation.
59///
60/// - `Enqueue` -- Routes through a named queue for async processing.
61/// - `Void` -- Fire-and-forget, no response.
62#[derive(Debug, Clone, Serialize, Deserialize)]
63#[serde(tag = "type", rename_all = "lowercase")]
64pub enum TriggerAction {
65    /// Routes the invocation through a named queue.
66    Enqueue { queue: String },
67    /// Fire-and-forget routing.
68    Void,
69}
70
71/// Result returned by the engine when a message is successfully enqueued.
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct EnqueueResult {
74    #[serde(rename = "messageReceiptId")]
75    pub message_receipt_id: String,
76}
77
78/// Request object for `trigger()`. Matches the Node/Python SDK signature:
79/// `trigger({ function_id, payload, action?, timeout_ms? })`
80///
81/// ```rust
82/// # use iii_sdk::protocol::{TriggerRequest, TriggerAction};
83/// # use serde_json::json;
84/// // Simple call
85/// TriggerRequest {
86///     function_id: "my::function".to_string(),
87///     payload: json!({ "key": "value" }),
88///     action: None,
89///     timeout_ms: None,
90/// };
91///
92/// // With action
93/// TriggerRequest {
94///     function_id: "my::function".to_string(),
95///     payload: json!({}),
96///     action: Some(TriggerAction::Enqueue { queue: "payments".to_string() }),
97///     timeout_ms: None,
98/// };
99/// ```
100#[derive(Debug, Clone)]
101pub struct TriggerRequest {
102    pub function_id: String,
103    pub payload: Value,
104    pub action: Option<TriggerAction>,
105    pub timeout_ms: Option<u64>,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
109#[serde(tag = "type", rename_all = "lowercase")]
110pub enum Message {
111    RegisterTriggerType {
112        id: String,
113        description: String,
114    },
115    RegisterTrigger {
116        id: String,
117        trigger_type: String,
118        function_id: String,
119        config: Value,
120    },
121    TriggerRegistrationResult {
122        id: String,
123        trigger_type: String,
124        function_id: String,
125        #[serde(skip_serializing_if = "Option::is_none")]
126        error: Option<ErrorBody>,
127    },
128    UnregisterTrigger {
129        id: String,
130        trigger_type: String,
131    },
132    UnregisterTriggerType {
133        id: String,
134    },
135    RegisterFunction {
136        id: String,
137        #[serde(skip_serializing_if = "Option::is_none")]
138        description: Option<String>,
139        #[serde(skip_serializing_if = "Option::is_none")]
140        request_format: Option<Value>,
141        #[serde(skip_serializing_if = "Option::is_none")]
142        response_format: Option<Value>,
143        #[serde(skip_serializing_if = "Option::is_none")]
144        metadata: Option<Value>,
145        #[serde(skip_serializing_if = "Option::is_none")]
146        invocation: Option<HttpInvocationConfig>,
147    },
148    UnregisterFunction {
149        id: String,
150    },
151    InvokeFunction {
152        invocation_id: Option<Uuid>,
153        function_id: String,
154        data: Value,
155        #[serde(skip_serializing_if = "Option::is_none")]
156        traceparent: Option<String>,
157        #[serde(skip_serializing_if = "Option::is_none")]
158        baggage: Option<String>,
159        #[serde(skip_serializing_if = "Option::is_none")]
160        action: Option<TriggerAction>,
161    },
162    InvocationResult {
163        invocation_id: Uuid,
164        function_id: String,
165        #[serde(skip_serializing_if = "Option::is_none")]
166        result: Option<Value>,
167        #[serde(skip_serializing_if = "Option::is_none")]
168        error: Option<ErrorBody>,
169        #[serde(skip_serializing_if = "Option::is_none")]
170        traceparent: Option<String>,
171        #[serde(skip_serializing_if = "Option::is_none")]
172        baggage: Option<String>,
173    },
174    RegisterService {
175        id: String,
176        #[serde(default)]
177        name: String,
178        #[serde(skip_serializing_if = "Option::is_none")]
179        description: Option<String>,
180        #[serde(default, skip_serializing_if = "Option::is_none")]
181        parent_service_id: Option<String>,
182    },
183    Ping,
184    Pong,
185    WorkerRegistered {
186        worker_id: String,
187    },
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct RegisterTriggerTypeMessage {
192    pub id: String,
193    pub description: String,
194}
195
196impl RegisterTriggerTypeMessage {
197    pub fn to_message(&self) -> Message {
198        Message::RegisterTriggerType {
199            id: self.id.clone(),
200            description: self.description.clone(),
201        }
202    }
203}
204
205/// Input for [`III::register_trigger`](crate::III::register_trigger).
206/// The `id` is auto-generated internally.
207#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct RegisterTriggerInput {
209    pub trigger_type: String,
210    pub function_id: String,
211    pub config: Value,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct RegisterTriggerMessage {
216    pub id: String,
217    pub trigger_type: String,
218    pub function_id: String,
219    pub config: Value,
220}
221
222impl RegisterTriggerMessage {
223    pub fn to_message(&self) -> Message {
224        Message::RegisterTrigger {
225            id: self.id.clone(),
226            trigger_type: self.trigger_type.clone(),
227            function_id: self.function_id.clone(),
228            config: self.config.clone(),
229        }
230    }
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct UnregisterTriggerMessage {
235    pub id: String,
236    pub trigger_type: String,
237}
238
239impl UnregisterTriggerMessage {
240    pub fn to_message(&self) -> Message {
241        Message::UnregisterTrigger {
242            id: self.id.clone(),
243            trigger_type: self.trigger_type.clone(),
244        }
245    }
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct UnregisterTriggerTypeMessage {
250    pub id: String,
251}
252
253impl UnregisterTriggerTypeMessage {
254    pub fn to_message(&self) -> Message {
255        Message::UnregisterTriggerType {
256            id: self.id.clone(),
257        }
258    }
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct RegisterFunctionMessage {
263    pub id: String,
264    #[serde(skip_serializing_if = "Option::is_none")]
265    pub description: Option<String>,
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub request_format: Option<Value>,
268    #[serde(skip_serializing_if = "Option::is_none")]
269    pub response_format: Option<Value>,
270    #[serde(skip_serializing_if = "Option::is_none")]
271    pub metadata: Option<Value>,
272    #[serde(skip_serializing_if = "Option::is_none")]
273    pub invocation: Option<HttpInvocationConfig>,
274}
275
276impl RegisterFunctionMessage {
277    pub fn with_id(name: String) -> Self {
278        RegisterFunctionMessage {
279            id: name,
280            description: None,
281            request_format: None,
282            response_format: None,
283            metadata: None,
284            invocation: None,
285        }
286    }
287    pub fn with_description(mut self, description: String) -> Self {
288        self.description = Some(description);
289        self
290    }
291    pub fn to_message(&self) -> Message {
292        Message::RegisterFunction {
293            id: self.id.clone(),
294            description: self.description.clone(),
295            request_format: self.request_format.clone(),
296            response_format: self.response_format.clone(),
297            metadata: self.metadata.clone(),
298            invocation: self.invocation.clone(),
299        }
300    }
301}
302
303#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct RegisterServiceMessage {
305    pub id: String,
306    pub name: String,
307    #[serde(skip_serializing_if = "Option::is_none")]
308    pub description: Option<String>,
309    #[serde(default, skip_serializing_if = "Option::is_none")]
310    pub parent_service_id: Option<String>,
311}
312
313impl RegisterServiceMessage {
314    pub fn to_message(&self) -> Message {
315        Message::RegisterService {
316            id: self.id.clone(),
317            name: self.name.clone(),
318            description: self.description.clone(),
319            parent_service_id: self.parent_service_id.clone(),
320        }
321    }
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct FunctionMessage {
326    pub function_id: String,
327    #[serde(skip_serializing_if = "Option::is_none")]
328    pub description: Option<String>,
329    #[serde(skip_serializing_if = "Option::is_none")]
330    pub request_format: Option<Value>,
331    #[serde(skip_serializing_if = "Option::is_none")]
332    pub response_format: Option<Value>,
333    #[serde(skip_serializing_if = "Option::is_none")]
334    pub metadata: Option<Value>,
335}
336
337#[derive(Debug, Clone, Serialize, Deserialize)]
338#[non_exhaustive]
339pub struct ErrorBody {
340    pub code: String,
341    pub message: String,
342    #[serde(skip_serializing_if = "Option::is_none")]
343    pub stacktrace: Option<String>,
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349
350    #[test]
351    fn register_function_to_message_and_serializes_type() {
352        let msg = RegisterFunctionMessage {
353            id: "functions.echo".to_string(),
354            description: Some("Echo function".to_string()),
355            request_format: None,
356            response_format: None,
357            metadata: None,
358            invocation: None,
359        };
360
361        let message = msg.to_message();
362        match &message {
363            Message::RegisterFunction {
364                id, description, ..
365            } => {
366                assert_eq!(id, "functions.echo");
367                assert_eq!(description.as_deref(), Some("Echo function"));
368            }
369            _ => panic!("unexpected message variant"),
370        }
371
372        let serialized = serde_json::to_value(&message).unwrap();
373        assert_eq!(serialized["type"], "registerfunction");
374        assert_eq!(serialized["id"], "functions.echo");
375        assert_eq!(serialized["description"], "Echo function");
376    }
377
378    #[test]
379    fn register_http_function_serializes_invocation() {
380        use super::{HttpInvocationConfig, HttpMethod};
381
382        let msg = RegisterFunctionMessage {
383            id: "external::my_lambda".to_string(),
384            description: None,
385            request_format: None,
386            response_format: None,
387            metadata: None,
388            invocation: Some(HttpInvocationConfig {
389                url: "https://example.com/invoke".to_string(),
390                method: HttpMethod::Post,
391                timeout_ms: Some(30000),
392                headers: HashMap::new(),
393                auth: None,
394            }),
395        };
396
397        let serialized = serde_json::to_value(msg.to_message()).unwrap();
398        assert_eq!(serialized["type"], "registerfunction");
399        assert_eq!(serialized["id"], "external::my_lambda");
400        assert!(serialized["invocation"].is_object());
401        assert_eq!(
402            serialized["invocation"]["url"],
403            "https://example.com/invoke"
404        );
405        assert_eq!(serialized["invocation"]["method"], "POST");
406    }
407}