1use serde::{Deserialize, Serialize};
25
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
29#[serde(rename_all = "camelCase")]
30#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
31pub struct PolicyModuleView {
32 pub id: String,
33 pub name: String,
34 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub description: Option<String>,
36 pub module: String,
38 #[serde(default, skip_serializing_if = "Vec::is_empty")]
40 pub applies_to: Vec<String>,
41 pub priority: i32,
43 pub enabled: bool,
44 pub version: u64,
47 pub created_at: String,
48 pub updated_at: String,
49 #[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
53 #[cfg_attr(feature = "openapi", schema(value_type = Object))]
54 pub ext: serde_json::Value,
55}
56
57#[derive(Debug, Clone, Default, Serialize, Deserialize)]
59#[serde(rename_all = "camelCase", deny_unknown_fields)]
60#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
61pub struct ListPoliciesBody {
62 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub context_id: Option<String>,
66 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
67 pub enabled_only: bool,
68 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub cursor: Option<String>,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub page_size: Option<u64>,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76#[serde(rename_all = "camelCase")]
77#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
78pub struct ListPoliciesResultBody {
79 pub policies: Vec<PolicyModuleView>,
80 pub truncated: bool,
82 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub cursor: Option<String>,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88#[serde(rename_all = "camelCase", deny_unknown_fields)]
89#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
90pub struct GetPolicyBody {
91 pub id: String,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96#[serde(rename_all = "camelCase")]
97#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
98pub struct GetPolicyResultBody {
99 pub policy: PolicyModuleView,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
110#[serde(rename_all = "camelCase", deny_unknown_fields)]
111#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
112pub struct UpsertPolicyBody {
113 #[serde(default, skip_serializing_if = "Option::is_none")]
115 pub id: Option<String>,
116 pub name: String,
117 #[serde(default, skip_serializing_if = "Option::is_none")]
118 pub description: Option<String>,
119 pub module: String,
120 #[serde(default, skip_serializing_if = "Vec::is_empty")]
121 pub applies_to: Vec<String>,
122 #[serde(default, skip_serializing_if = "Option::is_none")]
123 pub priority: Option<i32>,
124 pub enabled: bool,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub expected_version: Option<u64>,
129 #[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
130 #[cfg_attr(feature = "openapi", schema(value_type = Object))]
131 pub ext: serde_json::Value,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
136#[serde(rename_all = "camelCase")]
137#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
138pub struct UpsertPolicyResultBody {
139 pub policy: PolicyModuleView,
140 pub created: bool,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146#[serde(rename_all = "camelCase", deny_unknown_fields)]
147#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
148pub struct DeletePolicyBody {
149 pub id: String,
150 #[serde(default, skip_serializing_if = "Option::is_none")]
151 pub expected_version: Option<u64>,
152 #[serde(default, skip_serializing_if = "Option::is_none")]
154 pub reason: Option<String>,
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
159#[serde(rename_all = "camelCase")]
160#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
161pub struct DeletePolicyResultBody {
162 pub id: String,
164 pub deleted_at: String,
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[test]
173 fn upsert_body_is_camel_case_and_strict() {
174 let body = UpsertPolicyBody {
175 id: Some("approvals".into()),
176 name: "n".into(),
177 description: None,
178 module: "package vta.policy".into(),
179 applies_to: vec![],
180 priority: Some(100),
181 enabled: true,
182 expected_version: Some(3),
183 ext: serde_json::json!({ "openvtc.approvals": [] }),
184 };
185 let v = serde_json::to_value(&body).unwrap();
186 assert_eq!(v["expectedVersion"], 3);
187 assert!(v.get("appliesTo").is_none(), "empty vec must be omitted");
188
189 let snake = serde_json::json!({
192 "name": "n", "module": "m", "enabled": true, "expected_version": 3
193 });
194 assert!(serde_json::from_value::<UpsertPolicyBody>(snake).is_err());
195 }
196
197 #[test]
201 fn delete_result_matches_the_canonical_member_names() {
202 let v = serde_json::to_value(DeletePolicyResultBody {
203 id: "approvals".into(),
204 deleted_at: "2026-08-09T00:00:00Z".into(),
205 })
206 .unwrap();
207 assert_eq!(v["id"], "approvals");
208 assert_eq!(v["deletedAt"], "2026-08-09T00:00:00Z");
209 }
210}