Skip to main content

khive_gate/
decision.rs

1use serde::{Deserialize, Serialize};
2
3use crate::{GateValidationError, Obligation};
4
5// ---------- Decision ----------
6
7/// Gate decision: allow (with optional obligations) or deny (with reason).
8///
9/// `Deny` requires a non-empty `reason`. Enforced at construction and
10/// deserialization.
11#[derive(Clone, Debug, Serialize)]
12#[serde(tag = "decision", rename_all = "snake_case")]
13pub enum GateDecision {
14    Allow {
15        #[serde(default, skip_serializing_if = "Vec::is_empty")]
16        obligations: Vec<Obligation>,
17    },
18    Deny {
19        reason: String,
20    },
21}
22
23/// Raw deserialization target for [`GateDecision`] — validated via `TryFrom`.
24#[derive(Deserialize)]
25#[serde(tag = "decision", rename_all = "snake_case")]
26enum RawGateDecision {
27    Allow {
28        #[serde(default)]
29        obligations: Vec<Obligation>,
30    },
31    Deny {
32        reason: String,
33    },
34}
35
36impl TryFrom<RawGateDecision> for GateDecision {
37    type Error = GateValidationError;
38
39    fn try_from(raw: RawGateDecision) -> Result<Self, Self::Error> {
40        match raw {
41            RawGateDecision::Allow { obligations } => Ok(GateDecision::Allow { obligations }),
42            RawGateDecision::Deny { reason } => {
43                if reason.is_empty() {
44                    return Err(GateValidationError::EmptyDenyReason);
45                }
46                Ok(GateDecision::Deny { reason })
47            }
48        }
49    }
50}
51
52impl<'de> Deserialize<'de> for GateDecision {
53    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
54    where
55        D: serde::Deserializer<'de>,
56    {
57        let raw = RawGateDecision::deserialize(deserializer)?;
58        GateDecision::try_from(raw).map_err(serde::de::Error::custom)
59    }
60}
61
62impl GateDecision {
63    /// Returns an unconditional `Allow` with no obligations.
64    pub fn allow() -> Self {
65        Self::Allow {
66            obligations: Vec::new(),
67        }
68    }
69
70    /// Returns an `Allow` with the given policy obligations attached.
71    pub fn allow_with(obligations: Vec<Obligation>) -> Self {
72        Self::Allow { obligations }
73    }
74
75    /// Create a `Deny` decision. Returns `Err` if `reason` is empty.
76    pub fn try_deny(reason: impl Into<String>) -> Result<Self, GateValidationError> {
77        let reason = reason.into();
78        if reason.is_empty() {
79            return Err(GateValidationError::EmptyDenyReason);
80        }
81        Ok(Self::Deny { reason })
82    }
83
84    /// Returns a `Deny` with the given reason. Panics if `reason` is empty.
85    pub fn deny(reason: impl Into<String>) -> Self {
86        Self::try_deny(reason).expect("GateDecision::deny: reason must not be empty")
87    }
88
89    /// Returns `true` when the decision is `Allow`.
90    pub fn is_allow(&self) -> bool {
91        matches!(self, Self::Allow { .. })
92    }
93}