Skip to main content

mnesis_store/
stream_id.rs

1//! `StreamKey` — the store's stream-key type: validated raw bytes with a
2//! human-readable label.
3
4use core::fmt;
5
6use bytes::Bytes;
7
8/// The identifier of an event stream, as the store keys it: raw bytes.
9///
10/// The store is byte-level — at append/read it needs only a stream's **key
11/// bytes** and a **label** for diagnostics, never a domain aggregate identity.
12/// So the byte-level operations ([`RawEventStore::read_stream`](crate::RawEventStore::read_stream)
13/// / [`append`](crate::RawEventStore::append),
14/// [`export_stream`](crate::export::EventExporter::export_stream),
15/// [`list_streams`](crate::export::StreamLister::list_streams), and the
16/// [`import`](crate::import::EventImporter::import) target) speak `StreamKey`,
17/// not the kernel's [`Id`](mnesis::Id) — honestly separating "a stream key" from
18/// "a domain identity" (issue #245). A consumer's typed domain id lives at the
19/// repository layer; the repository translates it to a `StreamKey` (its job).
20///
21/// Like the wire-field newtypes in [`value`](crate::value) (`EventType`,
22/// `Payload`, `Metadata`), `StreamKey` is a [`Bytes`]-backed newtype — so a
23/// stream key can never be confused with a payload or metadata buffer, and it
24/// carries a [`Display`](fmt::Display) the raw bytes cannot. A **non-UTF-8** id
25/// round-trips losslessly (unlike the former `from_utf8_lossy` reconstruction).
26#[derive(Clone, Debug, Hash, PartialEq, Eq)]
27pub struct StreamKey(Bytes);
28
29impl StreamKey {
30    /// Build a `StreamKey` from any owned byte source (zero-copy for `Bytes` /
31    /// `Vec<u8>` / `String` / `&'static` slices and strs).
32    #[must_use]
33    pub fn from_bytes(bytes: impl Into<Bytes>) -> Self {
34        Self(bytes.into())
35    }
36
37    /// Build a `StreamKey` by copying a borrowed byte slice — the convenient
38    /// shape for an `import` route (`Fn(&[u8]) -> StreamKey`), for translating a
39    /// domain `id.as_ref()` at the repository boundary, or for any `&[u8]` key.
40    #[must_use]
41    pub fn from_slice(bytes: &[u8]) -> Self {
42        Self(Bytes::copy_from_slice(bytes))
43    }
44
45    /// The raw id bytes.
46    #[must_use]
47    pub fn as_bytes(&self) -> &[u8] {
48        &self.0
49    }
50
51    /// Consume into the backing `Bytes` (zero-copy).
52    #[must_use]
53    pub fn into_bytes(self) -> Bytes {
54        self.0
55    }
56}
57
58impl From<Bytes> for StreamKey {
59    fn from(bytes: Bytes) -> Self {
60        Self(bytes)
61    }
62}
63
64impl AsRef<[u8]> for StreamKey {
65    fn as_ref(&self) -> &[u8] {
66        &self.0
67    }
68}
69
70// A `StreamKey` is itself a valid `mnesis::Id` for free via the blanket impl: it
71// already carries every supertrait the trait requires (clone, send/sync, debug,
72// hash, eq, display below, `AsRef<[u8]>` above, `'static`). This is purely
73// additive — the byte-level store API stays concrete (`&StreamKey`), but a raw
74// key can flow through the typed convenience layers (`Subscription::subscribe`,
75// a repository whose `Aggregate::Id` is `StreamKey`) without a domain-id newtype.
76
77/// The string when the bytes are valid UTF-8, else `0x…` lowercase hex —
78/// faithful for binary ids, readable for the common string id, always
79/// unambiguous.
80impl fmt::Display for StreamKey {
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        if let Ok(s) = core::str::from_utf8(&self.0) {
83            return f.write_str(s);
84        }
85        f.write_str("0x")?;
86        for byte in &self.0 {
87            write!(f, "{byte:02x}")?;
88        }
89        Ok(())
90    }
91}
92
93#[cfg(test)]
94#[allow(clippy::unwrap_used, reason = "test code")]
95mod tests {
96    use super::StreamKey;
97    use bytes::Bytes;
98
99    #[test]
100    fn round_trips_bytes_including_non_utf8() {
101        let raw = Bytes::from_static(&[0x00, 0xff, 0x42]);
102        let key = StreamKey::from_bytes(raw.clone());
103        assert_eq!(key.as_bytes(), &[0x00, 0xff, 0x42]);
104        assert_eq!(key.clone().into_bytes(), raw);
105        assert_eq!(StreamKey::from_slice(&[0x00, 0xff, 0x42]), key);
106    }
107
108    #[test]
109    fn display_is_the_string_for_utf8_ids() {
110        assert_eq!(
111            StreamKey::from_slice(b"account-123").to_string(),
112            "account-123"
113        );
114        assert_eq!(StreamKey::from_slice(b"").to_string(), "");
115    }
116
117    #[test]
118    fn display_is_hex_for_non_utf8_ids() {
119        assert_eq!(
120            StreamKey::from_slice(&[0x00, 0xff, 0x42]).to_string(),
121            "0x00ff42"
122        );
123    }
124}