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.to_string().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.to_string().is_empty());
64    /// ```
65    pub fn generate_uuid_v4() -> Self {
66        Self(Uuid::new_v4().to_string())
67    }
68}
69
70impl Id {
71    // Low-level helper for hashing bytes
72    fn hash_bytes_to_hex(val: &[u8]) -> String {
73        let mut sha256 = Sha256::new();
74        sha256.update(val);
75        let result = sha256.finalize();
76
77        // Preallocate 64-char string
78        let mut hash = String::with_capacity(64);
79        for byte in result {
80            write!(&mut hash, "{:02X}", byte).unwrap();
81        }
82        hash
83    }
84}
85
86impl From<Id> for String {
87    fn from(item: Id) -> Self {
88        item.0
89    }
90}
91
92impl TryFrom<&String> for Id {
93    type Error = Error;
94
95    /// # Errors
96    ///
97    /// Returns [`Error::TooFewElements`] if the string is empty.
98    fn try_from(item: &String) -> Result<Self, Self::Error> {
99        if item.is_empty() {
100            Err(Error::TooFewElements {
101                geometry: "gml:id",
102                minimum: 1,
103                spec: None,
104                id: None,
105                detail: Some("id string must not be empty".to_string()),
106            })
107        } else {
108            Ok(Self(item.to_string()))
109        }
110    }
111}
112
113impl TryFrom<&str> for Id {
114    type Error = Error;
115
116    /// # Errors
117    ///
118    /// Returns [`Error::TooFewElements`] if the string slice is empty.
119    fn try_from(item: &str) -> Result<Self, Self::Error> {
120        if item.is_empty() {
121            Err(Error::TooFewElements {
122                geometry: "gml:id",
123                minimum: 1,
124                spec: None,
125                id: None,
126                detail: Some("id string must not be empty".to_string()),
127            })
128        } else {
129            Ok(Self(item.to_string()))
130        }
131    }
132}
133
134impl TryFrom<String> for Id {
135    type Error = Error;
136
137    /// # Errors
138    ///
139    /// Returns [`Error::TooFewElements`] if the owned string is empty.
140    fn try_from(item: String) -> Result<Self, Self::Error> {
141        if item.is_empty() {
142            Err(Error::TooFewElements {
143                geometry: "gml:id",
144                minimum: 1,
145                spec: None,
146                id: None,
147                detail: Some("id string must not be empty".to_string()),
148            })
149        } else {
150            Ok(Self(item))
151        }
152    }
153}
154
155/*impl From<String> for Id {
156    fn from(item: String) -> Self {
157        Self(item)
158    }
159}*/
160
161impl fmt::Display for Id {
162    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
163        write!(f, "{}", self.0)
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn id_from_empty_string() {
173        let result = Id::try_from("".to_string());
174
175        assert!(matches!(
176            result,
177            Err(Error::TooFewElements {
178                geometry: "gml:id",
179                minimum: 1,
180                ..
181            })
182        ));
183    }
184
185    #[test]
186    fn test() {
187        let xml_document = "<gml:Point>
188              <gml:pos srsDimension=\"3\">678000.9484065345 5403659.060043676 417.3802376791456</gml:pos>
189            </gml:Point>";
190
191        let id_a = Id::from_hashed_string(xml_document);
192        let id_b = Id::from_hashed_string(xml_document);
193
194        assert_eq!(id_a, id_b);
195    }
196}