@jpgilldev / steid

2.0 KBRaw
1//! Typed identifiers.
2//!
3//! Never pass a raw `String` where an entity reference is meant: `fn get(org: &OrgId,
4//! user: &UserId)` catches a swapped argument at compile time, `fn get(org: &str,
5//! user: &str)` fails at runtime instead.
6
7use std::fmt;
8
9use uuid::Uuid;
10
11/// Declares a newtype over `String` used as an entity identifier.
12macro_rules! typed_id {
13 ($(#[$meta:meta])* $name:ident) => {
14 $(#[$meta])*
15 #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
16 pub struct $name(String);
17
18 impl $name {
19 /// Generates a fresh random identifier.
20 pub fn generate() -> Self {
21 Self(Uuid::new_v4().to_string())
22 }
23
24 /// Wraps an existing identifier, typically one loaded from the database.
25 pub fn from_trusted(value: impl Into<String>) -> Self {
26 Self(value.into())
27 }
28
29 pub fn as_str(&self) -> &str {
30 &self.0
31 }
32 }
33
34 impl fmt::Display for $name {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 f.write_str(&self.0)
37 }
38 }
39 };
40}
41
42typed_id!(
43 /// Identifies a [`User`](crate::domain::User).
44 UserId
45);
46
47typed_id!(
48 /// Identifies an [`Organization`](crate::domain::Organization).
49 OrgId
50);
51
52typed_id!(
53 /// Identifies a [`Membership`](crate::domain::Membership).
54 MembershipId
55);
56
57typed_id!(
58 /// Identifies a [`Repository`](crate::domain::Repository).
59 RepoId
60);
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65
66 #[test]
67 fn generated_ids_are_unique() {
68 assert_ne!(UserId::generate(), UserId::generate());
69 }
70
71 #[test]
72 fn round_trips_through_str() {
73 let id = UserId::from_trusted("user-1");
74
75 assert_eq!(id.as_str(), "user-1");
76 assert_eq!(id.to_string(), "user-1");
77 }
78}
79
80typed_id!(
81 /// Identifies a [`PersonalAccessToken`](crate::domain::PersonalAccessToken).
82 TokenId
83);