1use crate::quill::{field_key, ui_key, CardSchema, FieldSchema, FieldType};
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(
17 field_key::NAME.to_string(),
18 Value::String(field_schema.name.clone()),
19 );
20
21 if let Some(ref field_type) = field_schema.r#type {
23 let (json_type, format) = match field_type {
24 FieldType::Str | FieldType::String => ("string", None),
25 FieldType::Number => ("number", None),
26 FieldType::Boolean => ("boolean", None),
27 FieldType::Array => ("array", None),
28 FieldType::Dict => ("object", None),
29 FieldType::Date => ("string", Some("date")),
30 FieldType::DateTime => ("string", Some("date-time")),
31 };
32 property.insert(
33 field_key::TYPE.to_string(),
34 Value::String(json_type.to_string()),
35 );
36
37 if let Some(fmt) = format {
39 property.insert(
40 field_key::FORMAT.to_string(),
41 Value::String(fmt.to_string()),
42 );
43 }
44 }
45
46 if let Some(ref title) = field_schema.title {
48 property.insert(field_key::TITLE.to_string(), Value::String(title.clone()));
49 }
50
51 property.insert(
53 field_key::DESCRIPTION.to_string(),
54 Value::String(field_schema.description.clone()),
55 );
56
57 if let Some(ref ui) = field_schema.ui {
59 let mut ui_obj = Map::new();
60
61 if let Some(ref group) = ui.group {
62 ui_obj.insert(ui_key::GROUP.to_string(), Value::String(group.clone()));
63 }
64
65 if let Some(order) = ui.order {
66 ui_obj.insert(ui_key::ORDER.to_string(), json!(order));
67 }
68
69 if !ui_obj.is_empty() {
70 property.insert("x-ui".to_string(), Value::Object(ui_obj));
71 }
72 }
73
74 if let Some(ref examples) = field_schema.examples {
76 if let Some(examples_array) = examples.as_array() {
77 if !examples_array.is_empty() {
78 property.insert(
79 field_key::EXAMPLES.to_string(),
80 Value::Array(examples_array.clone()),
81 );
82 }
83 }
84 }
85
86 if let Some(ref default) = field_schema.default {
88 property.insert(field_key::DEFAULT.to_string(), default.as_json().clone());
89 }
90
91 if let Some(ref enum_values) = field_schema.enum_values {
93 let enum_array: Vec<Value> = enum_values
94 .iter()
95 .map(|s| Value::String(s.clone()))
96 .collect();
97 property.insert(field_key::ENUM.to_string(), Value::Array(enum_array));
98 }
99
100 property
101}
102
103fn build_card_def(name: &str, card: &CardSchema) -> Map<String, Value> {
105 let mut def = Map::new();
106
107 def.insert("type".to_string(), Value::String("object".to_string()));
108
109 if let Some(ref title) = card.title {
111 def.insert("title".to_string(), Value::String(title.clone()));
112 }
113
114 if !card.description.is_empty() {
116 def.insert(
117 "description".to_string(),
118 Value::String(card.description.clone()),
119 );
120 }
121
122 let mut properties = Map::new();
124 let mut required = vec![Value::String("CARD".to_string())];
125
126 let mut card_prop = Map::new();
128 card_prop.insert("const".to_string(), Value::String(name.to_string()));
129 properties.insert("CARD".to_string(), Value::Object(card_prop));
130
131 for (field_name, field_schema) in &card.fields {
133 let field_prop = build_field_property(field_schema);
134 properties.insert(field_name.clone(), Value::Object(field_prop));
135
136 if field_schema.required {
137 required.push(Value::String(field_name.clone()));
138 }
139 }
140
141 def.insert("properties".to_string(), Value::Object(properties));
142 def.insert("required".to_string(), Value::Array(required));
143 def.insert("additionalProperties".to_string(), Value::Bool(true));
144
145 def
146}
147
148pub fn build_schema(
155 field_schemas: &HashMap<String, FieldSchema>,
156 card_schemas: &HashMap<String, CardSchema>,
157) -> Result<QuillValue, RenderError> {
158 let mut properties = Map::new();
159 let mut required_fields = Vec::new();
160 let mut defs = Map::new();
161
162 for (field_name, field_schema) in field_schemas {
164 let property = build_field_property(field_schema);
165 properties.insert(field_name.clone(), Value::Object(property));
166
167 if field_schema.required {
168 required_fields.push(field_name.clone());
169 }
170 }
171
172 if !card_schemas.is_empty() {
174 let mut one_of = Vec::new();
175 let mut discriminator_mapping = Map::new();
176
177 for (card_name, card_schema) in card_schemas {
178 let def_name = format!("{}_card", card_name);
179 let ref_path = format!("#/$defs/{}", def_name);
180
181 defs.insert(
183 def_name.clone(),
184 Value::Object(build_card_def(card_name, card_schema)),
185 );
186
187 let mut ref_obj = Map::new();
189 ref_obj.insert("$ref".to_string(), Value::String(ref_path.clone()));
190 one_of.push(Value::Object(ref_obj));
191
192 discriminator_mapping.insert(card_name.clone(), Value::String(ref_path));
194 }
195
196 let mut items_schema = Map::new();
198 items_schema.insert("oneOf".to_string(), Value::Array(one_of));
199
200 let mut cards_property = Map::new();
203 cards_property.insert("type".to_string(), Value::String("array".to_string()));
204 cards_property.insert("items".to_string(), Value::Object(items_schema));
205
206 properties.insert("CARDS".to_string(), Value::Object(cards_property));
207 }
208
209 let mut schema_map = Map::new();
211 schema_map.insert(
212 "$schema".to_string(),
213 Value::String("https://json-schema.org/draft/2019-09/schema".to_string()),
214 );
215 schema_map.insert("type".to_string(), Value::String("object".to_string()));
216
217 if !defs.is_empty() {
219 schema_map.insert("$defs".to_string(), Value::Object(defs));
220 }
221
222 schema_map.insert("properties".to_string(), Value::Object(properties));
223 schema_map.insert(
224 "required".to_string(),
225 Value::Array(required_fields.into_iter().map(Value::String).collect()),
226 );
227 schema_map.insert("additionalProperties".to_string(), Value::Bool(true));
228
229 let schema = Value::Object(schema_map);
230
231 Ok(QuillValue::from_json(schema))
232}
233
234pub fn build_schema_from_fields(
236 field_schemas: &HashMap<String, FieldSchema>,
237) -> Result<QuillValue, RenderError> {
238 build_schema(field_schemas, &HashMap::new())
239}
240
241pub fn extract_defaults_from_schema(
255 schema: &QuillValue,
256) -> HashMap<String, crate::value::QuillValue> {
257 let mut defaults = HashMap::new();
258
259 if let Some(properties) = schema.as_json().get("properties") {
261 if let Some(properties_obj) = properties.as_object() {
262 for (field_name, field_schema) in properties_obj {
263 if let Some(default_value) = field_schema.get("default") {
265 defaults.insert(
266 field_name.clone(),
267 QuillValue::from_json(default_value.clone()),
268 );
269 }
270 }
271 }
272 }
273
274 defaults
275}
276
277pub fn extract_examples_from_schema(
291 schema: &QuillValue,
292) -> HashMap<String, Vec<crate::value::QuillValue>> {
293 let mut examples = HashMap::new();
294
295 if let Some(properties) = schema.as_json().get("properties") {
297 if let Some(properties_obj) = properties.as_object() {
298 for (field_name, field_schema) in properties_obj {
299 if let Some(examples_value) = field_schema.get("examples") {
301 if let Some(examples_array) = examples_value.as_array() {
302 let examples_vec: Vec<QuillValue> = examples_array
303 .iter()
304 .map(|v| QuillValue::from_json(v.clone()))
305 .collect();
306 if !examples_vec.is_empty() {
307 examples.insert(field_name.clone(), examples_vec);
308 }
309 }
310 }
311 }
312 }
313 }
314
315 examples
316}
317
318pub fn extract_card_item_defaults(
332 schema: &QuillValue,
333) -> HashMap<String, HashMap<String, QuillValue>> {
334 let mut card_defaults = HashMap::new();
335
336 if let Some(properties) = schema.as_json().get("properties") {
338 if let Some(properties_obj) = properties.as_object() {
339 for (field_name, field_schema) in properties_obj {
340 let is_array = field_schema
342 .get("type")
343 .and_then(|t| t.as_str())
344 .map(|t| t == "array")
345 .unwrap_or(false);
346
347 if !is_array {
348 continue;
349 }
350
351 if let Some(items_schema) = field_schema.get("items") {
353 if let Some(item_props) = items_schema.get("properties") {
355 if let Some(item_props_obj) = item_props.as_object() {
356 let mut item_defaults = HashMap::new();
357
358 for (item_field_name, item_field_schema) in item_props_obj {
359 if let Some(default_value) = item_field_schema.get("default") {
361 item_defaults.insert(
362 item_field_name.clone(),
363 QuillValue::from_json(default_value.clone()),
364 );
365 }
366 }
367
368 if !item_defaults.is_empty() {
369 card_defaults.insert(field_name.clone(), item_defaults);
370 }
371 }
372 }
373 }
374 }
375 }
376 }
377
378 card_defaults
379}
380
381pub fn apply_card_item_defaults(
395 fields: &HashMap<String, QuillValue>,
396 card_defaults: &HashMap<String, HashMap<String, QuillValue>>,
397) -> HashMap<String, QuillValue> {
398 let mut result = fields.clone();
399
400 for (card_name, item_defaults) in card_defaults {
401 if let Some(card_value) = result.get(card_name) {
402 if let Some(items_array) = card_value.as_array() {
404 let mut updated_items: Vec<serde_json::Value> = Vec::new();
405
406 for item in items_array {
407 if let Some(item_obj) = item.as_object() {
409 let mut new_item = item_obj.clone();
410
411 for (default_field, default_value) in item_defaults {
413 if !new_item.contains_key(default_field) {
414 new_item
415 .insert(default_field.clone(), default_value.as_json().clone());
416 }
417 }
418
419 updated_items.push(serde_json::Value::Object(new_item));
420 } else {
421 updated_items.push(item.clone());
423 }
424 }
425
426 result.insert(
427 card_name.clone(),
428 QuillValue::from_json(serde_json::Value::Array(updated_items)),
429 );
430 }
431 }
432 }
433
434 result
435}
436
437pub fn validate_document(
439 schema: &QuillValue,
440 fields: &HashMap<String, crate::value::QuillValue>,
441) -> Result<(), Vec<String>> {
442 let mut doc_json = Map::new();
444 for (key, value) in fields {
445 doc_json.insert(key.clone(), value.as_json().clone());
446 }
447 let doc_value = Value::Object(doc_json);
448
449 let compiled = match jsonschema::Validator::new(schema.as_json()) {
451 Ok(c) => c,
452 Err(e) => return Err(vec![format!("Failed to compile schema: {}", e)]),
453 };
454
455 let mut all_errors = Vec::new();
457
458 if let Some(cards) = doc_value.get("CARDS").and_then(|v| v.as_array()) {
460 let card_errors = validate_cards_array(schema, cards);
461 all_errors.extend(card_errors);
462 }
463
464 let validation_result = compiled.validate(&doc_value);
466
467 match validation_result {
468 Ok(_) => {
469 if all_errors.is_empty() {
470 Ok(())
471 } else {
472 Err(all_errors)
473 }
474 }
475 Err(error) => {
476 let path = error.instance_path().to_string();
477 let path_display = if path.is_empty() {
478 "document".to_string()
479 } else {
480 path.clone()
481 };
482
483 let is_generic_card_error = path.starts_with("/CARDS/")
487 && error.to_string().contains("oneOf")
488 && !all_errors.is_empty();
489
490 if !is_generic_card_error {
491 if path.starts_with("/CARDS/") && error.to_string().contains("oneOf") {
493 if let Some(rest) = path.strip_prefix("/CARDS/") {
495 let is_item_error = !rest.contains('/');
498
499 if is_item_error {
500 if let Ok(idx) = rest.parse::<usize>() {
501 if let Some(cards) =
502 doc_value.get("CARDS").and_then(|v| v.as_array())
503 {
504 if let Some(item) = cards.get(idx) {
505 if let Some(card_type) =
507 item.get("CARD").and_then(|v| v.as_str())
508 {
509 let mut valid_types = Vec::new();
511 if let Some(defs) = schema
512 .as_json()
513 .get("$defs")
514 .and_then(|v| v.as_object())
515 {
516 for key in defs.keys() {
517 if let Some(name) = key.strip_suffix("_card") {
518 valid_types.push(name.to_string());
519 }
520 }
521 }
522
523 if !valid_types.is_empty()
525 && !valid_types.contains(&card_type.to_string())
526 {
527 valid_types.sort();
528 let valid_list = valid_types.join(", ");
529 let message = format!("Validation error at {}: Invalid card type '{}'. Valid types are: [{}]", path_display, card_type, valid_list);
530 all_errors.push(message);
531 return Err(all_errors);
532 }
533 }
534 }
535 }
536 }
537 }
538 }
539 }
540
541 let message = format!("Validation error at {}: {}", path_display, error);
542 all_errors.push(message);
543 }
544
545 Err(all_errors)
546 }
547 }
548}
549
550fn validate_cards_array(document_schema: &QuillValue, cards_array: &[Value]) -> Vec<String> {
552 let mut errors = Vec::new();
553
554 let defs = document_schema
556 .as_json()
557 .get("$defs")
558 .and_then(|v| v.as_object());
559
560 for (idx, card) in cards_array.iter().enumerate() {
561 if let Some(card_obj) = card.as_object() {
563 if let Some(card_type) = card_obj.get("CARD").and_then(|v| v.as_str()) {
564 let def_name = format!("{}_card", card_type);
566
567 if let Some(card_schema_json) = defs.and_then(|d| d.get(&def_name)) {
569 let mut card_fields = HashMap::new();
571 for (k, v) in card_obj {
572 card_fields.insert(k.clone(), QuillValue::from_json(v.clone()));
573 }
574
575 if let Err(card_errors) = validate_document(
577 &QuillValue::from_json(card_schema_json.clone()),
578 &card_fields,
579 ) {
580 for err in card_errors {
582 let prefix = format!("/CARDS/{}", idx);
588 let new_msg =
589 if let Some(rest) = err.strip_prefix("Validation error at ") {
590 if rest.starts_with("document") {
591 format!(
593 "Validation error at {}:{}",
594 prefix,
595 rest.strip_prefix("document").unwrap_or(rest)
596 )
597 } else {
598 format!("Validation error at {}{}", prefix, rest)
600 }
601 } else {
602 format!("Validation error at {}: {}", prefix, err)
603 };
604
605 errors.push(new_msg);
606 }
607 }
608 }
609 }
610 }
611 }
612
613 errors
614}
615
616fn coerce_value(value: &QuillValue, expected_type: &str) -> QuillValue {
625 let json_value = value.as_json();
626
627 match expected_type {
628 "array" => {
629 if json_value.is_array() {
631 return value.clone();
632 }
633 QuillValue::from_json(Value::Array(vec![json_value.clone()]))
635 }
636 "boolean" => {
637 if let Some(b) = json_value.as_bool() {
639 return QuillValue::from_json(Value::Bool(b));
640 }
641 if let Some(s) = json_value.as_str() {
643 let lower = s.to_lowercase();
644 if lower == "true" {
645 return QuillValue::from_json(Value::Bool(true));
646 } else if lower == "false" {
647 return QuillValue::from_json(Value::Bool(false));
648 }
649 }
650 if let Some(n) = json_value.as_i64() {
652 return QuillValue::from_json(Value::Bool(n != 0));
653 }
654 if let Some(n) = json_value.as_f64() {
655 if n.is_nan() {
657 return QuillValue::from_json(Value::Bool(false));
658 }
659 return QuillValue::from_json(Value::Bool(n.abs() > f64::EPSILON));
660 }
661 value.clone()
663 }
664 "number" => {
665 if json_value.is_number() {
667 return value.clone();
668 }
669 if let Some(s) = json_value.as_str() {
671 if let Ok(i) = s.parse::<i64>() {
673 return QuillValue::from_json(serde_json::Number::from(i).into());
674 }
675 if let Ok(f) = s.parse::<f64>() {
677 if let Some(num) = serde_json::Number::from_f64(f) {
678 return QuillValue::from_json(num.into());
679 }
680 }
681 }
682 if let Some(b) = json_value.as_bool() {
684 let num_value = if b { 1 } else { 0 };
685 return QuillValue::from_json(Value::Number(serde_json::Number::from(num_value)));
686 }
687 value.clone()
689 }
690 _ => {
691 value.clone()
693 }
694 }
695}
696
697pub fn coerce_document(
711 schema: &QuillValue,
712 fields: &HashMap<String, QuillValue>,
713) -> HashMap<String, QuillValue> {
714 let mut coerced_fields = HashMap::new();
715
716 let properties = match schema.as_json().get("properties") {
718 Some(props) => props,
719 None => {
720 return fields.clone();
722 }
723 };
724
725 let properties_obj = match properties.as_object() {
726 Some(obj) => obj,
727 None => {
728 return fields.clone();
730 }
731 };
732
733 for (field_name, field_value) in fields {
735 if let Some(field_schema) = properties_obj.get(field_name) {
737 if let Some(expected_type) = field_schema.get("type").and_then(|t| t.as_str()) {
739 let coerced_value = coerce_value(field_value, expected_type);
741 coerced_fields.insert(field_name.clone(), coerced_value);
742 continue;
743 }
744 }
745 coerced_fields.insert(field_name.clone(), field_value.clone());
747 }
748
749 if let Some(cards_value) = coerced_fields.get("CARDS") {
751 if let Some(cards_array) = cards_value.as_array() {
752 let coerced_cards = coerce_cards_array(schema, cards_array);
753 coerced_fields.insert(
754 "CARDS".to_string(),
755 QuillValue::from_json(Value::Array(coerced_cards)),
756 );
757 }
758 }
759
760 coerced_fields
761}
762
763fn coerce_cards_array(document_schema: &QuillValue, cards_array: &[Value]) -> Vec<Value> {
765 let mut coerced_cards = Vec::new();
766
767 let defs = document_schema
769 .as_json()
770 .get("$defs")
771 .and_then(|v| v.as_object());
772
773 for card in cards_array {
774 if let Some(card_obj) = card.as_object() {
776 if let Some(card_type) = card_obj.get("CARD").and_then(|v| v.as_str()) {
777 let def_name = format!("{}_card", card_type);
779
780 if let Some(card_schema_json) = defs.and_then(|d| d.get(&def_name)) {
782 let mut card_fields = HashMap::new();
784 for (k, v) in card_obj {
785 card_fields.insert(k.clone(), QuillValue::from_json(v.clone()));
786 }
787
788 let coerced_card_fields = coerce_document(
790 &QuillValue::from_json(card_schema_json.clone()),
791 &card_fields,
792 );
793
794 let mut coerced_card_obj = Map::new();
796 for (k, v) in coerced_card_fields {
797 coerced_card_obj.insert(k, v.into_json());
798 }
799
800 coerced_cards.push(Value::Object(coerced_card_obj));
801 continue;
802 }
803 }
804 }
805
806 coerced_cards.push(card.clone());
808 }
809
810 coerced_cards
811}
812
813#[cfg(test)]
814mod tests {
815 use super::*;
816 use crate::quill::FieldSchema;
817 use crate::value::QuillValue;
818
819 #[test]
820 fn test_build_schema_simple() {
821 let mut fields = HashMap::new();
822 let mut schema = FieldSchema::new(
823 "Author name".to_string(),
824 "The name of the author".to_string(),
825 );
826 schema.r#type = Some(FieldType::Str);
827 fields.insert("author".to_string(), schema);
828
829 let json_schema = build_schema_from_fields(&fields).unwrap().as_json().clone();
830 assert_eq!(json_schema["type"], "object");
831 assert_eq!(json_schema["properties"]["author"]["type"], "string");
832 assert_eq!(json_schema["properties"]["author"]["name"], "Author name");
833 assert_eq!(
834 json_schema["properties"]["author"]["description"],
835 "The name of the author"
836 );
837 }
838
839 #[test]
840 fn test_build_schema_with_default() {
841 let mut fields = HashMap::new();
842 let mut schema = FieldSchema::new(
843 "Field with default".to_string(),
844 "A field with a default value".to_string(),
845 );
846 schema.r#type = Some(FieldType::Str);
847 schema.default = Some(QuillValue::from_json(json!("default value")));
848 fields.insert("with_default".to_string(), schema);
850
851 build_schema_from_fields(&fields).unwrap();
852 }
853
854 #[test]
855 fn test_build_schema_date_types() {
856 let mut fields = HashMap::new();
857
858 let mut date_schema =
859 FieldSchema::new("Date field".to_string(), "A field for dates".to_string());
860 date_schema.r#type = Some(FieldType::Date);
861 fields.insert("date_field".to_string(), date_schema);
862
863 let mut datetime_schema = FieldSchema::new(
864 "DateTime field".to_string(),
865 "A field for date and time".to_string(),
866 );
867 datetime_schema.r#type = Some(FieldType::DateTime);
868 fields.insert("datetime_field".to_string(), datetime_schema);
869
870 let json_schema = build_schema_from_fields(&fields).unwrap().as_json().clone();
871 assert_eq!(json_schema["properties"]["date_field"]["type"], "string");
872 assert_eq!(json_schema["properties"]["date_field"]["format"], "date");
873 assert_eq!(
874 json_schema["properties"]["datetime_field"]["type"],
875 "string"
876 );
877 assert_eq!(
878 json_schema["properties"]["datetime_field"]["format"],
879 "date-time"
880 );
881 }
882
883 #[test]
884 fn test_validate_document_success() {
885 let schema = json!({
886 "$schema": "https://json-schema.org/draft/2019-09/schema",
887 "type": "object",
888 "properties": {
889 "title": {"type": "string"},
890 "count": {"type": "number"}
891 },
892 "required": ["title"],
893 "additionalProperties": true
894 });
895
896 let mut fields = HashMap::new();
897 fields.insert(
898 "title".to_string(),
899 QuillValue::from_json(json!("Test Title")),
900 );
901 fields.insert("count".to_string(), QuillValue::from_json(json!(42)));
902
903 let result = validate_document(&QuillValue::from_json(schema), &fields);
904 assert!(result.is_ok());
905 }
906
907 #[test]
908 fn test_validate_document_missing_required() {
909 let schema = json!({
910 "$schema": "https://json-schema.org/draft/2019-09/schema",
911 "type": "object",
912 "properties": {
913 "title": {"type": "string"}
914 },
915 "required": ["title"],
916 "additionalProperties": true
917 });
918
919 let fields = HashMap::new(); let result = validate_document(&QuillValue::from_json(schema), &fields);
922 assert!(result.is_err());
923 let errors = result.unwrap_err();
924 assert!(!errors.is_empty());
925 }
926
927 #[test]
928 fn test_validate_document_wrong_type() {
929 let schema = json!({
930 "$schema": "https://json-schema.org/draft/2019-09/schema",
931 "type": "object",
932 "properties": {
933 "count": {"type": "number"}
934 },
935 "additionalProperties": true
936 });
937
938 let mut fields = HashMap::new();
939 fields.insert(
940 "count".to_string(),
941 QuillValue::from_json(json!("not a number")),
942 );
943
944 let result = validate_document(&QuillValue::from_json(schema), &fields);
945 assert!(result.is_err());
946 }
947
948 #[test]
949 fn test_validate_document_allows_extra_fields() {
950 let schema = json!({
951 "$schema": "https://json-schema.org/draft/2019-09/schema",
952 "type": "object",
953 "properties": {
954 "title": {"type": "string"}
955 },
956 "required": ["title"],
957 "additionalProperties": true
958 });
959
960 let mut fields = HashMap::new();
961 fields.insert("title".to_string(), QuillValue::from_json(json!("Test")));
962 fields.insert("extra".to_string(), QuillValue::from_json(json!("allowed")));
963
964 let result = validate_document(&QuillValue::from_json(schema), &fields);
965 assert!(result.is_ok());
966 }
967
968 #[test]
969 fn test_build_schema_with_example() {
970 let mut fields = HashMap::new();
971 let mut schema = FieldSchema::new(
972 "memo_for".to_string(),
973 "List of recipient organization symbols".to_string(),
974 );
975 schema.r#type = Some(FieldType::Array);
976 schema.examples = Some(QuillValue::from_json(json!([[
977 "ORG1/SYMBOL",
978 "ORG2/SYMBOL"
979 ]])));
980 fields.insert("memo_for".to_string(), schema);
981
982 let json_schema = build_schema_from_fields(&fields).unwrap().as_json().clone();
983
984 assert!(json_schema["properties"]["memo_for"]
986 .as_object()
987 .unwrap()
988 .contains_key("examples"));
989
990 let example_value = &json_schema["properties"]["memo_for"]["examples"][0];
991 assert_eq!(example_value, &json!(["ORG1/SYMBOL", "ORG2/SYMBOL"]));
992 }
993
994 #[test]
995 fn test_build_schema_includes_default_in_properties() {
996 let mut fields = HashMap::new();
997 let mut schema = FieldSchema::new(
998 "ice_cream".to_string(),
999 "favorite ice cream flavor".to_string(),
1000 );
1001 schema.r#type = Some(FieldType::String);
1002 schema.default = Some(QuillValue::from_json(json!("taro")));
1003 fields.insert("ice_cream".to_string(), schema);
1004
1005 let json_schema = build_schema_from_fields(&fields).unwrap().as_json().clone();
1006
1007 assert!(json_schema["properties"]["ice_cream"]
1009 .as_object()
1010 .unwrap()
1011 .contains_key("default"));
1012
1013 let default_value = &json_schema["properties"]["ice_cream"]["default"];
1014 assert_eq!(default_value, &json!("taro"));
1015
1016 let required_fields = json_schema["required"].as_array().unwrap();
1018 assert!(!required_fields.contains(&json!("ice_cream")));
1019 }
1020
1021 #[test]
1022 fn test_extract_defaults_from_schema() {
1023 let schema = json!({
1025 "$schema": "https://json-schema.org/draft/2019-09/schema",
1026 "type": "object",
1027 "properties": {
1028 "title": {
1029 "type": "string",
1030 "description": "Document title"
1031 },
1032 "author": {
1033 "type": "string",
1034 "description": "Document author",
1035 "default": "Anonymous"
1036 },
1037 "status": {
1038 "type": "string",
1039 "description": "Document status",
1040 "default": "draft"
1041 },
1042 "count": {
1043 "type": "number",
1044 "default": 42
1045 }
1046 },
1047 "required": ["title"]
1048 });
1049
1050 let defaults = extract_defaults_from_schema(&QuillValue::from_json(schema));
1051
1052 assert_eq!(defaults.len(), 3);
1054 assert!(!defaults.contains_key("title")); assert!(defaults.contains_key("author"));
1056 assert!(defaults.contains_key("status"));
1057 assert!(defaults.contains_key("count"));
1058
1059 assert_eq!(defaults.get("author").unwrap().as_str(), Some("Anonymous"));
1061 assert_eq!(defaults.get("status").unwrap().as_str(), Some("draft"));
1062 assert_eq!(defaults.get("count").unwrap().as_json().as_i64(), Some(42));
1063 }
1064
1065 #[test]
1066 fn test_extract_defaults_from_schema_empty() {
1067 let schema = json!({
1069 "$schema": "https://json-schema.org/draft/2019-09/schema",
1070 "type": "object",
1071 "properties": {
1072 "title": {"type": "string"},
1073 "author": {"type": "string"}
1074 },
1075 "required": ["title"]
1076 });
1077
1078 let defaults = extract_defaults_from_schema(&QuillValue::from_json(schema));
1079 assert_eq!(defaults.len(), 0);
1080 }
1081
1082 #[test]
1083 fn test_extract_defaults_from_schema_no_properties() {
1084 let schema = json!({
1086 "$schema": "https://json-schema.org/draft/2019-09/schema",
1087 "type": "object"
1088 });
1089
1090 let defaults = extract_defaults_from_schema(&QuillValue::from_json(schema));
1091 assert_eq!(defaults.len(), 0);
1092 }
1093
1094 #[test]
1095 fn test_extract_examples_from_schema() {
1096 let schema = json!({
1098 "$schema": "https://json-schema.org/draft/2019-09/schema",
1099 "type": "object",
1100 "properties": {
1101 "title": {
1102 "type": "string",
1103 "description": "Document title"
1104 },
1105 "memo_for": {
1106 "type": "array",
1107 "description": "List of recipients",
1108 "examples": [
1109 ["ORG1/SYMBOL", "ORG2/SYMBOL"],
1110 ["DEPT/OFFICE"]
1111 ]
1112 },
1113 "author": {
1114 "type": "string",
1115 "description": "Document author",
1116 "examples": ["John Doe", "Jane Smith"]
1117 },
1118 "status": {
1119 "type": "string",
1120 "description": "Document status"
1121 }
1122 }
1123 });
1124
1125 let examples = extract_examples_from_schema(&QuillValue::from_json(schema));
1126
1127 assert_eq!(examples.len(), 2);
1129 assert!(!examples.contains_key("title")); assert!(examples.contains_key("memo_for"));
1131 assert!(examples.contains_key("author"));
1132 assert!(!examples.contains_key("status")); let memo_for_examples = examples.get("memo_for").unwrap();
1136 assert_eq!(memo_for_examples.len(), 2);
1137 assert_eq!(
1138 memo_for_examples[0].as_json(),
1139 &json!(["ORG1/SYMBOL", "ORG2/SYMBOL"])
1140 );
1141 assert_eq!(memo_for_examples[1].as_json(), &json!(["DEPT/OFFICE"]));
1142
1143 let author_examples = examples.get("author").unwrap();
1145 assert_eq!(author_examples.len(), 2);
1146 assert_eq!(author_examples[0].as_str(), Some("John Doe"));
1147 assert_eq!(author_examples[1].as_str(), Some("Jane Smith"));
1148 }
1149
1150 #[test]
1151 fn test_extract_examples_from_schema_empty() {
1152 let schema = json!({
1154 "$schema": "https://json-schema.org/draft/2019-09/schema",
1155 "type": "object",
1156 "properties": {
1157 "title": {"type": "string"},
1158 "author": {"type": "string"}
1159 }
1160 });
1161
1162 let examples = extract_examples_from_schema(&QuillValue::from_json(schema));
1163 assert_eq!(examples.len(), 0);
1164 }
1165
1166 #[test]
1167 fn test_extract_examples_from_schema_no_properties() {
1168 let schema = json!({
1170 "$schema": "https://json-schema.org/draft/2019-09/schema",
1171 "type": "object"
1172 });
1173
1174 let examples = extract_examples_from_schema(&QuillValue::from_json(schema));
1175 assert_eq!(examples.len(), 0);
1176 }
1177
1178 #[test]
1179 fn test_coerce_singular_to_array() {
1180 let schema = json!({
1181 "$schema": "https://json-schema.org/draft/2019-09/schema",
1182 "type": "object",
1183 "properties": {
1184 "tags": {"type": "array"}
1185 }
1186 });
1187
1188 let mut fields = HashMap::new();
1189 fields.insert(
1190 "tags".to_string(),
1191 QuillValue::from_json(json!("single-tag")),
1192 );
1193
1194 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
1195
1196 let tags = coerced.get("tags").unwrap();
1197 assert!(tags.as_array().is_some());
1198 let tags_array = tags.as_array().unwrap();
1199 assert_eq!(tags_array.len(), 1);
1200 assert_eq!(tags_array[0].as_str().unwrap(), "single-tag");
1201 }
1202
1203 #[test]
1204 fn test_coerce_array_unchanged() {
1205 let schema = json!({
1206 "$schema": "https://json-schema.org/draft/2019-09/schema",
1207 "type": "object",
1208 "properties": {
1209 "tags": {"type": "array"}
1210 }
1211 });
1212
1213 let mut fields = HashMap::new();
1214 fields.insert(
1215 "tags".to_string(),
1216 QuillValue::from_json(json!(["tag1", "tag2"])),
1217 );
1218
1219 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
1220
1221 let tags = coerced.get("tags").unwrap();
1222 let tags_array = tags.as_array().unwrap();
1223 assert_eq!(tags_array.len(), 2);
1224 }
1225
1226 #[test]
1227 fn test_coerce_string_to_boolean() {
1228 let schema = json!({
1229 "$schema": "https://json-schema.org/draft/2019-09/schema",
1230 "type": "object",
1231 "properties": {
1232 "active": {"type": "boolean"},
1233 "enabled": {"type": "boolean"}
1234 }
1235 });
1236
1237 let mut fields = HashMap::new();
1238 fields.insert("active".to_string(), QuillValue::from_json(json!("true")));
1239 fields.insert("enabled".to_string(), QuillValue::from_json(json!("FALSE")));
1240
1241 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
1242
1243 assert!(coerced.get("active").unwrap().as_bool().unwrap());
1244 assert!(!coerced.get("enabled").unwrap().as_bool().unwrap());
1245 }
1246
1247 #[test]
1248 fn test_coerce_number_to_boolean() {
1249 let schema = json!({
1250 "$schema": "https://json-schema.org/draft/2019-09/schema",
1251 "type": "object",
1252 "properties": {
1253 "flag1": {"type": "boolean"},
1254 "flag2": {"type": "boolean"},
1255 "flag3": {"type": "boolean"}
1256 }
1257 });
1258
1259 let mut fields = HashMap::new();
1260 fields.insert("flag1".to_string(), QuillValue::from_json(json!(0)));
1261 fields.insert("flag2".to_string(), QuillValue::from_json(json!(1)));
1262 fields.insert("flag3".to_string(), QuillValue::from_json(json!(42)));
1263
1264 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
1265
1266 assert!(!coerced.get("flag1").unwrap().as_bool().unwrap());
1267 assert!(coerced.get("flag2").unwrap().as_bool().unwrap());
1268 assert!(coerced.get("flag3").unwrap().as_bool().unwrap());
1269 }
1270
1271 #[test]
1272 fn test_coerce_float_to_boolean() {
1273 let schema = json!({
1274 "$schema": "https://json-schema.org/draft/2019-09/schema",
1275 "type": "object",
1276 "properties": {
1277 "flag1": {"type": "boolean"},
1278 "flag2": {"type": "boolean"},
1279 "flag3": {"type": "boolean"},
1280 "flag4": {"type": "boolean"}
1281 }
1282 });
1283
1284 let mut fields = HashMap::new();
1285 fields.insert("flag1".to_string(), QuillValue::from_json(json!(0.0)));
1286 fields.insert("flag2".to_string(), QuillValue::from_json(json!(0.5)));
1287 fields.insert("flag3".to_string(), QuillValue::from_json(json!(-1.5)));
1288 fields.insert("flag4".to_string(), QuillValue::from_json(json!(1e-100)));
1290
1291 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
1292
1293 assert!(!coerced.get("flag1").unwrap().as_bool().unwrap());
1294 assert!(coerced.get("flag2").unwrap().as_bool().unwrap());
1295 assert!(coerced.get("flag3").unwrap().as_bool().unwrap());
1296 assert!(!coerced.get("flag4").unwrap().as_bool().unwrap());
1298 }
1299
1300 #[test]
1301 fn test_coerce_string_to_number() {
1302 let schema = json!({
1303 "$schema": "https://json-schema.org/draft/2019-09/schema",
1304 "type": "object",
1305 "properties": {
1306 "count": {"type": "number"},
1307 "price": {"type": "number"}
1308 }
1309 });
1310
1311 let mut fields = HashMap::new();
1312 fields.insert("count".to_string(), QuillValue::from_json(json!("42")));
1313 fields.insert("price".to_string(), QuillValue::from_json(json!("19.99")));
1314
1315 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
1316
1317 assert_eq!(coerced.get("count").unwrap().as_i64().unwrap(), 42);
1318 assert_eq!(coerced.get("price").unwrap().as_f64().unwrap(), 19.99);
1319 }
1320
1321 #[test]
1322 fn test_coerce_boolean_to_number() {
1323 let schema = json!({
1324 "$schema": "https://json-schema.org/draft/2019-09/schema",
1325 "type": "object",
1326 "properties": {
1327 "active": {"type": "number"},
1328 "disabled": {"type": "number"}
1329 }
1330 });
1331
1332 let mut fields = HashMap::new();
1333 fields.insert("active".to_string(), QuillValue::from_json(json!(true)));
1334 fields.insert("disabled".to_string(), QuillValue::from_json(json!(false)));
1335
1336 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
1337
1338 assert_eq!(coerced.get("active").unwrap().as_i64().unwrap(), 1);
1339 assert_eq!(coerced.get("disabled").unwrap().as_i64().unwrap(), 0);
1340 }
1341
1342 #[test]
1343 fn test_coerce_no_schema_properties() {
1344 let schema = json!({
1345 "$schema": "https://json-schema.org/draft/2019-09/schema",
1346 "type": "object"
1347 });
1348
1349 let mut fields = HashMap::new();
1350 fields.insert("title".to_string(), QuillValue::from_json(json!("Test")));
1351
1352 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
1353
1354 assert_eq!(coerced.get("title").unwrap().as_str().unwrap(), "Test");
1356 }
1357
1358 #[test]
1359 fn test_coerce_field_without_type() {
1360 let schema = json!({
1361 "$schema": "https://json-schema.org/draft/2019-09/schema",
1362 "type": "object",
1363 "properties": {
1364 "title": {"description": "A title field"}
1365 }
1366 });
1367
1368 let mut fields = HashMap::new();
1369 fields.insert("title".to_string(), QuillValue::from_json(json!("Test")));
1370
1371 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
1372
1373 assert_eq!(coerced.get("title").unwrap().as_str().unwrap(), "Test");
1375 }
1376
1377 #[test]
1378 fn test_coerce_mixed_fields() {
1379 let schema = json!({
1380 "$schema": "https://json-schema.org/draft/2019-09/schema",
1381 "type": "object",
1382 "properties": {
1383 "tags": {"type": "array"},
1384 "active": {"type": "boolean"},
1385 "count": {"type": "number"},
1386 "title": {"type": "string"}
1387 }
1388 });
1389
1390 let mut fields = HashMap::new();
1391 fields.insert("tags".to_string(), QuillValue::from_json(json!("single")));
1392 fields.insert("active".to_string(), QuillValue::from_json(json!("true")));
1393 fields.insert("count".to_string(), QuillValue::from_json(json!("42")));
1394 fields.insert(
1395 "title".to_string(),
1396 QuillValue::from_json(json!("Test Title")),
1397 );
1398
1399 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
1400
1401 assert_eq!(coerced.get("tags").unwrap().as_array().unwrap().len(), 1);
1403 assert!(coerced.get("active").unwrap().as_bool().unwrap());
1404 assert_eq!(coerced.get("count").unwrap().as_i64().unwrap(), 42);
1405 assert_eq!(
1406 coerced.get("title").unwrap().as_str().unwrap(),
1407 "Test Title"
1408 );
1409 }
1410
1411 #[test]
1412 fn test_coerce_invalid_string_to_number() {
1413 let schema = json!({
1414 "$schema": "https://json-schema.org/draft/2019-09/schema",
1415 "type": "object",
1416 "properties": {
1417 "count": {"type": "number"}
1418 }
1419 });
1420
1421 let mut fields = HashMap::new();
1422 fields.insert(
1423 "count".to_string(),
1424 QuillValue::from_json(json!("not-a-number")),
1425 );
1426
1427 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
1428
1429 assert_eq!(
1431 coerced.get("count").unwrap().as_str().unwrap(),
1432 "not-a-number"
1433 );
1434 }
1435
1436 #[test]
1437 fn test_coerce_object_to_array() {
1438 let schema = json!({
1439 "$schema": "https://json-schema.org/draft/2019-09/schema",
1440 "type": "object",
1441 "properties": {
1442 "items": {"type": "array"}
1443 }
1444 });
1445
1446 let mut fields = HashMap::new();
1447 fields.insert(
1448 "items".to_string(),
1449 QuillValue::from_json(json!({"key": "value"})),
1450 );
1451
1452 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
1453
1454 let items = coerced.get("items").unwrap();
1456 assert!(items.as_array().is_some());
1457 let items_array = items.as_array().unwrap();
1458 assert_eq!(items_array.len(), 1);
1459 assert!(items_array[0].as_object().is_some());
1460 }
1461
1462 #[test]
1463 fn test_schema_card_in_defs() {
1464 use crate::quill::CardSchema;
1466
1467 let fields = HashMap::new();
1468 let mut cards = HashMap::new();
1469
1470 let mut name_schema = FieldSchema::new("name".to_string(), "Name field".to_string());
1471 name_schema.r#type = Some(FieldType::String);
1472
1473 let mut card_fields = HashMap::new();
1474 card_fields.insert("name".to_string(), name_schema);
1475
1476 let card = CardSchema {
1477 name: "endorsements".to_string(),
1478 title: Some("Endorsements".to_string()),
1479 description: "Chain of endorsements".to_string(),
1480 fields: card_fields,
1481 };
1482 cards.insert("endorsements".to_string(), card);
1483
1484 let json_schema = build_schema(&fields, &cards).unwrap().as_json().clone();
1485
1486 assert!(json_schema["$defs"].is_object());
1488 assert!(json_schema["$defs"]["endorsements_card"].is_object());
1489
1490 let card_def = &json_schema["$defs"]["endorsements_card"];
1492 assert_eq!(card_def["type"], "object");
1493 assert_eq!(card_def["title"], "Endorsements");
1494 assert_eq!(card_def["description"], "Chain of endorsements");
1495
1496 assert_eq!(card_def["properties"]["CARD"]["const"], "endorsements");
1498
1499 assert!(card_def["properties"]["name"].is_object());
1501 assert_eq!(card_def["properties"]["name"]["type"], "string");
1502
1503 let required = card_def["required"].as_array().unwrap();
1505 assert!(required.contains(&json!("CARD")));
1506 }
1507
1508 #[test]
1509 fn test_schema_cards_array() {
1510 use crate::quill::CardSchema;
1512
1513 let fields = HashMap::new();
1514 let mut cards = HashMap::new();
1515
1516 let mut name_schema = FieldSchema::new("name".to_string(), "Endorser name".to_string());
1517 name_schema.r#type = Some(FieldType::String);
1518 name_schema.required = true;
1519
1520 let mut org_schema = FieldSchema::new("org".to_string(), "Organization".to_string());
1521 org_schema.r#type = Some(FieldType::String);
1522 org_schema.default = Some(QuillValue::from_json(json!("Unknown")));
1523
1524 let mut card_fields = HashMap::new();
1525 card_fields.insert("name".to_string(), name_schema);
1526 card_fields.insert("org".to_string(), org_schema);
1527
1528 let card = CardSchema {
1529 name: "endorsements".to_string(),
1530 title: Some("Endorsements".to_string()),
1531 description: "Chain of endorsements".to_string(),
1532 fields: card_fields,
1533 };
1534 cards.insert("endorsements".to_string(), card);
1535
1536 let json_schema = build_schema(&fields, &cards).unwrap().as_json().clone();
1537
1538 let cards_prop = &json_schema["properties"]["CARDS"];
1540 assert_eq!(cards_prop["type"], "array");
1541
1542 let items = &cards_prop["items"];
1544 assert!(items["oneOf"].is_array());
1545 let one_of = items["oneOf"].as_array().unwrap();
1546 assert!(!one_of.is_empty());
1547 assert_eq!(one_of[0]["$ref"], "#/$defs/endorsements_card");
1548
1549 assert!(items.get("x-discriminator").is_none());
1551
1552 let card_def = &json_schema["$defs"]["endorsements_card"];
1554 assert_eq!(card_def["properties"]["name"]["type"], "string");
1555 assert_eq!(card_def["properties"]["org"]["default"], "Unknown");
1556
1557 let required = card_def["required"].as_array().unwrap();
1559 assert!(required.contains(&json!("CARD")));
1560 assert!(required.contains(&json!("name")));
1561 assert!(!required.contains(&json!("org")));
1562
1563 assert_eq!(card_def["additionalProperties"], true);
1565 }
1566
1567 #[test]
1568 fn test_extract_card_item_defaults() {
1569 let schema = json!({
1571 "$schema": "https://json-schema.org/draft/2019-09/schema",
1572 "type": "object",
1573 "properties": {
1574 "endorsements": {
1575 "type": "array",
1576 "items": {
1577 "type": "object",
1578 "properties": {
1579 "name": { "type": "string" },
1580 "org": { "type": "string", "default": "Unknown Org" },
1581 "rank": { "type": "string", "default": "N/A" }
1582 }
1583 }
1584 },
1585 "title": { "type": "string" }
1586 }
1587 });
1588
1589 let card_defaults = extract_card_item_defaults(&QuillValue::from_json(schema));
1590
1591 assert_eq!(card_defaults.len(), 1);
1593 assert!(card_defaults.contains_key("endorsements"));
1594
1595 let endorsements_defaults = card_defaults.get("endorsements").unwrap();
1596 assert_eq!(endorsements_defaults.len(), 2); assert!(!endorsements_defaults.contains_key("name")); assert_eq!(
1599 endorsements_defaults.get("org").unwrap().as_str(),
1600 Some("Unknown Org")
1601 );
1602 assert_eq!(
1603 endorsements_defaults.get("rank").unwrap().as_str(),
1604 Some("N/A")
1605 );
1606 }
1607
1608 #[test]
1609 fn test_extract_card_item_defaults_empty() {
1610 let schema = json!({
1612 "type": "object",
1613 "properties": {
1614 "title": { "type": "string" }
1615 }
1616 });
1617
1618 let card_defaults = extract_card_item_defaults(&QuillValue::from_json(schema));
1619 assert!(card_defaults.is_empty());
1620 }
1621
1622 #[test]
1623 fn test_extract_card_item_defaults_no_item_defaults() {
1624 let schema = json!({
1626 "type": "object",
1627 "properties": {
1628 "endorsements": {
1629 "type": "array",
1630 "items": {
1631 "type": "object",
1632 "properties": {
1633 "name": { "type": "string" },
1634 "org": { "type": "string" }
1635 }
1636 }
1637 }
1638 }
1639 });
1640
1641 let card_defaults = extract_card_item_defaults(&QuillValue::from_json(schema));
1642 assert!(card_defaults.is_empty()); }
1644
1645 #[test]
1646 fn test_apply_card_item_defaults() {
1647 let mut item_defaults = HashMap::new();
1649 item_defaults.insert(
1650 "org".to_string(),
1651 QuillValue::from_json(json!("Default Org")),
1652 );
1653
1654 let mut card_defaults = HashMap::new();
1655 card_defaults.insert("endorsements".to_string(), item_defaults);
1656
1657 let mut fields = HashMap::new();
1659 fields.insert(
1660 "endorsements".to_string(),
1661 QuillValue::from_json(json!([
1662 { "name": "John Doe" },
1663 { "name": "Jane Smith", "org": "Custom Org" }
1664 ])),
1665 );
1666
1667 let result = apply_card_item_defaults(&fields, &card_defaults);
1668
1669 let endorsements = result.get("endorsements").unwrap().as_array().unwrap();
1671 assert_eq!(endorsements.len(), 2);
1672
1673 assert_eq!(endorsements[0]["name"], "John Doe");
1675 assert_eq!(endorsements[0]["org"], "Default Org");
1676
1677 assert_eq!(endorsements[1]["name"], "Jane Smith");
1679 assert_eq!(endorsements[1]["org"], "Custom Org");
1680 }
1681
1682 #[test]
1683 fn test_apply_card_item_defaults_empty_card() {
1684 let mut item_defaults = HashMap::new();
1685 item_defaults.insert(
1686 "org".to_string(),
1687 QuillValue::from_json(json!("Default Org")),
1688 );
1689
1690 let mut card_defaults = HashMap::new();
1691 card_defaults.insert("endorsements".to_string(), item_defaults);
1692
1693 let mut fields = HashMap::new();
1695 fields.insert("endorsements".to_string(), QuillValue::from_json(json!([])));
1696
1697 let result = apply_card_item_defaults(&fields, &card_defaults);
1698
1699 let endorsements = result.get("endorsements").unwrap().as_array().unwrap();
1701 assert!(endorsements.is_empty());
1702 }
1703
1704 #[test]
1705 fn test_apply_card_item_defaults_no_matching_card() {
1706 let mut item_defaults = HashMap::new();
1707 item_defaults.insert(
1708 "org".to_string(),
1709 QuillValue::from_json(json!("Default Org")),
1710 );
1711
1712 let mut card_defaults = HashMap::new();
1713 card_defaults.insert("endorsements".to_string(), item_defaults);
1714
1715 let mut fields = HashMap::new();
1717 fields.insert(
1718 "reviews".to_string(),
1719 QuillValue::from_json(json!([{ "author": "Bob" }])),
1720 );
1721
1722 let result = apply_card_item_defaults(&fields, &card_defaults);
1723
1724 let reviews = result.get("reviews").unwrap().as_array().unwrap();
1726 assert_eq!(reviews.len(), 1);
1727 assert_eq!(reviews[0]["author"], "Bob");
1728 assert!(reviews[0].get("org").is_none());
1729 }
1730
1731 #[test]
1732 fn test_card_validation_with_required_fields() {
1733 let schema = json!({
1735 "$schema": "https://json-schema.org/draft/2019-09/schema",
1736 "type": "object",
1737 "properties": {
1738 "endorsements": {
1739 "type": "array",
1740 "items": {
1741 "type": "object",
1742 "properties": {
1743 "name": { "type": "string" },
1744 "org": { "type": "string", "default": "Unknown" }
1745 },
1746 "required": ["name"]
1747 }
1748 }
1749 }
1750 });
1751
1752 let mut valid_fields = HashMap::new();
1754 valid_fields.insert(
1755 "endorsements".to_string(),
1756 QuillValue::from_json(json!([{ "name": "John" }])),
1757 );
1758
1759 let result = validate_document(&QuillValue::from_json(schema.clone()), &valid_fields);
1760 assert!(result.is_ok());
1761
1762 let mut invalid_fields = HashMap::new();
1764 invalid_fields.insert(
1765 "endorsements".to_string(),
1766 QuillValue::from_json(json!([{ "org": "SomeOrg" }])),
1767 );
1768
1769 let result = validate_document(&QuillValue::from_json(schema), &invalid_fields);
1770 assert!(result.is_err());
1771 }
1772 #[test]
1773 fn test_validate_document_invalid_card_type() {
1774 use crate::quill::{CardSchema, FieldSchema};
1775
1776 let mut card_fields = HashMap::new();
1777 card_fields.insert(
1778 "field1".to_string(),
1779 FieldSchema::new("f1".to_string(), "desc".to_string()),
1780 );
1781 let mut card_schemas = HashMap::new();
1782 card_schemas.insert(
1783 "valid_card".to_string(),
1784 CardSchema {
1785 name: "valid_card".to_string(),
1786 title: None,
1787 description: "".to_string(),
1788 fields: card_fields,
1789 },
1790 );
1791
1792 let schema = build_schema(&HashMap::new(), &card_schemas).unwrap();
1793
1794 let mut fields = HashMap::new();
1795 let invalid_card = json!({
1797 "CARD": "invalid_type",
1798 "field1": "value" });
1800 fields.insert(
1801 "CARDS".to_string(),
1802 QuillValue::from_json(json!([invalid_card])),
1803 );
1804
1805 let result = validate_document(&QuillValue::from_json(schema.as_json().clone()), &fields);
1806 assert!(result.is_err());
1807 let errs = result.unwrap_err();
1808 let err_msg = &errs[0];
1810 assert!(err_msg.contains("Invalid card type 'invalid_type'"));
1811 assert!(err_msg.contains("Valid types are: [valid_card]"));
1812 }
1813
1814 #[test]
1815 fn test_coerce_document_cards() {
1816 let mut card_fields = HashMap::new();
1817 let mut count_schema = FieldSchema::new("Count".to_string(), "A number".to_string());
1818 count_schema.r#type = Some(FieldType::Number);
1819 card_fields.insert("count".to_string(), count_schema);
1820
1821 let mut active_schema = FieldSchema::new("Active".to_string(), "A boolean".to_string());
1822 active_schema.r#type = Some(FieldType::Boolean);
1823 card_fields.insert("active".to_string(), active_schema);
1824
1825 let mut card_schemas = HashMap::new();
1826 card_schemas.insert(
1827 "test_card".to_string(),
1828 CardSchema {
1829 name: "test_card".to_string(),
1830 title: None,
1831 description: "Test card".to_string(),
1832 fields: card_fields,
1833 },
1834 );
1835
1836 let schema = build_schema(&HashMap::new(), &card_schemas).unwrap();
1837
1838 let mut fields = HashMap::new();
1839 let card_value = json!({
1840 "CARD": "test_card",
1841 "count": "42",
1842 "active": "true"
1843 });
1844 fields.insert(
1845 "CARDS".to_string(),
1846 QuillValue::from_json(json!([card_value])),
1847 );
1848
1849 let coerced_fields = coerce_document(&schema, &fields);
1850
1851 let cards_array = coerced_fields.get("CARDS").unwrap().as_array().unwrap();
1852 let coerced_card = cards_array[0].as_object().unwrap();
1853
1854 assert_eq!(coerced_card.get("count").unwrap().as_i64(), Some(42));
1855 assert_eq!(coerced_card.get("active").unwrap().as_bool(), Some(true));
1856 }
1857
1858 #[test]
1859 fn test_validate_document_card_fields() {
1860 let mut card_fields = HashMap::new();
1861 let mut count_schema = FieldSchema::new("Count".to_string(), "A number".to_string());
1862 count_schema.r#type = Some(FieldType::Number);
1863 card_fields.insert("count".to_string(), count_schema);
1864
1865 let mut card_schemas = HashMap::new();
1866 card_schemas.insert(
1867 "test_card".to_string(),
1868 CardSchema {
1869 name: "test_card".to_string(),
1870 title: None,
1871 description: "Test card".to_string(),
1872 fields: card_fields,
1873 },
1874 );
1875
1876 let schema = build_schema(&HashMap::new(), &card_schemas).unwrap();
1877
1878 let mut fields = HashMap::new();
1879 let card_value = json!({
1880 "CARD": "test_card",
1881 "count": "not a number" });
1883 fields.insert(
1884 "CARDS".to_string(),
1885 QuillValue::from_json(json!([card_value])),
1886 );
1887
1888 let result = validate_document(&QuillValue::from_json(schema.as_json().clone()), &fields);
1889 assert!(result.is_err());
1890 let errs = result.unwrap_err();
1891
1892 let found_specific_error = errs
1894 .iter()
1895 .any(|e| e.contains("/CARDS/0") && e.contains("not a number") && !e.contains("oneOf"));
1896
1897 assert!(
1898 found_specific_error,
1899 "Did not find specific error msg in: {:?}",
1900 errs
1901 );
1902 }
1903
1904 #[test]
1905 fn test_card_field_ui_metadata() {
1906 use crate::quill::{CardSchema, UiSchema};
1908
1909 let mut field_schema = FieldSchema::new("from".to_string(), "Sender".to_string());
1910 field_schema.r#type = Some(FieldType::String);
1911 field_schema.ui = Some(UiSchema {
1912 group: Some("Header".to_string()),
1913 order: Some(0),
1914 });
1915
1916 let mut card_fields = HashMap::new();
1917 card_fields.insert("from".to_string(), field_schema);
1918
1919 let card = CardSchema {
1920 name: "indorsement".to_string(),
1921 title: Some("Indorsement".to_string()),
1922 description: "An indorsement".to_string(),
1923 fields: card_fields,
1924 };
1925
1926 let mut cards = HashMap::new();
1927 cards.insert("indorsement".to_string(), card);
1928
1929 let schema = build_schema(&HashMap::new(), &cards).unwrap();
1930 let card_def = &schema.as_json()["$defs"]["indorsement_card"];
1931 let from_field = &card_def["properties"]["from"];
1932
1933 assert_eq!(from_field["x-ui"]["group"], "Header");
1934 assert_eq!(from_field["x-ui"]["order"], 0);
1935 }
1936}