quillmark_core/
validation.rs1use crate::{quill::FieldSchema, QuillValue, RenderError};
7use serde_json::{json, Map, Value};
8use std::collections::HashMap;
9
10pub fn build_schema_from_fields(
12 field_schemas: &HashMap<String, FieldSchema>,
13) -> Result<QuillValue, RenderError> {
14 let mut properties = Map::new();
15 let mut required_fields = Vec::new();
16
17 for (field_name, field_schema) in field_schemas {
18 let mut property = Map::new();
20
21 property.insert("name".to_string(), Value::String(field_schema.name.clone()));
23
24 if let Some(ref field_type) = field_schema.r#type {
26 let json_type = match field_type.as_str() {
27 "str" => "string",
28 "number" => "number",
29 "array" => "array",
30 "dict" => "object",
31 "date" => "string",
32 "datetime" => "string",
33 _ => "string", };
35 property.insert("type".to_string(), Value::String(json_type.to_string()));
36
37 if field_type == "date" {
39 property.insert("format".to_string(), Value::String("date".to_string()));
40 } else if field_type == "datetime" {
41 property.insert("format".to_string(), Value::String("date-time".to_string()));
42 }
43 }
44
45 property.insert(
47 "description".to_string(),
48 Value::String(field_schema.description.clone()),
49 );
50
51 properties.insert(field_name.clone(), Value::Object(property));
52
53 if field_schema.default.is_none() {
57 required_fields.push(field_name.clone());
58 }
59 }
60
61 let schema = json!({
63 "$schema": "https://json-schema.org/draft/2019-09/schema",
64 "type": "object",
65 "properties": properties,
66 "required": required_fields,
67 "additionalProperties": true
68 });
69
70 Ok(QuillValue::from_json(schema))
71}
72
73pub fn validate_document(
75 schema: &QuillValue,
76 fields: &HashMap<String, crate::value::QuillValue>,
77) -> Result<(), Vec<String>> {
78 let mut doc_json = Map::new();
80 for (key, value) in fields {
81 doc_json.insert(key.clone(), value.as_json().clone());
82 }
83 let doc_value = Value::Object(doc_json);
84
85 let compiled = match jsonschema::Validator::new(schema.as_json()) {
87 Ok(c) => c,
88 Err(e) => return Err(vec![format!("Failed to compile schema: {}", e)]),
89 };
90
91 let validation_result = compiled.validate(&doc_value);
93
94 match validation_result {
95 Ok(_) => Ok(()),
96 Err(error) => {
97 let path = error.instance_path.to_string();
98 let path_display = if path.is_empty() {
99 "document".to_string()
100 } else {
101 path
102 };
103 let message = format!("Validation error at {}: {}", path_display, error);
104 Err(vec![message])
105 }
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use crate::quill::FieldSchema;
113 use crate::value::QuillValue;
114
115 #[test]
116 fn test_build_schema_simple() {
117 let mut fields = HashMap::new();
118 let mut schema = FieldSchema::new(
119 "Author name".to_string(),
120 "The name of the author".to_string(),
121 );
122 schema.r#type = Some("str".to_string());
123 fields.insert("author".to_string(), schema);
124
125 let json_schema = build_schema_from_fields(&fields).unwrap().as_json().clone();
126 assert_eq!(json_schema["type"], "object");
127 assert_eq!(json_schema["properties"]["author"]["type"], "string");
128 assert_eq!(json_schema["properties"]["author"]["name"], "Author name");
129 assert_eq!(
130 json_schema["properties"]["author"]["description"],
131 "The name of the author"
132 );
133 }
134
135 #[test]
136 fn test_build_schema_with_default() {
137 let mut fields = HashMap::new();
138 let mut schema = FieldSchema::new(
139 "Field with default".to_string(),
140 "A field with a default value".to_string(),
141 );
142 schema.r#type = Some("str".to_string());
143 schema.default = Some(QuillValue::from_json(json!("default value")));
144 fields.insert("with_default".to_string(), schema);
146
147 build_schema_from_fields(&fields).unwrap();
148 }
149
150 #[test]
151 fn test_build_schema_date_types() {
152 let mut fields = HashMap::new();
153
154 let mut date_schema =
155 FieldSchema::new("Date field".to_string(), "A field for dates".to_string());
156 date_schema.r#type = Some("date".to_string());
157 fields.insert("date_field".to_string(), date_schema);
158
159 let mut datetime_schema = FieldSchema::new(
160 "DateTime field".to_string(),
161 "A field for date and time".to_string(),
162 );
163 datetime_schema.r#type = Some("datetime".to_string());
164 fields.insert("datetime_field".to_string(), datetime_schema);
165
166 let json_schema = build_schema_from_fields(&fields).unwrap().as_json().clone();
167 assert_eq!(json_schema["properties"]["date_field"]["type"], "string");
168 assert_eq!(json_schema["properties"]["date_field"]["format"], "date");
169 assert_eq!(
170 json_schema["properties"]["datetime_field"]["type"],
171 "string"
172 );
173 assert_eq!(
174 json_schema["properties"]["datetime_field"]["format"],
175 "date-time"
176 );
177 }
178
179 #[test]
180 fn test_validate_document_success() {
181 let schema = json!({
182 "$schema": "https://json-schema.org/draft/2019-09/schema",
183 "type": "object",
184 "properties": {
185 "title": {"type": "string"},
186 "count": {"type": "number"}
187 },
188 "required": ["title"],
189 "additionalProperties": true
190 });
191
192 let mut fields = HashMap::new();
193 fields.insert(
194 "title".to_string(),
195 QuillValue::from_json(json!("Test Title")),
196 );
197 fields.insert("count".to_string(), QuillValue::from_json(json!(42)));
198
199 let result = validate_document(&QuillValue::from_json(schema), &fields);
200 assert!(result.is_ok());
201 }
202
203 #[test]
204 fn test_validate_document_missing_required() {
205 let schema = json!({
206 "$schema": "https://json-schema.org/draft/2019-09/schema",
207 "type": "object",
208 "properties": {
209 "title": {"type": "string"}
210 },
211 "required": ["title"],
212 "additionalProperties": true
213 });
214
215 let fields = HashMap::new(); let result = validate_document(&QuillValue::from_json(schema), &fields);
218 assert!(result.is_err());
219 let errors = result.unwrap_err();
220 assert!(!errors.is_empty());
221 }
222
223 #[test]
224 fn test_validate_document_wrong_type() {
225 let schema = json!({
226 "$schema": "https://json-schema.org/draft/2019-09/schema",
227 "type": "object",
228 "properties": {
229 "count": {"type": "number"}
230 },
231 "additionalProperties": true
232 });
233
234 let mut fields = HashMap::new();
235 fields.insert(
236 "count".to_string(),
237 QuillValue::from_json(json!("not a number")),
238 );
239
240 let result = validate_document(&QuillValue::from_json(schema), &fields);
241 assert!(result.is_err());
242 }
243
244 #[test]
245 fn test_validate_document_allows_extra_fields() {
246 let schema = json!({
247 "$schema": "https://json-schema.org/draft/2019-09/schema",
248 "type": "object",
249 "properties": {
250 "title": {"type": "string"}
251 },
252 "required": ["title"],
253 "additionalProperties": true
254 });
255
256 let mut fields = HashMap::new();
257 fields.insert("title".to_string(), QuillValue::from_json(json!("Test")));
258 fields.insert("extra".to_string(), QuillValue::from_json(json!("allowed")));
259
260 let result = validate_document(&QuillValue::from_json(schema), &fields);
261 assert!(result.is_ok());
262 }
263}