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}