use ferrin_core::generate_text::ApprovalStatus;
use ferrin_spec::JsonValue;
use serde::Deserialize;
use serde::Serialize;
pub const UNRECOGNIZED_DECISION: &str = "unrecognized policy decision";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "decision", rename_all = "kebab-case")]
#[non_exhaustive]
pub enum PolicyDecision {
Allow {
#[serde(default, skip_serializing_if = "Option::is_none")]
reason: Option<String>,
},
Deny {
#[serde(default, skip_serializing_if = "Option::is_none")]
reason: Option<String>,
},
RequiresApproval {
#[serde(default, skip_serializing_if = "Option::is_none")]
reason: Option<String>,
},
NotApplicable,
}
impl PolicyDecision {
#[must_use]
pub fn allow() -> Self {
Self::Allow { reason: None }
}
#[must_use]
pub fn deny() -> Self {
Self::Deny { reason: None }
}
#[must_use]
pub fn requires_approval() -> Self {
Self::RequiresApproval { reason: None }
}
#[must_use]
pub fn with_reason(self, reason: impl Into<String>) -> Self {
let reason = Some(reason.into());
match self {
Self::NotApplicable => Self::NotApplicable,
Self::Allow { .. } => Self::Allow { reason },
Self::Deny { .. } => Self::Deny { reason },
Self::RequiresApproval { .. } => Self::RequiresApproval { reason },
}
}
#[must_use]
pub fn reason(&self) -> Option<&str> {
match self {
Self::NotApplicable => None,
Self::Allow { reason } | Self::Deny { reason } | Self::RequiresApproval { reason } => {
reason.as_deref()
}
}
}
#[must_use]
pub fn normalize(raw: &JsonValue) -> Self {
let unrecognized = || Self::Deny {
reason: Some(UNRECOGNIZED_DECISION.to_owned()),
};
match raw {
JsonValue::Null => Self::NotApplicable,
JsonValue::Bool(true) => Self::allow(),
JsonValue::Bool(false) => Self::deny(),
JsonValue::Object(object) => {
let reason = object
.get("reason")
.and_then(JsonValue::as_str)
.map(str::to_owned);
if let Some(decision) = object.get("decision") {
return match decision.as_str() {
Some("allow") => Self::Allow { reason },
Some("deny") => Self::Deny { reason },
Some("requires-approval") => Self::RequiresApproval { reason },
Some("not-applicable") => Self::NotApplicable,
_ => unrecognized(),
};
}
match object.get("allow").and_then(JsonValue::as_bool) {
Some(true) => Self::Allow { reason },
Some(false) => Self::Deny { reason },
None => unrecognized(),
}
}
_ => unrecognized(),
}
}
#[must_use]
pub fn into_approval(self) -> Option<ApprovalStatus> {
match self {
Self::Allow { reason } => Some(ApprovalStatus::Approved { reason }),
Self::Deny { reason } => Some(ApprovalStatus::Denied { reason }),
Self::RequiresApproval { reason } => Some(ApprovalStatus::UserApproval { reason }),
Self::NotApplicable => None,
}
}
}