Skip to main content

iii_sdk/
protocol.rs

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