Skip to main content

molten_core/
form.rs

1//! This module defines the core structures for managing `FormDefinition`s,
2//! which serve as blueprints for documents within the Molten system.
3//!
4//! It includes `FormDefinition` to describe the overall structure of a form,
5//! containing a collection of `FieldDefinition`s and associated validation rules.
6//! The `FormBuilder` is provided for programmatic construction and validation
7//! of form definitions.
8use crate::field::FieldDefinition;
9use once_cell::sync::Lazy;
10use regex::Regex;
11use serde::{Deserialize, Serialize};
12use std::collections::HashSet;
13use validator::{Validate, ValidationError};
14
15// Only alphanumeric, hyphens, and underscores
16static ID_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[a-zA-Z0-9_-]+$").unwrap());
17
18/// Defines the structure of a Form (the "Table Schema").
19///
20/// A Form is a collection of fields with a unique identifier and versioning.
21#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
22#[serde(try_from = "FormBuilder")]
23pub struct FormDefinition {
24    /// The unique identifier for this form (e.g., "incident_report").
25    /// ID must be between 1 and 64 characters with only alhpanumeric, hyphens, and underscores
26    #[validate(length(min = 1, max = 64), regex(path = *ID_REGEX))]
27    id: String,
28
29    /// Human-readable name (e.g., "Incident Report").
30    #[validate(length(min = 1, max = 100))]
31    name: String,
32
33    /// A version number for schema evolution.
34    /// Useful when you update a form but want to keep old data compatible.
35    version: u32,
36
37    /// The list of fields that make up this form.
38    ///
39    /// # Validation
40    /// 1. Each field must be valid (nested validation).
41    /// 2. Field IDs must be unique within the form (custom validation).
42    #[validate(nested, custom(function = "validate_unique_field_ids"))]
43    fields: Vec<FieldDefinition>,
44}
45
46/// Custom validator to ensure no two fields share the same ID.
47fn validate_unique_field_ids(fields: &[FieldDefinition]) -> Result<(), ValidationError> {
48    let mut seen = HashSet::new();
49    for field in fields {
50        // We use the getter .id() because the field 'id' is private
51        if !seen.insert(field.id()) {
52            let mut err = ValidationError::new("duplicate_field_id");
53            err.add_param(std::borrow::Cow::from("duplicate_id"), &field.id());
54            return Err(err);
55        }
56    }
57    Ok(())
58}
59
60impl FormDefinition {
61    /// ID getter
62    pub fn id(&self) -> &str {
63        &self.id
64    }
65    /// Name getter
66    pub fn name(&self) -> &str {
67        &self.name
68    }
69    /// Version getter
70    pub fn version(&self) -> u32 {
71        self.version
72    }
73    /// Fields getter
74    pub fn fields(&self) -> &[FieldDefinition] {
75        &self.fields
76    }
77}
78
79/// Builder for constructing validated [`FormDefinition`] instances.
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct FormBuilder {
82    /// The unique identifier for this form.
83    pub id: String,
84    /// Human-readable name for the form.
85    pub name: String,
86    #[serde(default = "default_version")]
87    /// The version number for the form. Defaults to 1.
88    pub version: u32,
89    #[serde(default)]
90    /// The list of field definitions that make up this form.
91    pub fields: Vec<FieldDefinition>,
92}
93
94/// Provides the default version number for a form, which is `1`.
95fn default_version() -> u32 {
96    1
97}
98
99impl FormBuilder {
100    /// Creates a new `FormBuilder` instance with the given ID and name,
101    /// defaulting the version to 1 and fields to an empty list.
102    pub fn new(id: &str, name: &str) -> Self {
103        Self {
104            id: id.to_string(),
105            name: name.to_string(),
106            version: 1,
107            fields: Vec::new(),
108        }
109    }
110
111    /// Sets the version for the form.
112    pub fn version(mut self, version: u32) -> Self {
113        self.version = version;
114        self
115    }
116
117    /// Adds a `FieldDefinition` to the form.
118    pub fn add_field(mut self, field: FieldDefinition) -> Self {
119        self.fields.push(field);
120        self
121    }
122
123    /// Replaces the current list of fields with a new vector of `FieldDefinition`s.
124    pub fn with_fields(mut self, fields: Vec<FieldDefinition>) -> Self {
125        self.fields = fields;
126        self
127    }
128
129    /// Builds a validated `FormDefinition` from the `FormBuilder` instance.
130    ///
131    /// # Returns
132    /// A `Result` containing the `FormDefinition` if valid, or a
133    /// `validator::ValidationErrors` if validation fails.
134    pub fn build(self) -> Result<FormDefinition, validator::ValidationErrors> {
135        FormDefinition::try_from(self)
136    }
137}
138
139impl TryFrom<FormBuilder> for FormDefinition {
140    type Error = validator::ValidationErrors;
141
142    fn try_from(builder: FormBuilder) -> Result<Self, Self::Error> {
143        let form = FormDefinition {
144            id: builder.id,
145            name: builder.name,
146            version: builder.version,
147            fields: builder.fields,
148        };
149
150        form.validate()?;
151        Ok(form)
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use crate::field::{FieldBuilder, FieldType};
159    use serde_json::json;
160
161    fn create_field(id: &str) -> FieldDefinition {
162        FieldBuilder::new(id, "Label", FieldType::Text)
163            .build()
164            .unwrap()
165    }
166
167    #[test]
168    fn test_form_builder_valid() {
169        let first_name = create_field("first_name");
170        let last_name = create_field("last_name");
171
172        // Test add_field
173        let form = FormBuilder::new("user_profile", "User Profile")
174            .add_field(first_name.clone())
175            .add_field(last_name.clone())
176            .build();
177
178        assert!(form.is_ok());
179        let form = form.unwrap();
180        assert_eq!(form.fields.len(), 2);
181
182        // Test with_fields
183        let form = FormBuilder::new("user_profile", "User Profile")
184            .with_fields(vec![first_name, last_name])
185            .build();
186
187        assert!(form.is_ok());
188        let form = form.unwrap();
189        assert_eq!(form.fields.len(), 2);
190    }
191
192    #[test]
193    fn test_form_duplicate_fields() {
194        // Try to add two fields with the ID "email"
195        let form_res = FormBuilder::new("signup", "Sign Up")
196            .add_field(create_field("email"))
197            .add_field(create_field("email"))
198            .build();
199
200        assert!(form_res.is_err());
201
202        let err = form_res.unwrap_err();
203        assert!(err.to_string().contains("duplicate_field_id"));
204    }
205
206    #[test]
207    fn test_serde_integration() {
208        let json_input = json!({
209            "id": "bug_report",
210            "name": "Bug Report",
211            "fields": [
212                {
213                    "id": "title",
214                    "label": "Title",
215                    "field_type": {
216                        "kind": "text"
217                    }
218                },
219                {
220                    "id": "severity",
221                    "label": "Severity",
222                    "field_type": {
223                        "kind": "number",
224                        "config": { "min": 1.0, "max": 5.0 }
225                    }
226                }
227            ]
228        });
229
230        let form: FormDefinition = serde_json::from_value(json_input).expect("Should deserialize");
231        assert_eq!(form.id, "bug_report");
232        assert_eq!(form.fields.len(), 2);
233    }
234}