made_core/value_objects/
specialty.rs1use std::fmt;
9
10use serde::{Deserialize, Serialize};
11
12use crate::error::DomainError;
13
14const MAX_SPECIALTY_LEN: usize = 128;
15
16#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
18#[serde(transparent)]
19pub struct Specialty(String);
20
21impl Specialty {
22 pub fn new(raw: impl Into<String>) -> Result<Self, DomainError> {
28 let trimmed = raw.into().trim().to_owned();
29 if trimmed.is_empty() {
30 return Err(DomainError::EmptyField { field: "specialty" });
31 }
32 if trimmed.len() > MAX_SPECIALTY_LEN {
33 return Err(DomainError::FieldTooLong {
34 field: "specialty",
35 actual: trimmed.len(),
36 max: MAX_SPECIALTY_LEN,
37 });
38 }
39 if trimmed.chars().any(char::is_control) {
40 return Err(DomainError::InvalidCharacters { field: "specialty" });
41 }
42 Ok(Self(trimmed))
43 }
44
45 #[must_use]
46 pub fn as_str(&self) -> &str {
47 &self.0
48 }
49
50 #[must_use]
51 pub fn into_inner(self) -> String {
52 self.0
53 }
54}
55
56impl fmt::Display for Specialty {
57 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58 f.write_str(&self.0)
59 }
60}
61
62impl TryFrom<&str> for Specialty {
63 type Error = DomainError;
64 fn try_from(value: &str) -> Result<Self, Self::Error> {
65 Self::new(value)
66 }
67}
68
69impl TryFrom<String> for Specialty {
70 type Error = DomainError;
71 fn try_from(value: String) -> Result<Self, Self::Error> {
72 Self::new(value)
73 }
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79
80 #[test]
81 fn arbitrary_label_is_accepted() {
82 let s = Specialty::new("triage").unwrap();
83 assert_eq!(s.as_str(), "triage");
84 }
85
86 #[test]
87 fn label_is_trimmed() {
88 assert_eq!(Specialty::new(" planner ").unwrap().as_str(), "planner");
89 }
90
91 #[test]
92 fn empty_is_rejected() {
93 let err = Specialty::new(" ").unwrap_err();
94 assert!(matches!(
95 err,
96 DomainError::EmptyField { field: "specialty" }
97 ));
98 }
99
100 #[test]
101 fn control_characters_are_rejected() {
102 assert!(matches!(
103 Specialty::new("bad\nrole").unwrap_err(),
104 DomainError::InvalidCharacters { field: "specialty" }
105 ));
106 }
107
108 #[test]
109 fn overlong_is_rejected() {
110 let err = Specialty::new("a".repeat(MAX_SPECIALTY_LEN + 1)).unwrap_err();
111 assert!(matches!(err, DomainError::FieldTooLong { .. }));
112 }
113
114 #[test]
115 fn display_is_inner() {
116 assert_eq!(Specialty::new("x").unwrap().to_string(), "x");
117 }
118
119 #[test]
120 fn no_enum_of_known_specialties_exists() {
121 for label in [
124 "triage",
125 "investigator",
126 "reviewer",
127 "quality-check",
128 "anomaly-scout",
129 "clinical-intake",
130 "supply-sourcing",
131 ] {
132 Specialty::new(label).unwrap_or_else(|e| panic!("{label}: {e}"));
133 }
134 }
135
136 #[test]
137 fn serde_is_transparent() {
138 let s = Specialty::new("x").unwrap();
139 assert_eq!(serde_json::to_string(&s).unwrap(), "\"x\"");
140 }
141}