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