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