jj_cz/commit/types/
description.rs1#[derive(Debug, Clone, PartialEq, Eq)]
2#[repr(transparent)]
3pub struct Description(String);
4
5impl Description {
6 pub const MAX_LENGTH: usize = 50;
12
13 pub fn parse(value: impl Into<String>) -> Result<Self, DescriptionError> {
23 let value = value.into().trim().to_owned();
24 if value.is_empty() {
25 Err(DescriptionError::Empty)
26 } else {
27 Ok(Self(value))
28 }
29 }
30
31 pub fn as_str(&self) -> &str {
33 &self.0
34 }
35
36 #[allow(clippy::len_without_is_empty)]
41 pub fn len(&self) -> usize {
42 self.0.chars().count()
43 }
44}
45
46impl AsRef<str> for Description {
47 fn as_ref(&self) -> &str {
48 &self.0
49 }
50}
51
52impl std::fmt::Display for Description {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 write!(f, "{}", self.0)
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
59pub enum DescriptionError {
60 #[error("Description cannot be empty")]
61 Empty,
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67
68 #[test]
70 fn valid_description_accepted() {
71 let result = Description::parse("add new feature");
72 assert!(result.is_ok());
73 assert_eq!(result.unwrap().as_str(), "add new feature");
74 }
75
76 #[test]
78 fn single_character_description_accepted() {
79 let result = Description::parse("a");
80 assert!(result.is_ok());
81 assert_eq!(result.unwrap().as_str(), "a");
82 }
83
84 #[test]
86 fn description_with_numbers_accepted() {
87 let result = Description::parse("fix issue #123");
88 assert!(result.is_ok());
89 assert_eq!(result.unwrap().as_str(), "fix issue #123");
90 }
91
92 #[test]
94 fn description_with_special_chars_accepted() {
95 let result = Description::parse("add @decorator support (beta)");
96 assert!(result.is_ok());
97 assert_eq!(result.unwrap().as_str(), "add @decorator support (beta)");
98 }
99
100 #[test]
102 fn description_with_punctuation_accepted() {
103 let result = Description::parse("fix: handle edge case!");
104 assert!(result.is_ok());
105 assert_eq!(result.unwrap().as_str(), "fix: handle edge case!");
106 }
107
108 #[test]
110 fn empty_string_rejected() {
111 let result = Description::parse("");
112 assert!(result.is_err());
113 assert_eq!(result.unwrap_err(), DescriptionError::Empty);
114 }
115
116 #[test]
118 fn whitespace_only_rejected() {
119 let result = Description::parse(" ");
120 assert!(result.is_err());
121 assert_eq!(result.unwrap_err(), DescriptionError::Empty);
122 }
123
124 #[test]
126 fn tabs_only_rejected() {
127 let result = Description::parse("\t\t");
128 assert!(result.is_err());
129 assert_eq!(result.unwrap_err(), DescriptionError::Empty);
130 }
131
132 #[test]
134 fn mixed_whitespace_rejected() {
135 let result = Description::parse(" \t \n ");
136 assert!(result.is_err());
137 assert_eq!(result.unwrap_err(), DescriptionError::Empty);
138 }
139
140 #[test]
142 fn newline_only_rejected() {
143 let result = Description::parse("\n");
144 assert!(result.is_err());
145 assert_eq!(result.unwrap_err(), DescriptionError::Empty);
146 }
147
148 #[test]
150 fn leading_whitespace_trimmed() {
151 let result = Description::parse(" add feature");
152 assert!(result.is_ok());
153 assert_eq!(result.unwrap().as_str(), "add feature");
154 }
155
156 #[test]
158 fn trailing_whitespace_trimmed() {
159 let result = Description::parse("add feature ");
160 assert!(result.is_ok());
161 assert_eq!(result.unwrap().as_str(), "add feature");
162 }
163
164 #[test]
166 fn leading_and_trailing_whitespace_trimmed() {
167 let result = Description::parse(" add feature ");
168 assert!(result.is_ok());
169 assert_eq!(result.unwrap().as_str(), "add feature");
170 }
171
172 #[test]
174 fn internal_whitespace_preserved() {
175 let result = Description::parse("add multiple spaces");
176 assert!(result.is_ok());
177 assert_eq!(result.unwrap().as_str(), "add multiple spaces");
178 }
179
180 #[test]
185 fn description_over_soft_limit_accepted() {
186 let desc_51 = "a".repeat(51);
187 let result = Description::parse(&desc_51);
188 assert!(result.is_ok());
189 assert_eq!(result.unwrap().len(), 51);
190
191 let desc_72 = "a".repeat(72);
192 let result = Description::parse(&desc_72);
193 assert!(result.is_ok());
194 assert_eq!(result.unwrap().len(), 72);
195 }
196
197 #[test]
199 fn length_checked_after_trimming() {
200 let desc_with_spaces = format!(" {} ", "a".repeat(50));
202 let result = Description::parse(&desc_with_spaces);
203 assert!(result.is_ok());
204 assert_eq!(result.unwrap().as_str().len(), 50);
205 }
206
207 #[test]
209 fn fifty_characters_accepted() {
210 let desc_50 = "a".repeat(50);
211 let result = Description::parse(&desc_50);
212 assert!(result.is_ok());
213 assert_eq!(result.unwrap().as_str().len(), 50);
214 }
215
216 #[test]
218 fn max_length_constant_is_50() {
219 assert_eq!(Description::MAX_LENGTH, 50);
220 }
221
222 #[test]
224 fn as_str_returns_inner_string() {
225 let desc = Description::parse("my description").unwrap();
226 assert_eq!(desc.as_str(), "my description");
227 }
228
229 #[test]
231 fn len_returns_correct_length() {
232 let desc = Description::parse("hello").unwrap();
233 assert_eq!(desc.len(), 5);
234 }
235
236 #[test]
241 fn len_counts_unicode_chars_not_bytes() {
242 let desc = Description::parse("café").unwrap();
244 assert_eq!(desc.len(), 4);
245
246 let desc = Description::parse("fix 🐛").unwrap();
248 assert_eq!(desc.len(), 5);
249 }
250
251 #[test]
253 fn display_outputs_inner_string() {
254 let desc = Description::parse("add feature").unwrap();
255 assert_eq!(format!("{}", desc), "add feature");
256 }
257
258 #[test]
260 fn description_is_cloneable() {
261 let original = Description::parse("add feature").unwrap();
262 let cloned = original.clone();
263 assert_eq!(original, cloned);
264 }
265
266 #[test]
268 fn description_equality() {
269 let desc1 = Description::parse("add feature").unwrap();
270 let desc2 = Description::parse("add feature").unwrap();
271 let desc3 = Description::parse("fix bug").unwrap();
272 assert_eq!(desc1, desc2);
273 assert_ne!(desc1, desc3);
274 }
275
276 #[test]
278 fn description_has_debug() {
279 let desc = Description::parse("add feature").unwrap();
280 let debug_output = format!("{:?}", desc);
281 assert!(debug_output.contains("Description"));
282 assert!(debug_output.contains("add feature"));
283 }
284
285 #[test]
287 fn description_as_ref_str() {
288 let desc = Description::parse("add feature").unwrap();
289 let s: &str = desc.as_ref();
290 assert_eq!(s, "add feature");
291 }
292
293 #[test]
295 fn empty_error_display() {
296 let err = DescriptionError::Empty;
297 let msg = format!("{}", err);
298 assert!(msg.contains("cannot be empty"));
299 }
300
301 #[test]
303 fn whitespace_after_trim_is_empty() {
304 let whitespace_variants = [" ", " ", "\t", "\n", "\r\n", " \t \n "];
306 for ws in whitespace_variants {
307 let result = Description::parse(ws);
308 assert!(result.is_err(), "Expected error for whitespace: {:?}", ws);
309 assert_eq!(
310 result.unwrap_err(),
311 DescriptionError::Empty,
312 "Expected Empty error for whitespace: {:?}",
313 ws
314 );
315 }
316 }
317
318 #[test]
320 fn boundary_length_after_trim() {
321 let desc = format!(" {} ", "x".repeat(50));
323 let result = Description::parse(&desc);
324 assert!(result.is_ok());
325 assert_eq!(result.unwrap().len(), 50);
326 }
327
328 #[test]
332 fn over_soft_limit_after_trim_accepted() {
333 let desc = format!(" {} ", "x".repeat(51));
334 let result = Description::parse(&desc);
335 assert!(result.is_ok());
336 assert_eq!(result.unwrap().len(), 51);
337 }
338}