reinhardt_forms/fields/
boolean_field.rs1use crate::field::{FieldError, FieldResult, FormField, Widget};
4
5#[derive(Debug, Clone)]
7pub struct BooleanField {
8 pub name: String,
9 pub label: Option<String>,
10 pub required: bool,
11 pub help_text: Option<String>,
12 pub initial: Option<serde_json::Value>,
13}
14
15impl BooleanField {
16 pub fn new(name: String) -> Self {
28 Self {
29 name,
30 label: None,
31 required: false,
32 help_text: None,
33 initial: None,
34 }
35 }
36
37 pub fn required(mut self) -> Self {
48 self.required = true;
49 self
50 }
51
52 pub fn with_label(mut self, label: impl Into<String>) -> Self {
63 self.label = Some(label.into());
64 self
65 }
66
67 pub fn with_help_text(mut self, help_text: impl Into<String>) -> Self {
78 self.help_text = Some(help_text.into());
79 self
80 }
81
82 pub fn with_initial(mut self, initial: bool) -> Self {
93 self.initial = Some(serde_json::json!(initial));
94 self
95 }
96}
97
98impl FormField for BooleanField {
101 fn name(&self) -> &str {
102 &self.name
103 }
104
105 fn label(&self) -> Option<&str> {
106 self.label.as_deref()
107 }
108
109 fn required(&self) -> bool {
110 self.required
111 }
112
113 fn help_text(&self) -> Option<&str> {
114 self.help_text.as_deref()
115 }
116
117 fn widget(&self) -> &Widget {
118 &Widget::CheckboxInput
119 }
120
121 fn initial(&self) -> Option<&serde_json::Value> {
122 self.initial.as_ref()
123 }
124
125 fn clean(&self, value: Option<&serde_json::Value>) -> FieldResult<serde_json::Value> {
126 match value {
127 None => {
128 if self.required {
129 Err(FieldError::Required(self.name.clone()))
130 } else {
131 Ok(serde_json::Value::Bool(false))
132 }
133 }
134 Some(v) => {
135 let b = match v {
137 serde_json::Value::Bool(b) => *b,
138 serde_json::Value::String(s) => {
139 let s_lower = s.to_lowercase();
141 !(s.is_empty() || s_lower == "false" || s == "0")
142 }
143 serde_json::Value::Number(n) => {
144 if let Some(i) = n.as_i64() {
146 i != 0
147 } else if let Some(f) = n.as_f64() {
148 f != 0.0
149 } else {
150 true
151 }
152 }
153 serde_json::Value::Null => false,
154 _ => {
155 return Err(FieldError::Validation(
156 "Cannot convert to boolean".to_string(),
157 ));
158 }
159 };
160
161 if self.required && !b {
165 return Err(FieldError::Required(self.name.clone()));
166 }
167
168 Ok(serde_json::Value::Bool(b))
169 }
170 }
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177 use rstest::rstest;
178 use serde_json::json;
179
180 #[rstest]
181 fn test_required_boolean_rejects_false() {
182 let field = BooleanField::new("terms".to_string()).required();
184
185 assert!(
187 field.clean(Some(&json!(false))).is_err(),
188 "required BooleanField should reject false"
189 );
190 }
191
192 #[rstest]
193 fn test_required_boolean_accepts_true() {
194 let field = BooleanField::new("terms".to_string()).required();
196
197 let result = field.clean(Some(&json!(true)));
199
200 assert_eq!(result.unwrap(), json!(true));
202 }
203
204 #[rstest]
205 fn test_required_boolean_rejects_none() {
206 let field = BooleanField::new("terms".to_string()).required();
208
209 assert!(field.clean(None).is_err());
211 }
212
213 #[rstest]
214 fn test_required_boolean_rejects_false_string() {
215 let field = BooleanField::new("terms".to_string()).required();
217
218 assert!(field.clean(Some(&json!("false"))).is_err());
220 assert!(field.clean(Some(&json!("0"))).is_err());
221 assert!(field.clean(Some(&json!(""))).is_err());
222 }
223
224 #[rstest]
225 fn test_required_boolean_rejects_zero() {
226 let field = BooleanField::new("terms".to_string()).required();
228
229 assert!(field.clean(Some(&json!(0))).is_err());
231 }
232
233 #[rstest]
234 fn test_optional_boolean_accepts_false() {
235 let field = BooleanField::new("newsletter".to_string());
237
238 let result = field.clean(Some(&json!(false)));
240
241 assert_eq!(result.unwrap(), json!(false));
243 }
244
245 #[rstest]
246 fn test_optional_boolean_accepts_none() {
247 let field = BooleanField::new("newsletter".to_string());
249
250 let result = field.clean(None);
252
253 assert_eq!(result.unwrap(), json!(false));
255 }
256
257 #[rstest]
258 fn test_required_boolean_accepts_truthy_string() {
259 let field = BooleanField::new("terms".to_string()).required();
261
262 assert_eq!(field.clean(Some(&json!("true"))).unwrap(), json!(true));
264 assert_eq!(field.clean(Some(&json!("yes"))).unwrap(), json!(true));
265 assert_eq!(field.clean(Some(&json!("1"))).unwrap(), json!(true));
266 }
267
268 #[rstest]
269 fn test_required_boolean_rejects_null() {
270 let field = BooleanField::new("terms".to_string()).required();
272
273 assert!(field.clean(Some(&json!(null))).is_err());
275 }
276}