use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct PolicyModuleView {
pub id: String,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub module: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub applies_to: Vec<String>,
pub priority: i32,
pub enabled: bool,
pub version: u64,
pub created_at: String,
pub updated_at: String,
#[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
#[cfg_attr(feature = "openapi", schema(value_type = Object))]
pub ext: serde_json::Value,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct ListPoliciesBody {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context_id: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub enabled_only: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub page_size: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct ListPoliciesResultBody {
pub policies: Vec<PolicyModuleView>,
pub truncated: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct GetPolicyBody {
pub id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct GetPolicyResultBody {
pub policy: PolicyModuleView,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct UpsertPolicyBody {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub module: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub applies_to: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub priority: Option<i32>,
pub enabled: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expected_version: Option<u64>,
#[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
#[cfg_attr(feature = "openapi", schema(value_type = Object))]
pub ext: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct UpsertPolicyResultBody {
pub policy: PolicyModuleView,
pub created: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct DeletePolicyBody {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expected_version: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct DeletePolicyResultBody {
pub id: String,
pub deleted_at: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn upsert_body_is_camel_case_and_strict() {
let body = UpsertPolicyBody {
id: Some("approvals".into()),
name: "n".into(),
description: None,
module: "package vta.policy".into(),
applies_to: vec![],
priority: Some(100),
enabled: true,
expected_version: Some(3),
ext: serde_json::json!({ "openvtc.approvals": [] }),
};
let v = serde_json::to_value(&body).unwrap();
assert_eq!(v["expectedVersion"], 3);
assert!(v.get("appliesTo").is_none(), "empty vec must be omitted");
let snake = serde_json::json!({
"name": "n", "module": "m", "enabled": true, "expected_version": 3
});
assert!(serde_json::from_value::<UpsertPolicyBody>(snake).is_err());
}
#[test]
fn delete_result_matches_the_canonical_member_names() {
let v = serde_json::to_value(DeletePolicyResultBody {
id: "approvals".into(),
deleted_at: "2026-08-09T00:00:00Z".into(),
})
.unwrap();
assert_eq!(v["id"], "approvals");
assert_eq!(v["deletedAt"], "2026-08-09T00:00:00Z");
}
}