Skip to main content

khive_gate/
decision.rs

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