Skip to main content

egml_core/model/base/
id.rs

1use crate::Error;
2use sha2::{Digest, Sha256};
3use std::fmt;
4use std::fmt::Write;
5use uuid::Uuid;
6
7/// A stable, globally unique identifier for a GML object.
8///
9/// Corresponds to the `gml:id` XML attribute ([OGC 07-036 §7.2.4.5](https://docs.ogc.org/is/07-036/07-036.pdf)).
10/// An `Id` is a non-empty string; it can be constructed from arbitrary
11/// bytes or strings by hashing them with SHA-256, or generated as a
12/// random UUID v4.
13#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
14pub struct Id(String);
15
16impl Id {
17    /// Constructs an `Id` by hashing bytes using SHA-256.
18    ///
19    /// The resulting id is a 64-character uppercase hex string.
20    ///
21    /// # Examples
22    ///
23    /// ```rust
24    /// use egml_core::model::base::Id;
25    ///
26    /// let id = Id::from_hashed_bytes(b"hello");
27    /// assert_eq!(id.as_str().len(), 64);
28    /// ```
29    pub fn from_hashed_bytes(val: impl AsRef<[u8]>) -> Self {
30        Self(Self::hash_bytes_to_hex(val.as_ref()))
31    }
32
33    /// Constructs an `Id` by hashing a string using SHA-256.
34    ///
35    /// Two calls with equal strings always produce the same id.
36    ///
37    /// # Examples
38    ///
39    /// ```rust
40    /// use egml_core::model::base::Id;
41    ///
42    /// let id_a = Id::from_hashed_string("object-42");
43    /// let id_b = Id::from_hashed_string("object-42");
44    /// assert_eq!(id_a, id_b);
45    /// ```
46    pub fn from_hashed_string(val: &str) -> Self {
47        Self::from_hashed_bytes(val.as_bytes())
48    }
49
50    /// Constructs an `Id` by hashing a `u64` using SHA-256 (little-endian bytes).
51    pub fn from_hashed_u64(val: u64) -> Self {
52        Self::from_hashed_bytes(val.to_le_bytes())
53    }
54
55    /// Generates a random UUID v4 as an `Id`.
56    ///
57    /// # Examples
58    ///
59    /// ```rust
60    /// use egml_core::model::base::Id;
61    ///
62    /// let id = Id::generate_uuid_v4();
63    /// assert!(!id.as_str().is_empty());
64    /// ```
65    pub fn generate_uuid_v4() -> Self {
66        Self(Uuid::new_v4().to_string())
67    }
68
69    /// Generates a time-ordered UUID v7 as an `Id`.
70    ///
71    /// v7 encodes a millisecond-precision Unix timestamp in the high bits,
72    /// making ids lexicographically sortable by creation time — preferable
73    /// over v4 when ids are stored in a database index.
74    ///
75    /// # Examples
76    ///
77    /// ```rust
78    /// use egml_core::model::base::Id;
79    ///
80    /// let a = Id::generate_uuid_v7();
81    /// let b = Id::generate_uuid_v7();
82    /// assert!(a.as_str() <= b.as_str());
83    /// ```
84    pub fn generate_uuid_v7() -> Self {
85        Self(Uuid::now_v7().to_string())
86    }
87
88    /// Generates a deterministic UUID v5 from a namespace and a name.
89    ///
90    /// Two calls with the same `namespace` and `name` always produce the same
91    /// id. Use the predefined namespace constants on [`Uuid`] (e.g.
92    /// [`Uuid::NAMESPACE_URL`]) or supply a custom one.
93    ///
94    /// # Examples
95    ///
96    /// ```rust
97    /// use egml_core::model::base::Id;
98    /// use uuid::Uuid;
99    ///
100    /// let id_a = Id::generate_uuid_v5(&Uuid::NAMESPACE_URL, "https://example.com/object-1");
101    /// let id_b = Id::generate_uuid_v5(&Uuid::NAMESPACE_URL, "https://example.com/object-1");
102    /// assert_eq!(id_a, id_b);
103    /// ```
104    pub fn generate_uuid_v5(namespace: &Uuid, name: &str) -> Self {
105        Self(Uuid::new_v5(namespace, name.as_bytes()).to_string())
106    }
107
108    /// Returns the id as a string slice.
109    pub fn as_str(&self) -> &str {
110        &self.0
111    }
112}
113
114impl Id {
115    fn hash_bytes_to_hex(val: &[u8]) -> String {
116        let mut sha256 = Sha256::new();
117        sha256.update(val);
118        let result = sha256.finalize();
119
120        let mut hash = String::with_capacity(64);
121        for byte in result {
122            write!(&mut hash, "{:02X}", byte).unwrap();
123        }
124        hash
125    }
126
127    fn validate(s: &str) -> Result<(), Error> {
128        if s.is_empty() {
129            return Err(Error::EmptyId);
130        }
131        Ok(())
132    }
133}
134
135impl From<Id> for String {
136    fn from(item: Id) -> Self {
137        item.0
138    }
139}
140
141impl TryFrom<&str> for Id {
142    type Error = Error;
143
144    /// # Errors
145    ///
146    /// Returns [`Error::EmptyId`] if the string slice is empty.
147    fn try_from(item: &str) -> Result<Self, Self::Error> {
148        Self::validate(item)?;
149        Ok(Self(item.to_string()))
150    }
151}
152
153impl TryFrom<String> for Id {
154    type Error = Error;
155
156    /// # Errors
157    ///
158    /// Returns [`Error::EmptyId`] if the owned string is empty.
159    fn try_from(item: String) -> Result<Self, Self::Error> {
160        Self::validate(&item)?;
161        Ok(Self(item))
162    }
163}
164
165impl fmt::Display for Id {
166    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
167        f.write_str(&self.0)
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn try_from_empty_string_returns_empty_id_error() {
177        assert_eq!(Id::try_from(""), Err(Error::EmptyId));
178        assert_eq!(Id::try_from("".to_string()), Err(Error::EmptyId));
179    }
180
181    #[test]
182    fn from_hashed_string_is_deterministic() {
183        let xml = "<gml:Point><gml:pos>1 2 3</gml:pos></gml:Point>";
184        assert_eq!(Id::from_hashed_string(xml), Id::from_hashed_string(xml));
185    }
186}