use std::collections::HashMap;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct RawNetworkPolicy {
pub allowlist: Option<Vec<String>>,
#[serde(flatten)]
pub unknown: HashMap<String, serde_yaml::Value>,
}
#[derive(Debug, Deserialize)]
pub struct RawActiveHours {
pub start: Option<String>,
pub end: Option<String>,
pub timezone: Option<String>,
#[serde(flatten)]
pub unknown: HashMap<String, serde_yaml::Value>,
}
#[derive(Debug, Deserialize)]
pub struct RawSchedulePolicy {
pub active_hours: Option<RawActiveHours>,
#[serde(flatten)]
pub unknown: HashMap<String, serde_yaml::Value>,
}
#[derive(Debug, Deserialize)]
pub struct RawBudgetPolicy {
pub daily_limit_usd: Option<f64>,
pub monthly_limit_usd: Option<f64>,
pub org_daily_limit_usd: Option<f64>,
pub org_monthly_limit_usd: Option<f64>,
pub timezone: Option<String>,
pub action_on_exceed: Option<String>,
pub window: Option<String>,
#[serde(flatten)]
pub unknown: HashMap<String, serde_yaml::Value>,
}
#[derive(Debug, Deserialize)]
pub struct RawDataPolicy {
pub sensitive_patterns: Option<Vec<String>>,
pub credential_action: Option<String>,
#[serde(flatten)]
pub unknown: HashMap<String, serde_yaml::Value>,
}
#[derive(Debug, Deserialize)]
pub struct RawCapabilitySet {
pub allow: Option<Vec<String>>,
pub deny: Option<Vec<String>>,
#[serde(flatten)]
pub unknown: HashMap<String, serde_yaml::Value>,
}
#[derive(Debug, Deserialize)]
pub struct RawMetadata {
pub name: Option<String>,
pub version: Option<String>,
pub description: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct GovernancePolicyEnvelope {
#[serde(rename = "apiVersion")]
pub api_version: Option<String>,
pub kind: Option<String>,
pub metadata: Option<RawMetadata>,
pub spec: Option<serde_yaml::Value>,
}
#[derive(Debug, Deserialize)]
pub struct RawApprovalPolicy {
pub timeout_seconds: Option<u32>,
pub escalation_role: Option<String>,
#[serde(flatten)]
pub unknown: HashMap<String, serde_yaml::Value>,
}
#[derive(Debug, Deserialize)]
pub struct RawPolicyDocument {
pub version: Option<String>,
pub scope: Option<crate::policy::scope::PolicyScope>,
pub network: Option<RawNetworkPolicy>,
pub schedule: Option<RawSchedulePolicy>,
pub budget: Option<RawBudgetPolicy>,
pub data: Option<RawDataPolicy>,
pub tools: Option<HashMap<String, RawToolPolicy>>,
pub capabilities: Option<RawCapabilitySet>,
pub approval_timeout_secs: Option<u32>,
pub approval: Option<RawApprovalPolicy>,
#[serde(flatten)]
pub unknown: HashMap<String, serde_yaml::Value>,
}
#[derive(Debug, Deserialize)]
pub struct RawToolPolicy {
pub allow: Option<bool>,
pub limit_per_hour: Option<u32>,
pub requires_approval_if: Option<String>,
#[serde(flatten)]
pub unknown: HashMap<String, serde_yaml::Value>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn raw_network_deserializes_allowlist() {
let yaml = "allowlist:\n - api.openai.com\n - slack.com\n";
let raw: RawNetworkPolicy = serde_yaml::from_str(yaml).unwrap();
assert_eq!(
raw.allowlist,
Some(vec!["api.openai.com".to_string(), "slack.com".to_string()])
);
assert!(raw.unknown.is_empty());
}
#[test]
fn raw_network_captures_unknown_keys() {
let yaml = "allowlist:\n - api.openai.com\nblocklist:\n - \"*\"\n";
let raw: RawNetworkPolicy = serde_yaml::from_str(yaml).unwrap();
assert!(raw.unknown.contains_key("blocklist"));
}
#[test]
fn raw_network_absent_allowlist_is_none() {
let yaml = "{}\n";
let raw: RawNetworkPolicy = serde_yaml::from_str(yaml).unwrap();
assert_eq!(raw.allowlist, None);
}
#[test]
fn raw_policy_document_deserializes_version_and_sections() {
let yaml = "version: \"1.0\"\nnetwork:\n allowlist:\n - api.openai.com\nbudget:\n daily_limit_usd: 10.0\n";
let raw: RawPolicyDocument = serde_yaml::from_str(yaml).unwrap();
assert_eq!(raw.version, Some("1.0".to_string()));
assert!(raw.network.is_some());
assert!(raw.budget.is_some());
assert!(raw.unknown.is_empty());
}
#[test]
fn raw_policy_document_all_sections_absent_is_none() {
let yaml = "{}\n";
let raw: RawPolicyDocument = serde_yaml::from_str(yaml).unwrap();
assert!(raw.version.is_none());
assert!(raw.network.is_none());
assert!(raw.schedule.is_none());
assert!(raw.budget.is_none());
assert!(raw.data.is_none());
assert!(raw.tools.is_none());
}
#[test]
fn raw_policy_document_deserializes_approval_timeout() {
let yaml = "approval_timeout_secs: 600\n";
let raw: RawPolicyDocument = serde_yaml::from_str(yaml).unwrap();
assert_eq!(raw.approval_timeout_secs, Some(600));
}
#[test]
fn raw_policy_document_absent_approval_timeout_is_none() {
let yaml = "{}\n";
let raw: RawPolicyDocument = serde_yaml::from_str(yaml).unwrap();
assert_eq!(raw.approval_timeout_secs, None);
}
#[test]
fn raw_policy_document_captures_unknown_top_level_key() {
let yaml = "risk_tier: high\n";
let raw: RawPolicyDocument = serde_yaml::from_str(yaml).unwrap();
assert!(raw.unknown.contains_key("risk_tier"));
}
#[test]
fn raw_active_hours_deserializes_all_fields() {
let yaml = "start: \"09:00\"\nend: \"18:00\"\ntimezone: \"Asia/Taipei\"\n";
let raw: RawActiveHours = serde_yaml::from_str(yaml).unwrap();
assert_eq!(raw.start, Some("09:00".to_string()));
assert_eq!(raw.end, Some("18:00".to_string()));
assert_eq!(raw.timezone, Some("Asia/Taipei".to_string()));
assert!(raw.unknown.is_empty());
}
#[test]
fn raw_schedule_active_hours_absent_is_none() {
let yaml = "{}\n";
let raw: RawSchedulePolicy = serde_yaml::from_str(yaml).unwrap();
assert!(raw.active_hours.is_none());
}
#[test]
fn raw_budget_deserializes_daily_limit() {
let yaml = "daily_limit_usd: 50.0\n";
let raw: RawBudgetPolicy = serde_yaml::from_str(yaml).unwrap();
assert_eq!(raw.daily_limit_usd, Some(50.0));
}
#[test]
fn raw_budget_absent_limit_is_none() {
let yaml = "{}\n";
let raw: RawBudgetPolicy = serde_yaml::from_str(yaml).unwrap();
assert_eq!(raw.daily_limit_usd, None);
}
#[test]
fn raw_budget_deserializes_action_on_exceed() {
let yaml = "daily_limit_usd: 50.0\naction_on_exceed: suspend\n";
let raw: RawBudgetPolicy = serde_yaml::from_str(yaml).unwrap();
assert_eq!(raw.action_on_exceed, Some("suspend".to_string()));
}
#[test]
fn raw_budget_absent_action_on_exceed_is_none() {
let yaml = "daily_limit_usd: 50.0\n";
let raw: RawBudgetPolicy = serde_yaml::from_str(yaml).unwrap();
assert_eq!(raw.action_on_exceed, None);
}
#[test]
fn raw_data_deserializes_sensitive_patterns() {
let yaml = "sensitive_patterns:\n - \"sk-[a-zA-Z0-9]{48}\"\n - \"\\\\b\\\\d{4}\\\\b\"\n";
let raw: RawDataPolicy = serde_yaml::from_str(yaml).unwrap();
assert_eq!(raw.sensitive_patterns.as_ref().unwrap().len(), 2);
}
#[test]
fn raw_data_absent_patterns_is_none() {
let yaml = "{}\n";
let raw: RawDataPolicy = serde_yaml::from_str(yaml).unwrap();
assert_eq!(raw.sensitive_patterns, None);
}
#[test]
fn raw_data_deserializes_credential_action() {
let yaml = "credential_action: block\n";
let raw: RawDataPolicy = serde_yaml::from_str(yaml).unwrap();
assert_eq!(raw.credential_action.as_deref(), Some("block"));
}
#[test]
fn raw_tool_deserializes_all_fields() {
let yaml = "allow: true\nlimit_per_hour: 10\nrequires_approval_if: \"amount > 100\"\n";
let raw: RawToolPolicy = serde_yaml::from_str(yaml).unwrap();
assert_eq!(raw.allow, Some(true));
assert_eq!(raw.limit_per_hour, Some(10));
assert_eq!(raw.requires_approval_if, Some("amount > 100".to_string()));
assert!(raw.unknown.is_empty());
}
#[test]
fn raw_tool_allow_false_captured() {
let yaml = "allow: false\n";
let raw: RawToolPolicy = serde_yaml::from_str(yaml).unwrap();
assert_eq!(raw.allow, Some(false));
assert_eq!(raw.limit_per_hour, None);
}
#[test]
fn raw_tool_captures_unknown_key() {
let yaml = "allow: true\nconstraint: \"read-only\"\n";
let raw: RawToolPolicy = serde_yaml::from_str(yaml).unwrap();
assert!(raw.unknown.contains_key("constraint"));
}
#[test]
fn raw_capabilities_deserializes_allow_and_deny() {
let yaml = "capabilities:\n allow:\n - file_read\n deny:\n - terminal_exec\n";
let raw: RawPolicyDocument = serde_yaml::from_str(yaml).unwrap();
let caps = raw.capabilities.as_ref().unwrap();
assert_eq!(caps.allow, Some(vec!["file_read".to_string()]));
assert_eq!(caps.deny, Some(vec!["terminal_exec".to_string()]));
}
#[test]
fn raw_capabilities_absent_is_none() {
let yaml = "{}\n";
let raw: RawPolicyDocument = serde_yaml::from_str(yaml).unwrap();
assert!(raw.capabilities.is_none());
}
#[test]
fn raw_capabilities_captures_unknown_key() {
let yaml = "capabilities:\n allow: []\n extra_field: true\n";
let raw: RawPolicyDocument = serde_yaml::from_str(yaml).unwrap();
assert!(raw.capabilities.as_ref().unwrap().unknown.contains_key("extra_field"));
}
}