Skip to main content

reinhardt_forms/fields/
float_field.rs

1use crate::field::{FieldError, FieldResult, FormField, Widget};
2
3/// FloatField for floating-point number input
4pub struct FloatField {
5	pub name: String,
6	pub label: Option<String>,
7	pub required: bool,
8	pub help_text: Option<String>,
9	pub widget: Widget,
10	pub initial: Option<serde_json::Value>,
11	pub max_value: Option<f64>,
12	pub min_value: Option<f64>,
13}
14
15impl FloatField {
16	/// Create a new FloatField with the given name
17	///
18	/// # Examples
19	///
20	/// ```
21	/// use reinhardt_forms::fields::FloatField;
22	///
23	/// let field = FloatField::new("price".to_string());
24	/// assert_eq!(field.name, "price");
25	/// assert!(field.required);
26	/// ```
27	pub fn new(name: String) -> Self {
28		Self {
29			name,
30			label: None,
31			required: true,
32			help_text: None,
33			widget: Widget::NumberInput,
34			initial: None,
35			max_value: None,
36			min_value: None,
37		}
38	}
39}
40
41impl FormField for FloatField {
42	fn name(&self) -> &str {
43		&self.name
44	}
45
46	fn label(&self) -> Option<&str> {
47		self.label.as_deref()
48	}
49
50	fn required(&self) -> bool {
51		self.required
52	}
53
54	fn help_text(&self) -> Option<&str> {
55		self.help_text.as_deref()
56	}
57
58	fn widget(&self) -> &Widget {
59		&self.widget
60	}
61
62	fn initial(&self) -> Option<&serde_json::Value> {
63		self.initial.as_ref()
64	}
65
66	fn clean(&self, value: Option<&serde_json::Value>) -> FieldResult<serde_json::Value> {
67		match value {
68			None if self.required => Err(FieldError::required(None)),
69			None => Ok(serde_json::Value::Null),
70			Some(v) => {
71				// Parse float from either number or string
72				let num = if let Some(f) = v.as_f64() {
73					f
74				} else if let Some(s) = v.as_str() {
75					// Trim whitespace
76					let s = s.trim();
77
78					// Return None/error for empty string
79					if s.is_empty() {
80						if self.required {
81							return Err(FieldError::required(None));
82						}
83						return Ok(serde_json::Value::Null);
84					}
85
86					// Parse string to float
87					s.parse::<f64>()
88						.map_err(|_| FieldError::Invalid("Enter a number".to_string()))?
89				} else if let Some(i) = v.as_i64() {
90					// Convert integer to float
91					i as f64
92				} else {
93					return Err(FieldError::Invalid("Expected number or string".to_string()));
94				};
95
96				// Check for special values
97				if !num.is_finite() {
98					return Err(FieldError::Invalid("Enter a valid number".to_string()));
99				}
100
101				// Validate range
102				if let Some(max) = self.max_value
103					&& num > max
104				{
105					return Err(FieldError::Validation(format!(
106						"Ensure this value is less than or equal to {}",
107						max
108					)));
109				}
110
111				if let Some(min) = self.min_value
112					&& num < min
113				{
114					return Err(FieldError::Validation(format!(
115						"Ensure this value is greater than or equal to {}",
116						min
117					)));
118				}
119
120				Ok(serde_json::json!(num))
121			}
122		}
123	}
124}
125
126#[cfg(test)]
127mod tests {
128	use super::*;
129
130	#[test]
131	fn test_floatfield_basic() {
132		let field = FloatField::new("price".to_string());
133
134		assert_eq!(
135			field.clean(Some(&serde_json::json!(3.15))).unwrap(),
136			serde_json::json!(3.15)
137		);
138		assert_eq!(
139			field.clean(Some(&serde_json::json!(42))).unwrap(),
140			serde_json::json!(42.0)
141		);
142	}
143
144	#[test]
145	fn test_floatfield_string_parsing() {
146		let mut field = FloatField::new("value".to_string());
147		field.required = false;
148
149		assert_eq!(
150			field.clean(Some(&serde_json::json!("3.15"))).unwrap(),
151			serde_json::json!(3.15)
152		);
153		assert_eq!(
154			field.clean(Some(&serde_json::json!("  -2.5  "))).unwrap(),
155			serde_json::json!(-2.5)
156		);
157		assert_eq!(
158			field.clean(Some(&serde_json::json!("42"))).unwrap(),
159			serde_json::json!(42.0)
160		);
161	}
162
163	#[test]
164	fn test_floatfield_range() {
165		let mut field = FloatField::new("score".to_string());
166		field.min_value = Some(0.0);
167		field.max_value = Some(100.0);
168
169		assert!(field.clean(Some(&serde_json::json!(50.0))).is_ok());
170		assert!(field.clean(Some(&serde_json::json!(0.0))).is_ok());
171		assert!(field.clean(Some(&serde_json::json!(100.0))).is_ok());
172
173		assert!(matches!(
174			field.clean(Some(&serde_json::json!(-1.0))),
175			Err(FieldError::Validation(_))
176		));
177		assert!(matches!(
178			field.clean(Some(&serde_json::json!(101.0))),
179			Err(FieldError::Validation(_))
180		));
181	}
182
183	#[test]
184	fn test_floatfield_invalid() {
185		let field = FloatField::new("value".to_string());
186
187		assert!(matches!(
188			field.clean(Some(&serde_json::json!("abc"))),
189			Err(FieldError::Invalid(_))
190		));
191	}
192
193	#[test]
194	fn test_floatfield_required() {
195		let field = FloatField::new("value".to_string());
196
197		// Required field rejects None
198		assert!(field.clean(None).is_err());
199
200		// Required field rejects empty string
201		assert!(field.clean(Some(&serde_json::json!(""))).is_err());
202	}
203
204	#[test]
205	fn test_floatfield_not_required() {
206		let mut field = FloatField::new("value".to_string());
207		field.required = false;
208
209		// Not required accepts None
210		assert_eq!(field.clean(None).unwrap(), serde_json::Value::Null);
211
212		// Not required accepts empty string
213		assert_eq!(
214			field.clean(Some(&serde_json::json!(""))).unwrap(),
215			serde_json::Value::Null
216		);
217	}
218
219	#[test]
220	fn test_floatfield_negative_numbers() {
221		let mut field = FloatField::new("value".to_string());
222		field.required = false;
223
224		assert_eq!(
225			field.clean(Some(&serde_json::json!(-3.15))).unwrap(),
226			serde_json::json!(-3.15)
227		);
228		assert_eq!(
229			field.clean(Some(&serde_json::json!("-3.15"))).unwrap(),
230			serde_json::json!(-3.15)
231		);
232	}
233
234	#[test]
235	fn test_floatfield_scientific_notation() {
236		let mut field = FloatField::new("value".to_string());
237		field.required = false;
238
239		// Scientific notation as string
240		assert_eq!(
241			field.clean(Some(&serde_json::json!("1.5e2"))).unwrap(),
242			serde_json::json!(150.0)
243		);
244		assert_eq!(
245			field.clean(Some(&serde_json::json!("1e-3"))).unwrap(),
246			serde_json::json!(0.001)
247		);
248	}
249
250	#[test]
251	fn test_floatfield_infinity() {
252		let field = FloatField::new("value".to_string());
253
254		// Infinity is rejected
255		assert!(
256			field
257				.clean(Some(&serde_json::json!(f64::INFINITY)))
258				.is_err()
259		);
260		assert!(
261			field
262				.clean(Some(&serde_json::json!(f64::NEG_INFINITY)))
263				.is_err()
264		);
265	}
266
267	#[test]
268	fn test_floatfield_nan() {
269		let field = FloatField::new("value".to_string());
270
271		// NaN is rejected
272		assert!(field.clean(Some(&serde_json::json!(f64::NAN))).is_err());
273	}
274
275	#[test]
276	fn test_floatfield_very_small_numbers() {
277		let mut field = FloatField::new("value".to_string());
278		field.required = false;
279
280		// Very small numbers
281		assert_eq!(
282			field.clean(Some(&serde_json::json!(0.000001))).unwrap(),
283			serde_json::json!(0.000001)
284		);
285		assert_eq!(
286			field.clean(Some(&serde_json::json!("0.000001"))).unwrap(),
287			serde_json::json!(0.000001)
288		);
289	}
290
291	#[test]
292	fn test_floatfield_very_large_numbers() {
293		let mut field = FloatField::new("value".to_string());
294		field.required = false;
295
296		// Very large numbers
297		assert_eq!(
298			field.clean(Some(&serde_json::json!(1000000000.0))).unwrap(),
299			serde_json::json!(1000000000.0)
300		);
301	}
302
303	#[test]
304	fn test_floatfield_max_value_boundary() {
305		let mut field = FloatField::new("value".to_string());
306		field.max_value = Some(100.0);
307
308		// Exactly at boundary
309		assert!(field.clean(Some(&serde_json::json!(100.0))).is_ok());
310
311		// Just over boundary
312		assert!(field.clean(Some(&serde_json::json!(100.001))).is_err());
313	}
314
315	#[test]
316	fn test_floatfield_min_value_boundary() {
317		let mut field = FloatField::new("value".to_string());
318		field.min_value = Some(-100.0);
319
320		// Exactly at boundary
321		assert!(field.clean(Some(&serde_json::json!(-100.0))).is_ok());
322
323		// Just under boundary
324		assert!(field.clean(Some(&serde_json::json!(-100.001))).is_err());
325	}
326
327	#[test]
328	fn test_floatfield_zero() {
329		let field = FloatField::new("value".to_string());
330
331		assert_eq!(
332			field.clean(Some(&serde_json::json!(0.0))).unwrap(),
333			serde_json::json!(0.0)
334		);
335		assert_eq!(
336			field.clean(Some(&serde_json::json!("0.0"))).unwrap(),
337			serde_json::json!(0.0)
338		);
339	}
340
341	#[test]
342	fn test_floatfield_widget() {
343		let field = FloatField::new("value".to_string());
344		assert!(matches!(field.widget(), &Widget::NumberInput));
345	}
346}