Skip to main content

khive_types/
refusal.rs

1//! Stable refusal classifications emitted by operator-facing command surfaces.
2//!
3//! The token spellings are a machine contract. This vocabulary is closed (only
4//! the variants below are accepted) and append-only: variants may be added, but
5//! an existing token must never be renamed or reused for a different meaning.
6
7use core::fmt;
8
9use crate::UnknownVariant;
10
11/// Stable reason attached to a refused `kkernel exec` invocation or operation.
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
15#[non_exhaustive]
16pub enum RefusalReason {
17    /// The resolved actor was anonymous where attribution was required.
18    AnonymousActor,
19    /// The resolved actor did not match `--expect-actor`.
20    ExpectActorMismatch,
21    /// A write was refused by the content secret gate.
22    GateRefusal,
23    /// `--strict` observed at least one failed or aborted operation.
24    StrictOpFailure,
25    /// The supplied operation expression could not be parsed.
26    ParseError,
27    /// The requested verb was unknown or was not loaded.
28    VerbRefused,
29}
30
31impl RefusalReason {
32    /// Every currently defined reason, in documentation order.
33    pub const ALL: [Self; 6] = [
34        Self::AnonymousActor,
35        Self::ExpectActorMismatch,
36        Self::GateRefusal,
37        Self::StrictOpFailure,
38        Self::ParseError,
39        Self::VerbRefused,
40    ];
41
42    /// Exact machine token written to stderr and JSON envelopes.
43    pub const fn as_str(self) -> &'static str {
44        match self {
45            Self::AnonymousActor => "anonymous-actor",
46            Self::ExpectActorMismatch => "expect-actor-mismatch",
47            Self::GateRefusal => "gate-refusal",
48            Self::StrictOpFailure => "strict-op-failure",
49            Self::ParseError => "parse-error",
50            Self::VerbRefused => "verb-refused",
51        }
52    }
53
54    /// Parse one exact machine token without accepting aliases or case folding.
55    pub fn from_token(token: &str) -> Option<Self> {
56        match token {
57            "anonymous-actor" => Some(Self::AnonymousActor),
58            "expect-actor-mismatch" => Some(Self::ExpectActorMismatch),
59            "gate-refusal" => Some(Self::GateRefusal),
60            "strict-op-failure" => Some(Self::StrictOpFailure),
61            "parse-error" => Some(Self::ParseError),
62            "verb-refused" => Some(Self::VerbRefused),
63            _ => None,
64        }
65    }
66}
67
68impl fmt::Display for RefusalReason {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        f.write_str(self.as_str())
71    }
72}
73
74impl core::str::FromStr for RefusalReason {
75    type Err = UnknownVariant;
76
77    fn from_str(value: &str) -> Result<Self, Self::Err> {
78        Self::from_token(value).ok_or_else(|| {
79            UnknownVariant::new(
80                "refusal_reason",
81                value,
82                &[
83                    "anonymous-actor",
84                    "expect-actor-mismatch",
85                    "gate-refusal",
86                    "strict-op-failure",
87                    "parse-error",
88                    "verb-refused",
89                ],
90            )
91        })
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn token_vocabulary_is_exact_and_append_only() {
101        let tokens = RefusalReason::ALL.map(RefusalReason::as_str);
102        assert_eq!(
103            tokens,
104            [
105                "anonymous-actor",
106                "expect-actor-mismatch",
107                "gate-refusal",
108                "strict-op-failure",
109                "parse-error",
110                "verb-refused",
111            ]
112        );
113        for (reason, token) in RefusalReason::ALL.into_iter().zip(tokens) {
114            assert_eq!(RefusalReason::from_token(token), Some(reason));
115            assert_eq!(token.parse::<RefusalReason>().unwrap(), reason);
116        }
117        assert_eq!(RefusalReason::from_token("Gate-Refusal"), None);
118        assert_eq!(RefusalReason::from_token("gate_refusal"), None);
119    }
120
121    #[cfg(feature = "serde")]
122    #[test]
123    fn serde_uses_the_machine_token() {
124        let encoded = serde_json::to_string(&RefusalReason::GateRefusal).unwrap();
125        assert_eq!(encoded, "\"gate-refusal\"");
126        assert_eq!(
127            serde_json::from_str::<RefusalReason>(&encoded).unwrap(),
128            RefusalReason::GateRefusal
129        );
130    }
131}