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