component_id_index/
instance_id.rs
1use hex::FromHex;
5use std::fmt::Display;
6use std::str::FromStr;
7use thiserror::Error;
8
9#[cfg(feature = "serde")]
10use serde::{Deserialize, Deserializer, Serialize, Serializer};
11
12pub const INSTANCE_ID_LEN: usize = 32;
15
16#[derive(Clone, Debug, Eq, Hash, PartialEq)]
17pub struct InstanceId([u8; INSTANCE_ID_LEN]);
18
19impl InstanceId {
20 pub fn new_random(rng: &mut impl rand::Rng) -> Self {
22 let mut bytes: [u8; INSTANCE_ID_LEN] = [0; INSTANCE_ID_LEN];
23 rng.fill_bytes(&mut bytes);
24 InstanceId(bytes)
25 }
26
27 pub fn as_bytes(&self) -> &[u8] {
28 &self.0
29 }
30}
31
32impl Display for InstanceId {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
34 for byte in self.0 {
35 write!(f, "{:02x}", byte)?;
36 }
37 Ok(())
38 }
39}
40
41#[cfg_attr(feature = "serde", derive(Deserialize, Serialize), serde(rename_all = "snake_case"))]
42#[derive(Error, Clone, Debug, PartialEq)]
43pub enum InstanceIdError {
44 #[error("invalid length; must be 64 characters")]
45 InvalidLength,
46 #[error("string contains invalid hex character; must be [0-9a-f]")]
47 InvalidHexCharacter,
48}
49
50impl FromStr for InstanceId {
51 type Err = InstanceIdError;
52
53 fn from_str(s: &str) -> Result<Self, Self::Err> {
54 if s.len() != 64 {
57 return Err(InstanceIdError::InvalidLength);
58 }
59 if !s.chars().all(|ch| (ch.is_numeric() || ch.is_lowercase()) && ch.is_digit(16)) {
61 return Err(InstanceIdError::InvalidHexCharacter);
62 }
63 let bytes = <[u8; INSTANCE_ID_LEN]>::from_hex(&s).unwrap();
65 Ok(InstanceId(bytes))
66 }
67}
68
69#[cfg(feature = "serde")]
70impl Serialize for InstanceId {
71 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
72 where
73 S: Serializer,
74 {
75 serializer.collect_str(self)
76 }
77}
78
79#[cfg(feature = "serde")]
80impl<'de> Deserialize<'de> for InstanceId {
81 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
82 where
83 D: Deserializer<'de>,
84 {
85 String::deserialize(deserializer)?.parse().map_err(|err| {
86 let instance_id = InstanceId::new_random(&mut rand::thread_rng());
87 serde::de::Error::custom(format!(
88 "Invalid instance ID: {}\n\nHere is a valid, randomly generated ID: {}\n",
89 err, instance_id
90 ))
91 })
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use proptest::prelude::*;
99 use rand::SeedableRng as _;
100 use test_case::test_case;
101
102 #[test]
103 fn to_string() {
104 let id_str = "8c90d44863ff67586cf6961081feba4f760decab8bbbee376a3bfbc77b351280";
105 let id = id_str.parse::<InstanceId>().unwrap();
106 assert_eq!(id.to_string(), id_str);
107 }
108
109 proptest! {
110 #[test]
111 fn parse(id in "[a-f0-9]{64}") {
112 prop_assert!(id.parse::<InstanceId>().is_ok());
113 }
114 }
115
116 #[test_case("8c90d44863ff67586cf6961081feba4f760decab8bbbee376a3bfbc77b351280b351280"; "too long")]
117 #[test_case("8c90d44863ff67586cf6961081feba4f760decab8bbbee376a"; "too short")]
118 #[test_case("8C90D44863FF67586CF6961081FEBA4F760DECAB8BBBEE376A3BFBC77B351280"; "upper case chars are invalid")]
119 #[test_case("8;90d44863ff67586cf6961081feba4f760decab8bbbee376a3bfbc77b351280"; "hex chars only")]
120 fn parse_invalid(id: &str) {
121 assert!(id.parse::<InstanceId>().is_err());
122 }
123
124 #[test]
125 fn new_random_is_unique() {
126 let seed = rand::thread_rng().next_u64();
127 println!("using seed {}", seed);
128 let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
129 let mut prev_id = InstanceId::new_random(&mut rng);
130 for _i in 0..40 {
131 let id = InstanceId::new_random(&mut rng);
132 assert!(prev_id != id);
133 prev_id = id;
134 }
135 }
136
137 #[test]
138 fn parse_new_random() {
139 let seed = rand::thread_rng().next_u64();
140 println!("using seed {}", seed);
141 let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
142 for _i in 0..40 {
143 let id_str = InstanceId::new_random(&mut rng).to_string();
144 assert!(id_str.parse::<InstanceId>().is_ok());
145 }
146 }
147}