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}