use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::io::decision_translation;
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Suspension {
#[serde(default)]
pub id: String,
#[serde(default)]
pub action: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub message: String,
#[serde(default, skip_serializing_if = "Value::is_null")]
pub parameters: Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub response_schema: Option<Value>,
}
impl Suspension {
pub fn new(id: impl Into<String>, action: impl Into<String>) -> Self {
Self {
id: id.into(),
action: action.into(),
message: String::new(),
parameters: Value::Null,
response_schema: None,
}
}
pub fn with_message(mut self, message: impl Into<String>) -> Self {
self.message = message.into();
self
}
pub fn with_parameters(mut self, parameters: Value) -> Self {
self.parameters = parameters;
self
}
pub fn with_response_schema(mut self, schema: Value) -> Self {
self.response_schema = Some(schema);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SuspensionResponse {
pub target_id: String,
pub result: Value,
}
impl SuspensionResponse {
fn deny_string_token(value: &str) -> bool {
decision_translation::is_denied_token(value)
}
fn object_deny_flag(obj: &serde_json::Map<String, Value>) -> bool {
[
"denied",
"reject",
"rejected",
"cancel",
"canceled",
"cancelled",
"abort",
"aborted",
]
.iter()
.any(|key| obj.get(*key).and_then(Value::as_bool).unwrap_or(false))
|| ["status", "decision", "action"].iter().any(|key| {
obj.get(*key)
.and_then(Value::as_str)
.map(decision_translation::is_denied_token)
.unwrap_or(false)
})
}
pub fn new(target_id: impl Into<String>, result: Value) -> Self {
Self {
target_id: target_id.into(),
result,
}
}
pub fn is_approved(result: &Value) -> bool {
match result {
Value::Bool(b) => *b,
Value::String(s) => {
let lower = s.to_lowercase();
matches!(
lower.as_str(),
"true" | "yes" | "approved" | "allow" | "confirm" | "ok" | "accept"
)
}
Value::Object(obj) => {
obj.get("approved")
.and_then(|v| v.as_bool())
.unwrap_or(false)
|| obj
.get("allowed")
.and_then(|v| v.as_bool())
.unwrap_or(false)
}
_ => false,
}
}
pub fn is_denied(result: &Value) -> bool {
match result {
Value::Bool(b) => !*b,
Value::String(s) => {
let lower = s.trim().to_lowercase();
Self::deny_string_token(&lower)
}
Value::Object(obj) => {
obj.get("approved")
.and_then(|v| v.as_bool())
.map(|v| !v)
.unwrap_or(false)
|| Self::object_deny_flag(obj)
}
_ => false,
}
}
pub fn approved(&self) -> bool {
Self::is_approved(&self.result)
}
pub fn denied(&self) -> bool {
Self::is_denied(&self.result)
}
}
#[cfg(test)]
mod tests {
use super::SuspensionResponse;
use serde_json::json;
#[test]
fn suspension_response_treats_cancel_variants_as_denied() {
let denied_cases = [
json!("cancelled"),
json!("canceled"),
json!({"status":"cancelled"}),
json!({"decision":"abort"}),
json!({"canceled": true}),
json!({"cancelled": true}),
];
for case in denied_cases {
assert!(
SuspensionResponse::is_denied(&case),
"expected denied for case: {case}"
);
}
}
}