1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum ErrorPolicy {
15 #[default]
22 Block,
23
24 Allow,
31}
32
33impl ErrorPolicy {
34 pub fn blocks_on_error(&self) -> bool {
36 matches!(self, ErrorPolicy::Block)
37 }
38
39 pub fn allows_on_error(&self) -> bool {
41 matches!(self, ErrorPolicy::Allow)
42 }
43
44 pub fn apply_to_error(&self, error: &anyhow::Error) -> ErrorPolicyResult {
46 match self {
47 ErrorPolicy::Block => ErrorPolicyResult::Blocked {
48 reason: format!("Policy check error (fail-closed): {}", error),
49 },
50 ErrorPolicy::Allow => ErrorPolicyResult::Allowed {
51 warning: format!("Policy check error (fail-open): {}", error),
52 },
53 }
54 }
55}
56
57#[derive(Debug, Clone)]
59pub enum ErrorPolicyResult {
60 Blocked { reason: String },
62 Allowed { warning: String },
64}
65
66pub fn log_fail_safe(reason: &str, config_path: Option<&str>) {
71 tracing::warn!(
74 event = "assay.failsafe.triggered",
75 reason = %reason,
76 config_path = %config_path.unwrap_or("none"),
77 action = "allowed",
78 "Fail-safe triggered: {}", reason
79 );
80}
81
82impl ErrorPolicyResult {
83 pub fn is_blocked(&self) -> bool {
84 matches!(self, ErrorPolicyResult::Blocked { .. })
85 }
86
87 pub fn is_allowed(&self) -> bool {
88 matches!(self, ErrorPolicyResult::Allowed { .. })
89 }
90
91 pub fn message(&self) -> &str {
92 match self {
93 ErrorPolicyResult::Blocked { reason } => reason,
94 ErrorPolicyResult::Allowed { warning } => warning,
95 }
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn test_default_is_block() {
105 assert_eq!(ErrorPolicy::default(), ErrorPolicy::Block);
106 }
107
108 #[test]
109 fn test_block_policy() {
110 let policy = ErrorPolicy::Block;
111 assert!(policy.blocks_on_error());
112 assert!(!policy.allows_on_error());
113
114 let error = anyhow::anyhow!("Schema parse failed");
115 let result = policy.apply_to_error(&error);
116 assert!(result.is_blocked());
117 }
118
119 #[test]
120 fn test_allow_policy() {
121 let policy = ErrorPolicy::Allow;
122 assert!(!policy.blocks_on_error());
123 assert!(policy.allows_on_error());
124
125 let error = anyhow::anyhow!("Network timeout");
126 let result = policy.apply_to_error(&error);
127 assert!(result.is_allowed());
128 }
129
130 #[test]
131 fn test_serde_roundtrip() {
132 let block: ErrorPolicy = serde_yaml::from_str("block").unwrap();
133 assert_eq!(block, ErrorPolicy::Block);
134
135 let allow: ErrorPolicy = serde_yaml::from_str("allow").unwrap();
136 assert_eq!(allow, ErrorPolicy::Allow);
137
138 #[derive(Deserialize)]
140 struct Config {
141 on_error: ErrorPolicy,
142 }
143
144 let config: Config = serde_yaml::from_str("on_error: block").unwrap();
145 assert_eq!(config.on_error, ErrorPolicy::Block);
146
147 let config: Config = serde_yaml::from_str("on_error: allow").unwrap();
148 assert_eq!(config.on_error, ErrorPolicy::Allow);
149 }
150}