1use std::{fmt, num::NonZeroUsize, ops::Deref, str::FromStr};
2
3use non_empty_string::NonEmptyString;
4
5#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
10pub struct KindString(NonEmptyString);
11
12impl KindString {
13 pub fn new(s: String) -> Result<Self, InvalidKindError> {
20 let non_empty = NonEmptyString::new(s.clone()).map_err(|_| InvalidKindError(s.clone()))?;
22
23 if !s.chars().all(|c| c.is_ascii_uppercase()) {
25 return Err(InvalidKindError(s));
26 }
27
28 Ok(Self(non_empty))
29 }
30
31 #[must_use]
33 pub fn as_str(&self) -> &str {
34 self.0.as_str()
35 }
36}
37
38impl TryFrom<String> for KindString {
39 type Error = InvalidKindError;
40
41 fn try_from(value: String) -> Result<Self, Self::Error> {
42 Self::new(value)
43 }
44}
45
46impl TryFrom<&str> for KindString {
47 type Error = InvalidKindError;
48
49 fn try_from(value: &str) -> Result<Self, Self::Error> {
50 Self::new(value.to_string())
51 }
52}
53
54impl AsRef<str> for KindString {
55 fn as_ref(&self) -> &str {
56 self.0.as_str()
57 }
58}
59
60impl Deref for KindString {
61 type Target = str;
62
63 fn deref(&self) -> &Self::Target {
64 self.0.as_str()
65 }
66}
67
68impl fmt::Display for KindString {
69 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
70 write!(f, "{}", self.0)
71 }
72}
73
74impl FromStr for KindString {
75 type Err = InvalidKindError;
76
77 fn from_str(s: &str) -> Result<Self, Self::Err> {
78 Self::new(s.to_string())
79 }
80}
81
82#[derive(Debug, thiserror::Error, PartialEq, Eq)]
84#[error("Invalid kind string '{0}': must be non-empty and contain only uppercase letters (A-Z)")]
85pub struct InvalidKindError(String);
86
87#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
98pub struct Hrid {
99 namespace: Vec<KindString>,
100 kind: KindString,
101 id: NonZeroUsize,
102}
103
104impl Hrid {
105 #[must_use]
109 pub const fn new(kind: KindString, id: NonZeroUsize) -> Self {
110 Self::new_with_namespace(Vec::new(), kind, id)
111 }
112
113 #[must_use]
117 pub const fn new_with_namespace(
118 namespace: Vec<KindString>,
119 kind: KindString,
120 id: NonZeroUsize,
121 ) -> Self {
122 Self {
123 namespace,
124 kind,
125 id,
126 }
127 }
128
129 pub fn namespace(&self) -> Vec<&str> {
131 self.namespace.iter().map(KindString::as_str).collect()
132 }
133
134 #[must_use]
136 pub fn kind(&self) -> &str {
137 self.kind.as_str()
138 }
139
140 #[must_use]
142 pub const fn id(&self) -> NonZeroUsize {
143 self.id
144 }
145
146 #[must_use]
152 pub fn prefix(&self) -> String {
153 if self.namespace.is_empty() {
154 self.kind.to_string()
155 } else {
156 let namespace_str = self
157 .namespace
158 .iter()
159 .map(KindString::as_str)
160 .collect::<Vec<_>>()
161 .join("-");
162 format!("{}-{}", namespace_str, self.kind)
163 }
164 }
165}
166
167impl fmt::Display for Hrid {
168 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
169 let id_str = format!("{:03}", self.id);
170 if self.namespace.is_empty() {
171 write!(f, "{}-{}", self.kind, id_str)
172 } else {
173 let namespace_str = self
174 .namespace
175 .iter()
176 .map(KindString::as_str)
177 .collect::<Vec<_>>()
178 .join("-");
179 write!(f, "{}-{}-{}", namespace_str, self.kind, id_str)
180 }
181 }
182}
183
184#[derive(Debug, thiserror::Error, PartialEq, Eq)]
186pub enum Error {
187 #[error("Invalid HRID format: {0}")]
189 Syntax(String),
190
191 #[error("Invalid ID in HRID '{0}': expected a non-zero integer, got {1}")]
193 Id(String, String),
194
195 #[error("Invalid ID: cannot be zero")]
197 ZeroId,
198
199 #[error(transparent)]
201 Kind(InvalidKindError),
202}
203
204impl From<InvalidKindError> for Error {
205 fn from(err: InvalidKindError) -> Self {
206 Self::Kind(err)
207 }
208}
209
210impl FromStr for Hrid {
211 type Err = Error;
212
213 fn from_str(s: &str) -> Result<Self, Self::Err> {
214 if s.is_empty()
216 || s.starts_with('-')
217 || s.ends_with('-')
218 || s.contains("--")
219 || !s.contains('-')
220 {
221 return Err(Error::Syntax(s.to_string()));
222 }
223
224 let parts: Vec<&str> = s.split('-').collect();
225
226 if parts.len() < 2 {
228 return Err(Error::Syntax(s.to_string()));
229 }
230
231 let id_str = parts[parts.len() - 1];
233 let id_usize = id_str
234 .parse::<usize>()
235 .map_err(|_| Error::Id(s.to_string(), id_str.to_string()))?;
236 let id = NonZeroUsize::new(id_usize)
237 .ok_or_else(|| Error::Id(s.to_string(), id_str.to_string()))?;
238
239 let kind_str = parts[parts.len() - 2];
241 let kind = KindString::new(kind_str.to_string())?;
242
243 let namespace = if parts.len() > 2 {
245 parts[..parts.len() - 2]
246 .iter()
247 .map(|&segment| KindString::new(segment.to_string()))
248 .collect::<Result<Vec<_>, _>>()?
249 } else {
250 Vec::new()
251 };
252
253 Ok(Self::new_with_namespace(namespace, kind, id))
254 }
255}
256
257impl TryFrom<&str> for Hrid {
258 type Error = Error;
259
260 fn try_from(value: &str) -> Result<Self, Self::Error> {
261 Self::from_str(value)
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
270 fn hrid_creation_no_namespace() {
271 let kind = KindString::new("URS".to_string()).unwrap();
272 let id = NonZeroUsize::new(42).unwrap();
273 let hrid = Hrid::new(kind, id);
274 assert!(hrid.namespace().is_empty());
275 assert_eq!(hrid.kind(), "URS");
276 assert_eq!(hrid.id().get(), 42);
277 }
278
279 #[test]
280 fn hrid_creation_with_namespace() {
281 let namespace = vec![
282 KindString::new("COMPONENT".to_string()).unwrap(),
283 KindString::new("SUBCOMPONENT".to_string()).unwrap(),
284 ];
285 let kind = KindString::new("SYS".to_string()).unwrap();
286 let id = NonZeroUsize::new(5).unwrap();
287 let hrid = Hrid::new_with_namespace(namespace, kind, id);
288
289 assert_eq!(hrid.namespace(), vec!["COMPONENT", "SUBCOMPONENT"]);
290 assert_eq!(hrid.kind(), "SYS");
291 assert_eq!(hrid.id().get(), 5);
292 }
293
294 #[test]
295 fn hrid_creation_empty_kind_fails() {
296 assert!(KindString::new(String::new()).is_err());
297 }
298
299 #[test]
300 fn hrid_creation_lowercase_kind_fails() {
301 assert!(KindString::new("sys".to_string()).is_err());
302 }
303
304 #[test]
305 fn hrid_creation_zero_id_fails() {
306 assert!(NonZeroUsize::new(0).is_none());
307 }
308
309 #[test]
310 fn hrid_display_no_namespace() {
311 let hrid = Hrid::new(
312 KindString::new("SYS".to_string()).unwrap(),
313 NonZeroUsize::new(1).unwrap(),
314 );
315 assert_eq!(format!("{hrid}"), "SYS-001");
316
317 let hrid = Hrid::new(
318 KindString::new("URS".to_string()).unwrap(),
319 NonZeroUsize::new(42).unwrap(),
320 );
321 assert_eq!(format!("{hrid}"), "URS-042");
322
323 let hrid = Hrid::new(
324 KindString::new("TEST".to_string()).unwrap(),
325 NonZeroUsize::new(999).unwrap(),
326 );
327 assert_eq!(format!("{hrid}"), "TEST-999");
328 }
329
330 #[test]
331 fn hrid_display_with_namespace() {
332 let hrid = Hrid::new_with_namespace(
333 vec![KindString::new("COMPONENT".to_string()).unwrap()],
334 KindString::new("SYS".to_string()).unwrap(),
335 NonZeroUsize::new(5).unwrap(),
336 );
337 assert_eq!(format!("{hrid}"), "COMPONENT-SYS-005");
338
339 let hrid = Hrid::new_with_namespace(
340 vec![
341 KindString::new("COMPONENT".to_string()).unwrap(),
342 KindString::new("SUBCOMPONENT".to_string()).unwrap(),
343 ],
344 KindString::new("SYS".to_string()).unwrap(),
345 NonZeroUsize::new(5).unwrap(),
346 );
347 assert_eq!(format!("{hrid}"), "COMPONENT-SUBCOMPONENT-SYS-005");
348
349 let hrid = Hrid::new_with_namespace(
350 vec![
351 KindString::new("A".to_string()).unwrap(),
352 KindString::new("B".to_string()).unwrap(),
353 KindString::new("C".to_string()).unwrap(),
354 ],
355 KindString::new("REQ".to_string()).unwrap(),
356 NonZeroUsize::new(123).unwrap(),
357 );
358 assert_eq!(format!("{hrid}"), "A-B-C-REQ-123");
359 }
360
361 #[test]
362 fn hrid_display_large_numbers() {
363 let hrid = Hrid::new(
364 KindString::new("BIG".to_string()).unwrap(),
365 NonZeroUsize::new(1000).unwrap(),
366 );
367 assert_eq!(format!("{hrid}"), "BIG-1000");
368
369 let hrid = Hrid::new_with_namespace(
370 vec![KindString::new("NS".to_string()).unwrap()],
371 KindString::new("HUGE".to_string()).unwrap(),
372 NonZeroUsize::new(12345).unwrap(),
373 );
374 assert_eq!(format!("{hrid}"), "NS-HUGE-12345");
375 }
376
377 #[test]
378 fn try_from_valid_no_namespace() {
379 let hrid = Hrid::try_from("URS-001").unwrap();
380 assert!(hrid.namespace().is_empty());
381 assert_eq!(hrid.kind(), "URS");
382 assert_eq!(hrid.id().get(), 1);
383
384 let hrid = Hrid::try_from("SYS-042").unwrap();
385 assert!(hrid.namespace().is_empty());
386 assert_eq!(hrid.kind(), "SYS");
387 assert_eq!(hrid.id().get(), 42);
388
389 let hrid = Hrid::try_from("TEST-999").unwrap();
390 assert!(hrid.namespace().is_empty());
391 assert_eq!(hrid.kind(), "TEST");
392 assert_eq!(hrid.id().get(), 999);
393 }
394
395 #[test]
396 fn try_from_valid_with_namespace() {
397 let hrid = Hrid::try_from("COMPONENT-SYS-005").unwrap();
398 assert_eq!(hrid.namespace(), vec!["COMPONENT"]);
399 assert_eq!(hrid.kind(), "SYS");
400 assert_eq!(hrid.id().get(), 5);
401
402 let hrid = Hrid::try_from("COMPONENT-SUBCOMPONENT-SYS-005").unwrap();
403 assert_eq!(hrid.namespace(), vec!["COMPONENT", "SUBCOMPONENT"]);
404 assert_eq!(hrid.kind(), "SYS");
405 assert_eq!(hrid.id().get(), 5);
406
407 let hrid = Hrid::try_from("A-B-C-REQ-123").unwrap();
408 assert_eq!(hrid.namespace(), vec!["A", "B", "C"]);
409 assert_eq!(hrid.kind(), "REQ");
410 assert_eq!(hrid.id().get(), 123);
411 }
412
413 #[test]
414 fn try_from_valid_no_leading_zeros() {
415 let hrid = Hrid::try_from("URS-1").unwrap();
416 assert!(hrid.namespace().is_empty());
417 assert_eq!(hrid.kind(), "URS");
418 assert_eq!(hrid.id().get(), 1);
419
420 let hrid = Hrid::try_from("NS-SYS-42").unwrap();
421 assert_eq!(hrid.namespace(), vec!["NS"]);
422 assert_eq!(hrid.kind(), "SYS");
423 assert_eq!(hrid.id().get(), 42);
424 }
425
426 #[test]
427 fn try_from_valid_large_numbers() {
428 let hrid = Hrid::try_from("BIG-1000").unwrap();
429 assert!(hrid.namespace().is_empty());
430 assert_eq!(hrid.kind(), "BIG");
431 assert_eq!(hrid.id().get(), 1000);
432
433 let hrid = Hrid::try_from("NS-HUGE-12345").unwrap();
434 assert_eq!(hrid.namespace(), vec!["NS"]);
435 assert_eq!(hrid.kind(), "HUGE");
436 assert_eq!(hrid.id().get(), 12345);
437 }
438
439 #[test]
440 fn try_from_invalid_no_dash() {
441 let result = Hrid::try_from("URS001");
442 assert!(matches!(result, Err(Error::Syntax(_))));
443 }
444
445 #[test]
446 fn try_from_invalid_empty_string() {
447 let result = Hrid::try_from("");
448 assert!(matches!(result, Err(Error::Syntax(_))));
449 }
450
451 #[test]
452 fn try_from_invalid_only_dash() {
453 let result = Hrid::try_from("-");
454 assert!(matches!(result, Err(Error::Syntax(_))));
455 }
456
457 #[test]
458 fn try_from_invalid_single_part() {
459 let result = Hrid::try_from("JUSTONEWORD");
460 assert!(matches!(result, Err(Error::Syntax(_))));
461 }
462
463 #[test]
464 fn try_from_invalid_non_numeric_id() {
465 let result = Hrid::try_from("URS-abc");
466 assert!(matches!(result, Err(Error::Id(_, _))));
467
468 let result = Hrid::try_from("NS-URS-abc");
469 assert!(matches!(result, Err(Error::Id(_, _))));
470 }
471
472 #[test]
473 fn try_from_invalid_mixed_id() {
474 let result = Hrid::try_from("SYS-12abc");
475 assert!(matches!(result, Err(Error::Id(_, _))));
476 }
477
478 #[test]
479 fn try_from_invalid_negative_id() {
480 let result = Hrid::try_from("URS--1");
481 assert!(matches!(result, Err(Error::Syntax(_))));
482 }
483
484 #[test]
485 fn try_from_invalid_zero_id() {
486 let result = Hrid::try_from("URS-0");
487 assert!(matches!(result, Err(Error::Id(_, _))));
488 }
489
490 #[test]
491 fn try_from_invalid_lowercase_kind() {
492 let result = Hrid::try_from("urs-001");
493 assert!(matches!(result, Err(Error::Kind(_))));
494 }
495
496 #[test]
497 fn try_from_invalid_lowercase_namespace() {
498 let result = Hrid::try_from("ns-URS-001");
499 assert!(matches!(result, Err(Error::Kind(_))));
500 }
501
502 #[test]
503 fn try_from_empty_namespace_segment_fails() {
504 let result = Hrid::try_from("-NS-SYS-001");
505 assert!(matches!(result, Err(Error::Syntax(_))));
506
507 let result = Hrid::try_from("NS--SYS-001");
508 assert!(matches!(result, Err(Error::Syntax(_))));
509 }
510
511 #[test]
512 fn try_from_empty_kind_fails() {
513 let result = Hrid::try_from("-001");
514 assert!(matches!(result, Err(Error::Syntax(_))));
515 }
516
517 #[test]
518 fn hrid_clone_and_eq() {
519 let hrid1 = Hrid::new_with_namespace(
520 vec![KindString::new("NS".to_string()).unwrap()],
521 KindString::new("URS".to_string()).unwrap(),
522 NonZeroUsize::new(42).unwrap(),
523 );
524 let hrid2 = hrid1.clone();
525
526 assert_eq!(hrid1, hrid2);
527 assert_eq!(hrid1.namespace(), hrid2.namespace());
528 assert_eq!(hrid1.kind(), hrid2.kind());
529 assert_eq!(hrid1.id(), hrid2.id());
530 }
531
532 #[test]
533 fn hrid_not_eq() {
534 let hrid1 = Hrid::new(
535 KindString::new("URS".to_string()).unwrap(),
536 NonZeroUsize::new(42).unwrap(),
537 );
538 let hrid2 = Hrid::new(
539 KindString::new("SYS".to_string()).unwrap(),
540 NonZeroUsize::new(42).unwrap(),
541 );
542 let hrid3 = Hrid::new(
543 KindString::new("URS".to_string()).unwrap(),
544 NonZeroUsize::new(43).unwrap(),
545 );
546 let hrid4 = Hrid::new_with_namespace(
547 vec![KindString::new("NS".to_string()).unwrap()],
548 KindString::new("URS".to_string()).unwrap(),
549 NonZeroUsize::new(42).unwrap(),
550 );
551
552 assert_ne!(hrid1, hrid2);
553 assert_ne!(hrid1, hrid3);
554 assert_ne!(hrid1, hrid4);
555 }
556
557 #[test]
558 fn roundtrip_conversion_no_namespace() {
559 let original = Hrid::new(
560 KindString::new("TEST".to_string()).unwrap(),
561 NonZeroUsize::new(123).unwrap(),
562 );
563
564 let as_string = format!("{original}");
565 let parsed = Hrid::try_from(as_string.as_str()).unwrap();
566
567 assert_eq!(original, parsed);
568 }
569
570 #[test]
571 fn roundtrip_conversion_with_namespace() {
572 let original = Hrid::new_with_namespace(
573 vec![
574 KindString::new("COMPONENT".to_string()).unwrap(),
575 KindString::new("SUBCOMPONENT".to_string()).unwrap(),
576 ],
577 KindString::new("SYS".to_string()).unwrap(),
578 NonZeroUsize::new(5).unwrap(),
579 );
580
581 let as_string = format!("{original}");
582 let parsed = Hrid::try_from(as_string.as_str()).unwrap();
583
584 assert_eq!(original, parsed);
585 }
586
587 #[test]
588 fn strict_uppercase_validation() {
589 assert!(KindString::new("sys".to_string()).is_err());
591
592 let result = Hrid::from_str("component-sys-001");
594 assert!(matches!(result, Err(Error::Kind(_))));
595 }
596
597 #[test]
598 fn error_display() {
599 let syntax_error = Error::Syntax("bad-format".to_string());
600 assert_eq!(format!("{syntax_error}"), "Invalid HRID format: bad-format");
601
602 let id_error = Error::Id("URS-bad".to_string(), "bad".to_string());
603 assert_eq!(
604 format!("{id_error}"),
605 "Invalid ID in HRID 'URS-bad': expected a non-zero integer, got bad"
606 );
607 }
608}