Skip to main content

reinhardt_pages/
form_generated.rs

1//! Static Metadata Types for form! Macro Generated Code
2//!
3//! This module is always available (on both WASM and server) because it only
4//! depends on `serde`. It provides metadata structures specifically designed
5//! for the form! macro.
6//!
7//! Unlike `FormMetadata` from `reinhardt-forms::wasm_compat` which is extracted from
8//! runtime Form instances, these types are generated at compile-time and include
9//! additional styling and action information.
10//!
11//! ## Design Decision
12//!
13//! The form! macro generates static forms with compile-time known structure,
14//! which allows for:
15//! - Direct action URL and method specification
16//! - CSS class customization at the form and field level
17//! - Type-safe field accessors
18//!
19//! ## Example
20//!
21//! ```no_run
22//! use reinhardt_pages::form;
23//!
24//! let login_form = form! {
25//!     name: LoginForm,
26//!     action: "/api/login",
27//!     class: "login-form",
28//!
29//!     fields: {
30//!         username: CharField { required, class: "input-field" },
31//!         password: CharField { widget: PasswordInput },
32//!     },
33//! };
34//!
35//! // Access metadata
36//! let metadata = login_form.metadata();
37//! assert_eq!(metadata.action, "/api/login");
38//! ```
39
40use serde::{Deserialize, Serialize};
41
42/// Static form metadata for macro-generated forms.
43///
44/// This structure contains all information needed to render a form
45/// generated by the form! macro.
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct StaticFormMetadata {
48	/// HTML id attribute for the form element (auto-generated from form name)
49	pub id: String,
50
51	/// Form action URL or server function path
52	pub action: String,
53
54	/// HTTP method (GET, POST, PUT, PATCH, DELETE)
55	pub method: String,
56
57	/// CSS class for the form element
58	pub class: String,
59
60	/// Field metadata list
61	pub fields: Vec<StaticFieldMetadata>,
62}
63
64impl StaticFormMetadata {
65	/// Creates a new StaticFormMetadata.
66	pub fn new(
67		id: impl Into<String>,
68		action: impl Into<String>,
69		method: impl Into<String>,
70		class: impl Into<String>,
71	) -> Self {
72		Self {
73			id: id.into(),
74			action: action.into(),
75			method: method.into(),
76			class: class.into(),
77			fields: Vec::new(),
78		}
79	}
80
81	/// Adds a field to the metadata.
82	pub fn with_field(mut self, field: StaticFieldMetadata) -> Self {
83		self.fields.push(field);
84		self
85	}
86
87	/// Adds multiple fields to the metadata.
88	pub fn with_fields(mut self, fields: Vec<StaticFieldMetadata>) -> Self {
89		self.fields = fields;
90		self
91	}
92}
93
94/// Static field metadata for macro-generated forms.
95///
96/// This structure contains all information needed to render a single form field,
97/// including styling attributes that are specified at compile-time.
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct StaticFieldMetadata {
100	/// Field name (used as form data key and HTML name attribute)
101	pub name: String,
102
103	/// Field type identifier (e.g., "CharField", "IntegerField")
104	pub field_type: String,
105
106	/// Widget type identifier (e.g., "TextInput", "PasswordInput")
107	pub widget: String,
108
109	/// Whether the field is required
110	pub required: bool,
111
112	/// Human-readable label
113	pub label: String,
114
115	/// Placeholder text for input elements
116	pub placeholder: String,
117
118	/// CSS class for the input element
119	pub input_class: String,
120
121	/// CSS class for the wrapper div element
122	pub wrapper_class: String,
123
124	/// CSS class for the label element
125	pub label_class: String,
126
127	/// CSS class for error message elements
128	pub error_class: String,
129}
130
131impl StaticFieldMetadata {
132	/// Creates a new StaticFieldMetadata with default styling.
133	pub fn new(name: impl Into<String>, field_type: impl Into<String>) -> Self {
134		let name_str = name.into();
135		Self {
136			label: name_str.clone(),
137			name: name_str,
138			field_type: field_type.into(),
139			widget: "TextInput".to_string(),
140			required: false,
141			placeholder: String::new(),
142			input_class: "reinhardt-input".to_string(),
143			wrapper_class: "reinhardt-field".to_string(),
144			label_class: "reinhardt-label".to_string(),
145			error_class: "reinhardt-error".to_string(),
146		}
147	}
148
149	/// Sets the widget type.
150	pub fn with_widget(mut self, widget: impl Into<String>) -> Self {
151		self.widget = widget.into();
152		self
153	}
154
155	/// Sets the required flag.
156	pub fn with_required(mut self, required: bool) -> Self {
157		self.required = required;
158		self
159	}
160
161	/// Sets the label.
162	pub fn with_label(mut self, label: impl Into<String>) -> Self {
163		self.label = label.into();
164		self
165	}
166
167	/// Sets the placeholder.
168	pub fn with_placeholder(mut self, placeholder: impl Into<String>) -> Self {
169		self.placeholder = placeholder.into();
170		self
171	}
172
173	/// Sets the input CSS class.
174	pub fn with_input_class(mut self, class: impl Into<String>) -> Self {
175		self.input_class = class.into();
176		self
177	}
178
179	/// Sets the wrapper CSS class.
180	pub fn with_wrapper_class(mut self, class: impl Into<String>) -> Self {
181		self.wrapper_class = class.into();
182		self
183	}
184
185	/// Sets the label CSS class.
186	pub fn with_label_class(mut self, class: impl Into<String>) -> Self {
187		self.label_class = class.into();
188		self
189	}
190
191	/// Sets the error CSS class.
192	pub fn with_error_class(mut self, class: impl Into<String>) -> Self {
193		self.error_class = class.into();
194		self
195	}
196}
197
198#[cfg(test)]
199mod tests {
200	use super::*;
201	use rstest::rstest;
202
203	#[rstest]
204	fn test_static_form_metadata_creation() {
205		let form = StaticFormMetadata::new("login-form", "/api/login", "POST", "login-form");
206
207		assert_eq!(form.id, "login-form");
208		assert_eq!(form.action, "/api/login");
209		assert_eq!(form.method, "POST");
210		assert_eq!(form.class, "login-form");
211		assert!(form.fields.is_empty());
212	}
213
214	#[rstest]
215	fn test_static_form_metadata_with_fields() {
216		let form = StaticFormMetadata::new("my-form", "/api/submit", "POST", "my-form")
217			.with_fields(vec![
218				StaticFieldMetadata::new("username", "CharField").with_required(true),
219				StaticFieldMetadata::new("email", "EmailField"),
220			]);
221
222		assert_eq!(form.id, "my-form");
223		assert_eq!(form.fields.len(), 2);
224		assert_eq!(form.fields[0].name, "username");
225		assert!(form.fields[0].required);
226		assert_eq!(form.fields[1].name, "email");
227		assert!(!form.fields[1].required);
228	}
229
230	#[rstest]
231	fn test_static_field_metadata_defaults() {
232		let field = StaticFieldMetadata::new("test_field", "CharField");
233
234		assert_eq!(field.name, "test_field");
235		assert_eq!(field.label, "test_field");
236		assert_eq!(field.field_type, "CharField");
237		assert_eq!(field.widget, "TextInput");
238		assert!(!field.required);
239		assert!(field.placeholder.is_empty());
240		assert_eq!(field.input_class, "reinhardt-input");
241		assert_eq!(field.wrapper_class, "reinhardt-field");
242		assert_eq!(field.label_class, "reinhardt-label");
243		assert_eq!(field.error_class, "reinhardt-error");
244	}
245
246	#[rstest]
247	fn test_static_field_metadata_customization() {
248		let field = StaticFieldMetadata::new("email", "EmailField")
249			.with_widget("EmailInput")
250			.with_required(true)
251			.with_label("Email Address")
252			.with_placeholder("Enter your email")
253			.with_input_class("custom-input")
254			.with_wrapper_class("custom-wrapper");
255
256		assert_eq!(field.name, "email");
257		assert_eq!(field.label, "Email Address");
258		assert_eq!(field.widget, "EmailInput");
259		assert!(field.required);
260		assert_eq!(field.placeholder, "Enter your email");
261		assert_eq!(field.input_class, "custom-input");
262		assert_eq!(field.wrapper_class, "custom-wrapper");
263	}
264
265	#[rstest]
266	fn test_static_metadata_serialization() {
267		let form = StaticFormMetadata::new("test-form", "/api/test", "POST", "test-form")
268			.with_field(StaticFieldMetadata::new("name", "CharField").with_required(true));
269
270		let json = serde_json::to_string(&form).expect("Failed to serialize");
271		assert!(json.contains("\"id\":\"test-form\""));
272		assert!(json.contains("\"action\":\"/api/test\""));
273		assert!(json.contains("\"name\":\"name\""));
274
275		let deserialized: StaticFormMetadata =
276			serde_json::from_str(&json).expect("Failed to deserialize");
277		assert_eq!(deserialized.id, "test-form");
278		assert_eq!(deserialized.action, "/api/test");
279		assert_eq!(deserialized.fields.len(), 1);
280	}
281}