1use crate::field::{FieldError, FieldResult, FormField, Widget};
2use std::str::FromStr;
3
4pub struct DecimalField {
17 pub name: String,
18 pub label: Option<String>,
19 pub required: bool,
20 pub help_text: Option<String>,
21 pub widget: Widget,
22 pub initial: Option<serde_json::Value>,
23 pub max_value: Option<f64>,
24 pub min_value: Option<f64>,
25 pub max_digits: Option<usize>,
26 pub decimal_places: Option<usize>,
27 pub localize: bool,
28 pub locale: Option<String>,
29 pub use_thousands_separator: bool,
30}
31
32impl DecimalField {
33 pub fn new(name: String) -> Self {
45 Self {
46 name,
47 label: None,
48 required: true,
49 help_text: None,
50 widget: Widget::NumberInput,
51 initial: None,
52 max_value: None,
53 min_value: None,
54 max_digits: None,
55 decimal_places: None,
56 localize: false,
57 locale: None,
58 use_thousands_separator: false,
59 }
60 }
61 pub fn with_localize(mut self, localize: bool) -> Self {
62 self.localize = localize;
63 self
64 }
65 pub fn with_locale(mut self, locale: String) -> Self {
66 self.locale = Some(locale);
67 self
68 }
69 pub fn with_thousands_separator(mut self, use_separator: bool) -> Self {
70 self.use_thousands_separator = use_separator;
71 self
72 }
73
74 fn validate_decimal(&self, s: &str) -> Result<f64, String> {
75 let num = f64::from_str(s).map_err(|_| "Enter a number".to_string())?;
76
77 if !num.is_finite() {
78 return Err("Enter a valid number".to_string());
79 }
80
81 let integer_part = s.trim_start_matches('-');
84 let digits_before_dot = integer_part.split('.').next().unwrap_or(integer_part);
85 if digits_before_dot.len() > 1 && digits_before_dot.starts_with('0') {
86 return Err("Enter a number without leading zeros".to_string());
87 }
88
89 if let Some(max_digits) = self.max_digits {
91 let parts: Vec<&str> = s.split('.').collect();
92 let total_digits =
93 parts[0].trim_start_matches('-').len() + parts.get(1).map(|p| p.len()).unwrap_or(0);
94 if total_digits > max_digits {
95 return Err(format!(
96 "Ensure that there are no more than {} digits in total",
97 max_digits
98 ));
99 }
100 }
101
102 if let Some(decimal_places) = self.decimal_places {
104 let parts: Vec<&str> = s.split('.').collect();
105 if let Some(decimals) = parts.get(1)
106 && decimals.len() > decimal_places
107 {
108 return Err(format!(
109 "Ensure that there are no more than {} decimal places",
110 decimal_places
111 ));
112 }
113 }
114
115 Ok(num)
116 }
117}
118
119impl FormField for DecimalField {
120 fn name(&self) -> &str {
121 &self.name
122 }
123
124 fn label(&self) -> Option<&str> {
125 self.label.as_deref()
126 }
127
128 fn required(&self) -> bool {
129 self.required
130 }
131
132 fn help_text(&self) -> Option<&str> {
133 self.help_text.as_deref()
134 }
135
136 fn widget(&self) -> &Widget {
137 &self.widget
138 }
139
140 fn initial(&self) -> Option<&serde_json::Value> {
141 self.initial.as_ref()
142 }
143
144 fn clean(&self, value: Option<&serde_json::Value>) -> FieldResult<serde_json::Value> {
145 match value {
146 None if self.required => Err(FieldError::required(None)),
147 None => Ok(serde_json::Value::Null),
148 Some(v) => {
149 let (num, str_repr) = if let Some(s) = v.as_str() {
154 let s = s.trim();
155
156 if s.is_empty() {
157 if self.required {
158 return Err(FieldError::required(None));
159 }
160 return Ok(serde_json::Value::Null);
161 }
162
163 let n = self.validate_decimal(s).map_err(FieldError::Validation)?;
164 (n, s.to_string())
165 } else if let Some(f) = v.as_f64() {
166 if !f.is_finite() {
167 return Err(FieldError::Validation("Enter a valid number".to_string()));
168 }
169 (f, format!("{}", f))
170 } else if let Some(i) = v.as_i64() {
171 (i as f64, format!("{}", i))
172 } else {
173 return Err(FieldError::Invalid("Expected number or string".to_string()));
174 };
175
176 if !v.is_string() {
180 if let Some(max_digits) = self.max_digits {
181 let parts: Vec<&str> = str_repr.split('.').collect();
182 let total_digits = parts[0].trim_start_matches('-').len()
183 + parts.get(1).map(|p| p.len()).unwrap_or(0);
184 if total_digits > max_digits {
185 return Err(FieldError::Validation(format!(
186 "Ensure that there are no more than {} digits in total",
187 max_digits
188 )));
189 }
190 }
191
192 if let Some(decimal_places) = self.decimal_places {
193 let parts: Vec<&str> = str_repr.split('.').collect();
194 if let Some(decimals) = parts.get(1)
195 && decimals.len() > decimal_places
196 {
197 return Err(FieldError::Validation(format!(
198 "Ensure that there are no more than {} decimal places",
199 decimal_places
200 )));
201 }
202 }
203 }
204
205 if let Some(max) = self.max_value
207 && num > max
208 {
209 return Err(FieldError::Validation(format!(
210 "Ensure this value is less than or equal to {}",
211 max
212 )));
213 }
214
215 if let Some(min) = self.min_value
216 && num < min
217 {
218 return Err(FieldError::Validation(format!(
219 "Ensure this value is greater than or equal to {}",
220 min
221 )));
222 }
223
224 Ok(serde_json::json!(num))
225 }
226 }
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233 use rstest::rstest;
234
235 #[test]
236 fn test_decimalfield_basic() {
237 let field = DecimalField::new("amount".to_string());
238
239 assert_eq!(
240 field.clean(Some(&serde_json::json!(3.15))).unwrap(),
241 serde_json::json!(3.15)
242 );
243 assert_eq!(
244 field.clean(Some(&serde_json::json!("3.15"))).unwrap(),
245 serde_json::json!(3.15)
246 );
247 }
248
249 #[test]
250 fn test_decimalfield_max_digits() {
251 let mut field = DecimalField::new("amount".to_string());
252 field.max_digits = Some(5);
253 field.decimal_places = Some(2);
254
255 assert!(field.clean(Some(&serde_json::json!("123.45"))).is_ok());
256 assert!(matches!(
257 field.clean(Some(&serde_json::json!("1234.567"))),
258 Err(FieldError::Validation(_))
259 ));
260 assert!(matches!(
261 field.clean(Some(&serde_json::json!("123.456"))),
262 Err(FieldError::Validation(_))
263 ));
264 }
265
266 #[test]
267 fn test_decimalfield_range() {
268 let mut field = DecimalField::new("amount".to_string());
269 field.min_value = Some(0.0);
270 field.max_value = Some(100.0);
271
272 assert!(field.clean(Some(&serde_json::json!(50.0))).is_ok());
273 assert!(matches!(
274 field.clean(Some(&serde_json::json!(-1.0))),
275 Err(FieldError::Validation(_))
276 ));
277 assert!(matches!(
278 field.clean(Some(&serde_json::json!(101.0))),
279 Err(FieldError::Validation(_))
280 ));
281 }
282
283 #[test]
284 fn test_decimalfield_required() {
285 let field = DecimalField::new("price".to_string());
286
287 assert!(field.clean(None).is_err());
289
290 assert!(field.clean(Some(&serde_json::json!(""))).is_err());
292 }
293
294 #[test]
295 fn test_decimalfield_not_required() {
296 let mut field = DecimalField::new("price".to_string());
297 field.required = false;
298
299 assert_eq!(field.clean(None).unwrap(), serde_json::Value::Null);
301
302 assert_eq!(
304 field.clean(Some(&serde_json::json!(""))).unwrap(),
305 serde_json::Value::Null
306 );
307 }
308
309 #[test]
310 fn test_decimalfield_integer_input() {
311 let field = DecimalField::new("amount".to_string());
312
313 assert_eq!(
315 field.clean(Some(&serde_json::json!(42))).unwrap(),
316 serde_json::json!(42.0)
317 );
318
319 assert_eq!(
321 field.clean(Some(&serde_json::json!("42"))).unwrap(),
322 serde_json::json!(42.0)
323 );
324 }
325
326 #[test]
327 fn test_decimalfield_negative_numbers() {
328 let mut field = DecimalField::new("amount".to_string());
329 field.required = false;
330
331 assert_eq!(
332 field.clean(Some(&serde_json::json!(-3.15))).unwrap(),
333 serde_json::json!(-3.15)
334 );
335 assert_eq!(
336 field.clean(Some(&serde_json::json!("-3.15"))).unwrap(),
337 serde_json::json!(-3.15)
338 );
339 }
340
341 #[test]
342 fn test_decimalfield_whitespace_trimming() {
343 let field = DecimalField::new("amount".to_string());
344
345 assert_eq!(
346 field.clean(Some(&serde_json::json!(" 3.15 "))).unwrap(),
347 serde_json::json!(3.15)
348 );
349 }
350
351 #[test]
352 fn test_decimalfield_invalid_input() {
353 let field = DecimalField::new("amount".to_string());
354
355 assert!(field.clean(Some(&serde_json::json!("abc"))).is_err());
357
358 assert!(field.clean(Some(&serde_json::json!("3.15.15"))).is_err());
360 }
361
362 #[test]
363 fn test_decimalfield_infinity_nan() {
364 let field = DecimalField::new("amount".to_string());
365
366 assert!(
368 field
369 .clean(Some(&serde_json::json!(f64::INFINITY)))
370 .is_err()
371 );
372
373 assert!(field.clean(Some(&serde_json::json!(f64::NAN))).is_err());
375 }
376
377 #[test]
378 fn test_decimalfield_max_digits_exact() {
379 let mut field = DecimalField::new("amount".to_string());
380 field.max_digits = Some(5);
381
382 assert!(field.clean(Some(&serde_json::json!("12345"))).is_ok());
384 assert!(field.clean(Some(&serde_json::json!("123.45"))).is_ok());
385 assert!(field.clean(Some(&serde_json::json!("12.345"))).is_ok());
386 }
387
388 #[test]
389 fn test_decimalfield_decimal_places_exact() {
390 let mut field = DecimalField::new("amount".to_string());
391 field.decimal_places = Some(2);
392
393 assert!(field.clean(Some(&serde_json::json!("123.45"))).is_ok());
395
396 assert!(field.clean(Some(&serde_json::json!("123.4"))).is_ok());
398
399 assert!(field.clean(Some(&serde_json::json!("123.456"))).is_err());
401 }
402
403 #[test]
404 fn test_decimalfield_max_value_exact() {
405 let mut field = DecimalField::new("amount".to_string());
406 field.max_value = Some(100.0);
407
408 assert!(field.clean(Some(&serde_json::json!(100.0))).is_ok());
410
411 assert!(field.clean(Some(&serde_json::json!(100.1))).is_err());
413 }
414
415 #[test]
416 fn test_decimalfield_min_value_exact() {
417 let mut field = DecimalField::new("amount".to_string());
418 field.min_value = Some(0.0);
419
420 assert!(field.clean(Some(&serde_json::json!(0.0))).is_ok());
422
423 assert!(field.clean(Some(&serde_json::json!(-0.1))).is_err());
425 }
426
427 #[test]
428 fn test_decimalfield_combined_constraints() {
429 let mut field = DecimalField::new("amount".to_string());
430 field.min_value = Some(0.0);
431 field.max_value = Some(999.99);
432 field.max_digits = Some(5);
433 field.decimal_places = Some(2);
434
435 assert!(field.clean(Some(&serde_json::json!("0.00"))).is_ok());
437 assert!(field.clean(Some(&serde_json::json!("123.45"))).is_ok());
438 assert!(field.clean(Some(&serde_json::json!("999.99"))).is_ok());
439
440 assert!(field.clean(Some(&serde_json::json!("1000.00"))).is_err());
442
443 assert!(field.clean(Some(&serde_json::json!("-0.01"))).is_err());
445
446 assert!(field.clean(Some(&serde_json::json!("123.456"))).is_err());
448
449 assert!(field.clean(Some(&serde_json::json!("1234.56"))).is_err());
451 }
452
453 #[test]
454 fn test_decimalfield_localize_option() {
455 let field = DecimalField::new("amount".to_string()).with_localize(true);
456 assert!(field.localize);
457 }
458
459 #[test]
460 fn test_decimalfield_locale_option() {
461 let field = DecimalField::new("amount".to_string()).with_locale("en_US".to_string());
462 assert_eq!(field.locale, Some("en_US".to_string()));
463 }
464
465 #[test]
466 fn test_decimalfield_thousands_separator() {
467 let field = DecimalField::new("amount".to_string()).with_thousands_separator(true);
468 assert!(field.use_thousands_separator);
469 }
470
471 #[test]
472 fn test_decimalfield_widget() {
473 let field = DecimalField::new("amount".to_string());
474 assert!(matches!(field.widget(), &Widget::NumberInput));
475 }
476
477 #[rstest]
478 #[case("007", true)]
479 #[case("00.5", true)]
480 #[case("00", true)]
481 #[case("01", true)]
482 #[case("0123.45", true)]
483 #[case("0", false)]
484 #[case("0.5", false)]
485 #[case("7", false)]
486 #[case("123", false)]
487 #[case("10.5", false)]
488 #[case("-0", false)]
489 #[case("-0.5", false)]
490 #[case("-007", true)]
491 fn test_decimalfield_leading_zeros(#[case] input: &str, #[case] should_reject: bool) {
492 let field = DecimalField::new("amount".to_string());
494
495 let result = field.clean(Some(&serde_json::json!(input)));
497
498 if should_reject {
500 assert!(
501 matches!(result, Err(FieldError::Validation(ref msg)) if msg.contains("leading zeros")),
502 "Expected leading zeros rejection for input '{}', got: {:?}",
503 input,
504 result,
505 );
506 } else {
507 assert!(
508 result.is_ok(),
509 "Expected valid input '{}' to succeed, got: {:?}",
510 input,
511 result,
512 );
513 }
514 }
515}