scim_server/resource/value_objects/
external_id.rs1use crate::error::{ValidationError, ValidationResult};
7use crate::resource::value_objects::value_object_trait::{SchemaConstructible, ValueObject};
8use crate::schema::types::{AttributeDefinition, AttributeType};
9use serde::{Deserialize, Deserializer, Serialize, Serializer};
10use serde_json::Value;
11use std::any::Any;
12use std::fmt;
13
14#[derive(Debug, Clone, PartialEq, Eq, Hash)]
44pub struct ExternalId(String);
45
46impl ExternalId {
47 pub fn new(value: String) -> ValidationResult<Self> {
61 Self::validate_format(&value)?;
62 Ok(Self(value))
63 }
64
65 pub fn as_str(&self) -> &str {
67 &self.0
68 }
69
70 pub fn into_string(self) -> String {
72 self.0
73 }
74
75 fn validate_format(value: &str) -> ValidationResult<()> {
79 if value.is_empty() {
81 return Err(ValidationError::InvalidExternalId);
82 }
83
84 Ok(())
92 }
93}
94
95impl fmt::Display for ExternalId {
96 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 write!(f, "{}", self.0)
98 }
99}
100
101impl Serialize for ExternalId {
102 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
103 where
104 S: Serializer,
105 {
106 self.0.serialize(serializer)
107 }
108}
109
110impl<'de> Deserialize<'de> for ExternalId {
111 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
112 where
113 D: Deserializer<'de>,
114 {
115 let value = String::deserialize(deserializer)?;
116 Self::new(value).map_err(serde::de::Error::custom)
117 }
118}
119
120impl TryFrom<String> for ExternalId {
121 type Error = ValidationError;
122
123 fn try_from(value: String) -> ValidationResult<Self> {
124 Self::new(value)
125 }
126}
127
128impl TryFrom<&str> for ExternalId {
129 type Error = ValidationError;
130
131 fn try_from(value: &str) -> ValidationResult<Self> {
132 Self::new(value.to_string())
133 }
134}
135
136impl ValueObject for ExternalId {
137 fn attribute_type(&self) -> AttributeType {
138 AttributeType::String
139 }
140
141 fn attribute_name(&self) -> &str {
142 "externalId"
143 }
144
145 fn to_json(&self) -> ValidationResult<Value> {
146 Ok(Value::String(self.0.clone()))
147 }
148
149 fn validate_against_schema(&self, definition: &AttributeDefinition) -> ValidationResult<()> {
150 if definition.data_type != AttributeType::String {
151 return Err(ValidationError::InvalidAttributeType {
152 attribute: definition.name.clone(),
153 expected: "string".to_string(),
154 actual: format!("{:?}", definition.data_type),
155 });
156 }
157
158 if definition.name != "externalId" {
159 return Err(ValidationError::InvalidAttributeName {
160 actual: definition.name.clone(),
161 expected: "externalId".to_string(),
162 });
163 }
164
165 Ok(())
166 }
167
168 fn as_json_value(&self) -> Value {
169 Value::String(self.0.clone())
170 }
171
172 fn supports_definition(&self, definition: &AttributeDefinition) -> bool {
173 definition.data_type == AttributeType::String && definition.name == "externalId"
174 }
175
176 fn clone_boxed(&self) -> Box<dyn ValueObject> {
177 Box::new(self.clone())
178 }
179
180 fn as_any(&self) -> &dyn Any {
181 self
182 }
183}
184
185impl SchemaConstructible for ExternalId {
186 fn from_schema_and_value(
187 definition: &AttributeDefinition,
188 value: &Value,
189 ) -> ValidationResult<Self> {
190 if definition.name != "externalId" || definition.data_type != AttributeType::String {
191 return Err(ValidationError::UnsupportedAttributeType {
192 attribute: definition.name.clone(),
193 type_name: format!("{:?}", definition.data_type),
194 });
195 }
196
197 if let Some(ext_id_str) = value.as_str() {
198 Self::new(ext_id_str.to_string())
199 } else {
200 Err(ValidationError::InvalidAttributeType {
201 attribute: definition.name.clone(),
202 expected: "string".to_string(),
203 actual: "non-string".to_string(),
204 })
205 }
206 }
207
208 fn can_construct_from(definition: &AttributeDefinition) -> bool {
209 definition.name == "externalId" && definition.data_type == AttributeType::String
210 }
211
212 fn constructor_priority() -> u8 {
213 100 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220 use serde_json;
221
222 #[test]
223 fn test_valid_external_id() {
224 let ext_id = ExternalId::new("701984".to_string());
225 assert!(ext_id.is_ok());
226 assert_eq!(ext_id.unwrap().as_str(), "701984");
227 }
228
229 #[test]
230 fn test_valid_external_id_alphanumeric() {
231 let ext_id = ExternalId::new("EXT-123-ABC".to_string());
232 assert!(ext_id.is_ok());
233 assert_eq!(ext_id.unwrap().as_str(), "EXT-123-ABC");
234 }
235
236 #[test]
237 fn test_empty_external_id() {
238 let result = ExternalId::new("".to_string());
239 assert!(result.is_err());
240
241 match result.unwrap_err() {
242 ValidationError::InvalidExternalId => {} other => panic!("Expected InvalidExternalId error, got: {:?}", other),
244 }
245 }
246
247 #[test]
248 fn test_into_string() {
249 let ext_id = ExternalId::new("test-ext-id".to_string()).unwrap();
250 let string_value = ext_id.into_string();
251 assert_eq!(string_value, "test-ext-id");
252 }
253
254 #[test]
255 fn test_display() {
256 let ext_id = ExternalId::new("display-test".to_string()).unwrap();
257 assert_eq!(format!("{}", ext_id), "display-test");
258 }
259
260 #[test]
261 fn test_serialization() {
262 let ext_id = ExternalId::new("serialize-test".to_string()).unwrap();
263 let json = serde_json::to_string(&ext_id).unwrap();
264 assert_eq!(json, "\"serialize-test\"");
265 }
266
267 #[test]
268 fn test_deserialization_valid() {
269 let json = "\"deserialize-test\"";
270 let ext_id: ExternalId = serde_json::from_str(json).unwrap();
271 assert_eq!(ext_id.as_str(), "deserialize-test");
272 }
273
274 #[test]
275 fn test_deserialization_invalid() {
276 let json = "\"\""; let result: Result<ExternalId, _> = serde_json::from_str(json);
278 assert!(result.is_err());
279 }
280
281 #[test]
282 fn test_try_from_string() {
283 let result = ExternalId::try_from("try-from-test".to_string());
284 assert!(result.is_ok());
285 assert_eq!(result.unwrap().as_str(), "try-from-test");
286
287 let empty_result = ExternalId::try_from("".to_string());
288 assert!(empty_result.is_err());
289 }
290
291 #[test]
292 fn test_try_from_str() {
293 let result = ExternalId::try_from("try-from-str-test");
294 assert!(result.is_ok());
295 assert_eq!(result.unwrap().as_str(), "try-from-str-test");
296
297 let empty_result = ExternalId::try_from("");
298 assert!(empty_result.is_err());
299 }
300
301 #[test]
302 fn test_equality() {
303 let ext_id1 = ExternalId::new("same-ext-id".to_string()).unwrap();
304 let ext_id2 = ExternalId::new("same-ext-id".to_string()).unwrap();
305 let ext_id3 = ExternalId::new("different-ext-id".to_string()).unwrap();
306
307 assert_eq!(ext_id1, ext_id2);
308 assert_ne!(ext_id1, ext_id3);
309 }
310
311 #[test]
312 fn test_hash() {
313 use std::collections::HashMap;
314
315 let ext_id1 = ExternalId::new("hash-test-1".to_string()).unwrap();
316 let ext_id2 = ExternalId::new("hash-test-2".to_string()).unwrap();
317
318 let mut map = HashMap::new();
319 map.insert(ext_id1.clone(), "value1");
320 map.insert(ext_id2.clone(), "value2");
321
322 assert_eq!(map.get(&ext_id1), Some(&"value1"));
323 assert_eq!(map.get(&ext_id2), Some(&"value2"));
324 }
325
326 #[test]
327 fn test_clone() {
328 let ext_id = ExternalId::new("clone-test".to_string()).unwrap();
329 let cloned = ext_id.clone();
330
331 assert_eq!(ext_id, cloned);
332 assert_eq!(ext_id.as_str(), cloned.as_str());
333 }
334}