1use std::fmt;
22use std::str::FromStr;
23
24use serde::{Deserialize, Deserializer, Serialize, Serializer};
25use sha2::{Digest, Sha256};
26use thiserror::Error;
27
28use crate::util::hex::{self, HexError};
29
30pub const EVENT_ID_SIZE: usize = 32;
32
33#[derive(Debug, Clone, Copy, Error)]
35#[non_exhaustive]
36pub enum EventIdError {
37 #[error("invalid hex encoding: {0}")]
39 Hex(#[from] HexError),
40 #[error("invalid length: expected {EVENT_ID_SIZE} bytes, got {0}")]
42 InvalidLength(usize),
43}
44
45#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
49pub struct EventId([u8; EVENT_ID_SIZE]);
50
51impl EventId {
52 #[must_use]
54 pub const fn from_byte_array(bytes: [u8; EVENT_ID_SIZE]) -> Self {
55 Self(bytes)
56 }
57
58 pub fn from_slice(bytes: &[u8]) -> Result<Self, EventIdError> {
64 let array: [u8; EVENT_ID_SIZE] = bytes
65 .try_into()
66 .map_err(|_| EventIdError::InvalidLength(bytes.len()))?;
67 Ok(Self(array))
68 }
69
70 pub fn parse<S>(input: S) -> Result<Self, EventIdError>
76 where
77 S: AsRef<str>,
78 {
79 let bytes = hex::decode(input.as_ref())?;
80 Self::from_slice(&bytes)
81 }
82
83 #[must_use]
88 pub fn compute_from_canonical(canonical: &[u8]) -> Self {
89 let mut hasher = Sha256::new();
90 hasher.update(canonical);
91 let digest = hasher.finalize();
92 let mut bytes = [0_u8; EVENT_ID_SIZE];
93 bytes.copy_from_slice(&digest);
94 Self(bytes)
95 }
96
97 #[must_use]
99 pub const fn to_byte_array(self) -> [u8; EVENT_ID_SIZE] {
100 self.0
101 }
102
103 #[must_use]
105 pub const fn as_bytes(&self) -> &[u8; EVENT_ID_SIZE] {
106 &self.0
107 }
108
109 #[must_use]
111 pub fn to_hex(self) -> String {
112 hex::encode(self.0)
113 }
114}
115
116impl fmt::Debug for EventId {
117 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118 f.debug_tuple("EventId").field(&self.to_hex()).finish()
119 }
120}
121
122impl fmt::Display for EventId {
123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124 hex::fmt_lower(self.0, f)
125 }
126}
127
128impl fmt::LowerHex for EventId {
129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130 hex::fmt_lower(self.0, f)
131 }
132}
133
134impl FromStr for EventId {
135 type Err = EventIdError;
136
137 fn from_str(s: &str) -> Result<Self, Self::Err> {
138 Self::parse(s)
139 }
140}
141
142impl AsRef<[u8]> for EventId {
143 fn as_ref(&self) -> &[u8] {
144 &self.0
145 }
146}
147
148impl Serialize for EventId {
149 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
150 where
151 S: Serializer,
152 {
153 serializer.collect_str(self)
154 }
155}
156
157impl<'de> Deserialize<'de> for EventId {
158 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
159 where
160 D: Deserializer<'de>,
161 {
162 let raw = <&str>::deserialize(deserializer)?;
163 Self::parse(raw).map_err(serde::de::Error::custom)
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use hex_literal::hex;
170
171 use super::*;
172
173 const SHA256_EMPTY: [u8; 32] =
175 hex!("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
176
177 #[test]
178 fn from_byte_array_round_trip() {
179 let id = EventId::from_byte_array(SHA256_EMPTY);
180 assert_eq!(id.to_byte_array(), SHA256_EMPTY);
181 }
182
183 #[test]
184 fn from_slice_wrong_length() {
185 let err = EventId::from_slice(&[0_u8; 16]).unwrap_err();
186 assert!(matches!(err, EventIdError::InvalidLength(16)));
187 }
188
189 #[test]
190 fn parse_round_trip() {
191 let lower = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
192 let id = EventId::parse(lower).unwrap();
193 assert_eq!(id.to_hex(), lower);
194 }
195
196 #[test]
197 fn parse_rejects_bad_hex() {
198 let err = EventId::parse("zzzz").unwrap_err();
199 assert!(matches!(err, EventIdError::Hex(_)));
200 }
201
202 #[test]
203 fn compute_matches_known_sha256() {
204 let id = EventId::compute_from_canonical(b"");
205 assert_eq!(id.to_byte_array(), SHA256_EMPTY);
206 }
207
208 #[test]
209 fn compute_distinct_for_distinct_inputs() {
210 let lhs = EventId::compute_from_canonical(b"alice");
211 let rhs = EventId::compute_from_canonical(b"bob");
212 assert_ne!(lhs, rhs);
213 }
214
215 #[test]
216 fn display_lowercase() {
217 let id = EventId::from_byte_array(SHA256_EMPTY);
218 let s = format!("{id}");
219 assert_eq!(s.len(), 64);
220 assert!(
221 s.chars()
222 .all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase())
223 );
224 }
225
226 #[test]
227 fn debug_includes_hex() {
228 let id = EventId::from_byte_array(SHA256_EMPTY);
229 let dbg = format!("{id:?}");
230 assert!(dbg.contains(&id.to_hex()));
231 }
232
233 #[test]
234 fn ordering_is_lexicographic() {
235 let lhs = EventId::from_byte_array([0_u8; 32]);
236 let rhs = EventId::from_byte_array([1_u8; 32]);
237 assert!(lhs < rhs);
238 }
239
240 #[test]
241 fn serde_round_trip() {
242 let id = EventId::from_byte_array(SHA256_EMPTY);
243 let json = serde_json::to_string(&id).unwrap();
244 assert_eq!(
245 json,
246 r#""e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855""#
247 );
248 let parsed: EventId = serde_json::from_str(&json).unwrap();
249 assert_eq!(parsed, id);
250 }
251}