Skip to main content

khive_gate/
actor.rs

1use serde::{Deserialize, Serialize};
2
3use crate::GateValidationError;
4
5/// Kind prefixes reserved by runtime event attribution and its identity fixtures.
6///
7/// Add newly stamped kinds here and extend the per-kind event scope regressions.
8/// This is not a closed taxonomy: [`ActorRef::try_new`] accepts any non-empty kind.
9pub const RUNTIME_STAMPED_ACTOR_KINDS: &[&str] = &["actor", "anonymous", "agent"];
10
11/// Split a `kind:id` label when `kind` is one of [`RUNTIME_STAMPED_ACTOR_KINDS`].
12///
13/// Returns `None` when the label has no `:` or its prefix is not a stamped kind, so an id that
14/// merely contains a colon (such as `svc:build`) is left whole.
15pub fn split_stamped_label(label: &str) -> Option<(&str, &str)> {
16    label
17        .split_once(':')
18        .filter(|(kind, _)| RUNTIME_STAMPED_ACTOR_KINDS.contains(kind))
19}
20
21/// Caller identity with non-empty `kind` and `id`, validated on construction and deserialization.
22///
23/// See `crates/khive-gate/docs/api/policy-types.md`.
24#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
25pub struct ActorRef {
26    pub kind: String,
27    pub id: String,
28}
29
30/// Raw deserialization target for [`ActorRef`] — validated via `TryFrom`.
31#[derive(Deserialize)]
32struct RawActorRef {
33    kind: String,
34    id: String,
35}
36
37impl TryFrom<RawActorRef> for ActorRef {
38    type Error = GateValidationError;
39
40    fn try_from(raw: RawActorRef) -> Result<Self, Self::Error> {
41        Self::try_new(raw.kind, raw.id)
42    }
43}
44
45impl<'de> Deserialize<'de> for ActorRef {
46    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
47    where
48        D: serde::Deserializer<'de>,
49    {
50        let raw = RawActorRef::deserialize(deserializer)?;
51        ActorRef::try_from(raw).map_err(serde::de::Error::custom)
52    }
53}
54
55impl ActorRef {
56    /// Create a validated `ActorRef`. Returns `Err` if `kind` or `id` is empty.
57    pub fn try_new(
58        kind: impl Into<String>,
59        id: impl Into<String>,
60    ) -> Result<Self, GateValidationError> {
61        let kind = kind.into();
62        let id = id.into();
63        if kind.is_empty() {
64            return Err(GateValidationError::EmptyActorKind);
65        }
66        if id.is_empty() {
67            return Err(GateValidationError::EmptyActorId);
68        }
69        Ok(Self { kind, id })
70    }
71
72    /// Create a validated `ActorRef`. Panics if `kind` or `id` is empty.
73    pub fn new(kind: impl Into<String>, id: impl Into<String>) -> Self {
74        Self::try_new(kind, id).expect("ActorRef::new: kind and id must not be empty")
75    }
76
77    /// The implicit caller for unauthenticated local usage.
78    pub fn anonymous() -> Self {
79        Self {
80            kind: "anonymous".into(),
81            id: "local".into(),
82        }
83    }
84
85    /// Whether this actor is the implicit anonymous caller.
86    pub fn is_anonymous(&self) -> bool {
87        self.kind == "anonymous"
88    }
89
90    /// Return the explicit binding ID, or `None` for the anonymous caller.
91    ///
92    /// Anonymous identity must never participate in binding resolution. See
93    /// `crates/khive-gate/docs/api/policy-types.md`.
94    pub fn binding_id(&self) -> Option<&str> {
95        if self.is_anonymous() {
96            None
97        } else {
98            Some(self.id.as_str())
99        }
100    }
101
102    /// The actor as one label, `kind:id`, except that the plain `actor` kind collapses to its id
103    /// so a configured `lambda:khive` reads back as itself.
104    pub fn label(&self) -> String {
105        if self.kind == "actor" {
106            self.id.clone()
107        } else {
108            format!("{}:{}", self.kind, self.id)
109        }
110    }
111}