thread_structured_id/
lib.rs1use serde::{Deserialize, Deserializer, Serialize, Serializer};
15use std::fmt;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub struct StructuredId(pub u32);
20
21impl StructuredId {
22 pub fn new(category: u8, subcategory: u8, number: u16) -> Self {
27 assert!((1..=99).contains(&category), "category must be 1-99");
28 assert!(subcategory <= 99, "subcategory must be 0-99");
29 assert!((1..=9999).contains(&number), "number must be 1-9999");
30 Self(category as u32 * 1_000_000 + subcategory as u32 * 10_000 + number as u32)
31 }
32
33 pub fn try_new(category: u8, subcategory: u8, number: u16) -> Option<Self> {
35 if !(1..=99).contains(&category) || subcategory > 99 || !(1..=9999).contains(&number) {
36 return None;
37 }
38 Some(Self(
39 category as u32 * 1_000_000 + subcategory as u32 * 10_000 + number as u32,
40 ))
41 }
42
43 pub fn category(&self) -> u8 {
45 (self.0 / 1_000_000) as u8
46 }
47
48 pub fn subcategory(&self) -> u8 {
50 ((self.0 / 10_000) % 100) as u8
51 }
52
53 pub fn number(&self) -> u16 {
55 (self.0 % 10_000) as u16
56 }
57
58 pub fn domain(&self) -> IdDomain {
60 match self.category() {
61 10..=19 => IdDomain::Actors,
62 20..=29 => IdDomain::Items,
63 30..=39 => IdDomain::Skills,
64 40..=49 => IdDomain::World,
65 _ => IdDomain::Unknown,
66 }
67 }
68
69 pub fn to_id_string(&self) -> String {
71 format!("{:08}", self.0)
72 }
73
74 pub fn parse(s: &str) -> Option<Self> {
76 let val: u32 = s.parse().ok()?;
77 Self::from_raw(val)
78 }
79
80 pub fn from_raw(val: u32) -> Option<Self> {
82 let id = Self(val);
83 let cat = id.category();
84 let num = id.number();
85 if !(1..=99).contains(&cat) || !(1..=9999).contains(&num) {
86 return None;
87 }
88 Some(id)
89 }
90}
91
92impl fmt::Display for StructuredId {
93 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94 write!(f, "{:08}", self.0)
95 }
96}
97
98impl Serialize for StructuredId {
99 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
100 serializer.serialize_str(&self.to_id_string())
101 }
102}
103
104impl<'de> Deserialize<'de> for StructuredId {
105 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
106 struct IdVisitor;
107
108 impl<'de> serde::de::Visitor<'de> for IdVisitor {
109 type Value = StructuredId;
110
111 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
112 formatter.write_str("a structured ID string \"CCSSNNNN\" or a u32 number")
113 }
114
115 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<StructuredId, E> {
116 StructuredId::parse(v)
117 .ok_or_else(|| E::custom(format!("invalid structured ID: {v}")))
118 }
119
120 fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<StructuredId, E> {
121 if v > u32::MAX as u64 {
122 return Err(E::custom(format!("structured ID too large: {v}")));
123 }
124 StructuredId::from_raw(v as u32)
125 .ok_or_else(|| E::custom(format!("invalid structured ID value: {v}")))
126 }
127
128 fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<StructuredId, E> {
129 if v < 0 || v > u32::MAX as i64 {
130 return Err(E::custom(format!("structured ID out of range: {v}")));
131 }
132 self.visit_u64(v as u64)
133 }
134 }
135
136 deserializer.deserialize_any(IdVisitor)
137 }
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
142pub enum IdDomain {
143 Actors,
145 Items,
147 Skills,
149 World,
151 Unknown,
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn encodes_and_decodes_components() {
161 let id = StructuredId::new(21, 1, 23);
162 assert_eq!(id.0, 21_01_0023);
163 assert_eq!(id.category(), 21);
164 assert_eq!(id.subcategory(), 1);
165 assert_eq!(id.number(), 23);
166 assert_eq!(id.to_id_string(), "21010023");
167 }
168
169 #[test]
170 fn parses_and_validates_raw() {
171 assert_eq!(
172 StructuredId::parse("21010023"),
173 Some(StructuredId(21010023))
174 );
175 assert!(StructuredId::from_raw(0).is_none(), "category 0 is invalid");
176 assert!(StructuredId::try_new(0, 0, 1).is_none());
177 }
178
179 #[test]
180 fn domain_follows_category_ranges() {
181 assert_eq!(StructuredId::new(11, 0, 1).domain(), IdDomain::Actors);
182 assert_eq!(StructuredId::new(21, 0, 1).domain(), IdDomain::Items);
183 assert_eq!(StructuredId::new(99, 0, 1).domain(), IdDomain::Unknown);
184 }
185
186 #[test]
187 fn serde_accepts_string_or_number() {
188 let from_str: StructuredId = serde_json::from_str("\"21010023\"").unwrap();
189 let from_num: StructuredId = serde_json::from_str("21010023").unwrap();
190 assert_eq!(from_str, from_num);
191 assert_eq!(serde_json::to_string(&from_str).unwrap(), "\"21010023\"");
193 }
194}