reinhardt_forms/fields/
boolean_field.rs1use crate::field::{FieldError, FieldResult, FormField, Widget};
4
5#[derive(Debug, Clone)]
7pub struct BooleanField {
8 pub name: String,
10 pub label: Option<String>,
12 pub required: bool,
14 pub help_text: Option<String>,
16 pub initial: Option<serde_json::Value>,
18}
19
20impl BooleanField {
21 pub fn new(name: String) -> Self {
33 Self {
34 name,
35 label: None,
36 required: false,
37 help_text: None,
38 initial: None,
39 }
40 }
41
42 pub fn required(mut self) -> Self {
53 self.required = true;
54 self
55 }
56
57 pub fn with_label(mut self, label: impl Into<String>) -> Self {
68 self.label = Some(label.into());
69 self
70 }
71
72 pub fn with_help_text(mut self, help_text: impl Into<String>) -> Self {
83 self.help_text = Some(help_text.into());
84 self
85 }
86
87 pub fn with_initial(mut self, initial: bool) -> Self {
98 self.initial = Some(serde_json::json!(initial));
99 self
100 }
101}
102
103impl FormField for BooleanField {
106 fn name(&self) -> &str {
107 &self.name
108 }
109
110 fn label(&self) -> Option<&str> {
111 self.label.as_deref()
112 }
113
114 fn required(&self) -> bool {
115 self.required
116 }
117
118 fn help_text(&self) -> Option<&str> {
119 self.help_text.as_deref()
120 }
121
122 fn widget(&self) -> &Widget {
123 &Widget::CheckboxInput
124 }
125
126 fn initial(&self) -> Option<&serde_json::Value> {
127 self.initial.as_ref()
128 }
129
130 fn clean(&self, value: Option<&serde_json::Value>) -> FieldResult<serde_json::Value> {
131 match value {
132 None => {
133 if self.required {
134 Err(FieldError::Required(self.name.clone()))
135 } else {
136 Ok(serde_json::Value::Bool(false))
137 }
138 }
139 Some(v) => {
140 let b = match v {
142 serde_json::Value::Bool(b) => *b,
143 serde_json::Value::String(s) => {
144 let s_lower = s.to_lowercase();
146 !(s.is_empty() || s_lower == "false" || s == "0")
147 }
148 serde_json::Value::Number(n) => {
149 if let Some(i) = n.as_i64() {
151 i != 0
152 } else if let Some(f) = n.as_f64() {
153 f != 0.0
154 } else {
155 true
156 }
157 }
158 serde_json::Value::Null => false,
159 _ => {
160 return Err(FieldError::Validation(
161 "Cannot convert to boolean".to_string(),
162 ));
163 }
164 };
165
166 if self.required && !b {
170 return Err(FieldError::Required(self.name.clone()));
171 }
172
173 Ok(serde_json::Value::Bool(b))
174 }
175 }
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use rstest::rstest;
183 use serde_json::json;
184
185 #[rstest]
186 fn test_required_boolean_rejects_false() {
187 let field = BooleanField::new("terms".to_string()).required();
189
190 assert!(
192 field.clean(Some(&json!(false))).is_err(),
193 "required BooleanField should reject false"
194 );
195 }
196
197 #[rstest]
198 fn test_required_boolean_accepts_true() {
199 let field = BooleanField::new("terms".to_string()).required();
201
202 let result = field.clean(Some(&json!(true)));
204
205 assert_eq!(result.unwrap(), json!(true));
207 }
208
209 #[rstest]
210 fn test_required_boolean_rejects_none() {
211 let field = BooleanField::new("terms".to_string()).required();
213
214 assert!(field.clean(None).is_err());
216 }
217
218 #[rstest]
219 fn test_required_boolean_rejects_false_string() {
220 let field = BooleanField::new("terms".to_string()).required();
222
223 assert!(field.clean(Some(&json!("false"))).is_err());
225 assert!(field.clean(Some(&json!("0"))).is_err());
226 assert!(field.clean(Some(&json!(""))).is_err());
227 }
228
229 #[rstest]
230 fn test_required_boolean_rejects_zero() {
231 let field = BooleanField::new("terms".to_string()).required();
233
234 assert!(field.clean(Some(&json!(0))).is_err());
236 }
237
238 #[rstest]
239 fn test_optional_boolean_accepts_false() {
240 let field = BooleanField::new("newsletter".to_string());
242
243 let result = field.clean(Some(&json!(false)));
245
246 assert_eq!(result.unwrap(), json!(false));
248 }
249
250 #[rstest]
251 fn test_optional_boolean_accepts_none() {
252 let field = BooleanField::new("newsletter".to_string());
254
255 let result = field.clean(None);
257
258 assert_eq!(result.unwrap(), json!(false));
260 }
261
262 #[rstest]
263 fn test_required_boolean_accepts_truthy_string() {
264 let field = BooleanField::new("terms".to_string()).required();
266
267 assert_eq!(field.clean(Some(&json!("true"))).unwrap(), json!(true));
269 assert_eq!(field.clean(Some(&json!("yes"))).unwrap(), json!(true));
270 assert_eq!(field.clean(Some(&json!("1"))).unwrap(), json!(true));
271 }
272
273 #[rstest]
274 fn test_required_boolean_rejects_null() {
275 let field = BooleanField::new("terms".to_string()).required();
277
278 assert!(field.clean(Some(&json!(null))).is_err());
280 }
281}