scim_server/resource/value_objects/
phone_number.rs1use crate::error::{ValidationError, ValidationResult};
7use serde::{Deserialize, Serialize};
8use std::fmt;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48pub struct PhoneNumber {
49 pub value: String,
50 pub display: Option<String>,
51 #[serde(rename = "type")]
52 pub phone_type: Option<String>,
53 pub primary: Option<bool>,
54}
55
56impl PhoneNumber {
57 pub fn new(
74 value: String,
75 display: Option<String>,
76 phone_type: Option<String>,
77 primary: Option<bool>,
78 ) -> ValidationResult<Self> {
79 Self::validate_phone_value(&value)?;
81
82 if let Some(ref d) = display {
84 Self::validate_display(d)?;
85 }
86
87 if let Some(ref pt) = phone_type {
89 Self::validate_phone_type(pt)?;
90 }
91
92 Ok(Self {
93 value,
94 display,
95 phone_type,
96 primary,
97 })
98 }
99
100 pub fn new_simple(value: String, phone_type: String) -> ValidationResult<Self> {
114 Self::new(value, None, Some(phone_type), None)
115 }
116
117 pub fn new_work(value: String) -> ValidationResult<Self> {
130 Self::new(value, None, Some("work".to_string()), None)
131 }
132
133 pub fn new_mobile(value: String) -> ValidationResult<Self> {
146 Self::new(value, None, Some("mobile".to_string()), None)
147 }
148
149 pub fn value(&self) -> &str {
153 &self.value
154 }
155
156 pub fn display(&self) -> Option<&str> {
158 self.display.as_deref()
159 }
160
161 pub fn phone_type(&self) -> Option<&str> {
163 self.phone_type.as_deref()
164 }
165
166 pub fn is_primary(&self) -> bool {
168 self.primary.unwrap_or(false)
169 }
170
171 pub fn display_value(&self) -> &str {
175 self.display.as_deref().unwrap_or(&self.value)
176 }
177
178 pub fn is_rfc3966_format(&self) -> bool {
180 self.value.starts_with("tel:")
181 }
182
183 pub fn to_rfc3966(&self) -> String {
188 if self.is_rfc3966_format() {
189 self.value.clone()
190 } else {
191 let cleaned = self
193 .value
194 .chars()
195 .filter(|c| c.is_ascii_digit() || *c == '+' || *c == '-')
196 .collect::<String>();
197
198 if cleaned.starts_with('+') {
199 format!("tel:{}", cleaned)
200 } else {
201 format!("tel:+{}", cleaned)
202 }
203 }
204 }
205
206 fn validate_phone_value(value: &str) -> ValidationResult<()> {
208 if value.trim().is_empty() {
209 return Err(ValidationError::custom(
210 "value: Phone number value cannot be empty",
211 ));
212 }
213
214 if value.len() > 50 {
216 return Err(ValidationError::custom(
217 "value: Phone number exceeds maximum length of 50 characters",
218 ));
219 }
220
221 if value.starts_with("tel:") {
223 let phone_part = &value[4..];
224 if phone_part.is_empty() {
225 return Err(ValidationError::custom(
226 "value: RFC 3966 format phone number cannot be empty after 'tel:' prefix",
227 ));
228 }
229 }
230
231 if !value.chars().any(|c| c.is_ascii_digit()) {
233 return Err(ValidationError::custom(
234 "value: Phone number must contain at least one digit",
235 ));
236 }
237
238 let has_invalid_chars = value.chars().any(|c| {
240 !c.is_ascii_digit()
241 && c != '+'
242 && c != '-'
243 && c != '('
244 && c != ')'
245 && c != ' '
246 && c != '.'
247 && c != ':'
248 && !c.is_ascii_alphabetic() });
250
251 if has_invalid_chars {
252 return Err(ValidationError::custom(
253 "value: Phone number contains invalid characters",
254 ));
255 }
256
257 Ok(())
258 }
259
260 fn validate_display(display: &str) -> ValidationResult<()> {
262 if display.trim().is_empty() {
263 return Err(ValidationError::custom(
264 "display: Display name cannot be empty or contain only whitespace",
265 ));
266 }
267
268 if display.len() > 256 {
270 return Err(ValidationError::custom(
271 "display: Display name exceeds maximum length of 256 characters",
272 ));
273 }
274
275 Ok(())
276 }
277
278 fn validate_phone_type(phone_type: &str) -> ValidationResult<()> {
280 if phone_type.trim().is_empty() {
281 return Err(ValidationError::custom("type: Phone type cannot be empty"));
282 }
283
284 let valid_types = ["work", "home", "mobile", "fax", "pager", "other"];
286 if !valid_types.contains(&phone_type) {
287 return Err(ValidationError::custom(format!(
288 "type: '{}' is not a valid phone type. Valid types are: {:?}",
289 phone_type, valid_types
290 )));
291 }
292
293 Ok(())
294 }
295}
296
297impl fmt::Display for PhoneNumber {
298 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
299 if let Some(phone_type) = &self.phone_type {
300 write!(f, "{} ({})", self.display_value(), phone_type)
301 } else {
302 write!(f, "{}", self.display_value())
303 }
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310
311 #[test]
312 fn test_valid_phone_number_full() {
313 let phone = PhoneNumber::new(
314 "+1-201-555-0123".to_string(),
315 Some("Work Phone".to_string()),
316 Some("work".to_string()),
317 Some(true),
318 );
319
320 assert!(phone.is_ok());
321 let phone = phone.unwrap();
322 assert_eq!(phone.value(), "+1-201-555-0123");
323 assert_eq!(phone.display(), Some("Work Phone"));
324 assert_eq!(phone.phone_type(), Some("work"));
325 assert!(phone.is_primary());
326 }
327
328 #[test]
329 fn test_valid_phone_number_simple() {
330 let phone = PhoneNumber::new_simple("555-123-4567".to_string(), "mobile".to_string());
331
332 assert!(phone.is_ok());
333 let phone = phone.unwrap();
334 assert_eq!(phone.value(), "555-123-4567");
335 assert_eq!(phone.phone_type(), Some("mobile"));
336 assert!(!phone.is_primary());
337 }
338
339 #[test]
340 fn test_valid_phone_number_work() {
341 let phone = PhoneNumber::new_work("+1-555-123-4567".to_string());
342
343 assert!(phone.is_ok());
344 let phone = phone.unwrap();
345 assert_eq!(phone.phone_type(), Some("work"));
346 assert_eq!(phone.value(), "+1-555-123-4567");
347 }
348
349 #[test]
350 fn test_valid_phone_number_mobile() {
351 let phone = PhoneNumber::new_mobile("(555) 123-4567".to_string());
352
353 assert!(phone.is_ok());
354 let phone = phone.unwrap();
355 assert_eq!(phone.phone_type(), Some("mobile"));
356 assert_eq!(phone.value(), "(555) 123-4567");
357 }
358
359 #[test]
360 fn test_empty_phone_value() {
361 let result = PhoneNumber::new("".to_string(), None, Some("work".to_string()), None);
362 assert!(result.is_err());
363 assert!(
364 result
365 .unwrap_err()
366 .to_string()
367 .contains("Phone number value cannot be empty")
368 );
369 }
370
371 #[test]
372 fn test_invalid_phone_type() {
373 let result = PhoneNumber::new(
374 "555-123-4567".to_string(),
375 None,
376 Some("business".to_string()), None,
378 );
379 assert!(result.is_err());
380 assert!(
381 result
382 .unwrap_err()
383 .to_string()
384 .contains("not a valid phone type")
385 );
386 }
387
388 #[test]
389 fn test_too_long_phone_value() {
390 let long_phone = "1".repeat(60);
391 let result = PhoneNumber::new_work(long_phone);
392 assert!(result.is_err());
393 assert!(
394 result
395 .unwrap_err()
396 .to_string()
397 .contains("exceeds maximum length")
398 );
399 }
400
401 #[test]
402 fn test_phone_without_digits() {
403 let result = PhoneNumber::new_work("abc-def-ghij".to_string());
404 assert!(result.is_err());
405 assert!(
406 result
407 .unwrap_err()
408 .to_string()
409 .contains("must contain at least one digit")
410 );
411 }
412
413 #[test]
414 fn test_phone_with_invalid_characters() {
415 let result = PhoneNumber::new_work("555-123-4567#".to_string());
416 assert!(result.is_err());
417 assert!(
418 result
419 .unwrap_err()
420 .to_string()
421 .contains("contains invalid characters")
422 );
423 }
424
425 #[test]
426 fn test_empty_display() {
427 let result = PhoneNumber::new(
428 "555-123-4567".to_string(),
429 Some("".to_string()),
430 Some("work".to_string()),
431 None,
432 );
433 assert!(result.is_err());
434 assert!(
435 result
436 .unwrap_err()
437 .to_string()
438 .contains("Display name cannot be empty")
439 );
440 }
441
442 #[test]
443 fn test_rfc3966_format() {
444 let phone = PhoneNumber::new_work("tel:+1-201-555-0123".to_string()).unwrap();
445 assert!(phone.is_rfc3966_format());
446
447 let phone2 = PhoneNumber::new_work("555-123-4567".to_string()).unwrap();
448 assert!(!phone2.is_rfc3966_format());
449 }
450
451 #[test]
452 fn test_to_rfc3966() {
453 let phone = PhoneNumber::new_work("555-123-4567".to_string()).unwrap();
454 assert_eq!(phone.to_rfc3966(), "tel:+555-123-4567");
455
456 let phone2 = PhoneNumber::new_work("+1-555-123-4567".to_string()).unwrap();
457 assert_eq!(phone2.to_rfc3966(), "tel:+1-555-123-4567");
458
459 let phone3 = PhoneNumber::new_work("tel:+1-555-123-4567".to_string()).unwrap();
460 assert_eq!(phone3.to_rfc3966(), "tel:+1-555-123-4567");
461 }
462
463 #[test]
464 fn test_display_value() {
465 let phone = PhoneNumber::new(
466 "555-123-4567".to_string(),
467 Some("My Work Phone".to_string()),
468 Some("work".to_string()),
469 None,
470 )
471 .unwrap();
472 assert_eq!(phone.display_value(), "My Work Phone");
473
474 let phone2 = PhoneNumber::new_work("555-123-4567".to_string()).unwrap();
475 assert_eq!(phone2.display_value(), "555-123-4567");
476 }
477
478 #[test]
479 fn test_display() {
480 let phone = PhoneNumber::new(
481 "555-123-4567".to_string(),
482 Some("My Work Phone".to_string()),
483 Some("work".to_string()),
484 None,
485 )
486 .unwrap();
487 assert_eq!(format!("{}", phone), "My Work Phone (work)");
488
489 let phone2 = PhoneNumber::new(
490 "555-123-4567".to_string(),
491 None,
492 Some("mobile".to_string()),
493 None,
494 )
495 .unwrap();
496 assert_eq!(format!("{}", phone2), "555-123-4567 (mobile)");
497
498 let phone3 = PhoneNumber::new("555-123-4567".to_string(), None, None, None).unwrap();
499 assert_eq!(format!("{}", phone3), "555-123-4567");
500 }
501
502 #[test]
503 fn test_serialization() {
504 let phone = PhoneNumber::new(
505 "+1-201-555-0123".to_string(),
506 Some("Work Phone".to_string()),
507 Some("work".to_string()),
508 Some(true),
509 )
510 .unwrap();
511
512 let json = serde_json::to_string(&phone).unwrap();
513 assert!(json.contains("\"value\":\"+1-201-555-0123\""));
514 assert!(json.contains("\"display\":\"Work Phone\""));
515 assert!(json.contains("\"type\":\"work\""));
516 assert!(json.contains("\"primary\":true"));
517 }
518
519 #[test]
520 fn test_deserialization() {
521 let json = r#"{
522 "value": "+1-201-555-0123",
523 "display": "Work Phone",
524 "type": "work",
525 "primary": true
526 }"#;
527
528 let phone: PhoneNumber = serde_json::from_str(json).unwrap();
529 assert_eq!(phone.value(), "+1-201-555-0123");
530 assert_eq!(phone.display(), Some("Work Phone"));
531 assert_eq!(phone.phone_type(), Some("work"));
532 assert!(phone.is_primary());
533 }
534
535 #[test]
536 fn test_equality() {
537 let phone1 = PhoneNumber::new_work("555-123-4567".to_string()).unwrap();
538 let phone2 = PhoneNumber::new_work("555-123-4567".to_string()).unwrap();
539 let phone3 = PhoneNumber::new_mobile("555-123-4567".to_string()).unwrap();
540
541 assert_eq!(phone1, phone2);
542 assert_ne!(phone1, phone3);
543 }
544
545 #[test]
546 fn test_clone() {
547 let original = PhoneNumber::new(
548 "+1-555-123-4567".to_string(),
549 Some("Work Phone".to_string()),
550 Some("work".to_string()),
551 Some(true),
552 )
553 .unwrap();
554
555 let cloned = original.clone();
556 assert_eq!(original, cloned);
557 assert_eq!(cloned.value(), "+1-555-123-4567");
558 assert_eq!(cloned.phone_type(), Some("work"));
559 }
560
561 #[test]
562 fn test_valid_phone_types() {
563 for phone_type in ["work", "home", "mobile", "fax", "pager", "other"] {
564 let phone = PhoneNumber::new(
565 "555-123-4567".to_string(),
566 None,
567 Some(phone_type.to_string()),
568 None,
569 );
570 assert!(phone.is_ok(), "Phone type '{}' should be valid", phone_type);
571 }
572 }
573
574 #[test]
575 fn test_various_phone_formats() {
576 let formats = [
577 "555-123-4567",
578 "(555) 123-4567",
579 "+1-555-123-4567",
580 "tel:+1-555-123-4567",
581 "555.123.4567",
582 "1 555 123 4567",
583 ];
584
585 for format in &formats {
586 let phone = PhoneNumber::new_work(format.to_string());
587 assert!(phone.is_ok(), "Phone format '{}' should be valid", format);
588 }
589 }
590
591 #[test]
592 fn test_invalid_rfc3966_format() {
593 let result = PhoneNumber::new_work("tel:".to_string());
594 assert!(result.is_err());
595 let error_msg = result.unwrap_err().to_string();
596 assert!(error_msg.contains("cannot be empty after 'tel:' prefix"));
597
598 let result2 = PhoneNumber::new(
600 "tel:+1-555-123-4567".to_string(),
601 None,
602 Some("work".to_string()),
603 None,
604 );
605 assert!(result2.is_ok());
606 }
607}