Skip to main content

lenso_contracts/
operation.rs

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