Skip to main content

ferro_projection/
key.rs

1//! `ProjectionKey` — opaque stringly-typed identifier for a projection
2//! row (D-11).
3//!
4//! Newtype around `String`. The stringly-typed convention matches
5//! `ferro_audit::AuditTarget::id: String` (Phase 153 D-07) and the
6//! broader project-agnostic crate principle (CLAUDE.md §Architecture
7//! Principles). A typed `Key` per projection would force the runtime
8//! to carry a generic key parameter through the broadcast channel
9//! name, the DB column, and the per-key Mutex map — adding noise for
10//! no benefit.
11//!
12//! Consumers construct via [`ProjectionKey::new`] (preferred) or via
13//! `From<String>` / `From<&str>`. Multi-tenancy lives inside the key
14//! string by convention: `"tenant-7:inventory.warehouse-a"`,
15//! `"checkout.user-42.cart"`. The runtime does NOT auto-scope by
16//! tenant.
17
18use serde::{Deserialize, Serialize};
19use std::fmt;
20
21/// Opaque stringly-typed identifier (D-11).
22#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
23pub struct ProjectionKey(pub(crate) String);
24
25impl ProjectionKey {
26    /// Construct from any string-like input.
27    ///
28    /// ```
29    /// use ferro_projection::ProjectionKey;
30    /// let k = ProjectionKey::new("warehouse-a");
31    /// assert_eq!(k.as_str(), "warehouse-a");
32    /// ```
33    pub fn new(s: impl Into<String>) -> Self {
34        Self(s.into())
35    }
36
37    /// Borrow the inner string slice.
38    pub fn as_str(&self) -> &str {
39        &self.0
40    }
41}
42
43impl fmt::Display for ProjectionKey {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        f.write_str(&self.0)
46    }
47}
48
49impl From<String> for ProjectionKey {
50    fn from(s: String) -> Self {
51        Self(s)
52    }
53}
54
55impl From<&str> for ProjectionKey {
56    fn from(s: &str) -> Self {
57        Self(s.to_string())
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[test]
66    fn new_and_as_str_round_trip() {
67        let k = ProjectionKey::new("warehouse-a");
68        assert_eq!(k.as_str(), "warehouse-a");
69
70        let k2: ProjectionKey = "warehouse-b".into();
71        assert_eq!(k2.as_str(), "warehouse-b");
72
73        let k3 = ProjectionKey::from(String::from("warehouse-c"));
74        assert_eq!(k3.as_str(), "warehouse-c");
75    }
76
77    #[test]
78    fn display_renders_inner_string() {
79        let k = ProjectionKey::new("warehouse-a");
80        assert_eq!(format!("{k}"), "warehouse-a");
81    }
82
83    #[test]
84    fn serde_round_trip_via_json() {
85        let k = ProjectionKey::new("warehouse-a");
86        let s = serde_json::to_string(&k).expect("serialize");
87        assert_eq!(s, "\"warehouse-a\"");
88        let parsed: ProjectionKey = serde_json::from_str(&s).expect("deserialize");
89        assert_eq!(parsed, k);
90    }
91}