acton_ern/model/root.rs
1use std::fmt;
2use std::hash::Hash;
3
4use derive_more::{AsRef, From, Into};
5use mti::prelude::*;
6
7use crate::errors::ErnError;
8
9#[cfg(feature = "serde")]
10use serde::{Deserialize, Deserializer, Serialize, Serializer};
11
12/// Represents the root component in an Entity Resource Name (ERN).
13///
14/// The root component is a unique identifier for the base resource in the ERN hierarchy.
15/// It uses the `mti` crate's `MagicTypeId` with UUID v7 algorithm to generate
16/// time-ordered, unique identifiers that enable k-sortability.
17///
18/// When using `EntityRoot`, each call to create a new root with the same name will
19/// generate a different ID, as it incorporates the current timestamp. This makes
20/// `EntityRoot` suitable for resources that should be ordered by creation time.
21///
22/// For content-addressable, deterministic IDs, use `SHA1Name` instead.
23#[derive(AsRef, From, Into, Eq, Debug, PartialEq, Clone, Hash, Default, PartialOrd)]
24pub struct EntityRoot {
25 /// The unique identifier for this root entity, generated using the `mti` crate's
26 /// `MagicTypeId` type.
27 name: MagicTypeId,
28}
29
30impl EntityRoot {
31 /// Returns a reference to the underlying `MagicTypeId`.
32 ///
33 /// This is useful when you need to access the raw identifier for
34 /// comparison or sorting operations.
35 ///
36 /// # Example
37 ///
38 /// ```
39 /// # use acton_ern::prelude::*;
40 /// # fn example() -> Result<(), ErnError> {
41 /// let root1 = EntityRoot::new("resource1".to_string())?;
42 /// let root2 = EntityRoot::new("resource2".to_string())?;
43 ///
44 /// // Compare roots by their MagicTypeId
45 /// let comparison = root1.name().cmp(root2.name());
46 /// # Ok(())
47 /// # }
48 /// ```
49 pub fn name(&self) -> &MagicTypeId {
50 &self.name
51 }
52
53 /// Returns the string representation of this root's identifier.
54 ///
55 /// # Example
56 ///
57 /// ```
58 /// # use acton_ern::prelude::*;
59 /// # fn example() -> Result<(), ErnError> {
60 /// let root = EntityRoot::new("profile".to_string())?;
61 /// let id_str = root.as_str();
62 ///
63 /// // The string will contain the original name followed by a timestamp-based suffix
64 /// assert!(id_str.starts_with("profile_"));
65 /// # Ok(())
66 /// # }
67 /// ```
68 pub fn as_str(&self) -> &str {
69 &self.name
70 }
71
72 /// Creates a new `EntityRoot` with the given value.
73 ///
74 /// This method generates a time-ordered, unique identifier using the UUID v7 algorithm.
75 /// Each call to this method with the same input value will generate a different ID,
76 /// as it incorporates the current timestamp. This makes `EntityRoot` suitable for
77 /// resources that should be ordered by creation time.
78 ///
79 /// # Arguments
80 ///
81 /// * `value` - The string value to use as the base for the entity root ID
82 ///
83 /// # Validation Rules
84 ///
85 /// * Value cannot be empty
86 /// * Value must be between 1 and 255 characters
87 ///
88 /// # Returns
89 ///
90 /// * `Ok(EntityRoot)` - If validation passes
91 /// * `Err(ErnError)` - If validation fails
92 ///
93 /// # Example
94 ///
95 /// ```
96 /// # use acton_ern::prelude::*;
97 /// # fn example() -> Result<(), ErnError> {
98 /// let root = EntityRoot::new("profile".to_string())?;
99 ///
100 /// // The ID will contain the original name followed by a timestamp-based suffix
101 /// assert!(root.to_string().starts_with("profile_"));
102 /// # Ok(())
103 /// # }
104 /// ```
105 pub fn new(value: String) -> Result<Self, ErnError> {
106 // Check if empty
107 if value.is_empty() {
108 return Err(ErnError::ParseFailure(
109 "EntityRoot",
110 "cannot be empty".to_string(),
111 ));
112 }
113
114 // Check length
115 if value.len() > 255 {
116 return Err(ErnError::ParseFailure(
117 "EntityRoot",
118 format!(
119 "length exceeds maximum of 255 characters (got {})",
120 value.len()
121 ),
122 ));
123 }
124
125 Ok(EntityRoot {
126 name: value.create_type_id::<V7>(),
127 })
128 }
129}
130
131impl fmt::Display for EntityRoot {
132 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133 let id = &self.name;
134 write!(f, "{id}")
135 }
136}
137
138/// Implementation of `FromStr` for `EntityRoot` to create an entity root from a string.
139impl std::str::FromStr for EntityRoot {
140 type Err = ErnError;
141
142 /// Creates an `EntityRoot` from a string.
143 ///
144 /// This method generates a time-ordered, unique identifier using the UUID v7 algorithm.
145 /// Each call to this method with the same input string will generate a different ID,
146 /// as it incorporates the current timestamp.
147 ///
148 /// # Arguments
149 ///
150 /// * `s` - The string value to use as the base for the entity root ID
151 ///
152 /// # Returns
153 ///
154 /// * `Ok(EntityRoot)` - If validation passes
155 /// * `Err(ErnError)` - If validation fails
156 fn from_str(s: &str) -> Result<Self, Self::Err> {
157 // Check if empty
158 if s.is_empty() {
159 return Err(ErnError::ParseFailure(
160 "EntityRoot",
161 "cannot be empty".to_string(),
162 ));
163 }
164
165 // Check length
166 if s.len() > 255 {
167 return Err(ErnError::ParseFailure(
168 "EntityRoot",
169 format!("length exceeds maximum of 255 characters (got {})", s.len()),
170 ));
171 }
172
173 Ok(EntityRoot {
174 name: s.create_type_id::<V7>(),
175 })
176 }
177}
178
179#[cfg(feature = "serde")]
180impl Serialize for EntityRoot {
181 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
182 where
183 S: Serializer,
184 {
185 // Serialize the MagicTypeId as a string
186 serializer.serialize_str(self.name.as_ref())
187 }
188}
189
190#[cfg(feature = "serde")]
191impl<'de> Deserialize<'de> for EntityRoot {
192 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
193 where
194 D: Deserializer<'de>,
195 {
196 // Deserialize as a string, then create a new EntityRoot
197 let s = String::deserialize(deserializer)?;
198 EntityRoot::new(s).map_err(serde::de::Error::custom)
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use std::str::FromStr;
206
207 #[test]
208 fn test_entity_root_creation() -> anyhow::Result<()> {
209 let root = EntityRoot::new("test-entity".to_string())?;
210 assert!(!root.to_string().is_empty());
211 Ok(())
212 }
213
214 #[test]
215 fn test_entity_root_uniqueness() -> anyhow::Result<()> {
216 // EntityRoot should generate different IDs for the same input (non-deterministic)
217 let root1 = EntityRoot::new("same-content".to_string())?;
218 let root2 = EntityRoot::new("same-content".to_string())?;
219
220 // The string representations should be different
221 assert_ne!(root1.to_string(), root2.to_string());
222 Ok(())
223 }
224
225 #[test]
226 fn test_entity_root_from_str() -> anyhow::Result<()> {
227 let root = EntityRoot::from_str("test-entity")?;
228 assert!(!root.to_string().is_empty());
229 Ok(())
230 }
231
232 #[test]
233 fn test_entity_root_validation_empty() {
234 let result = EntityRoot::new("".to_string());
235 assert!(result.is_err());
236 match result {
237 Err(ErnError::ParseFailure(component, msg)) => {
238 assert_eq!(component, "EntityRoot");
239 assert!(msg.contains("empty"));
240 }
241 _ => panic!("Expected ParseFailure error for empty EntityRoot"),
242 }
243 }
244
245 #[test]
246 fn test_entity_root_validation_too_long() {
247 let long_value = "a".repeat(256);
248 let result = EntityRoot::new(long_value);
249 assert!(result.is_err());
250 match result {
251 Err(ErnError::ParseFailure(component, msg)) => {
252 assert_eq!(component, "EntityRoot");
253 assert!(msg.contains("length exceeds maximum"));
254 }
255 _ => panic!("Expected ParseFailure error for too long EntityRoot"),
256 }
257 }
258
259 #[test]
260 fn test_entity_root_from_str_validation() {
261 let result = EntityRoot::from_str("");
262 assert!(result.is_err());
263 match result {
264 Err(ErnError::ParseFailure(component, msg)) => {
265 assert_eq!(component, "EntityRoot");
266 assert!(msg.contains("empty"));
267 }
268 _ => panic!("Expected ParseFailure error for empty EntityRoot from_str"),
269 }
270 }
271}