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