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}