1use crate::quill::{CardSchema, FieldSchema};
7use crate::{QuillValue, RenderError};
8use serde_json::{json, Map, Value};
9use std::collections::HashMap;
10
11fn build_field_property(field_schema: &FieldSchema) -> Map<String, Value> {
13 let mut property = Map::new();
14
15 property.insert("name".to_string(), Value::String(field_schema.name.clone()));
17
18 if let Some(ref field_type) = field_schema.r#type {
20 let json_type = match field_type.as_str() {
21 "str" => "string",
22 "string" => "string",
23 "number" => "number",
24 "boolean" => "boolean",
25 "array" => "array",
26 "dict" => "object",
27 "date" => "string",
28 "datetime" => "string",
29 _ => "string", };
31 property.insert("type".to_string(), Value::String(json_type.to_string()));
32
33 if field_type == "date" {
35 property.insert("format".to_string(), Value::String("date".to_string()));
36 } else if field_type == "datetime" {
37 property.insert("format".to_string(), Value::String("date-time".to_string()));
38 }
39 }
40
41 if let Some(ref title) = field_schema.title {
43 property.insert("title".to_string(), Value::String(title.clone()));
44 }
45
46 property.insert(
48 "description".to_string(),
49 Value::String(field_schema.description.clone()),
50 );
51
52 if let Some(ref ui) = field_schema.ui {
54 let mut ui_obj = Map::new();
55
56 if let Some(ref group) = ui.group {
57 ui_obj.insert("group".to_string(), Value::String(group.clone()));
58 }
59
60 if let Some(order) = ui.order {
61 ui_obj.insert("order".to_string(), json!(order));
62 }
63
64 if !ui_obj.is_empty() {
65 property.insert("x-ui".to_string(), Value::Object(ui_obj));
66 }
67 }
68
69 if let Some(ref examples) = field_schema.examples {
71 if let Some(examples_array) = examples.as_array() {
72 if !examples_array.is_empty() {
73 property.insert("examples".to_string(), Value::Array(examples_array.clone()));
74 }
75 }
76 }
77
78 if let Some(ref default) = field_schema.default {
80 property.insert("default".to_string(), default.as_json().clone());
81 }
82
83 property
84}
85
86fn build_card_def(name: &str, card: &CardSchema) -> Map<String, Value> {
88 let mut def = Map::new();
89
90 def.insert("type".to_string(), Value::String("object".to_string()));
91
92 if let Some(ref title) = card.title {
94 def.insert("title".to_string(), Value::String(title.clone()));
95 }
96
97 if !card.description.is_empty() {
99 def.insert(
100 "description".to_string(),
101 Value::String(card.description.clone()),
102 );
103 }
104
105 let mut properties = Map::new();
107 let mut required = vec![Value::String("CARD".to_string())];
108
109 let mut card_prop = Map::new();
111 card_prop.insert("const".to_string(), Value::String(name.to_string()));
112 properties.insert("CARD".to_string(), Value::Object(card_prop));
113
114 for (field_name, field_schema) in &card.fields {
116 let field_prop = build_field_property(field_schema);
117 properties.insert(field_name.clone(), Value::Object(field_prop));
118
119 if field_schema.required {
120 required.push(Value::String(field_name.clone()));
121 }
122 }
123
124 def.insert("properties".to_string(), Value::Object(properties));
125 def.insert("required".to_string(), Value::Array(required));
126 def.insert("additionalProperties".to_string(), Value::Bool(true));
127
128 def
129}
130
131pub fn build_schema(
138 field_schemas: &HashMap<String, FieldSchema>,
139 card_schemas: &HashMap<String, CardSchema>,
140) -> Result<QuillValue, RenderError> {
141 let mut properties = Map::new();
142 let mut required_fields = Vec::new();
143 let mut defs = Map::new();
144
145 for (field_name, field_schema) in field_schemas {
147 let property = build_field_property(field_schema);
148 properties.insert(field_name.clone(), Value::Object(property));
149
150 if field_schema.required {
151 required_fields.push(field_name.clone());
152 }
153 }
154
155 if !card_schemas.is_empty() {
157 let mut one_of = Vec::new();
158 let mut discriminator_mapping = Map::new();
159
160 for (card_name, card_schema) in card_schemas {
161 let def_name = format!("{}_card", card_name);
162 let ref_path = format!("#/$defs/{}", def_name);
163
164 defs.insert(
166 def_name.clone(),
167 Value::Object(build_card_def(card_name, card_schema)),
168 );
169
170 let mut ref_obj = Map::new();
172 ref_obj.insert("$ref".to_string(), Value::String(ref_path.clone()));
173 one_of.push(Value::Object(ref_obj));
174
175 discriminator_mapping.insert(card_name.clone(), Value::String(ref_path));
177 }
178
179 let mut items_schema = Map::new();
181 items_schema.insert("oneOf".to_string(), Value::Array(one_of));
182
183 let mut cards_property = Map::new();
186 cards_property.insert("type".to_string(), Value::String("array".to_string()));
187 cards_property.insert("items".to_string(), Value::Object(items_schema));
188
189 properties.insert("CARDS".to_string(), Value::Object(cards_property));
190 }
191
192 let mut schema_map = Map::new();
194 schema_map.insert(
195 "$schema".to_string(),
196 Value::String("https://json-schema.org/draft/2019-09/schema".to_string()),
197 );
198 schema_map.insert("type".to_string(), Value::String("object".to_string()));
199
200 if !defs.is_empty() {
202 schema_map.insert("$defs".to_string(), Value::Object(defs));
203 }
204
205 schema_map.insert("properties".to_string(), Value::Object(properties));
206 schema_map.insert(
207 "required".to_string(),
208 Value::Array(required_fields.into_iter().map(Value::String).collect()),
209 );
210 schema_map.insert("additionalProperties".to_string(), Value::Bool(true));
211
212 let schema = Value::Object(schema_map);
213
214 Ok(QuillValue::from_json(schema))
215}
216
217pub fn build_schema_from_fields(
219 field_schemas: &HashMap<String, FieldSchema>,
220) -> Result<QuillValue, RenderError> {
221 build_schema(field_schemas, &HashMap::new())
222}
223
224pub fn extract_defaults_from_schema(
238 schema: &QuillValue,
239) -> HashMap<String, crate::value::QuillValue> {
240 let mut defaults = HashMap::new();
241
242 if let Some(properties) = schema.as_json().get("properties") {
244 if let Some(properties_obj) = properties.as_object() {
245 for (field_name, field_schema) in properties_obj {
246 if let Some(default_value) = field_schema.get("default") {
248 defaults.insert(
249 field_name.clone(),
250 QuillValue::from_json(default_value.clone()),
251 );
252 }
253 }
254 }
255 }
256
257 defaults
258}
259
260pub fn extract_examples_from_schema(
274 schema: &QuillValue,
275) -> HashMap<String, Vec<crate::value::QuillValue>> {
276 let mut examples = HashMap::new();
277
278 if let Some(properties) = schema.as_json().get("properties") {
280 if let Some(properties_obj) = properties.as_object() {
281 for (field_name, field_schema) in properties_obj {
282 if let Some(examples_value) = field_schema.get("examples") {
284 if let Some(examples_array) = examples_value.as_array() {
285 let examples_vec: Vec<QuillValue> = examples_array
286 .iter()
287 .map(|v| QuillValue::from_json(v.clone()))
288 .collect();
289 if !examples_vec.is_empty() {
290 examples.insert(field_name.clone(), examples_vec);
291 }
292 }
293 }
294 }
295 }
296 }
297
298 examples
299}
300
301pub fn extract_card_item_defaults(
315 schema: &QuillValue,
316) -> HashMap<String, HashMap<String, QuillValue>> {
317 let mut card_defaults = HashMap::new();
318
319 if let Some(properties) = schema.as_json().get("properties") {
321 if let Some(properties_obj) = properties.as_object() {
322 for (field_name, field_schema) in properties_obj {
323 let is_array = field_schema
325 .get("type")
326 .and_then(|t| t.as_str())
327 .map(|t| t == "array")
328 .unwrap_or(false);
329
330 if !is_array {
331 continue;
332 }
333
334 if let Some(items_schema) = field_schema.get("items") {
336 if let Some(item_props) = items_schema.get("properties") {
338 if let Some(item_props_obj) = item_props.as_object() {
339 let mut item_defaults = HashMap::new();
340
341 for (item_field_name, item_field_schema) in item_props_obj {
342 if let Some(default_value) = item_field_schema.get("default") {
344 item_defaults.insert(
345 item_field_name.clone(),
346 QuillValue::from_json(default_value.clone()),
347 );
348 }
349 }
350
351 if !item_defaults.is_empty() {
352 card_defaults.insert(field_name.clone(), item_defaults);
353 }
354 }
355 }
356 }
357 }
358 }
359 }
360
361 card_defaults
362}
363
364pub fn apply_card_item_defaults(
378 fields: &HashMap<String, QuillValue>,
379 card_defaults: &HashMap<String, HashMap<String, QuillValue>>,
380) -> HashMap<String, QuillValue> {
381 let mut result = fields.clone();
382
383 for (card_name, item_defaults) in card_defaults {
384 if let Some(card_value) = result.get(card_name) {
385 if let Some(items_array) = card_value.as_array() {
387 let mut updated_items: Vec<serde_json::Value> = Vec::new();
388
389 for item in items_array {
390 if let Some(item_obj) = item.as_object() {
392 let mut new_item = item_obj.clone();
393
394 for (default_field, default_value) in item_defaults {
396 if !new_item.contains_key(default_field) {
397 new_item
398 .insert(default_field.clone(), default_value.as_json().clone());
399 }
400 }
401
402 updated_items.push(serde_json::Value::Object(new_item));
403 } else {
404 updated_items.push(item.clone());
406 }
407 }
408
409 result.insert(
410 card_name.clone(),
411 QuillValue::from_json(serde_json::Value::Array(updated_items)),
412 );
413 }
414 }
415 }
416
417 result
418}
419
420pub fn validate_document(
422 schema: &QuillValue,
423 fields: &HashMap<String, crate::value::QuillValue>,
424) -> Result<(), Vec<String>> {
425 let mut doc_json = Map::new();
427 for (key, value) in fields {
428 doc_json.insert(key.clone(), value.as_json().clone());
429 }
430 let doc_value = Value::Object(doc_json);
431
432 let compiled = match jsonschema::Validator::new(schema.as_json()) {
434 Ok(c) => c,
435 Err(e) => return Err(vec![format!("Failed to compile schema: {}", e)]),
436 };
437
438 let validation_result = compiled.validate(&doc_value);
440
441 match validation_result {
442 Ok(_) => Ok(()),
443 Err(error) => {
444 let path = error.instance_path().to_string();
445 let path_display = if path.is_empty() {
446 "document".to_string()
447 } else {
448 path
449 };
450 let message = format!("Validation error at {}: {}", path_display, error);
451 Err(vec![message])
452 }
453 }
454}
455
456fn coerce_value(value: &QuillValue, expected_type: &str) -> QuillValue {
465 let json_value = value.as_json();
466
467 match expected_type {
468 "array" => {
469 if json_value.is_array() {
471 return value.clone();
472 }
473 QuillValue::from_json(Value::Array(vec![json_value.clone()]))
475 }
476 "boolean" => {
477 if let Some(b) = json_value.as_bool() {
479 return QuillValue::from_json(Value::Bool(b));
480 }
481 if let Some(s) = json_value.as_str() {
483 let lower = s.to_lowercase();
484 if lower == "true" {
485 return QuillValue::from_json(Value::Bool(true));
486 } else if lower == "false" {
487 return QuillValue::from_json(Value::Bool(false));
488 }
489 }
490 if let Some(n) = json_value.as_i64() {
492 return QuillValue::from_json(Value::Bool(n != 0));
493 }
494 if let Some(n) = json_value.as_f64() {
495 if n.is_nan() {
497 return QuillValue::from_json(Value::Bool(false));
498 }
499 return QuillValue::from_json(Value::Bool(n.abs() > f64::EPSILON));
500 }
501 value.clone()
503 }
504 "number" => {
505 if json_value.is_number() {
507 return value.clone();
508 }
509 if let Some(s) = json_value.as_str() {
511 if let Ok(i) = s.parse::<i64>() {
513 return QuillValue::from_json(serde_json::Number::from(i).into());
514 }
515 if let Ok(f) = s.parse::<f64>() {
517 if let Some(num) = serde_json::Number::from_f64(f) {
518 return QuillValue::from_json(num.into());
519 }
520 }
521 }
522 if let Some(b) = json_value.as_bool() {
524 let num_value = if b { 1 } else { 0 };
525 return QuillValue::from_json(Value::Number(serde_json::Number::from(num_value)));
526 }
527 value.clone()
529 }
530 _ => {
531 value.clone()
533 }
534 }
535}
536
537pub fn coerce_document(
551 schema: &QuillValue,
552 fields: &HashMap<String, QuillValue>,
553) -> HashMap<String, QuillValue> {
554 let mut coerced_fields = HashMap::new();
555
556 let properties = match schema.as_json().get("properties") {
558 Some(props) => props,
559 None => {
560 return fields.clone();
562 }
563 };
564
565 let properties_obj = match properties.as_object() {
566 Some(obj) => obj,
567 None => {
568 return fields.clone();
570 }
571 };
572
573 for (field_name, field_value) in fields {
575 if let Some(field_schema) = properties_obj.get(field_name) {
577 if let Some(expected_type) = field_schema.get("type").and_then(|t| t.as_str()) {
579 let coerced_value = coerce_value(field_value, expected_type);
581 coerced_fields.insert(field_name.clone(), coerced_value);
582 continue;
583 }
584 }
585 coerced_fields.insert(field_name.clone(), field_value.clone());
587 }
588
589 coerced_fields
590}
591
592#[cfg(test)]
593mod tests {
594 use super::*;
595 use crate::quill::FieldSchema;
596 use crate::value::QuillValue;
597
598 #[test]
599 fn test_build_schema_simple() {
600 let mut fields = HashMap::new();
601 let mut schema = FieldSchema::new(
602 "Author name".to_string(),
603 "The name of the author".to_string(),
604 );
605 schema.r#type = Some("str".to_string());
606 fields.insert("author".to_string(), schema);
607
608 let json_schema = build_schema_from_fields(&fields).unwrap().as_json().clone();
609 assert_eq!(json_schema["type"], "object");
610 assert_eq!(json_schema["properties"]["author"]["type"], "string");
611 assert_eq!(json_schema["properties"]["author"]["name"], "Author name");
612 assert_eq!(
613 json_schema["properties"]["author"]["description"],
614 "The name of the author"
615 );
616 }
617
618 #[test]
619 fn test_build_schema_with_default() {
620 let mut fields = HashMap::new();
621 let mut schema = FieldSchema::new(
622 "Field with default".to_string(),
623 "A field with a default value".to_string(),
624 );
625 schema.r#type = Some("str".to_string());
626 schema.default = Some(QuillValue::from_json(json!("default value")));
627 fields.insert("with_default".to_string(), schema);
629
630 build_schema_from_fields(&fields).unwrap();
631 }
632
633 #[test]
634 fn test_build_schema_date_types() {
635 let mut fields = HashMap::new();
636
637 let mut date_schema =
638 FieldSchema::new("Date field".to_string(), "A field for dates".to_string());
639 date_schema.r#type = Some("date".to_string());
640 fields.insert("date_field".to_string(), date_schema);
641
642 let mut datetime_schema = FieldSchema::new(
643 "DateTime field".to_string(),
644 "A field for date and time".to_string(),
645 );
646 datetime_schema.r#type = Some("datetime".to_string());
647 fields.insert("datetime_field".to_string(), datetime_schema);
648
649 let json_schema = build_schema_from_fields(&fields).unwrap().as_json().clone();
650 assert_eq!(json_schema["properties"]["date_field"]["type"], "string");
651 assert_eq!(json_schema["properties"]["date_field"]["format"], "date");
652 assert_eq!(
653 json_schema["properties"]["datetime_field"]["type"],
654 "string"
655 );
656 assert_eq!(
657 json_schema["properties"]["datetime_field"]["format"],
658 "date-time"
659 );
660 }
661
662 #[test]
663 fn test_validate_document_success() {
664 let schema = json!({
665 "$schema": "https://json-schema.org/draft/2019-09/schema",
666 "type": "object",
667 "properties": {
668 "title": {"type": "string"},
669 "count": {"type": "number"}
670 },
671 "required": ["title"],
672 "additionalProperties": true
673 });
674
675 let mut fields = HashMap::new();
676 fields.insert(
677 "title".to_string(),
678 QuillValue::from_json(json!("Test Title")),
679 );
680 fields.insert("count".to_string(), QuillValue::from_json(json!(42)));
681
682 let result = validate_document(&QuillValue::from_json(schema), &fields);
683 assert!(result.is_ok());
684 }
685
686 #[test]
687 fn test_validate_document_missing_required() {
688 let schema = json!({
689 "$schema": "https://json-schema.org/draft/2019-09/schema",
690 "type": "object",
691 "properties": {
692 "title": {"type": "string"}
693 },
694 "required": ["title"],
695 "additionalProperties": true
696 });
697
698 let fields = HashMap::new(); let result = validate_document(&QuillValue::from_json(schema), &fields);
701 assert!(result.is_err());
702 let errors = result.unwrap_err();
703 assert!(!errors.is_empty());
704 }
705
706 #[test]
707 fn test_validate_document_wrong_type() {
708 let schema = json!({
709 "$schema": "https://json-schema.org/draft/2019-09/schema",
710 "type": "object",
711 "properties": {
712 "count": {"type": "number"}
713 },
714 "additionalProperties": true
715 });
716
717 let mut fields = HashMap::new();
718 fields.insert(
719 "count".to_string(),
720 QuillValue::from_json(json!("not a number")),
721 );
722
723 let result = validate_document(&QuillValue::from_json(schema), &fields);
724 assert!(result.is_err());
725 }
726
727 #[test]
728 fn test_validate_document_allows_extra_fields() {
729 let schema = json!({
730 "$schema": "https://json-schema.org/draft/2019-09/schema",
731 "type": "object",
732 "properties": {
733 "title": {"type": "string"}
734 },
735 "required": ["title"],
736 "additionalProperties": true
737 });
738
739 let mut fields = HashMap::new();
740 fields.insert("title".to_string(), QuillValue::from_json(json!("Test")));
741 fields.insert("extra".to_string(), QuillValue::from_json(json!("allowed")));
742
743 let result = validate_document(&QuillValue::from_json(schema), &fields);
744 assert!(result.is_ok());
745 }
746
747 #[test]
748 fn test_build_schema_with_example() {
749 let mut fields = HashMap::new();
750 let mut schema = FieldSchema::new(
751 "memo_for".to_string(),
752 "List of recipient organization symbols".to_string(),
753 );
754 schema.r#type = Some("array".to_string());
755 schema.examples = Some(QuillValue::from_json(json!([[
756 "ORG1/SYMBOL",
757 "ORG2/SYMBOL"
758 ]])));
759 fields.insert("memo_for".to_string(), schema);
760
761 let json_schema = build_schema_from_fields(&fields).unwrap().as_json().clone();
762
763 assert!(json_schema["properties"]["memo_for"]
765 .as_object()
766 .unwrap()
767 .contains_key("examples"));
768
769 let example_value = &json_schema["properties"]["memo_for"]["examples"][0];
770 assert_eq!(example_value, &json!(["ORG1/SYMBOL", "ORG2/SYMBOL"]));
771 }
772
773 #[test]
774 fn test_build_schema_includes_default_in_properties() {
775 let mut fields = HashMap::new();
776 let mut schema = FieldSchema::new(
777 "ice_cream".to_string(),
778 "favorite ice cream flavor".to_string(),
779 );
780 schema.r#type = Some("string".to_string());
781 schema.default = Some(QuillValue::from_json(json!("taro")));
782 fields.insert("ice_cream".to_string(), schema);
783
784 let json_schema = build_schema_from_fields(&fields).unwrap().as_json().clone();
785
786 assert!(json_schema["properties"]["ice_cream"]
788 .as_object()
789 .unwrap()
790 .contains_key("default"));
791
792 let default_value = &json_schema["properties"]["ice_cream"]["default"];
793 assert_eq!(default_value, &json!("taro"));
794
795 let required_fields = json_schema["required"].as_array().unwrap();
797 assert!(!required_fields.contains(&json!("ice_cream")));
798 }
799
800 #[test]
801 fn test_extract_defaults_from_schema() {
802 let schema = json!({
804 "$schema": "https://json-schema.org/draft/2019-09/schema",
805 "type": "object",
806 "properties": {
807 "title": {
808 "type": "string",
809 "description": "Document title"
810 },
811 "author": {
812 "type": "string",
813 "description": "Document author",
814 "default": "Anonymous"
815 },
816 "status": {
817 "type": "string",
818 "description": "Document status",
819 "default": "draft"
820 },
821 "count": {
822 "type": "number",
823 "default": 42
824 }
825 },
826 "required": ["title"]
827 });
828
829 let defaults = extract_defaults_from_schema(&QuillValue::from_json(schema));
830
831 assert_eq!(defaults.len(), 3);
833 assert!(!defaults.contains_key("title")); assert!(defaults.contains_key("author"));
835 assert!(defaults.contains_key("status"));
836 assert!(defaults.contains_key("count"));
837
838 assert_eq!(defaults.get("author").unwrap().as_str(), Some("Anonymous"));
840 assert_eq!(defaults.get("status").unwrap().as_str(), Some("draft"));
841 assert_eq!(defaults.get("count").unwrap().as_json().as_i64(), Some(42));
842 }
843
844 #[test]
845 fn test_extract_defaults_from_schema_empty() {
846 let schema = json!({
848 "$schema": "https://json-schema.org/draft/2019-09/schema",
849 "type": "object",
850 "properties": {
851 "title": {"type": "string"},
852 "author": {"type": "string"}
853 },
854 "required": ["title"]
855 });
856
857 let defaults = extract_defaults_from_schema(&QuillValue::from_json(schema));
858 assert_eq!(defaults.len(), 0);
859 }
860
861 #[test]
862 fn test_extract_defaults_from_schema_no_properties() {
863 let schema = json!({
865 "$schema": "https://json-schema.org/draft/2019-09/schema",
866 "type": "object"
867 });
868
869 let defaults = extract_defaults_from_schema(&QuillValue::from_json(schema));
870 assert_eq!(defaults.len(), 0);
871 }
872
873 #[test]
874 fn test_extract_examples_from_schema() {
875 let schema = json!({
877 "$schema": "https://json-schema.org/draft/2019-09/schema",
878 "type": "object",
879 "properties": {
880 "title": {
881 "type": "string",
882 "description": "Document title"
883 },
884 "memo_for": {
885 "type": "array",
886 "description": "List of recipients",
887 "examples": [
888 ["ORG1/SYMBOL", "ORG2/SYMBOL"],
889 ["DEPT/OFFICE"]
890 ]
891 },
892 "author": {
893 "type": "string",
894 "description": "Document author",
895 "examples": ["John Doe", "Jane Smith"]
896 },
897 "status": {
898 "type": "string",
899 "description": "Document status"
900 }
901 }
902 });
903
904 let examples = extract_examples_from_schema(&QuillValue::from_json(schema));
905
906 assert_eq!(examples.len(), 2);
908 assert!(!examples.contains_key("title")); assert!(examples.contains_key("memo_for"));
910 assert!(examples.contains_key("author"));
911 assert!(!examples.contains_key("status")); let memo_for_examples = examples.get("memo_for").unwrap();
915 assert_eq!(memo_for_examples.len(), 2);
916 assert_eq!(
917 memo_for_examples[0].as_json(),
918 &json!(["ORG1/SYMBOL", "ORG2/SYMBOL"])
919 );
920 assert_eq!(memo_for_examples[1].as_json(), &json!(["DEPT/OFFICE"]));
921
922 let author_examples = examples.get("author").unwrap();
924 assert_eq!(author_examples.len(), 2);
925 assert_eq!(author_examples[0].as_str(), Some("John Doe"));
926 assert_eq!(author_examples[1].as_str(), Some("Jane Smith"));
927 }
928
929 #[test]
930 fn test_extract_examples_from_schema_empty() {
931 let schema = json!({
933 "$schema": "https://json-schema.org/draft/2019-09/schema",
934 "type": "object",
935 "properties": {
936 "title": {"type": "string"},
937 "author": {"type": "string"}
938 }
939 });
940
941 let examples = extract_examples_from_schema(&QuillValue::from_json(schema));
942 assert_eq!(examples.len(), 0);
943 }
944
945 #[test]
946 fn test_extract_examples_from_schema_no_properties() {
947 let schema = json!({
949 "$schema": "https://json-schema.org/draft/2019-09/schema",
950 "type": "object"
951 });
952
953 let examples = extract_examples_from_schema(&QuillValue::from_json(schema));
954 assert_eq!(examples.len(), 0);
955 }
956
957 #[test]
958 fn test_coerce_singular_to_array() {
959 let schema = json!({
960 "$schema": "https://json-schema.org/draft/2019-09/schema",
961 "type": "object",
962 "properties": {
963 "tags": {"type": "array"}
964 }
965 });
966
967 let mut fields = HashMap::new();
968 fields.insert(
969 "tags".to_string(),
970 QuillValue::from_json(json!("single-tag")),
971 );
972
973 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
974
975 let tags = coerced.get("tags").unwrap();
976 assert!(tags.as_array().is_some());
977 let tags_array = tags.as_array().unwrap();
978 assert_eq!(tags_array.len(), 1);
979 assert_eq!(tags_array[0].as_str().unwrap(), "single-tag");
980 }
981
982 #[test]
983 fn test_coerce_array_unchanged() {
984 let schema = json!({
985 "$schema": "https://json-schema.org/draft/2019-09/schema",
986 "type": "object",
987 "properties": {
988 "tags": {"type": "array"}
989 }
990 });
991
992 let mut fields = HashMap::new();
993 fields.insert(
994 "tags".to_string(),
995 QuillValue::from_json(json!(["tag1", "tag2"])),
996 );
997
998 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
999
1000 let tags = coerced.get("tags").unwrap();
1001 let tags_array = tags.as_array().unwrap();
1002 assert_eq!(tags_array.len(), 2);
1003 }
1004
1005 #[test]
1006 fn test_coerce_string_to_boolean() {
1007 let schema = json!({
1008 "$schema": "https://json-schema.org/draft/2019-09/schema",
1009 "type": "object",
1010 "properties": {
1011 "active": {"type": "boolean"},
1012 "enabled": {"type": "boolean"}
1013 }
1014 });
1015
1016 let mut fields = HashMap::new();
1017 fields.insert("active".to_string(), QuillValue::from_json(json!("true")));
1018 fields.insert("enabled".to_string(), QuillValue::from_json(json!("FALSE")));
1019
1020 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
1021
1022 assert_eq!(coerced.get("active").unwrap().as_bool().unwrap(), true);
1023 assert_eq!(coerced.get("enabled").unwrap().as_bool().unwrap(), false);
1024 }
1025
1026 #[test]
1027 fn test_coerce_number_to_boolean() {
1028 let schema = json!({
1029 "$schema": "https://json-schema.org/draft/2019-09/schema",
1030 "type": "object",
1031 "properties": {
1032 "flag1": {"type": "boolean"},
1033 "flag2": {"type": "boolean"},
1034 "flag3": {"type": "boolean"}
1035 }
1036 });
1037
1038 let mut fields = HashMap::new();
1039 fields.insert("flag1".to_string(), QuillValue::from_json(json!(0)));
1040 fields.insert("flag2".to_string(), QuillValue::from_json(json!(1)));
1041 fields.insert("flag3".to_string(), QuillValue::from_json(json!(42)));
1042
1043 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
1044
1045 assert_eq!(coerced.get("flag1").unwrap().as_bool().unwrap(), false);
1046 assert_eq!(coerced.get("flag2").unwrap().as_bool().unwrap(), true);
1047 assert_eq!(coerced.get("flag3").unwrap().as_bool().unwrap(), true);
1048 }
1049
1050 #[test]
1051 fn test_coerce_float_to_boolean() {
1052 let schema = json!({
1053 "$schema": "https://json-schema.org/draft/2019-09/schema",
1054 "type": "object",
1055 "properties": {
1056 "flag1": {"type": "boolean"},
1057 "flag2": {"type": "boolean"},
1058 "flag3": {"type": "boolean"},
1059 "flag4": {"type": "boolean"}
1060 }
1061 });
1062
1063 let mut fields = HashMap::new();
1064 fields.insert("flag1".to_string(), QuillValue::from_json(json!(0.0)));
1065 fields.insert("flag2".to_string(), QuillValue::from_json(json!(0.5)));
1066 fields.insert("flag3".to_string(), QuillValue::from_json(json!(-1.5)));
1067 fields.insert("flag4".to_string(), QuillValue::from_json(json!(1e-100)));
1069
1070 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
1071
1072 assert_eq!(coerced.get("flag1").unwrap().as_bool().unwrap(), false);
1073 assert_eq!(coerced.get("flag2").unwrap().as_bool().unwrap(), true);
1074 assert_eq!(coerced.get("flag3").unwrap().as_bool().unwrap(), true);
1075 assert_eq!(coerced.get("flag4").unwrap().as_bool().unwrap(), false);
1077 }
1078
1079 #[test]
1080 fn test_coerce_string_to_number() {
1081 let schema = json!({
1082 "$schema": "https://json-schema.org/draft/2019-09/schema",
1083 "type": "object",
1084 "properties": {
1085 "count": {"type": "number"},
1086 "price": {"type": "number"}
1087 }
1088 });
1089
1090 let mut fields = HashMap::new();
1091 fields.insert("count".to_string(), QuillValue::from_json(json!("42")));
1092 fields.insert("price".to_string(), QuillValue::from_json(json!("19.99")));
1093
1094 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
1095
1096 assert_eq!(coerced.get("count").unwrap().as_i64().unwrap(), 42);
1097 assert_eq!(coerced.get("price").unwrap().as_f64().unwrap(), 19.99);
1098 }
1099
1100 #[test]
1101 fn test_coerce_boolean_to_number() {
1102 let schema = json!({
1103 "$schema": "https://json-schema.org/draft/2019-09/schema",
1104 "type": "object",
1105 "properties": {
1106 "active": {"type": "number"},
1107 "disabled": {"type": "number"}
1108 }
1109 });
1110
1111 let mut fields = HashMap::new();
1112 fields.insert("active".to_string(), QuillValue::from_json(json!(true)));
1113 fields.insert("disabled".to_string(), QuillValue::from_json(json!(false)));
1114
1115 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
1116
1117 assert_eq!(coerced.get("active").unwrap().as_i64().unwrap(), 1);
1118 assert_eq!(coerced.get("disabled").unwrap().as_i64().unwrap(), 0);
1119 }
1120
1121 #[test]
1122 fn test_coerce_no_schema_properties() {
1123 let schema = json!({
1124 "$schema": "https://json-schema.org/draft/2019-09/schema",
1125 "type": "object"
1126 });
1127
1128 let mut fields = HashMap::new();
1129 fields.insert("title".to_string(), QuillValue::from_json(json!("Test")));
1130
1131 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
1132
1133 assert_eq!(coerced.get("title").unwrap().as_str().unwrap(), "Test");
1135 }
1136
1137 #[test]
1138 fn test_coerce_field_without_type() {
1139 let schema = json!({
1140 "$schema": "https://json-schema.org/draft/2019-09/schema",
1141 "type": "object",
1142 "properties": {
1143 "title": {"description": "A title field"}
1144 }
1145 });
1146
1147 let mut fields = HashMap::new();
1148 fields.insert("title".to_string(), QuillValue::from_json(json!("Test")));
1149
1150 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
1151
1152 assert_eq!(coerced.get("title").unwrap().as_str().unwrap(), "Test");
1154 }
1155
1156 #[test]
1157 fn test_coerce_mixed_fields() {
1158 let schema = json!({
1159 "$schema": "https://json-schema.org/draft/2019-09/schema",
1160 "type": "object",
1161 "properties": {
1162 "tags": {"type": "array"},
1163 "active": {"type": "boolean"},
1164 "count": {"type": "number"},
1165 "title": {"type": "string"}
1166 }
1167 });
1168
1169 let mut fields = HashMap::new();
1170 fields.insert("tags".to_string(), QuillValue::from_json(json!("single")));
1171 fields.insert("active".to_string(), QuillValue::from_json(json!("true")));
1172 fields.insert("count".to_string(), QuillValue::from_json(json!("42")));
1173 fields.insert(
1174 "title".to_string(),
1175 QuillValue::from_json(json!("Test Title")),
1176 );
1177
1178 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
1179
1180 assert_eq!(coerced.get("tags").unwrap().as_array().unwrap().len(), 1);
1182 assert_eq!(coerced.get("active").unwrap().as_bool().unwrap(), true);
1183 assert_eq!(coerced.get("count").unwrap().as_i64().unwrap(), 42);
1184 assert_eq!(
1185 coerced.get("title").unwrap().as_str().unwrap(),
1186 "Test Title"
1187 );
1188 }
1189
1190 #[test]
1191 fn test_coerce_invalid_string_to_number() {
1192 let schema = json!({
1193 "$schema": "https://json-schema.org/draft/2019-09/schema",
1194 "type": "object",
1195 "properties": {
1196 "count": {"type": "number"}
1197 }
1198 });
1199
1200 let mut fields = HashMap::new();
1201 fields.insert(
1202 "count".to_string(),
1203 QuillValue::from_json(json!("not-a-number")),
1204 );
1205
1206 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
1207
1208 assert_eq!(
1210 coerced.get("count").unwrap().as_str().unwrap(),
1211 "not-a-number"
1212 );
1213 }
1214
1215 #[test]
1216 fn test_coerce_object_to_array() {
1217 let schema = json!({
1218 "$schema": "https://json-schema.org/draft/2019-09/schema",
1219 "type": "object",
1220 "properties": {
1221 "items": {"type": "array"}
1222 }
1223 });
1224
1225 let mut fields = HashMap::new();
1226 fields.insert(
1227 "items".to_string(),
1228 QuillValue::from_json(json!({"key": "value"})),
1229 );
1230
1231 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
1232
1233 let items = coerced.get("items").unwrap();
1235 assert!(items.as_array().is_some());
1236 let items_array = items.as_array().unwrap();
1237 assert_eq!(items_array.len(), 1);
1238 assert!(items_array[0].as_object().is_some());
1239 }
1240
1241 #[test]
1242 fn test_schema_card_in_defs() {
1243 use crate::quill::CardSchema;
1245
1246 let fields = HashMap::new();
1247 let mut cards = HashMap::new();
1248
1249 let mut name_schema = FieldSchema::new("name".to_string(), "Name field".to_string());
1250 name_schema.r#type = Some("string".to_string());
1251
1252 let mut card_fields = HashMap::new();
1253 card_fields.insert("name".to_string(), name_schema);
1254
1255 let card = CardSchema {
1256 name: "endorsements".to_string(),
1257 title: Some("Endorsements".to_string()),
1258 description: "Chain of endorsements".to_string(),
1259 ui: None,
1260 fields: card_fields,
1261 };
1262 cards.insert("endorsements".to_string(), card);
1263
1264 let json_schema = build_schema(&fields, &cards).unwrap().as_json().clone();
1265
1266 assert!(json_schema["$defs"].is_object());
1268 assert!(json_schema["$defs"]["endorsements_card"].is_object());
1269
1270 let card_def = &json_schema["$defs"]["endorsements_card"];
1272 assert_eq!(card_def["type"], "object");
1273 assert_eq!(card_def["title"], "Endorsements");
1274 assert_eq!(card_def["description"], "Chain of endorsements");
1275
1276 assert_eq!(card_def["properties"]["CARD"]["const"], "endorsements");
1278
1279 assert!(card_def["properties"]["name"].is_object());
1281 assert_eq!(card_def["properties"]["name"]["type"], "string");
1282
1283 let required = card_def["required"].as_array().unwrap();
1285 assert!(required.contains(&json!("CARD")));
1286 }
1287
1288 #[test]
1289 fn test_schema_cards_array() {
1290 use crate::quill::CardSchema;
1292
1293 let fields = HashMap::new();
1294 let mut cards = HashMap::new();
1295
1296 let mut name_schema = FieldSchema::new("name".to_string(), "Endorser name".to_string());
1297 name_schema.r#type = Some("string".to_string());
1298 name_schema.required = true;
1299
1300 let mut org_schema = FieldSchema::new("org".to_string(), "Organization".to_string());
1301 org_schema.r#type = Some("string".to_string());
1302 org_schema.default = Some(QuillValue::from_json(json!("Unknown")));
1303
1304 let mut card_fields = HashMap::new();
1305 card_fields.insert("name".to_string(), name_schema);
1306 card_fields.insert("org".to_string(), org_schema);
1307
1308 let card = CardSchema {
1309 name: "endorsements".to_string(),
1310 title: Some("Endorsements".to_string()),
1311 description: "Chain of endorsements".to_string(),
1312 ui: None,
1313 fields: card_fields,
1314 };
1315 cards.insert("endorsements".to_string(), card);
1316
1317 let json_schema = build_schema(&fields, &cards).unwrap().as_json().clone();
1318
1319 let cards_prop = &json_schema["properties"]["CARDS"];
1321 assert_eq!(cards_prop["type"], "array");
1322
1323 let items = &cards_prop["items"];
1325 assert!(items["oneOf"].is_array());
1326 let one_of = items["oneOf"].as_array().unwrap();
1327 assert!(!one_of.is_empty());
1328 assert_eq!(one_of[0]["$ref"], "#/$defs/endorsements_card");
1329
1330 assert!(items.get("x-discriminator").is_none());
1332
1333 let card_def = &json_schema["$defs"]["endorsements_card"];
1335 assert_eq!(card_def["properties"]["name"]["type"], "string");
1336 assert_eq!(card_def["properties"]["org"]["default"], "Unknown");
1337
1338 let required = card_def["required"].as_array().unwrap();
1340 assert!(required.contains(&json!("CARD")));
1341 assert!(required.contains(&json!("name")));
1342 assert!(!required.contains(&json!("org")));
1343
1344 assert_eq!(card_def["additionalProperties"], true);
1346 }
1347
1348 #[test]
1349 fn test_extract_card_item_defaults() {
1350 let schema = json!({
1352 "$schema": "https://json-schema.org/draft/2019-09/schema",
1353 "type": "object",
1354 "properties": {
1355 "endorsements": {
1356 "type": "array",
1357 "items": {
1358 "type": "object",
1359 "properties": {
1360 "name": { "type": "string" },
1361 "org": { "type": "string", "default": "Unknown Org" },
1362 "rank": { "type": "string", "default": "N/A" }
1363 }
1364 }
1365 },
1366 "title": { "type": "string" }
1367 }
1368 });
1369
1370 let card_defaults = extract_card_item_defaults(&QuillValue::from_json(schema));
1371
1372 assert_eq!(card_defaults.len(), 1);
1374 assert!(card_defaults.contains_key("endorsements"));
1375
1376 let endorsements_defaults = card_defaults.get("endorsements").unwrap();
1377 assert_eq!(endorsements_defaults.len(), 2); assert!(!endorsements_defaults.contains_key("name")); assert_eq!(
1380 endorsements_defaults.get("org").unwrap().as_str(),
1381 Some("Unknown Org")
1382 );
1383 assert_eq!(
1384 endorsements_defaults.get("rank").unwrap().as_str(),
1385 Some("N/A")
1386 );
1387 }
1388
1389 #[test]
1390 fn test_extract_card_item_defaults_empty() {
1391 let schema = json!({
1393 "type": "object",
1394 "properties": {
1395 "title": { "type": "string" }
1396 }
1397 });
1398
1399 let card_defaults = extract_card_item_defaults(&QuillValue::from_json(schema));
1400 assert!(card_defaults.is_empty());
1401 }
1402
1403 #[test]
1404 fn test_extract_card_item_defaults_no_item_defaults() {
1405 let schema = json!({
1407 "type": "object",
1408 "properties": {
1409 "endorsements": {
1410 "type": "array",
1411 "items": {
1412 "type": "object",
1413 "properties": {
1414 "name": { "type": "string" },
1415 "org": { "type": "string" }
1416 }
1417 }
1418 }
1419 }
1420 });
1421
1422 let card_defaults = extract_card_item_defaults(&QuillValue::from_json(schema));
1423 assert!(card_defaults.is_empty()); }
1425
1426 #[test]
1427 fn test_apply_card_item_defaults() {
1428 let mut item_defaults = HashMap::new();
1430 item_defaults.insert(
1431 "org".to_string(),
1432 QuillValue::from_json(json!("Default Org")),
1433 );
1434
1435 let mut card_defaults = HashMap::new();
1436 card_defaults.insert("endorsements".to_string(), item_defaults);
1437
1438 let mut fields = HashMap::new();
1440 fields.insert(
1441 "endorsements".to_string(),
1442 QuillValue::from_json(json!([
1443 { "name": "John Doe" },
1444 { "name": "Jane Smith", "org": "Custom Org" }
1445 ])),
1446 );
1447
1448 let result = apply_card_item_defaults(&fields, &card_defaults);
1449
1450 let endorsements = result.get("endorsements").unwrap().as_array().unwrap();
1452 assert_eq!(endorsements.len(), 2);
1453
1454 assert_eq!(endorsements[0]["name"], "John Doe");
1456 assert_eq!(endorsements[0]["org"], "Default Org");
1457
1458 assert_eq!(endorsements[1]["name"], "Jane Smith");
1460 assert_eq!(endorsements[1]["org"], "Custom Org");
1461 }
1462
1463 #[test]
1464 fn test_apply_card_item_defaults_empty_card() {
1465 let mut item_defaults = HashMap::new();
1466 item_defaults.insert(
1467 "org".to_string(),
1468 QuillValue::from_json(json!("Default Org")),
1469 );
1470
1471 let mut card_defaults = HashMap::new();
1472 card_defaults.insert("endorsements".to_string(), item_defaults);
1473
1474 let mut fields = HashMap::new();
1476 fields.insert("endorsements".to_string(), QuillValue::from_json(json!([])));
1477
1478 let result = apply_card_item_defaults(&fields, &card_defaults);
1479
1480 let endorsements = result.get("endorsements").unwrap().as_array().unwrap();
1482 assert!(endorsements.is_empty());
1483 }
1484
1485 #[test]
1486 fn test_apply_card_item_defaults_no_matching_card() {
1487 let mut item_defaults = HashMap::new();
1488 item_defaults.insert(
1489 "org".to_string(),
1490 QuillValue::from_json(json!("Default Org")),
1491 );
1492
1493 let mut card_defaults = HashMap::new();
1494 card_defaults.insert("endorsements".to_string(), item_defaults);
1495
1496 let mut fields = HashMap::new();
1498 fields.insert(
1499 "reviews".to_string(),
1500 QuillValue::from_json(json!([{ "author": "Bob" }])),
1501 );
1502
1503 let result = apply_card_item_defaults(&fields, &card_defaults);
1504
1505 let reviews = result.get("reviews").unwrap().as_array().unwrap();
1507 assert_eq!(reviews.len(), 1);
1508 assert_eq!(reviews[0]["author"], "Bob");
1509 assert!(reviews[0].get("org").is_none());
1510 }
1511
1512 #[test]
1513 fn test_card_validation_with_required_fields() {
1514 let schema = json!({
1516 "$schema": "https://json-schema.org/draft/2019-09/schema",
1517 "type": "object",
1518 "properties": {
1519 "endorsements": {
1520 "type": "array",
1521 "items": {
1522 "type": "object",
1523 "properties": {
1524 "name": { "type": "string" },
1525 "org": { "type": "string", "default": "Unknown" }
1526 },
1527 "required": ["name"]
1528 }
1529 }
1530 }
1531 });
1532
1533 let mut valid_fields = HashMap::new();
1535 valid_fields.insert(
1536 "endorsements".to_string(),
1537 QuillValue::from_json(json!([{ "name": "John" }])),
1538 );
1539
1540 let result = validate_document(&QuillValue::from_json(schema.clone()), &valid_fields);
1541 assert!(result.is_ok());
1542
1543 let mut invalid_fields = HashMap::new();
1545 invalid_fields.insert(
1546 "endorsements".to_string(),
1547 QuillValue::from_json(json!([{ "org": "SomeOrg" }])),
1548 );
1549
1550 let result = validate_document(&QuillValue::from_json(schema), &invalid_fields);
1551 assert!(result.is_err());
1552 }
1553}