Skip to main content

knowledge_runtime/
ids.rs

1// Re-export shared identity types from stack-ids (single source of truth).
2//
3// EntityId, Scope, and ScopeKey are defined in `stack-ids` and re-exported
4// here so that all knowledge-runtime internal code can continue to use
5// `crate::ids::EntityId` etc. without change.
6//
7// ProjectionId and ProjectionKind remain runtime-owned because they carry
8// runtime-specific structure (kind enum + key + scope) that goes beyond
9// the opaque string ID in stack-ids.
10
11pub use stack_ids::{EntityId, Scope, ScopeKey};
12
13use serde::{Deserialize, Serialize};
14
15// --- Projection Identity ---
16
17/// Identifies a specific projection instance.
18///
19/// A projection is identified by its kind, a key within the kind, and
20/// the scope it belongs to. Two projections with the same kind and key
21/// but different scopes are distinct.
22#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
23pub struct ProjectionId {
24    /// Projection kind.
25    pub kind: ProjectionKind,
26    /// Unique key within the kind (often an `EntityId` or scope key).
27    pub key: String,
28    /// Scope this projection belongs to.
29    pub scope: ScopeKey,
30}
31
32impl ProjectionId {
33    pub fn new(kind: ProjectionKind, key: impl Into<String>, scope: ScopeKey) -> Self {
34        Self {
35            kind,
36            key: key.into(),
37            scope,
38        }
39    }
40}
41
42impl std::fmt::Display for ProjectionId {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        write!(f, "{}:{}@{}", self.kind.as_str(), self.key, self.scope)
45    }
46}
47
48/// Discriminant for projection types.
49#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
50#[serde(rename_all = "snake_case")]
51pub enum ProjectionKind {
52    /// Entity registry projection.
53    Entity,
54    /// Temporal claim projection.
55    Temporal,
56    /// Route statistics projection.
57    RouteStats,
58    /// Caller-defined projection kind.
59    Custom(String),
60}
61
62impl ProjectionKind {
63    pub fn as_str(&self) -> &str {
64        match self {
65            Self::Entity => "entity",
66            Self::Temporal => "temporal",
67            Self::RouteStats => "route_stats",
68            Self::Custom(s) => s.as_str(),
69        }
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn scope_key_equality() {
79        let s1 = Scope::new("ns").with_repo("repo-a");
80        let s2 = Scope::new("ns").with_repo("repo-a");
81        assert_eq!(s1.key(), s2.key());
82    }
83
84    #[test]
85    fn scope_key_inequality_different_repo() {
86        let s1 = Scope::new("ns").with_repo("repo-a");
87        let s2 = Scope::new("ns").with_repo("repo-b");
88        assert_ne!(s1.key(), s2.key());
89    }
90
91    #[test]
92    fn scope_key_display() {
93        let s = Scope::new("prod")
94            .with_domain("code")
95            .with_workspace("ws1")
96            .with_repo("myrepo");
97        assert_eq!(s.key().to_string(), "prod/code@ws1#myrepo");
98    }
99
100    #[test]
101    fn projection_id_includes_scope() {
102        let sk = ScopeKey::namespace_only("ns");
103        let pid = ProjectionId::new(ProjectionKind::Entity, "test", sk);
104        let display = pid.to_string();
105        assert!(display.contains("entity"));
106        assert!(display.contains("test"));
107        assert!(display.contains("ns"));
108    }
109}