Skip to main content

dpm_model/
identity.rs

1use serde::{Deserialize, Serialize};
2use uuid::Uuid;
3macro_rules! uuid_id {
4    ($name:ident) => {
5        uuid_id!($name, new_v4);
6    };
7    ($name:ident, $mint:ident) => {
8        #[derive(
9            Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
10        )]
11        #[serde(transparent)]
12        #[doc = concat!("Stable UUID identifying a ", stringify!($name), ".")]
13        pub struct $name(#[doc = "Underlying UUID."] pub Uuid);
14
15        impl $name {
16            #[must_use]
17            /// Create a new identifier.
18            pub fn new() -> Self {
19                Self(Uuid::$mint())
20            }
21        }
22
23        impl Default for $name {
24            fn default() -> Self {
25                Self::new()
26            }
27        }
28
29        impl std::fmt::Display for $name {
30            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31                self.0.fmt(f)
32            }
33        }
34
35        impl std::str::FromStr for $name {
36            type Err = uuid::Error;
37
38            fn from_str(text: &str) -> Result<Self, Self::Err> {
39                Uuid::parse_str(text).map(Self)
40            }
41        }
42    };
43}
44
45uuid_id!(WorkspaceId);
46uuid_id!(ProjectId);
47uuid_id!(WorkItemId);
48uuid_id!(RequirementId);
49uuid_id!(ArtifactId);
50uuid_id!(DecisionId);
51uuid_id!(RiskId);
52// Time-ordered identities sort by creation, so an operation log and its lineages index well.
53uuid_id!(OperationId, now_v7);
54uuid_id!(LineageId, now_v7);
55uuid_id!(AssetId);
56uuid_id!(ExternalReferenceId);
57uuid_id!(DependencyId);
58// A run and each of its lifecycle transitions are caller-supplied idempotency keys like operations.
59uuid_id!(RunId, now_v7);
60uuid_id!(RunEventId, now_v7);
61
62macro_rules! time_ordered {
63    ($($name:ident),*) => {$(
64        impl $name {
65            /// Whether this is a version 7 UUID, the only form a client may supply as an
66            /// idempotency key.
67            #[must_use]
68            pub fn is_time_ordered(&self) -> bool {
69                self.0.get_version() == Some(uuid::Version::SortRand)
70            }
71        }
72    )*};
73}
74
75time_ordered!(OperationId, RunId, RunEventId);
76
77#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
78/// Human-readable identifier, unique within its entity category.
79pub struct Key(#[doc = "Underlying key text."] pub String);
80
81impl Key {
82    /// Create a new identifier.
83    pub fn new(value: impl Into<String>) -> Self {
84        Self(value.into())
85    }
86
87    /// Order keys as people number them: runs of ASCII digits compare by value, so `OP-2` sorts
88    /// before `OP-10`, and everything else compares by character.
89    ///
90    /// Equal values with different leading zeros (`T-7`, `T-07`) and any remaining tie fall back
91    /// to plain text order, so the order is total and consistent with equality.
92    #[must_use]
93    pub fn natural_cmp(&self, other: &Self) -> std::cmp::Ordering {
94        let (mut left, mut right) = (self.0.as_str(), other.0.as_str());
95        loop {
96            let (a, rest_a) = next_run(left);
97            let (b, rest_b) = next_run(right);
98            let order = match (a, b) {
99                (None, None) => return self.0.cmp(&other.0),
100                (None, Some(_)) => std::cmp::Ordering::Less,
101                (Some(_), None) => std::cmp::Ordering::Greater,
102                (Some(a), Some(b)) => compare_runs(a, b),
103            };
104            if order.is_ne() {
105                return order;
106            }
107            (left, right) = (rest_a, rest_b);
108        }
109    }
110}
111
112/// The leading run of `text` (all ASCII digits or no ASCII digits) and the text after it.
113fn next_run(text: &str) -> (Option<&str>, &str) {
114    let digits = text.starts_with(|c: char| c.is_ascii_digit());
115    let end = text
116        .find(|c: char| c.is_ascii_digit() != digits)
117        .unwrap_or(text.len());
118    if end == 0 {
119        return (None, text);
120    }
121    let (run, rest) = text.split_at(end);
122    (Some(run), rest)
123}
124
125fn compare_runs(a: &str, b: &str) -> std::cmp::Ordering {
126    let numeric = |run: &str| run.starts_with(|c: char| c.is_ascii_digit());
127    if numeric(a) && numeric(b) {
128        let (a, b) = (a.trim_start_matches('0'), b.trim_start_matches('0'));
129        a.len().cmp(&b.len()).then_with(|| a.cmp(b))
130    } else {
131        a.cmp(b)
132    }
133}
134
135impl std::fmt::Display for Key {
136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137        self.0.fmt(f)
138    }
139}
140
141#[cfg(test)]
142mod tests;