scim_server/resource/value_objects/
group_member.rs

1//! Group membership value objects for SCIM resources.
2//!
3//! This module provides value objects for handling group membership relationships
4//! in SCIM resources, following SCIM 2.0 specifications for group membership.
5//!
6//! ## Design Principles
7//!
8//! - **Resource Relationships**: Type-safe representation of group member relationships
9//! - **SCIM Compliance**: Follows SCIM 2.0 group membership attribute patterns
10//! - **Multi-Valued Support**: Integrates with MultiValuedAttribute for collections
11//! - **Type Safety**: Ensures valid member references and display names
12//!
13//! ## Usage Pattern
14//!
15//! ```rust
16//! use scim_server::resource::value_objects::{GroupMember, GroupMembers, ResourceId};
17//! use scim_server::error::ValidationResult;
18//!
19//! fn main() -> Result<(), Box<dyn std::error::Error>> {
20//!     // Create individual group member
21//!     let member_id = ResourceId::new("user-123".to_string())?;
22//!     let member = GroupMember::new(member_id, Some("John Doe".to_string()), Some("User".to_string()))?;
23//!
24//!     // Create collection of group members
25//!     let members = vec![member];
26//!     let group_members = GroupMembers::new(members)?;
27//!
28//!     // Access members
29//!     for member in group_members.iter() {
30//!         println!("Member: {} ({})", member.display_name().unwrap_or("Unknown"), member.value().as_str());
31//!     }
32//!
33//!     Ok(())
34//! }
35//! ```
36
37use crate::error::{ValidationError, ValidationResult};
38use crate::resource::value_objects::{MultiValuedAttribute, ResourceId};
39use serde::{Deserialize, Serialize};
40use std::fmt;
41
42/// A value object representing a single group member in SCIM.
43///
44/// This type encapsulates the relationship between a group and its member,
45/// including the member's resource ID, display name, and member type.
46///
47/// # Examples
48///
49/// ```rust
50/// use scim_server::resource::value_objects::{GroupMember, ResourceId};
51///
52/// fn main() -> Result<(), Box<dyn std::error::Error>> {
53///     let member_id = ResourceId::new("user-123".to_string())?;
54///     let member = GroupMember::new(
55///         member_id,
56///         Some("John Doe".to_string()),
57///         Some("User".to_string())
58///     )?;
59///
60///     assert_eq!(member.display_name(), Some("John Doe"));
61///     assert_eq!(member.member_type(), Some("User"));
62///
63///     Ok(())
64/// }
65/// ```
66#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
67pub struct GroupMember {
68    /// The unique identifier of the member resource
69    value: ResourceId,
70    /// Human-readable display name for the member
71    #[serde(skip_serializing_if = "Option::is_none")]
72    display: Option<String>,
73    /// The type of member (e.g., "User", "Group")
74    #[serde(skip_serializing_if = "Option::is_none")]
75    #[serde(rename = "type")]
76    member_type: Option<String>,
77}
78
79impl GroupMember {
80    /// Creates a new group member with validation.
81    ///
82    /// # Arguments
83    ///
84    /// * `value` - The resource ID of the member
85    /// * `display` - Optional display name for the member
86    /// * `member_type` - Optional type of the member (e.g., "User", "Group")
87    ///
88    /// # Returns
89    ///
90    /// * `Ok(GroupMember)` - Successfully created group member
91    /// * `Err(ValidationError)` - If validation fails
92    ///
93    /// # Examples
94    ///
95    /// ```rust
96    /// use scim_server::resource::value_objects::{GroupMember, ResourceId};
97    ///
98    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
99    ///     let member_id = ResourceId::new("user-123".to_string())?;
100    ///     let member = GroupMember::new(
101    ///         member_id,
102    ///         Some("John Doe".to_string()),
103    ///         Some("User".to_string())
104    ///     )?;
105    ///
106    ///     Ok(())
107    /// }
108    /// ```
109    pub fn new(
110        value: ResourceId,
111        display: Option<String>,
112        member_type: Option<String>,
113    ) -> ValidationResult<Self> {
114        // Validate display name if provided
115        if let Some(ref display_name) = display {
116            Self::validate_display_name(display_name)?;
117        }
118
119        // Validate member type if provided
120        if let Some(ref mtype) = member_type {
121            Self::validate_member_type(mtype)?;
122        }
123
124        Ok(Self {
125            value,
126            display,
127            member_type,
128        })
129    }
130
131    /// Creates a new group member for a User resource.
132    ///
133    /// # Arguments
134    ///
135    /// * `value` - The resource ID of the user
136    /// * `display` - Optional display name for the user
137    ///
138    /// # Returns
139    ///
140    /// A group member with member_type set to "User"
141    ///
142    /// # Examples
143    ///
144    /// ```rust
145    /// use scim_server::resource::value_objects::{GroupMember, ResourceId};
146    ///
147    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
148    ///     let user_id = ResourceId::new("user-123".to_string())?;
149    ///     let member = GroupMember::new_user(user_id, Some("John Doe".to_string()))?;
150    ///     assert_eq!(member.member_type(), Some("User"));
151    ///
152    ///     Ok(())
153    /// }
154    /// ```
155    pub fn new_user(value: ResourceId, display: Option<String>) -> ValidationResult<Self> {
156        Self::new(value, display, Some("User".to_string()))
157    }
158
159    /// Creates a new group member for a Group resource.
160    ///
161    /// # Arguments
162    ///
163    /// * `value` - The resource ID of the group
164    /// * `display` - Optional display name for the group
165    ///
166    /// # Returns
167    ///
168    /// A group member with member_type set to "Group"
169    ///
170    /// # Examples
171    ///
172    /// ```rust
173    /// use scim_server::resource::value_objects::{GroupMember, ResourceId};
174    ///
175    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
176    ///     let group_id = ResourceId::new("group-456".to_string())?;
177    ///     let member = GroupMember::new_group(group_id, Some("Admin Group".to_string()))?;
178    ///     assert_eq!(member.member_type(), Some("Group"));
179    ///
180    ///     Ok(())
181    /// }
182    /// ```
183    pub fn new_group(value: ResourceId, display: Option<String>) -> ValidationResult<Self> {
184        Self::new(value, display, Some("Group".to_string()))
185    }
186
187    /// Creates a new group member without validation for internal use.
188    ///
189    /// This method bypasses validation and should only be used internally
190    /// where the inputs are already known to be valid.
191    ///
192    /// # Arguments
193
194    /// Returns the resource ID of the member.
195    ///
196    /// # Examples
197    ///
198    /// ```rust
199    /// use scim_server::resource::value_objects::{GroupMember, ResourceId};
200    ///
201    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
202    ///     let user_id = ResourceId::new("user-123".to_string())?;
203    ///     let member = GroupMember::new_user(user_id, None)?;
204    ///     let id = member.value();
205    ///     println!("Member ID: {}", id.as_str());
206    ///     Ok(())
207    /// }
208    /// ```
209    pub fn value(&self) -> &ResourceId {
210        &self.value
211    }
212
213    /// Returns the display name of the member, if set.
214    ///
215    /// # Examples
216    ///
217    /// ```rust
218    /// use scim_server::resource::value_objects::{GroupMember, ResourceId};
219    ///
220    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
221    ///     let user_id = ResourceId::new("user-123".to_string())?;
222    ///     let member = GroupMember::new_user(user_id, Some("John Doe".to_string()))?;
223    ///     if let Some(name) = member.display_name() {
224    ///         println!("Member name: {}", name);
225    ///     }
226    ///     Ok(())
227    /// }
228    /// ```
229    pub fn display_name(&self) -> Option<&str> {
230        self.display.as_deref()
231    }
232
233    /// Returns the member type, if set.
234    ///
235    /// # Examples
236    ///
237    /// ```rust
238    /// use scim_server::resource::value_objects::{GroupMember, ResourceId};
239    ///
240    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
241    ///     let user_id = ResourceId::new("user-123".to_string())?;
242    ///     let member = GroupMember::new_user(user_id, None)?;
243    ///     if let Some(mtype) = member.member_type() {
244    ///         println!("Member type: {}", mtype);
245    ///     }
246    ///     Ok(())
247    /// }
248    /// ```
249    pub fn member_type(&self) -> Option<&str> {
250        self.member_type.as_deref()
251    }
252
253    /// Returns true if this member represents a User resource.
254    ///
255    /// # Examples
256    ///
257    /// ```rust
258    /// use scim_server::resource::value_objects::{GroupMember, ResourceId};
259    ///
260    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
261    ///     let user_id = ResourceId::new("user-123".to_string())?;
262    ///     let user_member = GroupMember::new_user(user_id, None)?;
263    ///     assert!(user_member.is_user());
264    ///     Ok(())
265    /// }
266    /// ```
267    pub fn is_user(&self) -> bool {
268        self.member_type.as_deref() == Some("User")
269    }
270
271    /// Returns true if this member represents a Group resource.
272    ///
273    /// # Examples
274    ///
275    /// ```rust
276    /// use scim_server::resource::value_objects::{GroupMember, ResourceId};
277    ///
278    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
279    ///     let group_id = ResourceId::new("group-456".to_string())?;
280    ///     let group_member = GroupMember::new_group(group_id, None)?;
281    ///     assert!(group_member.is_group());
282    ///     Ok(())
283    /// }
284    /// ```
285    pub fn is_group(&self) -> bool {
286        self.member_type.as_deref() == Some("Group")
287    }
288
289    /// Returns the effective display name for the member.
290    ///
291    /// This returns the display name if set, otherwise falls back to the resource ID.
292    ///
293    /// # Examples
294    ///
295    /// ```rust
296    /// use scim_server::resource::value_objects::{GroupMember, ResourceId};
297    ///
298    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
299    ///     let user_id = ResourceId::new("user-123".to_string())?;
300    ///     let member_with_name = GroupMember::new_user(user_id.clone(), Some("John".to_string()))?;
301    ///     assert_eq!(member_with_name.effective_display_name(), "John");
302    ///
303    ///     let member_without_name = GroupMember::new_user(user_id, None)?;
304    ///     assert_eq!(member_without_name.effective_display_name(), "user-123");
305    ///     Ok(())
306    /// }
307    /// ```
308    pub fn effective_display_name(&self) -> &str {
309        self.display.as_deref().unwrap_or(self.value.as_str())
310    }
311
312    /// Validates a display name.
313    ///
314    /// # Arguments
315    ///
316    /// * `display_name` - The display name to validate
317    ///
318    /// # Returns
319    ///
320    /// * `Ok(())` - Display name is valid
321    /// * `Err(ValidationError)` - Display name is invalid
322    fn validate_display_name(display_name: &str) -> ValidationResult<()> {
323        if display_name.is_empty() {
324            return Err(ValidationError::custom("Display name cannot be empty"));
325        }
326
327        if display_name.len() > 256 {
328            return Err(ValidationError::custom(
329                "Display name cannot exceed 256 characters",
330            ));
331        }
332
333        // Check for control characters
334        if display_name.chars().any(|c| c.is_control() && c != '\t') {
335            return Err(ValidationError::custom(
336                "Display name cannot contain control characters",
337            ));
338        }
339
340        Ok(())
341    }
342
343    /// Validates a member type.
344    ///
345    /// # Arguments
346    ///
347    /// * `member_type` - The member type to validate
348    ///
349    /// # Returns
350    ///
351    /// * `Ok(())` - Member type is valid
352    /// * `Err(ValidationError)` - Member type is invalid
353    fn validate_member_type(member_type: &str) -> ValidationResult<()> {
354        if member_type.is_empty() {
355            return Err(ValidationError::custom("Member type cannot be empty"));
356        }
357
358        match member_type {
359            "User" | "Group" => Ok(()),
360            _ => Err(ValidationError::custom(format!(
361                "Invalid member type '{}'. Must be 'User' or 'Group'",
362                member_type
363            ))),
364        }
365    }
366}
367
368impl fmt::Display for GroupMember {
369    /// Formats the group member for display.
370    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
371        match (&self.display, &self.member_type) {
372            (Some(display), Some(mtype)) => {
373                write!(f, "{} ({}) [{}]", display, mtype, self.value.as_str())
374            }
375            (Some(display), None) => write!(f, "{} [{}]", display, self.value.as_str()),
376            (None, Some(mtype)) => write!(f, "({}) [{}]", mtype, self.value.as_str()),
377            (None, None) => write!(f, "[{}]", self.value.as_str()),
378        }
379    }
380}
381
382/// Type alias for a collection of group members using MultiValuedAttribute.
383///
384/// This provides a type-safe way to handle multiple group members with
385/// support for primary member designation if needed.
386///
387/// # Examples
388///
389/// ```rust
390/// use scim_server::resource::value_objects::{GroupMembers, GroupMember, ResourceId};
391///
392/// fn main() -> Result<(), Box<dyn std::error::Error>> {
393///     let members = vec![
394///         GroupMember::new_user(ResourceId::new("user1".to_string())?, Some("John".to_string()))?,
395///         GroupMember::new_user(ResourceId::new("user2".to_string())?, Some("Jane".to_string()))?,
396///     ];
397///
398///     let group_members = GroupMembers::new(members)?;
399///     assert_eq!(group_members.len(), 2);
400///     Ok(())
401/// }
402/// ```
403pub type GroupMembers = MultiValuedAttribute<GroupMember>;
404
405/// Type alias for a collection of email addresses using MultiValuedAttribute.
406///
407/// This provides a type-safe way to handle multiple email addresses with
408/// support for primary email designation.
409pub type MultiValuedEmails = MultiValuedAttribute<crate::resource::value_objects::EmailAddress>;
410
411/// Type alias for a collection of addresses using MultiValuedAttribute.
412///
413/// This provides a type-safe way to handle multiple addresses with
414/// support for primary address designation.
415pub type MultiValuedAddresses = MultiValuedAttribute<crate::resource::value_objects::Address>;
416
417/// Type alias for a collection of phone numbers using MultiValuedAttribute.
418///
419/// This provides a type-safe way to handle multiple phone numbers with
420/// support for primary phone number designation.
421pub type MultiValuedPhoneNumbers =
422    MultiValuedAttribute<crate::resource::value_objects::PhoneNumber>;
423
424#[cfg(test)]
425mod tests {
426    use super::*;
427
428    fn create_test_resource_id(id: &str) -> ResourceId {
429        ResourceId::new(id.to_string()).unwrap()
430    }
431
432    #[test]
433    fn test_group_member_new_valid() {
434        let member_id = create_test_resource_id("user-123");
435        let member = GroupMember::new(
436            member_id.clone(),
437            Some("John Doe".to_string()),
438            Some("User".to_string()),
439        )
440        .unwrap();
441
442        assert_eq!(member.value(), &member_id);
443        assert_eq!(member.display_name(), Some("John Doe"));
444        assert_eq!(member.member_type(), Some("User"));
445        assert!(member.is_user());
446        assert!(!member.is_group());
447    }
448
449    #[test]
450    fn test_group_member_new_user() {
451        let member_id = create_test_resource_id("user-123");
452        let member =
453            GroupMember::new_user(member_id.clone(), Some("John Doe".to_string())).unwrap();
454
455        assert_eq!(member.value(), &member_id);
456        assert_eq!(member.display_name(), Some("John Doe"));
457        assert_eq!(member.member_type(), Some("User"));
458        assert!(member.is_user());
459    }
460
461    #[test]
462    fn test_group_member_new_group() {
463        let group_id = create_test_resource_id("group-456");
464        let member =
465            GroupMember::new_group(group_id.clone(), Some("Admin Group".to_string())).unwrap();
466
467        assert_eq!(member.value(), &group_id);
468        assert_eq!(member.display_name(), Some("Admin Group"));
469        assert_eq!(member.member_type(), Some("Group"));
470        assert!(member.is_group());
471    }
472
473    #[test]
474    fn test_group_member_minimal() {
475        let member_id = create_test_resource_id("user-123");
476        let member = GroupMember::new(member_id.clone(), None, None).unwrap();
477
478        assert_eq!(member.value(), &member_id);
479        assert_eq!(member.display_name(), None);
480        assert_eq!(member.member_type(), None);
481        assert!(!member.is_user());
482        assert!(!member.is_group());
483    }
484
485    #[test]
486    fn test_group_member_invalid_display_name() {
487        let member_id = create_test_resource_id("user-123");
488
489        // Empty display name
490        let result = GroupMember::new(member_id.clone(), Some("".to_string()), None);
491        assert!(result.is_err());
492        assert!(result.unwrap_err().to_string().contains("cannot be empty"));
493
494        // Too long display name
495        let long_name = "a".repeat(257);
496        let result = GroupMember::new(member_id.clone(), Some(long_name), None);
497        assert!(result.is_err());
498        assert!(
499            result
500                .unwrap_err()
501                .to_string()
502                .contains("cannot exceed 256")
503        );
504
505        // Control characters
506        let result = GroupMember::new(member_id, Some("John\x00Doe".to_string()), None);
507        assert!(result.is_err());
508        assert!(
509            result
510                .unwrap_err()
511                .to_string()
512                .contains("control characters")
513        );
514    }
515
516    #[test]
517    fn test_group_member_invalid_member_type() {
518        let member_id = create_test_resource_id("user-123");
519
520        // Empty member type
521        let result = GroupMember::new(member_id.clone(), None, Some("".to_string()));
522        assert!(result.is_err());
523        assert!(result.unwrap_err().to_string().contains("cannot be empty"));
524
525        // Invalid member type
526        let result = GroupMember::new(member_id, None, Some("Invalid".to_string()));
527        assert!(result.is_err());
528        assert!(
529            result
530                .unwrap_err()
531                .to_string()
532                .contains("Invalid member type")
533        );
534    }
535
536    #[test]
537    fn test_group_member_effective_display_name() {
538        let member_id = create_test_resource_id("user-123");
539
540        // With display name
541        let member_with_name =
542            GroupMember::new_user(member_id.clone(), Some("John Doe".to_string())).unwrap();
543        assert_eq!(member_with_name.effective_display_name(), "John Doe");
544
545        // Without display name
546        let member_without_name = GroupMember::new_user(member_id.clone(), None).unwrap();
547        assert_eq!(member_without_name.effective_display_name(), "user-123");
548    }
549
550    #[test]
551    fn test_group_member_display() {
552        let member_id = create_test_resource_id("user-123");
553
554        // Full member
555        let full_member = GroupMember::new(
556            member_id.clone(),
557            Some("John Doe".to_string()),
558            Some("User".to_string()),
559        )
560        .unwrap();
561        let display_str = format!("{}", full_member);
562        assert!(display_str.contains("John Doe"));
563        assert!(display_str.contains("User"));
564        assert!(display_str.contains("user-123"));
565
566        // Display name only
567        let display_only =
568            GroupMember::new(member_id.clone(), Some("John Doe".to_string()), None).unwrap();
569        let display_str = format!("{}", display_only);
570        assert!(display_str.contains("John Doe"));
571        assert!(display_str.contains("user-123"));
572
573        // Type only
574        let type_only =
575            GroupMember::new(member_id.clone(), None, Some("User".to_string())).unwrap();
576        let display_str = format!("{}", type_only);
577        assert!(display_str.contains("User"));
578        assert!(display_str.contains("user-123"));
579
580        // Minimal
581        let minimal = GroupMember::new(member_id.clone(), None, None).unwrap();
582        let display_str = format!("{}", minimal);
583        assert_eq!(display_str, "[user-123]");
584    }
585
586    #[test]
587    fn test_group_members_collection() {
588        let member1 = GroupMember::new_user(
589            create_test_resource_id("user-1"),
590            Some("John Doe".to_string()),
591        )
592        .unwrap();
593        let member2 = GroupMember::new_user(
594            create_test_resource_id("user-2"),
595            Some("Jane Smith".to_string()),
596        )
597        .unwrap();
598
599        let members = vec![member1.clone(), member2.clone()];
600        let group_members = GroupMembers::new(members).unwrap();
601
602        assert_eq!(group_members.len(), 2);
603        assert_eq!(group_members.get(0), Some(&member1));
604        assert_eq!(group_members.get(1), Some(&member2));
605    }
606
607    #[test]
608    fn test_group_members_with_primary() {
609        let member1 = GroupMember::new_user(
610            create_test_resource_id("user-1"),
611            Some("John Doe".to_string()),
612        )
613        .unwrap();
614        let member2 = GroupMember::new_user(
615            create_test_resource_id("user-2"),
616            Some("Jane Smith".to_string()),
617        )
618        .unwrap();
619
620        let members = vec![member1.clone(), member2.clone()];
621        let group_members = GroupMembers::new(members).unwrap().with_primary(1).unwrap();
622
623        assert_eq!(group_members.primary(), Some(&member2));
624        assert_eq!(group_members.primary_index(), Some(1));
625    }
626
627    #[test]
628    fn test_serialization() {
629        let member_id = create_test_resource_id("user-123");
630        let member = GroupMember::new(
631            member_id,
632            Some("John Doe".to_string()),
633            Some("User".to_string()),
634        )
635        .unwrap();
636
637        let json = serde_json::to_string(&member).unwrap();
638        let deserialized: GroupMember = serde_json::from_str(&json).unwrap();
639
640        assert_eq!(member, deserialized);
641    }
642
643    #[test]
644    fn test_serialization_optional_fields() {
645        let member_id = create_test_resource_id("user-123");
646        let member = GroupMember::new(member_id, None, None).unwrap();
647
648        let json = serde_json::to_string(&member).unwrap();
649
650        // Optional fields should not be present in JSON when None
651        assert!(!json.contains("display"));
652        assert!(!json.contains("type"));
653
654        let deserialized: GroupMember = serde_json::from_str(&json).unwrap();
655        assert_eq!(member, deserialized);
656    }
657}