1use crate::error::DocumentValidationError;
8use molten_core::document::Document;
9use molten_core::field::{FieldDefinition, FieldType};
10use molten_core::form::FormDefinition;
11use serde_json::Value;
12
13pub fn validate_document(
29 doc: &Document,
30 form: &FormDefinition,
31) -> Result<(), Vec<DocumentValidationError>> {
32 let mut errors = Vec::new();
33
34 if doc.form_id != form.id() {
36 errors.push(DocumentValidationError::FormIdMismatch {
37 doc_form: doc.form_id.clone(),
38 def_id: form.id().to_string(),
39 });
40 return Err(errors);
41 }
42
43 for field_def in form.fields() {
45 let value = doc.get_value(field_def.id());
46
47 if field_def.is_required() && (value.is_none() || value.unwrap().is_null()) {
49 errors.push(DocumentValidationError::MissingRequiredField(
50 field_def.id().to_string(),
51 ));
52 continue; }
54
55 if let Some(val) = value
57 && !val.is_null()
58 {
59 if let Err(e) = validate_value(val, field_def) {
60 errors.push(e);
61 }
62 }
63 }
64
65 if errors.is_empty() {
66 Ok(())
67 } else {
68 Err(errors)
69 }
70}
71
72fn validate_value(value: &Value, field: &FieldDefinition) -> Result<(), DocumentValidationError> {
86 match field.field_type() {
87 FieldType::Text | FieldType::TextArea => {
88 if !value.is_string() {
89 return Err(DocumentValidationError::InvalidType {
90 field_id: field.id().to_string(),
91 expected_type: "String".to_string(),
92 got_type: get_json_type(value),
93 });
94 }
95 }
97 FieldType::Number { min, max } => {
98 let num = value
99 .as_f64()
100 .ok_or_else(|| DocumentValidationError::InvalidType {
101 field_id: field.id().to_string(),
102 expected_type: "Number".to_string(),
103 got_type: get_json_type(value),
104 })?;
105
106 if let Some(min_val) = min
107 && num < *min_val
108 {
109 return Err(DocumentValidationError::ValueTooLow {
110 field_id: field.id().to_string(),
111 value: num,
112 min: *min_val,
113 });
114 }
115 if let Some(max_val) = max
116 && num > *max_val
117 {
118 return Err(DocumentValidationError::ValueTooHigh {
119 field_id: field.id().to_string(),
120 value: num,
121 max: *max_val,
122 });
123 }
124 }
125 FieldType::Boolean => {
126 if !value.is_boolean() {
127 return Err(DocumentValidationError::InvalidType {
128 field_id: field.id().to_string(),
129 expected_type: "Boolean".to_string(),
130 got_type: get_json_type(value),
131 });
132 }
133 }
134 FieldType::Select {
135 options,
136 allow_multiple,
137 } => {
138 if *allow_multiple {
139 let arr = value
141 .as_array()
142 .ok_or_else(|| DocumentValidationError::InvalidType {
143 field_id: field.id().to_string(),
144 expected_type: "Array".to_string(),
145 got_type: get_json_type(value),
146 })?;
147
148 for item in arr {
149 let s = item
150 .as_str()
151 .ok_or_else(|| DocumentValidationError::InvalidType {
152 field_id: field.id().to_string(),
153 expected_type: "String".to_string(),
154 got_type: get_json_type(item),
155 })?;
156 if !options.contains(&s.to_string()) {
157 return Err(DocumentValidationError::InvalidSelection {
158 field_id: field.id().to_string(),
159 value: s.to_string(),
160 allowed: options.clone(),
161 });
162 }
163 }
164 } else {
165 let s = value
167 .as_str()
168 .ok_or_else(|| DocumentValidationError::InvalidType {
169 field_id: field.id().to_string(),
170 expected_type: "String".to_string(),
171 got_type: get_json_type(value),
172 })?;
173
174 if !options.contains(&s.to_string()) {
175 return Err(DocumentValidationError::InvalidSelection {
176 field_id: field.id().to_string(),
177 value: s.to_string(),
178 allowed: options.clone(),
179 });
180 }
181 }
182 }
183 FieldType::DateTime => {
184 let s = value
185 .as_str()
186 .ok_or_else(|| DocumentValidationError::InvalidType {
187 field_id: field.id().to_string(),
188 expected_type: "String (ISO 8601)".to_string(),
189 got_type: get_json_type(value),
190 })?;
191
192 if chrono::DateTime::parse_from_rfc3339(s).is_err() {
194 return Err(DocumentValidationError::InvalidDateFormat {
195 field_id: field.id().to_string(),
196 value: s.to_string(),
197 });
198 }
199 }
200 }
201 Ok(())
202}
203
204fn get_json_type(v: &Value) -> String {
212 match v {
213 Value::Null => "Null",
214 Value::Bool(_) => "Boolean",
215 Value::Number(_) => "Number",
216 Value::String(_) => "String",
217 Value::Array(_) => "Array",
218 Value::Object(_) => "Object",
219 }
220 .to_string()
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use molten_core::document::Document;
227 use molten_core::field::{FieldBuilder, FieldType};
228 use molten_core::form::FormBuilder;
229 use serde_json::json;
230
231 fn create_test_form() -> FormDefinition {
232 FormBuilder::new("ticket", "Ticket")
233 .add_field(
234 FieldBuilder::new("title", "Title", FieldType::Text)
235 .required(true)
236 .build()
237 .unwrap(),
238 )
239 .add_field(
240 FieldBuilder::new(
241 "severity",
242 "Severity",
243 FieldType::Number {
244 min: Some(1.0),
245 max: Some(5.0),
246 },
247 )
248 .build()
249 .unwrap(),
250 )
251 .add_field(
252 FieldBuilder::new(
253 "status",
254 "Status",
255 FieldType::Select {
256 options: vec!["Open".into(), "Closed".into()],
257 allow_multiple: false,
258 },
259 )
260 .build()
261 .unwrap(),
262 )
263 .build()
264 .unwrap()
265 }
266
267 #[test]
268 fn test_valid_document() {
269 let form = create_test_form();
270 let mut doc = Document::new("doc1", "ticket", "flow_ticket");
271 doc.set_value("title", json!("Server Down"));
272 doc.set_value("severity", json!(3));
273 doc.set_value("status", json!("Open"));
274
275 assert!(validate_document(&doc, &form).is_ok());
276 }
277
278 #[test]
279 fn test_missing_required() {
280 let form = create_test_form();
281 let doc = Document::new("doc1", "ticket", "flow_ticket");
282 let res = validate_document(&doc, &form);
285 assert!(res.is_err());
286 let errs = res.unwrap_err();
287 assert!(matches!(
288 errs[0],
289 DocumentValidationError::MissingRequiredField(_)
290 ));
291 }
292
293 #[test]
294 fn test_type_mismatch() {
295 let form = create_test_form();
296 let mut doc = Document::new("doc1", "ticket", "flow_ticket");
297 doc.set_value("title", json!("Valid"));
298 doc.set_value("severity", json!("Five")); let res = validate_document(&doc, &form);
301 assert!(res.is_err());
302 assert!(format!("{:?}", res.unwrap_err()).contains("InvalidType"));
303 }
304
305 #[test]
306 fn test_number_range() {
307 let form = create_test_form();
308 let mut doc = Document::new("doc1", "ticket", "flow_ticket");
309 doc.set_value("title", json!("Valid"));
310 doc.set_value("severity", json!(10)); let res = validate_document(&doc, &form);
313 assert!(res.is_err());
314 assert!(matches!(
315 res.unwrap_err()[0],
316 DocumentValidationError::ValueTooHigh { .. }
317 ));
318 }
319
320 #[test]
321 fn test_select_options() {
322 let form = create_test_form();
323 let mut doc = Document::new("doc1", "ticket", "flow_ticket");
324 doc.set_value("title", json!("Valid"));
325 doc.set_value("status", json!("In Progress")); let res = validate_document(&doc, &form);
328 assert!(res.is_err());
329 assert!(matches!(
330 res.unwrap_err()[0],
331 DocumentValidationError::InvalidSelection { .. }
332 ));
333 }
334}