Skip to main content

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}