scim_server/resource/value_objects/
email_address.rs

1//! EmailAddress value object for SCIM email addresses.
2//!
3//! This module provides a type-safe wrapper around email addresses with built-in validation.
4//! Email addresses are complex multi-valued attributes in SCIM with specific structure requirements.
5
6use crate::error::{ValidationError, ValidationResult};
7use crate::resource::value_objects::value_object_trait::{SchemaConstructible, ValueObject};
8use crate::schema::types::{AttributeDefinition, AttributeType};
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use std::any::Any;
12use std::fmt;
13
14/// A validated SCIM email address.
15///
16/// EmailAddress represents an email address with optional metadata like type and primary flag.
17/// It enforces validation rules at construction time, ensuring that only valid email addresses
18/// can exist in the system.
19///
20/// ## Validation Rules
21///
22/// - Email value must not be empty
23/// - Email value must be a valid string
24/// - Email type, if provided, must not be empty
25/// - Primary flag is optional
26/// - Display name is optional
27///
28/// ## Examples
29///
30/// ```rust
31/// use scim_server::resource::value_objects::EmailAddress;
32///
33/// fn main() -> Result<(), Box<dyn std::error::Error>> {
34///     // Valid email address
35///     let email = EmailAddress::new(
36///         "bjensen@example.com".to_string(),
37///         Some("work".to_string()),
38///         Some(true),
39///         Some("Barbara Jensen".to_string())
40///     )?;
41///     println!("Email: {}", email.value());
42///
43///     // Simple email without metadata
44///     let simple_email = EmailAddress::new_simple("user@example.com".to_string())?;
45///
46///     Ok(())
47/// }
48/// ```
49#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
50pub struct EmailAddress {
51    pub value: String,
52    #[serde(rename = "type")]
53    pub email_type: Option<String>,
54    pub primary: Option<bool>,
55    pub display: Option<String>,
56}
57
58impl EmailAddress {
59    /// Create a new EmailAddress with full metadata.
60    ///
61    /// This is the primary constructor that enforces all validation rules.
62    /// Use this method when creating EmailAddress instances from untrusted input.
63    ///
64    /// # Arguments
65    ///
66    /// * `value` - The email address string to validate
67    /// * `email_type` - Optional type designation (e.g., "work", "home")
68    /// * `primary` - Optional flag indicating if this is the primary email
69    /// * `display` - Optional display name for the email
70    ///
71    /// # Returns
72    ///
73    /// * `Ok(EmailAddress)` - If all values are valid
74    /// * `Err(ValidationError)` - If any value violates validation rules
75    pub fn new(
76        value: String,
77        email_type: Option<String>,
78        primary: Option<bool>,
79        display: Option<String>,
80    ) -> ValidationResult<Self> {
81        Self::validate_value(&value)?;
82        if let Some(ref type_val) = email_type {
83            Self::validate_type(type_val)?;
84        }
85        if let Some(ref display_val) = display {
86            Self::validate_display(display_val)?;
87        }
88
89        Ok(Self {
90            value,
91            email_type,
92            primary,
93            display,
94        })
95    }
96
97    /// Create a simple EmailAddress with just the email value.
98    ///
99    /// Convenience constructor for creating email addresses without metadata.
100    ///
101    /// # Arguments
102    ///
103    /// * `value` - The email address string to validate
104    ///
105    /// # Returns
106    ///
107    /// * `Ok(EmailAddress)` - If the value is valid
108    /// * `Err(ValidationError)` - If the value violates validation rules
109    pub fn new_simple(value: String) -> ValidationResult<Self> {
110        Self::new(value, None, None, None)
111    }
112
113    /// Get the email address value.
114    pub fn value(&self) -> &str {
115        &self.value
116    }
117
118    /// Get the email type.
119    pub fn email_type(&self) -> Option<&str> {
120        self.email_type.as_deref()
121    }
122
123    /// Get the primary flag.
124    pub fn primary(&self) -> Option<bool> {
125        self.primary
126    }
127
128    /// Get the display name.
129    pub fn display(&self) -> Option<&str> {
130        self.display.as_deref()
131    }
132
133    /// Check if this is marked as the primary email.
134    pub fn is_primary(&self) -> bool {
135        self.primary.unwrap_or(false)
136    }
137
138    /// Validate the email address value.
139    fn validate_value(value: &str) -> ValidationResult<()> {
140        if value.is_empty() {
141            return Err(ValidationError::MissingRequiredSubAttribute {
142                attribute: "emails".to_string(),
143                sub_attribute: "value".to_string(),
144            });
145        }
146
147        // TODO: Add more sophisticated email validation if needed
148        // For now, we accept any non-empty string as a valid email
149        // Future enhancements might include:
150        // - RFC 5322 email format validation
151        // - Domain validation
152        // - Length limits
153        // - Character set restrictions
154
155        Ok(())
156    }
157
158    /// Validate the email type value.
159    fn validate_type(email_type: &str) -> ValidationResult<()> {
160        if email_type.is_empty() {
161            return Err(ValidationError::InvalidStringFormat {
162                attribute: "emails.type".to_string(),
163                details: "Email type cannot be empty".to_string(),
164            });
165        }
166
167        // TODO: Add canonical value validation if needed
168        // Common types include: "work", "home", "other"
169
170        Ok(())
171    }
172
173    /// Validate the display name value.
174    fn validate_display(_display: &str) -> ValidationResult<()> {
175        // Display name can be empty, so no validation needed for now
176        // Future enhancements might include:
177        // - Length limits
178        // - Character set restrictions
179
180        Ok(())
181    }
182}
183
184impl fmt::Display for EmailAddress {
185    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186        if let Some(ref display) = self.display {
187            write!(f, "{} <{}>", display, self.value)
188        } else {
189            write!(f, "{}", self.value)
190        }
191    }
192}
193
194impl TryFrom<String> for EmailAddress {
195    type Error = ValidationError;
196
197    fn try_from(value: String) -> ValidationResult<Self> {
198        Self::new_simple(value)
199    }
200}
201
202impl TryFrom<&str> for EmailAddress {
203    type Error = ValidationError;
204
205    fn try_from(value: &str) -> ValidationResult<Self> {
206        Self::new_simple(value.to_string())
207    }
208}
209
210impl ValueObject for EmailAddress {
211    fn attribute_type(&self) -> AttributeType {
212        AttributeType::Complex
213    }
214
215    fn attribute_name(&self) -> &str {
216        "emails"
217    }
218
219    fn to_json(&self) -> ValidationResult<Value> {
220        Ok(serde_json::to_value(self)?)
221    }
222
223    fn validate_against_schema(&self, definition: &AttributeDefinition) -> ValidationResult<()> {
224        if definition.data_type != AttributeType::Complex {
225            return Err(ValidationError::InvalidAttributeType {
226                attribute: definition.name.clone(),
227                expected: "complex".to_string(),
228                actual: format!("{:?}", definition.data_type),
229            });
230        }
231
232        // EmailAddress can work with both "emails" and "value" attribute names
233        if definition.name != "emails" && definition.name != "value" {
234            return Err(ValidationError::InvalidAttributeName {
235                actual: definition.name.clone(),
236                expected: "emails or value".to_string(),
237            });
238        }
239
240        Ok(())
241    }
242
243    fn as_json_value(&self) -> Value {
244        serde_json::to_value(self).unwrap_or(Value::Null)
245    }
246
247    fn supports_definition(&self, definition: &AttributeDefinition) -> bool {
248        definition.data_type == AttributeType::Complex
249            && (definition.name == "emails" || definition.name == "value")
250    }
251
252    fn clone_boxed(&self) -> Box<dyn ValueObject> {
253        Box::new(self.clone())
254    }
255
256    fn as_any(&self) -> &dyn Any {
257        self
258    }
259}
260
261impl SchemaConstructible for EmailAddress {
262    fn from_schema_and_value(
263        definition: &AttributeDefinition,
264        value: &Value,
265    ) -> ValidationResult<Self> {
266        if definition.data_type != AttributeType::Complex {
267            return Err(ValidationError::UnsupportedAttributeType {
268                attribute: definition.name.clone(),
269                type_name: format!("{:?}", definition.data_type),
270            });
271        }
272
273        if let Value::Object(obj) = value {
274            let email_value = obj.get("value").and_then(|v| v.as_str()).ok_or_else(|| {
275                ValidationError::InvalidAttributeType {
276                    attribute: definition.name.clone(),
277                    expected: "object with 'value' field".to_string(),
278                    actual: "object without 'value' field".to_string(),
279                }
280            })?;
281
282            let email_type = obj
283                .get("type")
284                .and_then(|v| v.as_str())
285                .map(|s| s.to_string());
286
287            let primary = obj.get("primary").and_then(|v| v.as_bool());
288
289            let display = obj
290                .get("display")
291                .and_then(|v| v.as_str())
292                .map(|s| s.to_string());
293
294            Self::new(email_value.to_string(), email_type, primary, display)
295        } else if let Some(email_str) = value.as_str() {
296            // Handle simple string case
297            Self::new(email_str.to_string(), None, None, None)
298        } else {
299            Err(ValidationError::InvalidAttributeType {
300                attribute: definition.name.clone(),
301                expected: "object or string".to_string(),
302                actual: "neither object nor string".to_string(),
303            })
304        }
305    }
306
307    fn can_construct_from(definition: &AttributeDefinition) -> bool {
308        definition.data_type == AttributeType::Complex
309            && (definition.name == "emails"
310                || definition.name == "value"
311                || definition.name.contains("email"))
312    }
313
314    fn constructor_priority() -> u8 {
315        80 // Lower priority than exact name matches
316    }
317}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322    use serde_json;
323
324    #[test]
325    fn test_valid_email_full() {
326        let email = EmailAddress::new(
327            "bjensen@example.com".to_string(),
328            Some("work".to_string()),
329            Some(true),
330            Some("Barbara Jensen".to_string()),
331        );
332        assert!(email.is_ok());
333
334        let email = email.unwrap();
335        assert_eq!(email.value(), "bjensen@example.com");
336        assert_eq!(email.email_type(), Some("work"));
337        assert_eq!(email.primary(), Some(true));
338        assert_eq!(email.display(), Some("Barbara Jensen"));
339        assert!(email.is_primary());
340    }
341
342    #[test]
343    fn test_valid_email_simple() {
344        let email = EmailAddress::new_simple("user@example.com".to_string());
345        assert!(email.is_ok());
346
347        let email = email.unwrap();
348        assert_eq!(email.value(), "user@example.com");
349        assert_eq!(email.email_type(), None);
350        assert_eq!(email.primary(), None);
351        assert_eq!(email.display(), None);
352        assert!(!email.is_primary());
353    }
354
355    #[test]
356    fn test_empty_email_value() {
357        let result = EmailAddress::new_simple("".to_string());
358        assert!(result.is_err());
359
360        match result.unwrap_err() {
361            ValidationError::MissingRequiredSubAttribute {
362                attribute,
363                sub_attribute,
364            } => {
365                assert_eq!(attribute, "emails");
366                assert_eq!(sub_attribute, "value");
367            }
368            other => panic!(
369                "Expected MissingRequiredSubAttribute error, got: {:?}",
370                other
371            ),
372        }
373    }
374
375    #[test]
376    fn test_empty_email_type() {
377        let result = EmailAddress::new(
378            "test@example.com".to_string(),
379            Some("".to_string()),
380            None,
381            None,
382        );
383        assert!(result.is_err());
384
385        match result.unwrap_err() {
386            ValidationError::InvalidStringFormat { attribute, details } => {
387                assert_eq!(attribute, "emails.type");
388                assert!(details.contains("cannot be empty"));
389            }
390            other => panic!("Expected InvalidStringFormat error, got: {:?}", other),
391        }
392    }
393
394    #[test]
395    fn test_display() {
396        let email_with_display = EmailAddress::new(
397            "test@example.com".to_string(),
398            None,
399            None,
400            Some("Test User".to_string()),
401        )
402        .unwrap();
403        assert_eq!(
404            format!("{}", email_with_display),
405            "Test User <test@example.com>"
406        );
407
408        let email_without_display =
409            EmailAddress::new_simple("test@example.com".to_string()).unwrap();
410        assert_eq!(format!("{}", email_without_display), "test@example.com");
411    }
412
413    #[test]
414    fn test_serialization() {
415        let email = EmailAddress::new(
416            "serialize@example.com".to_string(),
417            Some("work".to_string()),
418            Some(true),
419            Some("Serialize Test".to_string()),
420        )
421        .unwrap();
422
423        let json = serde_json::to_string(&email).unwrap();
424        let expected = r#"{"value":"serialize@example.com","type":"work","primary":true,"display":"Serialize Test"}"#;
425        assert_eq!(json, expected);
426    }
427
428    #[test]
429    fn test_deserialization() {
430        let json = r#"{"value":"deserialize@example.com","type":"home","primary":false}"#;
431        let email: EmailAddress = serde_json::from_str(json).unwrap();
432
433        assert_eq!(email.value(), "deserialize@example.com");
434        assert_eq!(email.email_type(), Some("home"));
435        assert_eq!(email.primary(), Some(false));
436        assert_eq!(email.display(), None);
437    }
438
439    #[test]
440    fn test_try_from_string() {
441        let result = EmailAddress::try_from("try-from@example.com".to_string());
442        assert!(result.is_ok());
443        assert_eq!(result.unwrap().value(), "try-from@example.com");
444
445        let empty_result = EmailAddress::try_from("".to_string());
446        assert!(empty_result.is_err());
447    }
448
449    #[test]
450    fn test_try_from_str() {
451        let result = EmailAddress::try_from("try-from-str@example.com");
452        assert!(result.is_ok());
453        assert_eq!(result.unwrap().value(), "try-from-str@example.com");
454
455        let empty_result = EmailAddress::try_from("");
456        assert!(empty_result.is_err());
457    }
458
459    #[test]
460    fn test_equality() {
461        let email1 = EmailAddress::new(
462            "same@example.com".to_string(),
463            Some("work".to_string()),
464            Some(true),
465            None,
466        )
467        .unwrap();
468        let email2 = EmailAddress::new(
469            "same@example.com".to_string(),
470            Some("work".to_string()),
471            Some(true),
472            None,
473        )
474        .unwrap();
475        let email3 = EmailAddress::new_simple("different@example.com".to_string()).unwrap();
476
477        assert_eq!(email1, email2);
478        assert_ne!(email1, email3);
479    }
480
481    #[test]
482    fn test_hash() {
483        use std::collections::HashMap;
484
485        let email1 = EmailAddress::new_simple("hash-test-1@example.com".to_string()).unwrap();
486        let email2 = EmailAddress::new_simple("hash-test-2@example.com".to_string()).unwrap();
487
488        let mut map = HashMap::new();
489        map.insert(email1.clone(), "value1");
490        map.insert(email2.clone(), "value2");
491
492        assert_eq!(map.get(&email1), Some(&"value1"));
493        assert_eq!(map.get(&email2), Some(&"value2"));
494    }
495
496    #[test]
497    fn test_clone() {
498        let email = EmailAddress::new(
499            "clone@example.com".to_string(),
500            Some("work".to_string()),
501            Some(true),
502            Some("Clone Test".to_string()),
503        )
504        .unwrap();
505        let cloned = email.clone();
506
507        assert_eq!(email, cloned);
508        assert_eq!(email.value(), cloned.value());
509        assert_eq!(email.email_type(), cloned.email_type());
510        assert_eq!(email.primary(), cloned.primary());
511        assert_eq!(email.display(), cloned.display());
512    }
513}