1use 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 if let Some(ref ui) = field_schema.ui {
53 let mut ui_obj = Map::new();
54
55 if let Some(ref group) = ui.group {
56 ui_obj.insert("group".to_string(), Value::String(group.clone()));
57 }
58
59 if let Some(ref tooltip) = ui.tooltip {
60 ui_obj.insert("tooltip".to_string(), Value::String(tooltip.clone()));
61 }
62
63 if let Some(order) = ui.order {
64 ui_obj.insert("order".to_string(), json!(order));
65 }
66
67 property.insert("x-ui".to_string(), Value::Object(ui_obj));
68 }
69
70 let mut examples_array = if let Some(ref examples) = field_schema.examples {
71 examples.as_array().cloned().unwrap_or_else(Vec::new)
72 } else {
73 Vec::new()
74 };
75
76 if let Some(ref example) = field_schema.example {
78 examples_array.push(example.as_json().clone());
79 }
80 if !examples_array.is_empty() {
81 property.insert("examples".to_string(), Value::Array(examples_array));
82 }
83
84 if let Some(ref default) = field_schema.default {
86 property.insert("default".to_string(), default.as_json().clone());
87 }
88
89 properties.insert(field_name.clone(), Value::Object(property));
90
91 if field_schema.default.is_none() {
95 required_fields.push(field_name.clone());
96 }
97 }
98
99 let schema = json!({
101 "$schema": "https://json-schema.org/draft/2019-09/schema",
102 "type": "object",
103 "properties": properties,
104 "required": required_fields,
105 "additionalProperties": true
106 });
107
108 Ok(QuillValue::from_json(schema))
109}
110
111pub fn extract_defaults_from_schema(
125 schema: &QuillValue,
126) -> HashMap<String, crate::value::QuillValue> {
127 let mut defaults = HashMap::new();
128
129 if let Some(properties) = schema.as_json().get("properties") {
131 if let Some(properties_obj) = properties.as_object() {
132 for (field_name, field_schema) in properties_obj {
133 if let Some(default_value) = field_schema.get("default") {
135 defaults.insert(
136 field_name.clone(),
137 QuillValue::from_json(default_value.clone()),
138 );
139 }
140 }
141 }
142 }
143
144 defaults
145}
146
147pub fn extract_examples_from_schema(
161 schema: &QuillValue,
162) -> HashMap<String, Vec<crate::value::QuillValue>> {
163 let mut examples = HashMap::new();
164
165 if let Some(properties) = schema.as_json().get("properties") {
167 if let Some(properties_obj) = properties.as_object() {
168 for (field_name, field_schema) in properties_obj {
169 if let Some(examples_value) = field_schema.get("examples") {
171 if let Some(examples_array) = examples_value.as_array() {
172 let examples_vec: Vec<QuillValue> = examples_array
173 .iter()
174 .map(|v| QuillValue::from_json(v.clone()))
175 .collect();
176 if !examples_vec.is_empty() {
177 examples.insert(field_name.clone(), examples_vec);
178 }
179 }
180 }
181 }
182 }
183 }
184
185 examples
186}
187
188pub fn validate_document(
190 schema: &QuillValue,
191 fields: &HashMap<String, crate::value::QuillValue>,
192) -> Result<(), Vec<String>> {
193 let mut doc_json = Map::new();
195 for (key, value) in fields {
196 doc_json.insert(key.clone(), value.as_json().clone());
197 }
198 let doc_value = Value::Object(doc_json);
199
200 let compiled = match jsonschema::Validator::new(schema.as_json()) {
202 Ok(c) => c,
203 Err(e) => return Err(vec![format!("Failed to compile schema: {}", e)]),
204 };
205
206 let validation_result = compiled.validate(&doc_value);
208
209 match validation_result {
210 Ok(_) => Ok(()),
211 Err(error) => {
212 let path = error.instance_path.to_string();
213 let path_display = if path.is_empty() {
214 "document".to_string()
215 } else {
216 path
217 };
218 let message = format!("Validation error at {}: {}", path_display, error);
219 Err(vec![message])
220 }
221 }
222}
223
224fn coerce_value(value: &QuillValue, expected_type: &str) -> QuillValue {
233 let json_value = value.as_json();
234
235 match expected_type {
236 "array" => {
237 if json_value.is_array() {
239 return value.clone();
240 }
241 QuillValue::from_json(Value::Array(vec![json_value.clone()]))
243 }
244 "boolean" => {
245 if let Some(b) = json_value.as_bool() {
247 return QuillValue::from_json(Value::Bool(b));
248 }
249 if let Some(s) = json_value.as_str() {
251 let lower = s.to_lowercase();
252 if lower == "true" {
253 return QuillValue::from_json(Value::Bool(true));
254 } else if lower == "false" {
255 return QuillValue::from_json(Value::Bool(false));
256 }
257 }
258 if let Some(n) = json_value.as_i64() {
260 return QuillValue::from_json(Value::Bool(n != 0));
261 }
262 if let Some(n) = json_value.as_f64() {
263 if n.is_nan() {
265 return QuillValue::from_json(Value::Bool(false));
266 }
267 return QuillValue::from_json(Value::Bool(n.abs() > f64::EPSILON));
268 }
269 value.clone()
271 }
272 "number" => {
273 if json_value.is_number() {
275 return value.clone();
276 }
277 if let Some(s) = json_value.as_str() {
279 if let Ok(i) = s.parse::<i64>() {
281 return QuillValue::from_json(serde_json::Number::from(i).into());
282 }
283 if let Ok(f) = s.parse::<f64>() {
285 if let Some(num) = serde_json::Number::from_f64(f) {
286 return QuillValue::from_json(num.into());
287 }
288 }
289 }
290 if let Some(b) = json_value.as_bool() {
292 let num_value = if b { 1 } else { 0 };
293 return QuillValue::from_json(Value::Number(serde_json::Number::from(num_value)));
294 }
295 value.clone()
297 }
298 _ => {
299 value.clone()
301 }
302 }
303}
304
305pub fn coerce_document(
319 schema: &QuillValue,
320 fields: &HashMap<String, QuillValue>,
321) -> HashMap<String, QuillValue> {
322 let mut coerced_fields = HashMap::new();
323
324 let properties = match schema.as_json().get("properties") {
326 Some(props) => props,
327 None => {
328 return fields.clone();
330 }
331 };
332
333 let properties_obj = match properties.as_object() {
334 Some(obj) => obj,
335 None => {
336 return fields.clone();
338 }
339 };
340
341 for (field_name, field_value) in fields {
343 if let Some(field_schema) = properties_obj.get(field_name) {
345 if let Some(expected_type) = field_schema.get("type").and_then(|t| t.as_str()) {
347 let coerced_value = coerce_value(field_value, expected_type);
349 coerced_fields.insert(field_name.clone(), coerced_value);
350 continue;
351 }
352 }
353 coerced_fields.insert(field_name.clone(), field_value.clone());
355 }
356
357 coerced_fields
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363 use crate::quill::FieldSchema;
364 use crate::value::QuillValue;
365
366 #[test]
367 fn test_build_schema_simple() {
368 let mut fields = HashMap::new();
369 let mut schema = FieldSchema::new(
370 "Author name".to_string(),
371 "The name of the author".to_string(),
372 );
373 schema.r#type = Some("str".to_string());
374 fields.insert("author".to_string(), schema);
375
376 let json_schema = build_schema_from_fields(&fields).unwrap().as_json().clone();
377 assert_eq!(json_schema["type"], "object");
378 assert_eq!(json_schema["properties"]["author"]["type"], "string");
379 assert_eq!(json_schema["properties"]["author"]["name"], "Author name");
380 assert_eq!(
381 json_schema["properties"]["author"]["description"],
382 "The name of the author"
383 );
384 }
385
386 #[test]
387 fn test_build_schema_with_default() {
388 let mut fields = HashMap::new();
389 let mut schema = FieldSchema::new(
390 "Field with default".to_string(),
391 "A field with a default value".to_string(),
392 );
393 schema.r#type = Some("str".to_string());
394 schema.default = Some(QuillValue::from_json(json!("default value")));
395 fields.insert("with_default".to_string(), schema);
397
398 build_schema_from_fields(&fields).unwrap();
399 }
400
401 #[test]
402 fn test_build_schema_date_types() {
403 let mut fields = HashMap::new();
404
405 let mut date_schema =
406 FieldSchema::new("Date field".to_string(), "A field for dates".to_string());
407 date_schema.r#type = Some("date".to_string());
408 fields.insert("date_field".to_string(), date_schema);
409
410 let mut datetime_schema = FieldSchema::new(
411 "DateTime field".to_string(),
412 "A field for date and time".to_string(),
413 );
414 datetime_schema.r#type = Some("datetime".to_string());
415 fields.insert("datetime_field".to_string(), datetime_schema);
416
417 let json_schema = build_schema_from_fields(&fields).unwrap().as_json().clone();
418 assert_eq!(json_schema["properties"]["date_field"]["type"], "string");
419 assert_eq!(json_schema["properties"]["date_field"]["format"], "date");
420 assert_eq!(
421 json_schema["properties"]["datetime_field"]["type"],
422 "string"
423 );
424 assert_eq!(
425 json_schema["properties"]["datetime_field"]["format"],
426 "date-time"
427 );
428 }
429
430 #[test]
431 fn test_validate_document_success() {
432 let schema = json!({
433 "$schema": "https://json-schema.org/draft/2019-09/schema",
434 "type": "object",
435 "properties": {
436 "title": {"type": "string"},
437 "count": {"type": "number"}
438 },
439 "required": ["title"],
440 "additionalProperties": true
441 });
442
443 let mut fields = HashMap::new();
444 fields.insert(
445 "title".to_string(),
446 QuillValue::from_json(json!("Test Title")),
447 );
448 fields.insert("count".to_string(), QuillValue::from_json(json!(42)));
449
450 let result = validate_document(&QuillValue::from_json(schema), &fields);
451 assert!(result.is_ok());
452 }
453
454 #[test]
455 fn test_validate_document_missing_required() {
456 let schema = json!({
457 "$schema": "https://json-schema.org/draft/2019-09/schema",
458 "type": "object",
459 "properties": {
460 "title": {"type": "string"}
461 },
462 "required": ["title"],
463 "additionalProperties": true
464 });
465
466 let fields = HashMap::new(); let result = validate_document(&QuillValue::from_json(schema), &fields);
469 assert!(result.is_err());
470 let errors = result.unwrap_err();
471 assert!(!errors.is_empty());
472 }
473
474 #[test]
475 fn test_validate_document_wrong_type() {
476 let schema = json!({
477 "$schema": "https://json-schema.org/draft/2019-09/schema",
478 "type": "object",
479 "properties": {
480 "count": {"type": "number"}
481 },
482 "additionalProperties": true
483 });
484
485 let mut fields = HashMap::new();
486 fields.insert(
487 "count".to_string(),
488 QuillValue::from_json(json!("not a number")),
489 );
490
491 let result = validate_document(&QuillValue::from_json(schema), &fields);
492 assert!(result.is_err());
493 }
494
495 #[test]
496 fn test_validate_document_allows_extra_fields() {
497 let schema = json!({
498 "$schema": "https://json-schema.org/draft/2019-09/schema",
499 "type": "object",
500 "properties": {
501 "title": {"type": "string"}
502 },
503 "required": ["title"],
504 "additionalProperties": true
505 });
506
507 let mut fields = HashMap::new();
508 fields.insert("title".to_string(), QuillValue::from_json(json!("Test")));
509 fields.insert("extra".to_string(), QuillValue::from_json(json!("allowed")));
510
511 let result = validate_document(&QuillValue::from_json(schema), &fields);
512 assert!(result.is_ok());
513 }
514
515 #[test]
516 fn test_build_schema_with_example() {
517 let mut fields = HashMap::new();
518 let mut schema = FieldSchema::new(
519 "memo_for".to_string(),
520 "List of recipient organization symbols".to_string(),
521 );
522 schema.r#type = Some("array".to_string());
523 schema.example = Some(QuillValue::from_json(json!(["ORG1/SYMBOL", "ORG2/SYMBOL"])));
524 fields.insert("memo_for".to_string(), schema);
525
526 let json_schema = build_schema_from_fields(&fields).unwrap().as_json().clone();
527
528 assert!(json_schema["properties"]["memo_for"]
530 .as_object()
531 .unwrap()
532 .contains_key("examples"));
533
534 let example_value = &json_schema["properties"]["memo_for"]["examples"][0];
535 assert_eq!(example_value, &json!(["ORG1/SYMBOL", "ORG2/SYMBOL"]));
536 }
537
538 #[test]
539 fn test_build_schema_includes_default_in_properties() {
540 let mut fields = HashMap::new();
541 let mut schema = FieldSchema::new(
542 "ice_cream".to_string(),
543 "favorite ice cream flavor".to_string(),
544 );
545 schema.r#type = Some("string".to_string());
546 schema.default = Some(QuillValue::from_json(json!("taro")));
547 fields.insert("ice_cream".to_string(), schema);
548
549 let json_schema = build_schema_from_fields(&fields).unwrap().as_json().clone();
550
551 assert!(json_schema["properties"]["ice_cream"]
553 .as_object()
554 .unwrap()
555 .contains_key("default"));
556
557 let default_value = &json_schema["properties"]["ice_cream"]["default"];
558 assert_eq!(default_value, &json!("taro"));
559
560 let required_fields = json_schema["required"].as_array().unwrap();
562 assert!(!required_fields.contains(&json!("ice_cream")));
563 }
564
565 #[test]
566 fn test_extract_defaults_from_schema() {
567 let schema = json!({
569 "$schema": "https://json-schema.org/draft/2019-09/schema",
570 "type": "object",
571 "properties": {
572 "title": {
573 "type": "string",
574 "description": "Document title"
575 },
576 "author": {
577 "type": "string",
578 "description": "Document author",
579 "default": "Anonymous"
580 },
581 "status": {
582 "type": "string",
583 "description": "Document status",
584 "default": "draft"
585 },
586 "count": {
587 "type": "number",
588 "default": 42
589 }
590 },
591 "required": ["title"]
592 });
593
594 let defaults = extract_defaults_from_schema(&QuillValue::from_json(schema));
595
596 assert_eq!(defaults.len(), 3);
598 assert!(!defaults.contains_key("title")); assert!(defaults.contains_key("author"));
600 assert!(defaults.contains_key("status"));
601 assert!(defaults.contains_key("count"));
602
603 assert_eq!(defaults.get("author").unwrap().as_str(), Some("Anonymous"));
605 assert_eq!(defaults.get("status").unwrap().as_str(), Some("draft"));
606 assert_eq!(defaults.get("count").unwrap().as_json().as_i64(), Some(42));
607 }
608
609 #[test]
610 fn test_extract_defaults_from_schema_empty() {
611 let schema = json!({
613 "$schema": "https://json-schema.org/draft/2019-09/schema",
614 "type": "object",
615 "properties": {
616 "title": {"type": "string"},
617 "author": {"type": "string"}
618 },
619 "required": ["title"]
620 });
621
622 let defaults = extract_defaults_from_schema(&QuillValue::from_json(schema));
623 assert_eq!(defaults.len(), 0);
624 }
625
626 #[test]
627 fn test_extract_defaults_from_schema_no_properties() {
628 let schema = json!({
630 "$schema": "https://json-schema.org/draft/2019-09/schema",
631 "type": "object"
632 });
633
634 let defaults = extract_defaults_from_schema(&QuillValue::from_json(schema));
635 assert_eq!(defaults.len(), 0);
636 }
637
638 #[test]
639 fn test_extract_examples_from_schema() {
640 let schema = json!({
642 "$schema": "https://json-schema.org/draft/2019-09/schema",
643 "type": "object",
644 "properties": {
645 "title": {
646 "type": "string",
647 "description": "Document title"
648 },
649 "memo_for": {
650 "type": "array",
651 "description": "List of recipients",
652 "examples": [
653 ["ORG1/SYMBOL", "ORG2/SYMBOL"],
654 ["DEPT/OFFICE"]
655 ]
656 },
657 "author": {
658 "type": "string",
659 "description": "Document author",
660 "examples": ["John Doe", "Jane Smith"]
661 },
662 "status": {
663 "type": "string",
664 "description": "Document status"
665 }
666 }
667 });
668
669 let examples = extract_examples_from_schema(&QuillValue::from_json(schema));
670
671 assert_eq!(examples.len(), 2);
673 assert!(!examples.contains_key("title")); assert!(examples.contains_key("memo_for"));
675 assert!(examples.contains_key("author"));
676 assert!(!examples.contains_key("status")); let memo_for_examples = examples.get("memo_for").unwrap();
680 assert_eq!(memo_for_examples.len(), 2);
681 assert_eq!(
682 memo_for_examples[0].as_json(),
683 &json!(["ORG1/SYMBOL", "ORG2/SYMBOL"])
684 );
685 assert_eq!(memo_for_examples[1].as_json(), &json!(["DEPT/OFFICE"]));
686
687 let author_examples = examples.get("author").unwrap();
689 assert_eq!(author_examples.len(), 2);
690 assert_eq!(author_examples[0].as_str(), Some("John Doe"));
691 assert_eq!(author_examples[1].as_str(), Some("Jane Smith"));
692 }
693
694 #[test]
695 fn test_extract_examples_from_schema_empty() {
696 let schema = json!({
698 "$schema": "https://json-schema.org/draft/2019-09/schema",
699 "type": "object",
700 "properties": {
701 "title": {"type": "string"},
702 "author": {"type": "string"}
703 }
704 });
705
706 let examples = extract_examples_from_schema(&QuillValue::from_json(schema));
707 assert_eq!(examples.len(), 0);
708 }
709
710 #[test]
711 fn test_extract_examples_from_schema_no_properties() {
712 let schema = json!({
714 "$schema": "https://json-schema.org/draft/2019-09/schema",
715 "type": "object"
716 });
717
718 let examples = extract_examples_from_schema(&QuillValue::from_json(schema));
719 assert_eq!(examples.len(), 0);
720 }
721
722 #[test]
723 fn test_coerce_singular_to_array() {
724 let schema = json!({
725 "$schema": "https://json-schema.org/draft/2019-09/schema",
726 "type": "object",
727 "properties": {
728 "tags": {"type": "array"}
729 }
730 });
731
732 let mut fields = HashMap::new();
733 fields.insert(
734 "tags".to_string(),
735 QuillValue::from_json(json!("single-tag")),
736 );
737
738 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
739
740 let tags = coerced.get("tags").unwrap();
741 assert!(tags.as_array().is_some());
742 let tags_array = tags.as_array().unwrap();
743 assert_eq!(tags_array.len(), 1);
744 assert_eq!(tags_array[0].as_str().unwrap(), "single-tag");
745 }
746
747 #[test]
748 fn test_coerce_array_unchanged() {
749 let schema = json!({
750 "$schema": "https://json-schema.org/draft/2019-09/schema",
751 "type": "object",
752 "properties": {
753 "tags": {"type": "array"}
754 }
755 });
756
757 let mut fields = HashMap::new();
758 fields.insert(
759 "tags".to_string(),
760 QuillValue::from_json(json!(["tag1", "tag2"])),
761 );
762
763 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
764
765 let tags = coerced.get("tags").unwrap();
766 let tags_array = tags.as_array().unwrap();
767 assert_eq!(tags_array.len(), 2);
768 }
769
770 #[test]
771 fn test_coerce_string_to_boolean() {
772 let schema = json!({
773 "$schema": "https://json-schema.org/draft/2019-09/schema",
774 "type": "object",
775 "properties": {
776 "active": {"type": "boolean"},
777 "enabled": {"type": "boolean"}
778 }
779 });
780
781 let mut fields = HashMap::new();
782 fields.insert("active".to_string(), QuillValue::from_json(json!("true")));
783 fields.insert("enabled".to_string(), QuillValue::from_json(json!("FALSE")));
784
785 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
786
787 assert_eq!(coerced.get("active").unwrap().as_bool().unwrap(), true);
788 assert_eq!(coerced.get("enabled").unwrap().as_bool().unwrap(), false);
789 }
790
791 #[test]
792 fn test_coerce_number_to_boolean() {
793 let schema = json!({
794 "$schema": "https://json-schema.org/draft/2019-09/schema",
795 "type": "object",
796 "properties": {
797 "flag1": {"type": "boolean"},
798 "flag2": {"type": "boolean"},
799 "flag3": {"type": "boolean"}
800 }
801 });
802
803 let mut fields = HashMap::new();
804 fields.insert("flag1".to_string(), QuillValue::from_json(json!(0)));
805 fields.insert("flag2".to_string(), QuillValue::from_json(json!(1)));
806 fields.insert("flag3".to_string(), QuillValue::from_json(json!(42)));
807
808 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
809
810 assert_eq!(coerced.get("flag1").unwrap().as_bool().unwrap(), false);
811 assert_eq!(coerced.get("flag2").unwrap().as_bool().unwrap(), true);
812 assert_eq!(coerced.get("flag3").unwrap().as_bool().unwrap(), true);
813 }
814
815 #[test]
816 fn test_coerce_float_to_boolean() {
817 let schema = json!({
818 "$schema": "https://json-schema.org/draft/2019-09/schema",
819 "type": "object",
820 "properties": {
821 "flag1": {"type": "boolean"},
822 "flag2": {"type": "boolean"},
823 "flag3": {"type": "boolean"},
824 "flag4": {"type": "boolean"}
825 }
826 });
827
828 let mut fields = HashMap::new();
829 fields.insert("flag1".to_string(), QuillValue::from_json(json!(0.0)));
830 fields.insert("flag2".to_string(), QuillValue::from_json(json!(0.5)));
831 fields.insert("flag3".to_string(), QuillValue::from_json(json!(-1.5)));
832 fields.insert("flag4".to_string(), QuillValue::from_json(json!(1e-100)));
834
835 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
836
837 assert_eq!(coerced.get("flag1").unwrap().as_bool().unwrap(), false);
838 assert_eq!(coerced.get("flag2").unwrap().as_bool().unwrap(), true);
839 assert_eq!(coerced.get("flag3").unwrap().as_bool().unwrap(), true);
840 assert_eq!(coerced.get("flag4").unwrap().as_bool().unwrap(), false);
842 }
843
844 #[test]
845 fn test_coerce_string_to_number() {
846 let schema = json!({
847 "$schema": "https://json-schema.org/draft/2019-09/schema",
848 "type": "object",
849 "properties": {
850 "count": {"type": "number"},
851 "price": {"type": "number"}
852 }
853 });
854
855 let mut fields = HashMap::new();
856 fields.insert("count".to_string(), QuillValue::from_json(json!("42")));
857 fields.insert("price".to_string(), QuillValue::from_json(json!("19.99")));
858
859 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
860
861 assert_eq!(coerced.get("count").unwrap().as_i64().unwrap(), 42);
862 assert_eq!(coerced.get("price").unwrap().as_f64().unwrap(), 19.99);
863 }
864
865 #[test]
866 fn test_coerce_boolean_to_number() {
867 let schema = json!({
868 "$schema": "https://json-schema.org/draft/2019-09/schema",
869 "type": "object",
870 "properties": {
871 "active": {"type": "number"},
872 "disabled": {"type": "number"}
873 }
874 });
875
876 let mut fields = HashMap::new();
877 fields.insert("active".to_string(), QuillValue::from_json(json!(true)));
878 fields.insert("disabled".to_string(), QuillValue::from_json(json!(false)));
879
880 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
881
882 assert_eq!(coerced.get("active").unwrap().as_i64().unwrap(), 1);
883 assert_eq!(coerced.get("disabled").unwrap().as_i64().unwrap(), 0);
884 }
885
886 #[test]
887 fn test_coerce_no_schema_properties() {
888 let schema = json!({
889 "$schema": "https://json-schema.org/draft/2019-09/schema",
890 "type": "object"
891 });
892
893 let mut fields = HashMap::new();
894 fields.insert("title".to_string(), QuillValue::from_json(json!("Test")));
895
896 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
897
898 assert_eq!(coerced.get("title").unwrap().as_str().unwrap(), "Test");
900 }
901
902 #[test]
903 fn test_coerce_field_without_type() {
904 let schema = json!({
905 "$schema": "https://json-schema.org/draft/2019-09/schema",
906 "type": "object",
907 "properties": {
908 "title": {"description": "A title field"}
909 }
910 });
911
912 let mut fields = HashMap::new();
913 fields.insert("title".to_string(), QuillValue::from_json(json!("Test")));
914
915 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
916
917 assert_eq!(coerced.get("title").unwrap().as_str().unwrap(), "Test");
919 }
920
921 #[test]
922 fn test_coerce_mixed_fields() {
923 let schema = json!({
924 "$schema": "https://json-schema.org/draft/2019-09/schema",
925 "type": "object",
926 "properties": {
927 "tags": {"type": "array"},
928 "active": {"type": "boolean"},
929 "count": {"type": "number"},
930 "title": {"type": "string"}
931 }
932 });
933
934 let mut fields = HashMap::new();
935 fields.insert("tags".to_string(), QuillValue::from_json(json!("single")));
936 fields.insert("active".to_string(), QuillValue::from_json(json!("true")));
937 fields.insert("count".to_string(), QuillValue::from_json(json!("42")));
938 fields.insert(
939 "title".to_string(),
940 QuillValue::from_json(json!("Test Title")),
941 );
942
943 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
944
945 assert_eq!(coerced.get("tags").unwrap().as_array().unwrap().len(), 1);
947 assert_eq!(coerced.get("active").unwrap().as_bool().unwrap(), true);
948 assert_eq!(coerced.get("count").unwrap().as_i64().unwrap(), 42);
949 assert_eq!(
950 coerced.get("title").unwrap().as_str().unwrap(),
951 "Test Title"
952 );
953 }
954
955 #[test]
956 fn test_coerce_invalid_string_to_number() {
957 let schema = json!({
958 "$schema": "https://json-schema.org/draft/2019-09/schema",
959 "type": "object",
960 "properties": {
961 "count": {"type": "number"}
962 }
963 });
964
965 let mut fields = HashMap::new();
966 fields.insert(
967 "count".to_string(),
968 QuillValue::from_json(json!("not-a-number")),
969 );
970
971 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
972
973 assert_eq!(
975 coerced.get("count").unwrap().as_str().unwrap(),
976 "not-a-number"
977 );
978 }
979
980 #[test]
981 fn test_coerce_object_to_array() {
982 let schema = json!({
983 "$schema": "https://json-schema.org/draft/2019-09/schema",
984 "type": "object",
985 "properties": {
986 "items": {"type": "array"}
987 }
988 });
989
990 let mut fields = HashMap::new();
991 fields.insert(
992 "items".to_string(),
993 QuillValue::from_json(json!({"key": "value"})),
994 );
995
996 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
997
998 let items = coerced.get("items").unwrap();
1000 assert!(items.as_array().is_some());
1001 let items_array = items.as_array().unwrap();
1002 assert_eq!(items_array.len(), 1);
1003 assert!(items_array[0].as_object().is_some());
1004 }
1005}