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("EntityRoot", "cannot be empty".to_string()));
109 }
110
111 // Check length
112 if value.len() > 255 {
113 return Err(ErnError::ParseFailure(
114 "EntityRoot",
115 format!("length exceeds maximum of 255 characters (got {})", value.len())
116 ));
117 }
118
119 Ok(EntityRoot {
120 name: value.create_type_id::<V7>(),
121 })
122 }
123}
124
125impl fmt::Display for EntityRoot {
126 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127 let id = &self.name;
128 write!(f, "{id}")
129 }
130}
131
132/// Implementation of `FromStr` for `EntityRoot` to create an entity root from a string.
133impl std::str::FromStr for EntityRoot {
134 type Err = ErnError;
135
136 /// Creates an `EntityRoot` from a string.
137 ///
138 /// This method generates a time-ordered, unique identifier using the UUID v7 algorithm.
139 /// Each call to this method with the same input string will generate a different ID,
140 /// as it incorporates the current timestamp.
141 ///
142 /// # Arguments
143 ///
144 /// * `s` - The string value to use as the base for the entity root ID
145 ///
146 /// # Returns
147 ///
148 /// * `Ok(EntityRoot)` - If validation passes
149 /// * `Err(ErnError)` - If validation fails
150 fn from_str(s: &str) -> Result<Self, Self::Err> {
151 // Check if empty
152 if s.is_empty() {
153 return Err(ErnError::ParseFailure("EntityRoot", "cannot be empty".to_string()));
154 }
155
156 // Check length
157 if s.len() > 255 {
158 return Err(ErnError::ParseFailure(
159 "EntityRoot",
160 format!("length exceeds maximum of 255 characters (got {})", s.len())
161 ));
162 }
163
164 Ok(EntityRoot {
165 name: s.create_type_id::<V7>(),
166 })
167 }
168}
169
170#[cfg(feature = "serde")]
171impl Serialize for EntityRoot {
172 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
173 where
174 S: Serializer,
175 {
176 // Serialize the MagicTypeId as a string
177 serializer.serialize_str(self.name.as_ref())
178 }
179}
180
181#[cfg(feature = "serde")]
182impl<'de> Deserialize<'de> for EntityRoot {
183 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
184 where
185 D: Deserializer<'de>,
186 {
187 // Deserialize as a string, then create a new EntityRoot
188 let s = String::deserialize(deserializer)?;
189 EntityRoot::new(s).map_err(serde::de::Error::custom)
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196 use std::str::FromStr;
197
198 #[test]
199 fn test_entity_root_creation() -> anyhow::Result<()> {
200 let root = EntityRoot::new("test-entity".to_string())?;
201 assert!(!root.to_string().is_empty());
202 Ok(())
203 }
204
205 #[test]
206 fn test_entity_root_uniqueness() -> anyhow::Result<()> {
207 // EntityRoot should generate different IDs for the same input (non-deterministic)
208 let root1 = EntityRoot::new("same-content".to_string())?;
209 let root2 = EntityRoot::new("same-content".to_string())?;
210
211 // The string representations should be different
212 assert_ne!(root1.to_string(), root2.to_string());
213 Ok(())
214 }
215
216 #[test]
217 fn test_entity_root_from_str() -> anyhow::Result<()> {
218 let root = EntityRoot::from_str("test-entity")?;
219 assert!(!root.to_string().is_empty());
220 Ok(())
221 }
222
223 #[test]
224 fn test_entity_root_validation_empty() {
225 let result = EntityRoot::new("".to_string());
226 assert!(result.is_err());
227 match result {
228 Err(ErnError::ParseFailure(component, msg)) => {
229 assert_eq!(component, "EntityRoot");
230 assert!(msg.contains("empty"));
231 }
232 _ => panic!("Expected ParseFailure error for empty EntityRoot"),
233 }
234 }
235
236 #[test]
237 fn test_entity_root_validation_too_long() {
238 let long_value = "a".repeat(256);
239 let result = EntityRoot::new(long_value);
240 assert!(result.is_err());
241 match result {
242 Err(ErnError::ParseFailure(component, msg)) => {
243 assert_eq!(component, "EntityRoot");
244 assert!(msg.contains("length exceeds maximum"));
245 }
246 _ => panic!("Expected ParseFailure error for too long EntityRoot"),
247 }
248 }
249
250 #[test]
251 fn test_entity_root_from_str_validation() {
252 let result = EntityRoot::from_str("");
253 assert!(result.is_err());
254 match result {
255 Err(ErnError::ParseFailure(component, msg)) => {
256 assert_eq!(component, "EntityRoot");
257 assert!(msg.contains("empty"));
258 }
259 _ => panic!("Expected ParseFailure error for empty EntityRoot from_str"),
260 }
261 }
262}