lenso_contracts/
operation.rs1use 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}