Skip to main content

khive_gate/
obligation.rs

1use serde::{Deserialize, Serialize};
2
3use crate::GateValidationError;
4
5/// Policy instructions attached to an allow; only `Audit` has v0 runtime handling.
6///
7/// `RateLimit` requires positive values but is not enforced in v0. See
8/// `crates/khive-gate/docs/api/policy-types.md`.
9#[derive(Clone, Debug, Serialize)]
10#[serde(tag = "kind", rename_all = "snake_case")]
11pub enum Obligation {
12    Audit {
13        tag: String,
14    },
15    RateLimit {
16        window_secs: u64,
17        max: u32,
18    },
19    /// Policy-specific arbitrary JSON; the struct form preserves internally tagged scalar values.
20    Custom {
21        value: serde_json::Value,
22    },
23}
24
25/// Raw deserialization target for [`Obligation`] — validated via `TryFrom`.
26#[derive(Deserialize)]
27#[serde(tag = "kind", rename_all = "snake_case")]
28enum RawObligation {
29    Audit { tag: String },
30    RateLimit { window_secs: u64, max: u32 },
31    Custom { value: serde_json::Value },
32}
33
34impl TryFrom<RawObligation> for Obligation {
35    type Error = GateValidationError;
36
37    fn try_from(raw: RawObligation) -> Result<Self, Self::Error> {
38        match raw {
39            RawObligation::Audit { tag } => {
40                if tag.is_empty() {
41                    return Err(GateValidationError::EmptyAuditTag);
42                }
43                Ok(Obligation::Audit { tag })
44            }
45            RawObligation::RateLimit { window_secs, max } => {
46                Obligation::try_rate_limit(window_secs, max)
47            }
48            RawObligation::Custom { value } => Ok(Obligation::Custom { value }),
49        }
50    }
51}
52
53impl<'de> Deserialize<'de> for Obligation {
54    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
55    where
56        D: serde::Deserializer<'de>,
57    {
58        let raw = RawObligation::deserialize(deserializer)?;
59        Obligation::try_from(raw).map_err(serde::de::Error::custom)
60    }
61}
62
63impl Obligation {
64    /// Create a validated `RateLimit` obligation.
65    /// Returns `Err` if `window_secs` or `max` is zero.
66    pub fn try_rate_limit(window_secs: u64, max: u32) -> Result<Self, GateValidationError> {
67        if window_secs == 0 {
68            return Err(GateValidationError::ZeroRateLimitWindow);
69        }
70        if max == 0 {
71            return Err(GateValidationError::ZeroRateLimitMax);
72        }
73        Ok(Self::RateLimit { window_secs, max })
74    }
75
76    /// Create a validated `RateLimit` obligation. Panics if `window_secs` or `max` is zero.
77    pub fn rate_limit(window_secs: u64, max: u32) -> Self {
78        Self::try_rate_limit(window_secs, max)
79            .expect("Obligation::rate_limit: window_secs and max must be > 0")
80    }
81}