reinhardt_forms/wasm_compat.rs
1//! WASM Compatibility Layer for Forms (Week 5 Day 1-2)
2//!
3//! This module provides serializable metadata structures that allow Django-style
4//! Forms to be rendered on the client-side (WASM) without requiring the full
5//! `Form` struct with its trait objects and non-serializable closures.
6//!
7//! ## Architecture
8//!
9//! The metadata extraction follows this pattern:
10//!
11//! ```mermaid
12//! flowchart LR
13//! subgraph Server["Server-side"]
14//! Form["Form<br/>(traits, closures)"]
15//! end
16//!
17//! subgraph Client["Client-side (WASM)"]
18//! FormMetadata["FormMetadata<br/>(plain data, serializable)"]
19//! FormComponent["FormComponent<br/>(WASM UI)"]
20//! end
21//!
22//! Form -->|"to_metadata()"| FormMetadata
23//! FormMetadata --> FormComponent
24//! ```
25//!
26//! ## Example
27//!
28//! ```
29//! use reinhardt_forms::{Form, CharField, Field};
30//! use reinhardt_forms::wasm_compat::{FormMetadata, FormExt};
31//!
32//! // Server-side: Create form
33//! let mut form = Form::new();
34//! form.add_field(Box::new(CharField::new("username".to_string())));
35//!
36//! // Extract metadata for client
37//! let metadata: FormMetadata = form.to_metadata();
38//!
39//! // Serialize and send to WASM
40//! let json = serde_json::to_string(&metadata).unwrap();
41//! ```
42
43use crate::field::Widget;
44use crate::form::Form;
45use serde::{Deserialize, Serialize};
46use std::collections::HashMap;
47
48/// Validation rule types for client-side validation (Phase 2-A)
49///
50/// These rules enable client-side validation for better UX, while
51/// server-side validation remains mandatory for security.
52///
53/// ## Security Note
54///
55/// Client-side validation is for UX enhancement only and MUST NOT
56/// be relied upon for security. Server-side validation is always required.
57///
58/// ## Design Principle
59///
60/// All validation rules are declarative and type-safe. We do NOT use
61/// JavaScript expressions to prevent arbitrary code execution vulnerabilities.
62/// Instead, each rule type has specific parameters that are validated in Rust.
63#[derive(Debug, Clone, Serialize, Deserialize)]
64#[serde(tag = "type", rename_all = "snake_case")]
65pub enum ValidationRule {
66 /// Minimum length validation for string fields
67 MinLength {
68 /// Field name to validate
69 field_name: String,
70 /// Minimum required length
71 min: usize,
72 /// Error message to display when validation fails
73 error_message: String,
74 },
75 /// Maximum length validation for string fields
76 MaxLength {
77 /// Field name to validate
78 field_name: String,
79 /// Maximum allowed length
80 max: usize,
81 /// Error message to display when validation fails
82 error_message: String,
83 },
84 /// Regex pattern validation for string fields
85 Pattern {
86 /// Field name to validate
87 field_name: String,
88 /// Regex pattern to match (must be valid regex)
89 pattern: String,
90 /// Error message to display when validation fails
91 error_message: String,
92 },
93 /// Minimum value validation for numeric fields
94 MinValue {
95 /// Field name to validate
96 field_name: String,
97 /// Minimum allowed value
98 min: f64,
99 /// Error message to display when validation fails
100 error_message: String,
101 },
102 /// Maximum value validation for numeric fields
103 MaxValue {
104 /// Field name to validate
105 field_name: String,
106 /// Maximum allowed value
107 max: f64,
108 /// Error message to display when validation fails
109 error_message: String,
110 },
111 /// Email format validation
112 Email {
113 /// Field name to validate
114 field_name: String,
115 /// Error message to display when validation fails
116 error_message: String,
117 },
118 /// URL format validation
119 Url {
120 /// Field name to validate
121 field_name: String,
122 /// Error message to display when validation fails
123 error_message: String,
124 },
125 /// Cross-field equality validation (e.g., password confirmation)
126 FieldsEqual {
127 /// Field names to compare for equality
128 field_names: Vec<String>,
129 /// Error message to display when validation fails
130 error_message: String,
131 /// Target field for error display (None = non-field error)
132 target_field: Option<String>,
133 },
134 /// Date range validation (end_date >= start_date)
135 DateRange {
136 /// Start date field name
137 start_field: String,
138 /// End date field name
139 end_field: String,
140 /// Error message to display when validation fails
141 error_message: String,
142 /// Target field for error display
143 target_field: Option<String>,
144 },
145 /// Numeric range validation (max >= min)
146 NumericRange {
147 /// Minimum value field name
148 min_field: String,
149 /// Maximum value field name
150 max_field: String,
151 /// Error message to display when validation fails
152 error_message: String,
153 /// Target field for error display
154 target_field: Option<String>,
155 },
156 /// Reference to reinhardt-validators Validator
157 ValidatorRef {
158 /// Field name to validate
159 field_name: String,
160 /// Validator identifier (e.g., "email", "url", "min_length")
161 validator_id: String,
162 /// Validator parameters as JSON
163 /// Example: {"min": 8, "max": 20} for MinMaxLengthValidator
164 params: serde_json::Value,
165 /// Error message to display when validation fails
166 error_message: String,
167 },
168}
169
170/// Serializable form metadata for client-side rendering (Week 5 Day 1)
171///
172/// This structure contains all information needed to render a form on the
173/// client-side without requiring the full `Form` struct with its trait objects.
174///
175/// ## Fields
176///
177/// - `fields`: Metadata for each form field
178/// - `initial`: Initial values for the form (form-level)
179/// - `prefix`: Field name prefix (for multiple forms on same page)
180/// - `is_bound`: Whether the form has been bound with data
181/// - `errors`: Validation errors (if any)
182/// - `validation_rules`: Client-side validation rules (Phase 2-A)
183/// - `non_field_errors`: Form-level validation errors (Phase 2-A)
184#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct FormMetadata {
186 /// Field metadata list
187 pub fields: Vec<FieldMetadata>,
188
189 /// Initial values (form-level)
190 pub initial: HashMap<String, serde_json::Value>,
191
192 /// Field name prefix
193 pub prefix: String,
194
195 /// Whether the form has been bound with data
196 pub is_bound: bool,
197
198 /// Validation errors (field name -> error messages)
199 pub errors: HashMap<String, Vec<String>>,
200
201 /// Client-side validation rules (Phase 2-A)
202 /// These rules enable immediate feedback to users without server round-trips.
203 /// Server-side validation is still mandatory for security.
204 #[serde(default)]
205 pub validation_rules: Vec<ValidationRule>,
206
207 /// Non-field errors (form-level errors) (Phase 2-A)
208 /// These are errors that don't belong to a specific field (e.g., "Passwords don't match")
209 #[serde(default)]
210 pub non_field_errors: Vec<String>,
211}
212
213/// Serializable field metadata for client-side rendering (Week 5 Day 1)
214///
215/// This structure contains all information needed to render a single form field
216/// on the client-side.
217///
218/// ## Fields
219///
220/// - `name`: Field name (used as form data key)
221/// - `label`: Human-readable label (defaults to field name if None)
222/// - `required`: Whether the field is required
223/// - `help_text`: Help text displayed below the field
224/// - `widget`: Widget type for rendering (TextInput, Select, etc.)
225/// - `initial`: Initial value for this field
226#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct FieldMetadata {
228 /// Field name
229 pub name: String,
230
231 /// Human-readable label (optional)
232 pub label: Option<String>,
233
234 /// Whether the field is required
235 pub required: bool,
236
237 /// Help text (optional)
238 pub help_text: Option<String>,
239
240 /// Widget type for rendering
241 pub widget: Widget,
242
243 /// Initial value (optional)
244 pub initial: Option<serde_json::Value>,
245}
246
247/// Extension trait for Form to extract metadata (Week 5 Day 1)
248///
249/// This trait provides the `to_metadata()` method that converts a `Form`
250/// into a serializable `FormMetadata` structure.
251pub trait FormExt {
252 /// Extract serializable metadata from the form
253 ///
254 /// This method creates a `FormMetadata` structure containing all
255 /// information needed to render the form on the client-side.
256 ///
257 /// # Examples
258 ///
259 /// ```
260 /// use reinhardt_forms::{Form, CharField, Field};
261 /// use reinhardt_forms::wasm_compat::{FormMetadata, FormExt};
262 ///
263 /// let mut form = Form::new();
264 /// form.add_field(Box::new(CharField::new("email".to_string())));
265 ///
266 /// let metadata: FormMetadata = form.to_metadata();
267 /// assert_eq!(metadata.fields.len(), 1);
268 /// assert_eq!(metadata.fields[0].name, "email");
269 /// ```
270 fn to_metadata(&self) -> FormMetadata;
271}
272
273impl FormExt for Form {
274 fn to_metadata(&self) -> FormMetadata {
275 // Extract field metadata
276 let fields = self
277 .fields()
278 .iter()
279 .map(|field| FieldMetadata {
280 name: field.name().to_string(),
281 label: field.label().map(|s| s.to_string()),
282 required: field.required(),
283 help_text: field.help_text().map(|s| s.to_string()),
284 widget: field.widget().clone(),
285 initial: field.initial().cloned(),
286 })
287 .collect();
288
289 FormMetadata {
290 fields,
291 initial: self.initial().clone(),
292 prefix: self.prefix().to_string(),
293 is_bound: self.is_bound(),
294 errors: self.errors().clone(),
295 // Phase 2-A: Clone validation rules from Form
296 validation_rules: self.validation_rules().to_vec(),
297 non_field_errors: self
298 .errors()
299 .get(crate::form::ALL_FIELDS_KEY)
300 .cloned()
301 .unwrap_or_default(),
302 }
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309 use crate::fields::CharField;
310
311 #[test]
312 fn test_form_metadata_extraction() {
313 let mut form = Form::new();
314 form.add_field(Box::new(CharField::new("username".to_string())));
315 form.add_field(Box::new(CharField::new("email".to_string())));
316
317 let metadata = form.to_metadata();
318
319 assert_eq!(metadata.fields.len(), 2);
320 assert_eq!(metadata.fields[0].name, "username");
321 assert_eq!(metadata.fields[1].name, "email");
322 assert!(!metadata.is_bound);
323 }
324
325 #[test]
326 fn test_form_metadata_with_prefix() {
327 let mut form = Form::with_prefix("user".to_string());
328 form.add_field(Box::new(CharField::new("name".to_string())));
329
330 let metadata = form.to_metadata();
331
332 assert_eq!(metadata.prefix, "user");
333 assert_eq!(metadata.fields.len(), 1);
334 }
335
336 #[test]
337 fn test_form_metadata_serialization() {
338 let mut form = Form::new();
339 form.add_field(Box::new(CharField::new("test".to_string())));
340
341 let metadata = form.to_metadata();
342
343 // Test JSON serialization
344 let json = serde_json::to_string(&metadata).expect("Failed to serialize");
345 assert!(json.contains("\"name\":\"test\""));
346
347 // Test deserialization
348 let deserialized: FormMetadata =
349 serde_json::from_str(&json).expect("Failed to deserialize");
350 assert_eq!(deserialized.fields[0].name, "test");
351 }
352
353 #[test]
354 fn test_field_metadata_with_all_attributes() {
355 use crate::fields::CharField;
356
357 let field = CharField::new("bio".to_string())
358 .with_label("Biography")
359 .with_help_text("Tell us about yourself")
360 .required();
361
362 let mut form = Form::new();
363 form.add_field(Box::new(field));
364
365 let metadata = form.to_metadata();
366 let field_meta = &metadata.fields[0];
367
368 assert_eq!(field_meta.name, "bio");
369 assert_eq!(field_meta.label, Some("Biography".to_string()));
370 assert_eq!(
371 field_meta.help_text,
372 Some("Tell us about yourself".to_string())
373 );
374 assert!(field_meta.required);
375 }
376
377 #[test]
378 fn test_form_metadata_with_initial_values() {
379 use serde_json::json;
380
381 let mut initial = HashMap::new();
382 initial.insert("username".to_string(), json!("john_doe"));
383 initial.insert("age".to_string(), json!(25));
384
385 let mut form = Form::with_initial(initial);
386 form.add_field(Box::new(CharField::new("username".to_string())));
387
388 let metadata = form.to_metadata();
389
390 assert_eq!(metadata.initial.get("username"), Some(&json!("john_doe")));
391 assert_eq!(metadata.initial.get("age"), Some(&json!(25)));
392 }
393
394 #[test]
395 fn test_form_metadata_with_errors() {
396 use serde_json::json;
397
398 let mut form = Form::new();
399 // Create a required field - empty value should fail validation
400 form.add_field(Box::new(CharField::new("email".to_string()).required()));
401
402 // Bind with invalid data to generate errors (empty string for required field)
403 let mut data = HashMap::new();
404 data.insert("email".to_string(), json!("")); // Empty required field should fail
405 form.bind(data);
406
407 // Validate to populate errors
408 let is_valid = form.is_valid();
409
410 let metadata = form.to_metadata();
411
412 // Should have validation error for the required email field
413 assert!(!is_valid);
414 assert!(!metadata.errors.is_empty());
415 assert!(metadata.errors.contains_key("email"));
416 }
417}