Skip to main content

lenso_contracts/
operation.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use utoipa::ToSchema;
4
5#[derive(
6    Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, ToSchema, schemars::JsonSchema,
7)]
8#[serde(rename_all = "camelCase")]
9pub struct ServiceOperationMetadata {
10    #[serde(default, skip_serializing_if = "Option::is_none")]
11    pub operation_id: Option<String>,
12    #[serde(default, skip_serializing_if = "Option::is_none")]
13    pub summary: Option<String>,
14    #[serde(default, skip_serializing_if = "Option::is_none")]
15    pub input_schema: Option<Value>,
16    #[serde(default, skip_serializing_if = "Option::is_none")]
17    pub output_schema: Option<Value>,
18    #[serde(default, skip_serializing_if = "Option::is_none")]
19    pub safe_probe: Option<ServiceOperationSafeProbe>,
20    #[serde(default, skip_serializing_if = "Option::is_none")]
21    pub timeout_ms: Option<u64>,
22    #[serde(default, skip_serializing_if = "Option::is_none")]
23    pub idempotency: Option<ServiceOperationIdempotency>,
24}
25
26#[derive(
27    Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, ToSchema, schemars::JsonSchema,
28)]
29#[serde(rename_all = "camelCase")]
30pub struct ServiceOperationSafeProbe {
31    #[serde(default, skip_serializing_if = "Option::is_none")]
32    pub method: Option<String>,
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    pub path: Option<String>,
35    #[serde(default, skip_serializing_if = "Option::is_none")]
36    pub input: Option<Value>,
37    #[serde(default, skip_serializing_if = "Option::is_none")]
38    pub expect_status: Option<u16>,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema, schemars::JsonSchema)]
42#[serde(rename_all = "snake_case")]
43pub enum ServiceOperationIdempotency {
44    None,
45    Idempotent,
46    RequiresKey,
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52    use crate::{
53        AdminAction, AdminActionDangerLevel, ModuleHttpMethod, ModuleHttpRoute,
54        RuntimeFunctionDeclaration,
55    };
56    use serde_json::json;
57
58    #[test]
59    fn operation_metadata_serializes_on_route_runtime_and_action() {
60        let route = ModuleHttpRoute {
61            method: ModuleHttpMethod::Get,
62            path: "/tickets".to_owned(),
63            capability: Some("support_ticket.tickets.read".to_owned()),
64            display_name: Some("List tickets".to_owned()),
65            story_title: Some("Tickets listed".to_owned()),
66            operation: Some(ServiceOperationMetadata {
67                operation_id: Some("support-ticket/http/GET:/tickets".to_owned()),
68                summary: Some("List tickets".to_owned()),
69                safe_probe: Some(ServiceOperationSafeProbe {
70                    method: Some("GET".to_owned()),
71                    path: Some("/tickets".to_owned()),
72                    input: None,
73                    expect_status: Some(200),
74                }),
75                ..ServiceOperationMetadata::default()
76            }),
77        };
78        let route_json = serde_json::to_value(route).unwrap();
79        assert_eq!(
80            route_json["operation"]["operationId"],
81            "support-ticket/http/GET:/tickets"
82        );
83        assert_eq!(route_json["operation"]["safeProbe"]["expectStatus"], 200);
84
85        let function = RuntimeFunctionDeclaration {
86            name: "support-ticket.escalate-ticket.v1".to_owned(),
87            version: 1,
88            queue: "support-ticket".to_owned(),
89            input_schema: None,
90            retry_policy: None,
91            operation: Some(ServiceOperationMetadata {
92                idempotency: Some(ServiceOperationIdempotency::RequiresKey),
93                timeout_ms: Some(2_000),
94                ..ServiceOperationMetadata::default()
95            }),
96        };
97        let function_json = serde_json::to_value(function).unwrap();
98        assert_eq!(function_json["operation"]["idempotency"], "requires_key");
99        assert_eq!(function_json["operation"]["timeoutMs"], 2000);
100
101        let action = AdminAction {
102            name: "assign_ticket".to_owned(),
103            label: "Assign ticket".to_owned(),
104            capability: "support_ticket.tickets.write".to_owned(),
105            input_schema: None,
106            confirmation: None,
107            danger_level: AdminActionDangerLevel::Low,
108            operation: Some(ServiceOperationMetadata {
109                output_schema: Some(json!({ "type": "object" })),
110                ..ServiceOperationMetadata::default()
111            }),
112        };
113        let action_json = serde_json::to_value(action).unwrap();
114        assert_eq!(action_json["operation"]["outputSchema"]["type"], "object");
115    }
116}