Skip to main content

reinhardt_forms/
model_form.rs

1//! ModelForm implementation for ORM integration
2//!
3//! ModelForms automatically generate forms from ORM models, handling field
4//! inference, validation, and saving.
5
6use crate::{CharField, EmailField, FloatField, Form, FormError, FormField, IntegerField, Widget};
7use serde_json::Value;
8use std::collections::HashMap;
9use std::marker::PhantomData;
10
11/// Field type metadata for ModelForm field inference
12#[derive(Debug, Clone)]
13pub enum FieldType {
14	/// Character field with optional maximum length constraint.
15	Char {
16		/// Maximum character length, if any.
17		max_length: Option<usize>,
18	},
19	/// Multi-line text field rendered as a textarea.
20	Text,
21	/// Integer number field.
22	Integer,
23	/// Floating-point number field.
24	Float,
25	/// Boolean field rendered as a checkbox.
26	Boolean,
27	/// Date and time field.
28	DateTime,
29	/// Date-only field.
30	Date,
31	/// Time-only field.
32	Time,
33	/// Email address field with format validation.
34	Email,
35	/// URL field with format validation.
36	Url,
37	/// JSON data field.
38	Json,
39}
40
41/// Trait for models that can be used with ModelForm
42///
43/// This trait is specifically for form models. For ORM models, use `reinhardt_db::orm::Model`.
44pub trait FormModel: Send + Sync {
45	/// Get the model's field names
46	fn field_names() -> Vec<String>;
47
48	/// Get field type metadata for form field inference
49	///
50	/// # Examples
51	///
52	/// ```no_run
53	/// # use reinhardt_forms::model_form::FieldType;
54	/// fn field_type(name: &str) -> Option<FieldType> {
55	///     match name {
56	///         "name" => Some(FieldType::Char { max_length: Some(100) }),
57	///         "email" => Some(FieldType::Email),
58	///         "age" => Some(FieldType::Integer),
59	///         _ => None,
60	///     }
61	/// }
62	/// ```
63	fn field_type(_name: &str) -> Option<FieldType> {
64		None
65	}
66
67	/// Get a field value by name
68	fn get_field(&self, name: &str) -> Option<Value>;
69
70	/// Set a field value by name
71	fn set_field(&mut self, name: &str, value: Value) -> Result<(), String>;
72
73	/// Save the model to the database
74	fn save(&mut self) -> Result<(), String>;
75
76	/// Validate the model
77	fn validate(&self) -> Result<(), Vec<String>> {
78		Ok(())
79	}
80
81	/// Convert model instance to a choice label for display in forms
82	///
83	/// Default implementation returns the string representation of the primary key.
84	/// Override this method to provide custom display labels.
85	///
86	/// # Examples
87	///
88	/// ```no_run
89	/// # struct Example { id: i32, name: String }
90	/// # impl Example {
91	/// fn to_choice_label(&self) -> String {
92	///     format!("{} - {}", self.id, self.name)
93	/// }
94	/// # }
95	/// ```
96	fn to_choice_label(&self) -> String {
97		// Default: use the "id" field or empty string
98		self.get_field("id")
99			.and_then(|v| v.as_i64().map(|i| i.to_string()))
100			.or_else(|| {
101				self.get_field("id")
102					.and_then(|v| v.as_str().map(|s| s.to_string()))
103			})
104			.unwrap_or_default()
105	}
106
107	/// Get the primary key value as a string for form field validation
108	///
109	/// Default implementation uses the "id" field.
110	///
111	/// # Examples
112	///
113	/// ```no_run
114	/// # struct Example { id: i32 }
115	/// # impl Example {
116	/// fn to_choice_value(&self) -> String {
117	///     self.id.to_string()
118	/// }
119	/// # }
120	/// ```
121	fn to_choice_value(&self) -> String {
122		self.get_field("id")
123			.and_then(|v| v.as_i64().map(|i| i.to_string()))
124			.or_else(|| {
125				self.get_field("id")
126					.and_then(|v| v.as_str().map(|s| s.to_string()))
127			})
128			.unwrap_or_default()
129	}
130}
131
132/// ModelForm configuration
133#[derive(Debug, Clone, Default)]
134pub struct ModelFormConfig {
135	/// Fields to include in the form (None = all fields)
136	pub fields: Option<Vec<String>>,
137	/// Fields to exclude from the form
138	pub exclude: Vec<String>,
139	/// Custom widgets for specific fields
140	pub widgets: HashMap<String, crate::Widget>,
141	/// Custom labels for specific fields
142	pub labels: HashMap<String, String>,
143	/// Custom help text for specific fields
144	pub help_texts: HashMap<String, String>,
145}
146
147impl ModelFormConfig {
148	/// Creates a new default configuration.
149	pub fn new() -> Self {
150		Self::default()
151	}
152	/// Sets the list of fields to include in the generated form.
153	pub fn fields(mut self, fields: Vec<String>) -> Self {
154		self.fields = Some(fields);
155		self
156	}
157	/// Sets the list of fields to exclude from the generated form.
158	pub fn exclude(mut self, exclude: Vec<String>) -> Self {
159		self.exclude = exclude;
160		self
161	}
162	/// Overrides the widget for a specific field.
163	pub fn widget(mut self, field: String, widget: crate::Widget) -> Self {
164		self.widgets.insert(field, widget);
165		self
166	}
167	/// Sets a custom label for a specific field.
168	pub fn label(mut self, field: String, label: String) -> Self {
169		self.labels.insert(field, label);
170		self
171	}
172	/// Sets custom help text for a specific field.
173	pub fn help_text(mut self, field: String, text: String) -> Self {
174		self.help_texts.insert(field, text);
175		self
176	}
177}
178
179/// A form that is automatically generated from a Model
180pub struct ModelForm<T: FormModel> {
181	form: Form,
182	instance: Option<T>,
183	// Allow dead_code: config stored for future form rendering customization
184	#[allow(dead_code)]
185	config: ModelFormConfig,
186	_phantom: PhantomData<T>,
187}
188
189impl<T: FormModel> ModelForm<T> {
190	/// Create a form field from field type metadata
191	fn create_form_field(
192		name: &str,
193		field_type: FieldType,
194		config: &ModelFormConfig,
195	) -> Box<dyn FormField> {
196		let label = config.labels.get(name).cloned();
197		let help_text = config.help_texts.get(name).cloned();
198		let widget = config.widgets.get(name).cloned();
199
200		match field_type {
201			FieldType::Char { max_length } => {
202				let mut field = CharField::new(name.to_string());
203				if let Some(label) = label {
204					field.label = Some(label);
205				}
206				if let Some(help) = help_text {
207					field.help_text = Some(help);
208				}
209				if let Some(w) = widget {
210					field.widget = w;
211				}
212				field.max_length = max_length;
213				Box::new(field)
214			}
215			FieldType::Text => {
216				let mut field = CharField::new(name.to_string());
217				if let Some(label) = label {
218					field.label = Some(label);
219				}
220				if let Some(help) = help_text {
221					field.help_text = Some(help);
222				}
223				if let Some(w) = widget {
224					field.widget = w;
225				} else {
226					field.widget = Widget::TextArea;
227				}
228				Box::new(field)
229			}
230			FieldType::Email => {
231				let mut field = EmailField::new(name.to_string());
232				if let Some(label) = label {
233					field.label = Some(label);
234				}
235				if let Some(help) = help_text {
236					field.help_text = Some(help);
237				}
238				if let Some(w) = widget {
239					field.widget = w;
240				}
241				Box::new(field)
242			}
243			FieldType::Integer => {
244				let mut field = IntegerField::new(name.to_string());
245				if let Some(label) = label {
246					field.label = Some(label);
247				}
248				if let Some(help) = help_text {
249					field.help_text = Some(help);
250				}
251				if let Some(w) = widget {
252					field.widget = w;
253				}
254				Box::new(field)
255			}
256			FieldType::Float => {
257				let mut field = FloatField::new(name.to_string());
258				if let Some(label) = label {
259					field.label = Some(label);
260				}
261				if let Some(help) = help_text {
262					field.help_text = Some(help);
263				}
264				if let Some(w) = widget {
265					field.widget = w;
266				}
267				Box::new(field)
268			}
269			// For unsupported types, default to CharField
270			_ => {
271				let mut field = CharField::new(name.to_string());
272				if let Some(label) = label {
273					field.label = Some(label);
274				}
275				if let Some(help) = help_text {
276					field.help_text = Some(help);
277				}
278				if let Some(w) = widget {
279					field.widget = w;
280				}
281				Box::new(field)
282			}
283		}
284	}
285
286	/// Create a new ModelForm from a model instance
287	///
288	/// # Examples
289	///
290	/// ```ignore
291	/// use reinhardt_forms::{ModelForm, ModelFormConfig};
292	///
293	/// // Assuming we have a model that implements the Model trait
294	/// let config = ModelFormConfig::new();
295	/// let form = ModelForm::new(Some(instance), config);
296	/// ```
297	pub fn new(instance: Option<T>, config: ModelFormConfig) -> Self {
298		let mut form = Form::new();
299
300		// Get field names from model
301		let all_fields = T::field_names();
302
303		// Filter fields based on config
304		let fields_to_include: Vec<String> = if let Some(ref include) = config.fields {
305			include
306				.iter()
307				.filter(|f| !config.exclude.contains(f))
308				.cloned()
309				.collect()
310		} else {
311			all_fields
312				.iter()
313				.filter(|f| !config.exclude.contains(f))
314				.cloned()
315				.collect()
316		};
317
318		// Infer field types from model metadata and add to form
319		for field_name in &fields_to_include {
320			if let Some(field_type) = T::field_type(field_name) {
321				let form_field = Self::create_form_field(field_name, field_type, &config);
322				form.add_field(form_field);
323			}
324		}
325
326		// If instance exists, populate initial data from the instance
327		if let Some(ref inst) = instance {
328			let mut initial = HashMap::new();
329			for field_name in &fields_to_include {
330				if let Some(value) = inst.get_field(field_name) {
331					initial.insert(field_name.clone(), value);
332				}
333			}
334			form.bind(initial);
335		}
336
337		Self {
338			form,
339			instance,
340			config,
341			_phantom: PhantomData,
342		}
343	}
344	/// Create a new ModelForm without an instance (for creation)
345	///
346	/// # Examples
347	///
348	/// ```ignore
349	/// use reinhardt_forms::{ModelForm, ModelFormConfig};
350	///
351	/// let config = ModelFormConfig::new();
352	/// let form = ModelForm::<MyModel>::empty(config);
353	/// ```
354	pub fn empty(config: ModelFormConfig) -> Self {
355		Self::new(None, config)
356	}
357	/// Bind data to the form
358	///
359	/// # Examples
360	///
361	/// ```ignore
362	/// use reinhardt_forms::{ModelForm, ModelFormConfig};
363	/// use std::collections::HashMap;
364	/// use serde_json::json;
365	///
366	/// let config = ModelFormConfig::new();
367	/// let mut form = ModelForm::<MyModel>::empty(config);
368	/// let mut data = HashMap::new();
369	/// data.insert("field".to_string(), json!("value"));
370	/// form.bind(data);
371	/// ```
372	pub fn bind(&mut self, data: HashMap<String, Value>) -> &mut Self {
373		// Bind data to the underlying form
374		self.form.bind(data);
375		self
376	}
377	/// Check if the form is valid
378	///
379	/// # Examples
380	///
381	/// ```ignore
382	/// use reinhardt_forms::{ModelForm, ModelFormConfig};
383	///
384	/// let config = ModelFormConfig::new();
385	/// let mut form = ModelForm::<MyModel>::empty(config);
386	/// let is_valid = form.is_valid();
387	/// ```
388	pub fn is_valid(&mut self) -> bool {
389		// Validate the model if instance exists
390		if let Some(ref instance) = self.instance
391			&& let Err(_errors) = instance.validate()
392		{
393			return false;
394		}
395
396		true
397	}
398	/// Save the form data to the model instance
399	///
400	/// Returns `FormError::NoInstance` if no model instance is available.
401	///
402	/// # Examples
403	///
404	/// ```ignore
405	/// use reinhardt_forms::{ModelForm, ModelFormConfig};
406	///
407	/// let config = ModelFormConfig::new();
408	/// let mut form = ModelForm::<MyModel>::empty(config);
409	/// // Returns Err(FormError::NoInstance) without an instance
410	/// assert!(form.save().is_err());
411	/// ```
412	pub fn save(&mut self) -> Result<T, FormError> {
413		if !self.is_valid() {
414			return Err(FormError::Validation("Form is not valid".to_string()));
415		}
416
417		// Get existing instance or return error
418		let mut instance = self.instance.take().ok_or(FormError::NoInstance)?;
419
420		// Set field values from form's cleaned_data
421		let cleaned_data = self.form.cleaned_data();
422		for (field_name, value) in cleaned_data.iter() {
423			if let Err(e) = instance.set_field(field_name, value.clone()) {
424				return Err(FormError::Validation(format!(
425					"Failed to set field {}: {}",
426					field_name, e
427				)));
428			}
429		}
430
431		// Save the instance
432		if let Err(e) = instance.save() {
433			return Err(FormError::Validation(format!("Failed to save: {}", e)));
434		}
435
436		Ok(instance)
437	}
438	/// Set a field value directly on the model instance.
439	///
440	/// This is used by `InlineFormSet` to set foreign key values on child
441	/// instances before saving.
442	///
443	/// If no instance exists, this method is a no-op.
444	pub fn set_field_value(&mut self, field_name: &str, value: Value) {
445		if let Some(ref mut instance) = self.instance {
446			// Silently ignore errors from set_field, as the field may not exist
447			// on all model types (defensive approach for inline formsets)
448			let _ = instance.set_field(field_name, value);
449		}
450	}
451
452	/// Returns a reference to the underlying form.
453	pub fn form(&self) -> &Form {
454		&self.form
455	}
456	/// Returns a mutable reference to the underlying form.
457	pub fn form_mut(&mut self) -> &mut Form {
458		&mut self.form
459	}
460	/// Returns a reference to the model instance, if one exists.
461	pub fn instance(&self) -> Option<&T> {
462		self.instance.as_ref()
463	}
464}
465
466/// Builder for creating ModelForm instances
467pub struct ModelFormBuilder<T: FormModel> {
468	config: ModelFormConfig,
469	_phantom: PhantomData<T>,
470}
471
472impl<T: FormModel> ModelFormBuilder<T> {
473	/// Creates a new `ModelFormBuilder` with default configuration.
474	pub fn new() -> Self {
475		Self {
476			config: ModelFormConfig::default(),
477			_phantom: PhantomData,
478		}
479	}
480	/// Sets the list of fields to include in the form.
481	pub fn fields(mut self, fields: Vec<String>) -> Self {
482		self.config.fields = Some(fields);
483		self
484	}
485	/// Sets the list of fields to exclude from the form.
486	pub fn exclude(mut self, exclude: Vec<String>) -> Self {
487		self.config.exclude = exclude;
488		self
489	}
490	/// Overrides the widget for a specific field.
491	pub fn widget(mut self, field: String, widget: crate::Widget) -> Self {
492		self.config.widgets.insert(field, widget);
493		self
494	}
495	/// Overrides the label for a specific field.
496	pub fn label(mut self, field: String, label: String) -> Self {
497		self.config.labels.insert(field, label);
498		self
499	}
500	/// Overrides the help text for a specific field.
501	pub fn help_text(mut self, field: String, text: String) -> Self {
502		self.config.help_texts.insert(field, text);
503		self
504	}
505	/// Build the ModelForm with the configured settings
506	///
507	/// # Examples
508	///
509	/// ```ignore
510	/// use reinhardt_forms::{ModelFormBuilder, ModelFormConfig};
511	///
512	/// let config = ModelFormConfig::new();
513	/// let builder = ModelFormBuilder::<MyModel>::new();
514	/// let form = builder.build(None);
515	/// ```
516	pub fn build(self, instance: Option<T>) -> ModelForm<T> {
517		ModelForm::new(instance, self.config)
518	}
519}
520
521impl<T: FormModel> Default for ModelFormBuilder<T> {
522	fn default() -> Self {
523		Self::new()
524	}
525}
526
527#[cfg(test)]
528mod tests {
529	use super::*;
530	use rstest::rstest;
531
532	// Mock model for testing
533	#[derive(Debug)]
534	struct TestModel {
535		id: i32,
536		name: String,
537		email: String,
538	}
539
540	impl FormModel for TestModel {
541		fn field_names() -> Vec<String> {
542			vec!["id".to_string(), "name".to_string(), "email".to_string()]
543		}
544
545		fn field_type(name: &str) -> Option<FieldType> {
546			match name {
547				"id" => Some(FieldType::Integer),
548				"name" => Some(FieldType::Char {
549					max_length: Some(100),
550				}),
551				"email" => Some(FieldType::Email),
552				_ => None,
553			}
554		}
555
556		fn get_field(&self, name: &str) -> Option<Value> {
557			match name {
558				"id" => Some(Value::Number(self.id.into())),
559				"name" => Some(Value::String(self.name.clone())),
560				"email" => Some(Value::String(self.email.clone())),
561				_ => None,
562			}
563		}
564
565		fn set_field(&mut self, name: &str, value: Value) -> Result<(), String> {
566			match name {
567				"id" => {
568					if let Value::Number(n) = value {
569						self.id = n.as_i64().unwrap() as i32;
570						Ok(())
571					} else {
572						Err("Invalid type for id".to_string())
573					}
574				}
575				"name" => {
576					if let Value::String(s) = value {
577						self.name = s;
578						Ok(())
579					} else {
580						Err("Invalid type for name".to_string())
581					}
582				}
583				"email" => {
584					if let Value::String(s) = value {
585						self.email = s;
586						Ok(())
587					} else {
588						Err("Invalid type for email".to_string())
589					}
590				}
591				_ => Err(format!("Unknown field: {}", name)),
592			}
593		}
594
595		fn save(&mut self) -> Result<(), String> {
596			// Mock save
597			Ok(())
598		}
599	}
600
601	#[rstest]
602	fn test_model_form_config() {
603		// Arrange
604		let config = ModelFormConfig::new()
605			.fields(vec!["name".to_string(), "email".to_string()])
606			.exclude(vec!["id".to_string()]);
607
608		// Assert
609		assert_eq!(
610			config.fields,
611			Some(vec!["name".to_string(), "email".to_string()])
612		);
613		assert_eq!(config.exclude, vec!["id".to_string()]);
614	}
615
616	#[rstest]
617	fn test_model_form_builder() {
618		// Arrange
619		let instance = TestModel {
620			id: 1,
621			name: "John".to_string(),
622			email: "john@example.com".to_string(),
623		};
624
625		// Act
626		let form = ModelFormBuilder::<TestModel>::new()
627			.fields(vec!["name".to_string(), "email".to_string()])
628			.build(Some(instance));
629
630		// Assert
631		assert!(form.instance().is_some());
632	}
633
634	#[rstest]
635	fn test_model_field_names() {
636		// Act
637		let fields = TestModel::field_names();
638
639		// Assert
640		assert_eq!(
641			fields,
642			vec!["id".to_string(), "name".to_string(), "email".to_string()]
643		);
644	}
645
646	#[rstest]
647	fn test_save_without_instance_returns_no_instance_error() {
648		// Arrange
649		let config = ModelFormConfig::new();
650		let mut form = ModelForm::<TestModel>::empty(config);
651
652		// Act
653		let result = form.save();
654
655		// Assert
656		assert!(result.is_err());
657		let err = result.unwrap_err();
658		assert!(
659			matches!(err, FormError::NoInstance),
660			"Expected FormError::NoInstance, got: {err}"
661		);
662	}
663}