molten_core/field.rs
1//! This module defines the core structures for managing the definition and
2//! behavior of individual fields within a form.
3//!
4//! It includes `FieldType` to enumerate the various data types a field can hold,
5//! `FieldDefinition` to describe the metadata and validation rules for a field,
6//! and `FieldBuilder` for constructing `FieldDefinition` instances programmatically.
7use serde::{Deserialize, Serialize};
8use validator::Validate;
9
10/// The specific data type of a field.
11///
12/// This enum determines:
13/// 1. How the data is validated.
14/// 2. How the data is stored in the document JSON.
15/// 3. How the UI renders the input (e.g., Checkbox vs Text Input).
16///
17/// # Serde Serialization
18/// This enum uses "Adjacently Tagged" serialization.
19///
20/// **Example JSON:**
21/// ```json
22/// {
23/// "kind": "select",
24/// "config": {
25/// "options": ["A", "B"],
26/// "allow_multiple": false
27/// }
28/// }
29/// ```
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31#[serde(tag = "kind", content = "config", rename_all = "snake_case")]
32pub enum FieldType {
33 /// Standard single-line text input.
34 Text,
35
36 /// Multi-line text area, suitable for descriptions or comments.
37 TextArea,
38
39 /// Numerical input (integer or floating point).
40 ///
41 /// Optional validation constraints can be applied via `min` and `max`.
42 Number {
43 /// The minimum allowed value (inclusive).
44 #[serde(default)]
45 min: Option<f64>,
46 /// The maximum allowed value (inclusive).
47 #[serde(default)]
48 max: Option<f64>,
49 },
50
51 /// A boolean flag (True/False).
52 Boolean,
53
54 /// A specific date and time.
55 DateTime,
56
57 /// A selection from a predefined list of options.
58 Select {
59 /// The list of valid strings a user can choose from.
60 options: Vec<String>,
61 /// If true, the user can select more than one option.
62 #[serde(default)]
63 allow_multiple: bool,
64 },
65}
66
67/// Defines the validated schema and metadata for a single field in a Form.
68///
69/// A `FieldDefinition` does not hold the data itself; rather, it describes
70/// what the data *should* look like.
71#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
72#[serde(try_from = "FieldBuilder")]
73pub struct FieldDefinition {
74 /// The unique key used to store this field's data in the Document.
75 ///
76 /// *Best Practice:* Use `snake_case` (e.g., `incident_date`, `employee_id`).
77 /// validation: Must be between 1-64 characters
78 #[validate(length(min = 1, max = 64))]
79 id: String,
80
81 /// The human-readable label displayed in the UI.
82 /// validation: Must be between 1-100 characters
83 #[validate(length(min = 1, max = 100))]
84 label: String,
85
86 /// The data type configuration.
87 field_type: FieldType,
88
89 /// If `true`, the document validation will fail if this field is missing or null.
90 /// Applied globally to this field. For field requirements conditional on phase
91 /// transitions, see documentation for [`crate::workflow::Transition`]
92 required: bool,
93
94 /// An optional tooltip or help text to guide the user.
95 #[serde(skip_serializing_if = "Option::is_none")]
96 description: Option<String>,
97}
98
99impl FieldDefinition {
100 /// Getter method to obtain Field ID
101 pub fn id(&self) -> &str {
102 &self.id
103 }
104 /// Getter method to obtain Field Label
105 pub fn label(&self) -> &str {
106 &self.label
107 }
108 /// Getter method to obtain Field Type
109 pub fn field_type(&self) -> &FieldType {
110 &self.field_type
111 }
112 /// Getter method to return whether a field is required
113 pub fn is_required(&self) -> bool {
114 self.required
115 }
116 /// Getter method to obtain Field Description
117 pub fn description(&self) -> Option<&str> {
118 self.description.as_deref()
119 }
120}
121
122impl TryFrom<FieldBuilder> for FieldDefinition {
123 type Error = validator::ValidationErrors;
124
125 fn try_from(builder: FieldBuilder) -> Result<Self, Self::Error> {
126 let def = FieldDefinition {
127 id: builder.id,
128 label: builder.label,
129 field_type: builder.field_type,
130 required: builder.required,
131 description: builder.description,
132 };
133
134 def.validate()?;
135
136 Ok(def)
137 }
138}
139
140/// Builder for constructing validated [`FieldDefinition`] instances.
141///
142/// `FieldBuilder` defines form fields and validates upon construction.
143/// This allows callers to incrementally configure a field and receive a validated
144/// [`FieldDefinition`] only once all required metadata has been supplied.
145///
146/// # Examples
147/// ```
148/// use molten_core::field::{FieldBuilder, FieldType};
149///
150/// let field = FieldBuilder::new(
151/// "age",
152/// "User Age",
153/// FieldType::Number { min: Some(0.0), max: Some(120.0) },
154/// )
155/// .required(true)
156/// .with_description("Please enter your age in years.")
157/// .build()
158/// .expect("Field definition should be valid");
159/// ```
160///
161/// # Errors
162/// Returns [`validator::ValidationErrors`] if the constructed
163/// [`FieldDefinition`] violates any declared validation constraints.
164#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct FieldBuilder {
166 id: String,
167 label: String,
168 field_type: FieldType,
169 #[serde(default)]
170 required: bool,
171 description: Option<String>,
172}
173
174impl FieldBuilder {
175 /// Creates a new FieldDefinition with default settings (optional, no description).
176 pub fn new(id: &str, label: &str, field_type: FieldType) -> Self {
177 Self {
178 id: id.to_string(),
179 label: label.to_string(),
180 field_type,
181 required: false,
182 description: None,
183 }
184 }
185
186 /// Sets the required flag.
187 pub fn required(mut self, is_required: bool) -> Self {
188 self.required = is_required;
189 self
190 }
191
192 /// Adds a description/tooltip.
193 pub fn with_description(mut self, description: &str) -> Self {
194 self.description = Some(description.to_string());
195 self
196 }
197
198 /// Creates a validated FieldDefinition entity using the builder pattern
199 pub fn build(self) -> Result<FieldDefinition, validator::ValidationErrors> {
200 FieldDefinition::try_from(self)
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use serde_json::json;
208
209 #[test]
210 fn test_field_builder() {
211 let field = FieldBuilder::new("test_id", "Test Label", FieldType::Text)
212 .required(true)
213 .with_description("A test field")
214 .build()
215 .expect("Field builder should produce a valid FieldDefinition here.");
216
217 assert_eq!(field.id, "test_id");
218 assert_eq!(field.required, true);
219 assert_eq!(field.description, Some("A test field".to_string()));
220 assert!(matches!(field.field_type, FieldType::Text));
221 }
222
223 #[test]
224 fn test_serialization_text() {
225 // Test simple unit variant (Text)
226 let field_type = FieldType::Text;
227 let json = serde_json::to_value(&field_type).unwrap();
228
229 // Adjacently tagged unit variants usually serialize to just the tag object
230 // or tag with null config depending on specific serde behavior.
231 // Let's verify exact output.
232 assert_eq!(json, json!({ "kind": "text" }));
233 }
234
235 #[test]
236 fn test_serialization_number_config() {
237 // Test variant with config (Number)
238 let field_type = FieldType::Number {
239 min: Some(0.0),
240 max: Some(100.0),
241 };
242 let json = serde_json::to_value(&field_type).unwrap();
243
244 assert_eq!(
245 json,
246 json!({
247 "kind": "number",
248 "config": {
249 "min": 0.0,
250 "max": 100.0
251 }
252 })
253 );
254 }
255
256 #[test]
257 fn test_deserialization_select() {
258 // Test parsing JSON back into Rust structs
259 let json_input = json!({
260 "id": "status",
261 "label": "Status",
262 "required": true,
263 "field_type": {
264 "kind": "select",
265 "config": {
266 "options": ["Open", "Closed"],
267 "allow_multiple": true
268 }
269 }
270 });
271
272 let field: FieldDefinition = serde_json::from_value(json_input).unwrap();
273
274 assert_eq!(field.id, "status");
275 match field.field_type {
276 FieldType::Select {
277 options,
278 allow_multiple,
279 } => {
280 assert_eq!(options, vec!["Open", "Closed"]);
281 assert_eq!(allow_multiple, true);
282 }
283 _ => panic!("Wrong field type deserialized"),
284 }
285 }
286
287 #[test]
288 fn test_deserialization_defaults() {
289 // Test that optional fields (min/max/required) use defaults if missing in JSON
290 let json_input = json!({
291 "id": "score",
292 "label": "Score",
293 "field_type": {
294 "kind": "number",
295 "config": {}
296 }
297 });
298
299 let field: FieldDefinition =
300 serde_json::from_value(json_input).expect("Field definition should be valid");
301
302 assert_eq!(field.required, false); // Default is false
303
304 match field.field_type {
305 FieldType::Number { min, max } => {
306 assert_eq!(min, None);
307 assert_eq!(max, None);
308 }
309 _ => panic!("Wrong field type"),
310 }
311 }
312
313 #[test]
314 fn test_serde_validation_integration() {
315 // 1. Valid JSON
316 let valid_json = json!({
317 "id": "short_id",
318 "label": "Valid Label",
319 "field_type": { "kind": "text" }
320 });
321 let result: Result<FieldDefinition, _> = serde_json::from_value(valid_json);
322 assert!(result.is_ok());
323
324 // 2. Invalid JSON (ID exceeds length constraint)
325 let long_id =
326 "this_id_is_way_too_long_to_pass_validation_rules_that_we_set_at_sixty_four_characters";
327 let invalid_json = json!({
328 "id": long_id,
329 "label": "Invalid ID",
330 "field_type": { "kind": "text" }
331 });
332 let result: Result<FieldDefinition, _> = serde_json::from_value(invalid_json);
333
334 assert!(result.is_err());
335 let err = result.unwrap_err();
336 // Is it an input data-related error?
337 assert!(err.is_data());
338 // Is the error caused by invalid length?
339 let err_string = err.to_string();
340 assert!(err_string.contains("length"));
341
342 // 3. Invalid JSON (Label exceeds length constraint )
343 let long_label = "a".repeat(101);
344 let invalid_json = json!({
345 "id": "this_id_is_an_ok_length",
346 "label": long_label,
347 "field_type": { "kind": "text" }
348 });
349 let result: Result<FieldDefinition, _> = serde_json::from_value(invalid_json);
350
351 assert!(result.is_err());
352 let err = result.unwrap_err();
353 // Is it an input data-related error?
354 assert!(err.is_data());
355 // Is the error caused by invalid length?
356 let err_string = err.to_string();
357 assert!(err_string.contains("length"));
358 }
359}