Skip to main content

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}