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 let (json_type, format, content_media_type) = match field_schema.r#type {
17 FieldType::String => ("string", None, None),
18 FieldType::Number => ("number", None, None),
19 FieldType::Boolean => ("boolean", None, None),
20 FieldType::Array => ("array", None, None),
21 FieldType::Object => ("object", None, None),
22 FieldType::Date => ("string", Some("date"), None),
23 FieldType::DateTime => ("string", Some("date-time"), None),
24 FieldType::Markdown => ("string", None, Some("text/markdown")),
25 };
26 property.insert(
27 field_key::TYPE.to_string(),
28 Value::String(json_type.to_string()),
29 );
30
31 if let Some(fmt) = format {
33 property.insert(
34 field_key::FORMAT.to_string(),
35 Value::String(fmt.to_string()),
36 );
37 }
38
39 if let Some(media_type) = content_media_type {
41 property.insert(
42 "contentMediaType".to_string(),
43 Value::String(media_type.to_string()),
44 );
45 }
46
47 if let Some(ref title) = field_schema.title {
49 property.insert(field_key::TITLE.to_string(), Value::String(title.clone()));
50 }
51
52 if let Some(ref description) = field_schema.description {
54 property.insert(
55 field_key::DESCRIPTION.to_string(),
56 Value::String(description.clone()),
57 );
58 }
59
60 if let Some(ref ui) = field_schema.ui {
62 let mut ui_obj = Map::new();
63
64 if let Some(ref group) = ui.group {
65 ui_obj.insert(ui_key::GROUP.to_string(), json!(group));
66 }
67
68 if let Some(order) = ui.order {
69 ui_obj.insert(ui_key::ORDER.to_string(), json!(order));
70 }
71
72 if !ui_obj.is_empty() {
73 property.insert("x-ui".to_string(), Value::Object(ui_obj));
74 }
75 }
76
77 if let Some(ref examples) = field_schema.examples {
79 if let Some(examples_array) = examples.as_array() {
80 if !examples_array.is_empty() {
81 property.insert(
82 field_key::EXAMPLES.to_string(),
83 Value::Array(examples_array.clone()),
84 );
85 }
86 }
87 }
88
89 if let Some(ref default) = field_schema.default {
91 property.insert(field_key::DEFAULT.to_string(), default.as_json().clone());
92 }
93
94 if let Some(ref enum_values) = field_schema.enum_values {
96 let enum_array: Vec<Value> = enum_values
97 .iter()
98 .map(|s| Value::String(s.clone()))
99 .collect();
100 property.insert(field_key::ENUM.to_string(), Value::Array(enum_array));
101 }
102
103 if let Some(ref properties) = field_schema.properties {
105 let mut props_map = Map::new();
106 let mut required_fields = Vec::new();
107
108 for (prop_name, prop_schema) in properties {
109 props_map.insert(
110 prop_name.clone(),
111 Value::Object(build_field_property(prop_schema)),
112 );
113
114 if prop_schema.required {
115 required_fields.push(Value::String(prop_name.clone()));
116 }
117 }
118
119 property.insert("properties".to_string(), Value::Object(props_map));
120
121 if !required_fields.is_empty() {
122 property.insert("required".to_string(), Value::Array(required_fields));
123 }
124 }
125
126 if let Some(ref items) = field_schema.items {
128 property.insert(
129 "items".to_string(),
130 Value::Object(build_field_property(items)),
131 );
132 }
133
134 property
135}
136
137fn build_card_def(name: &str, card: &CardSchema) -> Map<String, Value> {
139 let mut def = Map::new();
140
141 def.insert("type".to_string(), Value::String("object".to_string()));
142
143 if let Some(ref title) = card.title {
145 def.insert("title".to_string(), Value::String(title.clone()));
146 }
147
148 if let Some(ref description) = card.description {
150 if !description.is_empty() {
151 def.insert(
152 "description".to_string(),
153 Value::String(description.clone()),
154 );
155 }
156 }
157
158 if let Some(ref ui) = card.ui {
160 let mut ui_obj = Map::new();
161 if let Some(hide_body) = ui.hide_body {
162 ui_obj.insert(ui_key::HIDE_BODY.to_string(), Value::Bool(hide_body));
163 }
164 if !ui_obj.is_empty() {
165 def.insert("x-ui".to_string(), Value::Object(ui_obj));
166 }
167 }
168
169 let mut properties = Map::new();
171 let mut required = vec![Value::String("CARD".to_string())];
172
173 let mut card_prop = Map::new();
175 card_prop.insert("const".to_string(), Value::String(name.to_string()));
176 properties.insert("CARD".to_string(), Value::Object(card_prop));
177
178 for (field_name, field_schema) in &card.fields {
180 let field_prop = build_field_property(field_schema);
181 properties.insert(field_name.clone(), Value::Object(field_prop));
182
183 if field_schema.required {
184 required.push(Value::String(field_name.clone()));
185 }
186 }
187
188 def.insert("properties".to_string(), Value::Object(properties));
189 def.insert("required".to_string(), Value::Array(required));
190
191 def
192}
193
194pub fn build_schema(
201 document: &CardSchema,
202 definitions: &HashMap<String, CardSchema>,
203) -> Result<QuillValue, RenderError> {
204 let mut properties = Map::new();
205 let mut required_fields = Vec::new();
206 let mut defs = Map::new();
207
208 for (field_name, field_schema) in &document.fields {
210 let property = build_field_property(field_schema);
211 properties.insert(field_name.clone(), Value::Object(property));
212
213 if field_schema.required {
214 required_fields.push(field_name.clone());
215 }
216 }
217
218 if !properties.contains_key("BODY") {
220 let mut body_property = Map::new();
221 body_property.insert("type".to_string(), Value::String("string".to_string()));
222 body_property.insert(
223 "contentMediaType".to_string(),
224 Value::String("text/markdown".to_string()),
225 );
226 properties.insert("BODY".to_string(), Value::Object(body_property));
227 }
228
229 if !definitions.is_empty() {
231 let mut one_of = Vec::new();
232 let mut discriminator_mapping = Map::new();
233
234 for (card_name, card_schema) in definitions {
235 let def_name = format!("{}_card", card_name);
236 let ref_path = format!("#/$defs/{}", def_name);
237
238 defs.insert(
240 def_name.clone(),
241 Value::Object(build_card_def(card_name, card_schema)),
242 );
243
244 let mut ref_obj = Map::new();
246 ref_obj.insert("$ref".to_string(), Value::String(ref_path.clone()));
247 one_of.push(Value::Object(ref_obj));
248
249 discriminator_mapping.insert(card_name.clone(), Value::String(ref_path));
251 }
252
253 let mut items_schema = Map::new();
255 items_schema.insert("oneOf".to_string(), Value::Array(one_of));
256
257 let mut cards_property = Map::new();
260 cards_property.insert("type".to_string(), Value::String("array".to_string()));
261 cards_property.insert("items".to_string(), Value::Object(items_schema));
262
263 properties.insert("CARDS".to_string(), Value::Object(cards_property));
264 }
265
266 let mut schema_map = Map::new();
268 schema_map.insert(
269 "$schema".to_string(),
270 Value::String("https://json-schema.org/draft/2019-09/schema".to_string()),
271 );
272 schema_map.insert("type".to_string(), Value::String("object".to_string()));
273
274 if !defs.is_empty() {
276 schema_map.insert("$defs".to_string(), Value::Object(defs));
277 }
278
279 if let Some(ref description) = document.description {
281 if !description.is_empty() {
282 schema_map.insert(
283 "description".to_string(),
284 Value::String(description.clone()),
285 );
286 }
287 }
288
289 if let Some(ref ui) = document.ui {
291 let mut ui_obj = Map::new();
292 if let Some(hide_body) = ui.hide_body {
293 ui_obj.insert(ui_key::HIDE_BODY.to_string(), Value::Bool(hide_body));
294 }
295 if !ui_obj.is_empty() {
296 schema_map.insert("x-ui".to_string(), Value::Object(ui_obj));
297 }
298 }
299
300 schema_map.insert("properties".to_string(), Value::Object(properties));
301 schema_map.insert(
302 "required".to_string(),
303 Value::Array(required_fields.into_iter().map(Value::String).collect()),
304 );
305
306 let schema = Value::Object(schema_map);
310
311 Ok(QuillValue::from_json(schema))
312}
313
314pub fn strip_schema_fields(schema: &mut Value, fields: &[&str]) {
325 match schema {
326 Value::Object(map) => {
327 for field in fields {
329 map.remove(*field);
330 }
331
332 for value in map.values_mut() {
334 strip_schema_fields(value, fields);
335 }
336 }
337 Value::Array(arr) => {
338 for item in arr {
340 strip_schema_fields(item, fields);
341 }
342 }
343 _ => {}
344 }
345}
346
347pub fn build_schema_from_fields(
349 field_schemas: &HashMap<String, FieldSchema>,
350) -> Result<QuillValue, RenderError> {
351 let document = CardSchema {
352 name: "root".to_string(),
353 title: None,
354 description: None,
355 fields: field_schemas.clone(),
356 ui: None,
357 };
358 build_schema(&document, &HashMap::new())
359}
360
361pub fn extract_defaults_from_schema(
375 schema: &QuillValue,
376) -> HashMap<String, crate::value::QuillValue> {
377 let mut defaults = HashMap::new();
378
379 if let Some(properties) = schema.as_json().get("properties") {
381 if let Some(properties_obj) = properties.as_object() {
382 for (field_name, field_schema) in properties_obj {
383 if let Some(default_value) = field_schema.get("default") {
385 defaults.insert(
386 field_name.clone(),
387 QuillValue::from_json(default_value.clone()),
388 );
389 }
390 }
391 }
392 }
393
394 defaults
395}
396
397pub fn extract_examples_from_schema(
411 schema: &QuillValue,
412) -> HashMap<String, Vec<crate::value::QuillValue>> {
413 let mut examples = HashMap::new();
414
415 if let Some(properties) = schema.as_json().get("properties") {
417 if let Some(properties_obj) = properties.as_object() {
418 for (field_name, field_schema) in properties_obj {
419 if let Some(examples_value) = field_schema.get("examples") {
421 if let Some(examples_array) = examples_value.as_array() {
422 let examples_vec: Vec<QuillValue> = examples_array
423 .iter()
424 .map(|v| QuillValue::from_json(v.clone()))
425 .collect();
426 if !examples_vec.is_empty() {
427 examples.insert(field_name.clone(), examples_vec);
428 }
429 }
430 }
431 }
432 }
433 }
434
435 examples
436}
437
438pub fn extract_card_item_defaults(
452 schema: &QuillValue,
453) -> HashMap<String, HashMap<String, QuillValue>> {
454 let mut card_defaults = HashMap::new();
455
456 if let Some(properties) = schema.as_json().get("properties") {
458 if let Some(properties_obj) = properties.as_object() {
459 for (field_name, field_schema) in properties_obj {
460 let is_array = field_schema
462 .get("type")
463 .and_then(|t| t.as_str())
464 .map(|t| t == "array")
465 .unwrap_or(false);
466
467 if !is_array {
468 continue;
469 }
470
471 if let Some(items_schema) = field_schema.get("items") {
473 if let Some(item_props) = items_schema.get("properties") {
475 if let Some(item_props_obj) = item_props.as_object() {
476 let mut item_defaults = HashMap::new();
477
478 for (item_field_name, item_field_schema) in item_props_obj {
479 if let Some(default_value) = item_field_schema.get("default") {
481 item_defaults.insert(
482 item_field_name.clone(),
483 QuillValue::from_json(default_value.clone()),
484 );
485 }
486 }
487
488 if !item_defaults.is_empty() {
489 card_defaults.insert(field_name.clone(), item_defaults);
490 }
491 }
492 }
493 }
494 }
495 }
496 }
497
498 card_defaults
499}
500
501pub fn apply_card_item_defaults(
515 fields: &HashMap<String, QuillValue>,
516 card_defaults: &HashMap<String, HashMap<String, QuillValue>>,
517) -> HashMap<String, QuillValue> {
518 let mut result = fields.clone();
519
520 for (card_name, item_defaults) in card_defaults {
521 if let Some(card_value) = result.get(card_name) {
522 if let Some(items_array) = card_value.as_array() {
524 let mut updated_items: Vec<serde_json::Value> = Vec::new();
525
526 for item in items_array {
527 if let Some(item_obj) = item.as_object() {
529 let mut new_item = item_obj.clone();
530
531 for (default_field, default_value) in item_defaults {
533 if !new_item.contains_key(default_field) {
534 new_item
535 .insert(default_field.clone(), default_value.as_json().clone());
536 }
537 }
538
539 updated_items.push(serde_json::Value::Object(new_item));
540 } else {
541 updated_items.push(item.clone());
543 }
544 }
545
546 result.insert(
547 card_name.clone(),
548 QuillValue::from_json(serde_json::Value::Array(updated_items)),
549 );
550 }
551 }
552 }
553
554 result
555}
556
557pub fn validate_document(
559 schema: &QuillValue,
560 fields: &HashMap<String, crate::value::QuillValue>,
561) -> Result<(), Vec<String>> {
562 let mut doc_json = Map::new();
564 for (key, value) in fields {
565 doc_json.insert(key.clone(), value.as_json().clone());
566 }
567 let doc_value = Value::Object(doc_json);
568
569 let compiled = match jsonschema::Validator::new(schema.as_json()) {
571 Ok(c) => c,
572 Err(e) => return Err(vec![format!("Failed to compile schema: {}", e)]),
573 };
574
575 let mut all_errors = Vec::new();
577
578 if let Some(cards) = doc_value.get("CARDS").and_then(|v| v.as_array()) {
580 let card_errors = validate_cards_array(schema, cards);
581 all_errors.extend(card_errors);
582 }
583
584 let validation_result = compiled.validate(&doc_value);
586
587 match validation_result {
588 Ok(_) => {
589 if all_errors.is_empty() {
590 Ok(())
591 } else {
592 Err(all_errors)
593 }
594 }
595 Err(error) => {
596 let path = error.instance_path().to_string();
597 let path_display = if path.is_empty() {
598 "document".to_string()
599 } else {
600 path.clone()
601 };
602
603 let is_generic_card_error = path.starts_with("/CARDS/")
607 && error.to_string().contains("oneOf")
608 && !all_errors.is_empty();
609
610 if !is_generic_card_error {
611 if path.starts_with("/CARDS/") && error.to_string().contains("oneOf") {
613 if let Some(rest) = path.strip_prefix("/CARDS/") {
615 let is_item_error = !rest.contains('/');
618
619 if is_item_error {
620 if let Ok(idx) = rest.parse::<usize>() {
621 if let Some(cards) =
622 doc_value.get("CARDS").and_then(|v| v.as_array())
623 {
624 if let Some(item) = cards.get(idx) {
625 if let Some(card_type) =
627 item.get("CARD").and_then(|v| v.as_str())
628 {
629 let mut valid_types = Vec::new();
631 if let Some(defs) = schema
632 .as_json()
633 .get("$defs")
634 .and_then(|v| v.as_object())
635 {
636 for key in defs.keys() {
637 if let Some(name) = key.strip_suffix("_card") {
638 valid_types.push(name.to_string());
639 }
640 }
641 }
642
643 if !valid_types.is_empty()
645 && !valid_types.contains(&card_type.to_string())
646 {
647 valid_types.sort();
648 let valid_list = valid_types.join(", ");
649 let message = format!("Validation error at {}: Invalid card type '{}'. Valid types are: [{}]", path_display, card_type, valid_list);
650 all_errors.push(message);
651 return Err(all_errors);
652 }
653 }
654 }
655 }
656 }
657 }
658 }
659 }
660
661 let message = format!("Validation error at {}: {}", path_display, error);
662 all_errors.push(message);
663 }
664
665 Err(all_errors)
666 }
667 }
668}
669
670fn validate_cards_array(document_schema: &QuillValue, cards_array: &[Value]) -> Vec<String> {
672 let mut errors = Vec::new();
673
674 let defs = document_schema
676 .as_json()
677 .get("$defs")
678 .and_then(|v| v.as_object());
679
680 for (idx, card) in cards_array.iter().enumerate() {
681 if let Some(card_obj) = card.as_object() {
683 if let Some(card_type) = card_obj.get("CARD").and_then(|v| v.as_str()) {
684 let def_name = format!("{}_card", card_type);
686
687 if let Some(card_schema_json) = defs.and_then(|d| d.get(&def_name)) {
689 let mut card_fields = HashMap::new();
691 for (k, v) in card_obj {
692 card_fields.insert(k.clone(), QuillValue::from_json(v.clone()));
693 }
694
695 if let Err(card_errors) = validate_document(
697 &QuillValue::from_json(card_schema_json.clone()),
698 &card_fields,
699 ) {
700 for err in card_errors {
702 let prefix = format!("/CARDS/{}", idx);
708 let new_msg =
709 if let Some(rest) = err.strip_prefix("Validation error at ") {
710 if rest.starts_with("document") {
711 format!(
713 "Validation error at {}:{}",
714 prefix,
715 rest.strip_prefix("document").unwrap_or(rest)
716 )
717 } else {
718 format!("Validation error at {}{}", prefix, rest)
720 }
721 } else {
722 format!("Validation error at {}: {}", prefix, err)
723 };
724
725 errors.push(new_msg);
726 }
727 }
728 }
729 }
730 }
731 }
732
733 errors
734}
735
736fn coerce_value(value: &QuillValue, expected_type: &str) -> QuillValue {
745 let json_value = value.as_json();
746
747 match expected_type {
748 "array" => {
749 if json_value.is_array() {
751 return value.clone();
752 }
753 QuillValue::from_json(Value::Array(vec![json_value.clone()]))
755 }
756 "boolean" => {
757 if let Some(b) = json_value.as_bool() {
759 return QuillValue::from_json(Value::Bool(b));
760 }
761 if let Some(s) = json_value.as_str() {
763 let lower = s.to_lowercase();
764 if lower == "true" {
765 return QuillValue::from_json(Value::Bool(true));
766 } else if lower == "false" {
767 return QuillValue::from_json(Value::Bool(false));
768 }
769 }
770 if let Some(n) = json_value.as_i64() {
772 return QuillValue::from_json(Value::Bool(n != 0));
773 }
774 if let Some(n) = json_value.as_f64() {
775 if n.is_nan() {
777 return QuillValue::from_json(Value::Bool(false));
778 }
779 return QuillValue::from_json(Value::Bool(n.abs() > f64::EPSILON));
780 }
781 value.clone()
783 }
784 "number" => {
785 if json_value.is_number() {
787 return value.clone();
788 }
789 if let Some(s) = json_value.as_str() {
791 if let Ok(i) = s.parse::<i64>() {
793 return QuillValue::from_json(serde_json::Number::from(i).into());
794 }
795 if let Ok(f) = s.parse::<f64>() {
797 if let Some(num) = serde_json::Number::from_f64(f) {
798 return QuillValue::from_json(num.into());
799 }
800 }
801 }
802 if let Some(b) = json_value.as_bool() {
804 let num_value = if b { 1 } else { 0 };
805 return QuillValue::from_json(Value::Number(serde_json::Number::from(num_value)));
806 }
807 value.clone()
809 }
810 "string" => {
811 if json_value.is_string() {
813 return value.clone();
814 }
815 if let Some(arr) = json_value.as_array() {
817 if arr.len() == 1 {
818 if let Some(s) = arr[0].as_str() {
819 return QuillValue::from_json(Value::String(s.to_string()));
820 }
821 }
822 }
823 value.clone()
825 }
826 _ => {
827 value.clone()
829 }
830 }
831}
832
833pub fn coerce_document(
847 schema: &QuillValue,
848 fields: &HashMap<String, QuillValue>,
849) -> HashMap<String, QuillValue> {
850 let mut coerced_fields = HashMap::new();
851
852 let properties = match schema.as_json().get("properties") {
854 Some(props) => props,
855 None => {
856 return fields.clone();
858 }
859 };
860
861 let properties_obj = match properties.as_object() {
862 Some(obj) => obj,
863 None => {
864 return fields.clone();
866 }
867 };
868
869 for (field_name, field_value) in fields {
871 if let Some(field_schema) = properties_obj.get(field_name) {
873 if let Some(expected_type) = field_schema.get("type").and_then(|t| t.as_str()) {
875 let coerced_value = coerce_value(field_value, expected_type);
877 coerced_fields.insert(field_name.clone(), coerced_value);
878 continue;
879 }
880 }
881 coerced_fields.insert(field_name.clone(), field_value.clone());
883 }
884
885 if let Some(cards_value) = coerced_fields.get("CARDS") {
887 if let Some(cards_array) = cards_value.as_array() {
888 let coerced_cards = coerce_cards_array(schema, cards_array);
889 coerced_fields.insert(
890 "CARDS".to_string(),
891 QuillValue::from_json(Value::Array(coerced_cards)),
892 );
893 }
894 }
895
896 coerced_fields
897}
898
899fn coerce_cards_array(document_schema: &QuillValue, cards_array: &[Value]) -> Vec<Value> {
901 let mut coerced_cards = Vec::new();
902
903 let defs = document_schema
905 .as_json()
906 .get("$defs")
907 .and_then(|v| v.as_object());
908
909 for card in cards_array {
910 if let Some(card_obj) = card.as_object() {
912 if let Some(card_type) = card_obj.get("CARD").and_then(|v| v.as_str()) {
913 let def_name = format!("{}_card", card_type);
915
916 if let Some(card_schema_json) = defs.and_then(|d| d.get(&def_name)) {
918 let mut card_fields = HashMap::new();
920 for (k, v) in card_obj {
921 card_fields.insert(k.clone(), QuillValue::from_json(v.clone()));
922 }
923
924 let coerced_card_fields = coerce_document(
926 &QuillValue::from_json(card_schema_json.clone()),
927 &card_fields,
928 );
929
930 let mut coerced_card_obj = Map::new();
932 for (k, v) in coerced_card_fields {
933 coerced_card_obj.insert(k, v.into_json());
934 }
935
936 coerced_cards.push(Value::Object(coerced_card_obj));
937 continue;
938 }
939 }
940 }
941
942 coerced_cards.push(card.clone());
944 }
945
946 coerced_cards
947}
948
949#[cfg(test)]
950mod tests {
951 use super::*;
952 use crate::quill::FieldSchema;
953 use crate::value::QuillValue;
954
955 #[test]
956 fn test_build_schema_simple() {
957 let mut fields = HashMap::new();
958 let schema = FieldSchema::new(
959 "author".to_string(),
960 FieldType::String,
961 Some("The name of the author".to_string()),
962 );
963 fields.insert("author".to_string(), schema);
964
965 let json_schema = build_schema_from_fields(&fields).unwrap().as_json().clone();
966 assert_eq!(json_schema["type"], "object");
967 assert_eq!(json_schema["properties"]["author"]["type"], "string");
968 assert_eq!(
969 json_schema["properties"]["author"]["description"],
970 "The name of the author"
971 );
972 }
973
974 #[test]
975 fn test_build_schema_with_default() {
976 let mut fields = HashMap::new();
977 let mut schema = FieldSchema::new(
978 "Field with default".to_string(),
979 FieldType::String,
980 Some("A field with a default value".to_string()),
981 );
982 schema.default = Some(QuillValue::from_json(json!("default value")));
983 fields.insert("with_default".to_string(), schema);
985
986 build_schema_from_fields(&fields).unwrap();
987 }
988
989 #[test]
990 fn test_build_schema_date_types() {
991 let mut fields = HashMap::new();
992
993 let date_schema = FieldSchema::new(
994 "Date field".to_string(),
995 FieldType::Date,
996 Some("A field for dates".to_string()),
997 );
998 fields.insert("date_field".to_string(), date_schema);
999
1000 let datetime_schema = FieldSchema::new(
1001 "DateTime field".to_string(),
1002 FieldType::DateTime,
1003 Some("A field for date and time".to_string()),
1004 );
1005 fields.insert("datetime_field".to_string(), datetime_schema);
1006
1007 let json_schema = build_schema_from_fields(&fields).unwrap().as_json().clone();
1008 assert_eq!(json_schema["properties"]["date_field"]["type"], "string");
1009 assert_eq!(json_schema["properties"]["date_field"]["format"], "date");
1010 assert_eq!(
1011 json_schema["properties"]["datetime_field"]["type"],
1012 "string"
1013 );
1014 assert_eq!(
1015 json_schema["properties"]["datetime_field"]["format"],
1016 "date-time"
1017 );
1018 }
1019
1020 #[test]
1021 fn test_validate_document_success() {
1022 let schema = json!({
1023 "$schema": "https://json-schema.org/draft/2019-09/schema",
1024 "type": "object",
1025 "properties": {
1026 "title": {"type": "string"},
1027 "count": {"type": "number"}
1028 },
1029 "required": ["title"],
1030 "additionalProperties": true
1031 });
1032
1033 let mut fields = HashMap::new();
1034 fields.insert(
1035 "title".to_string(),
1036 QuillValue::from_json(json!("Test Title")),
1037 );
1038 fields.insert("count".to_string(), QuillValue::from_json(json!(42)));
1039
1040 let result = validate_document(&QuillValue::from_json(schema), &fields);
1041 assert!(result.is_ok());
1042 }
1043
1044 #[test]
1045 fn test_validate_document_missing_required() {
1046 let schema = json!({
1047 "$schema": "https://json-schema.org/draft/2019-09/schema",
1048 "type": "object",
1049 "properties": {
1050 "title": {"type": "string"}
1051 },
1052 "required": ["title"],
1053 "additionalProperties": true
1054 });
1055
1056 let fields = HashMap::new(); let result = validate_document(&QuillValue::from_json(schema), &fields);
1059 assert!(result.is_err());
1060 let errors = result.unwrap_err();
1061 assert!(!errors.is_empty());
1062 }
1063
1064 #[test]
1065 fn test_validate_document_wrong_type() {
1066 let schema = json!({
1067 "$schema": "https://json-schema.org/draft/2019-09/schema",
1068 "type": "object",
1069 "properties": {
1070 "count": {"type": "number"}
1071 },
1072 "additionalProperties": true
1073 });
1074
1075 let mut fields = HashMap::new();
1076 fields.insert(
1077 "count".to_string(),
1078 QuillValue::from_json(json!("not a number")),
1079 );
1080
1081 let result = validate_document(&QuillValue::from_json(schema), &fields);
1082 assert!(result.is_err());
1083 }
1084
1085 #[test]
1086 fn test_validate_document_allows_extra_fields() {
1087 let schema = json!({
1088 "$schema": "https://json-schema.org/draft/2019-09/schema",
1089 "type": "object",
1090 "properties": {
1091 "title": {"type": "string"}
1092 },
1093 "required": ["title"],
1094 "additionalProperties": true
1095 });
1096
1097 let mut fields = HashMap::new();
1098 fields.insert("title".to_string(), QuillValue::from_json(json!("Test")));
1099 fields.insert("extra".to_string(), QuillValue::from_json(json!("allowed")));
1100
1101 let result = validate_document(&QuillValue::from_json(schema), &fields);
1102 assert!(result.is_ok());
1103 }
1104
1105 #[test]
1106 fn test_build_schema_with_example() {
1107 let mut fields = HashMap::new();
1108 let mut schema = FieldSchema::new(
1109 "memo_for".to_string(),
1110 FieldType::Array,
1111 Some("List of recipient organization symbols".to_string()),
1112 );
1113 schema.examples = Some(QuillValue::from_json(json!([[
1114 "ORG1/SYMBOL",
1115 "ORG2/SYMBOL"
1116 ]])));
1117 fields.insert("memo_for".to_string(), schema);
1118
1119 let json_schema = build_schema_from_fields(&fields).unwrap().as_json().clone();
1120
1121 assert!(json_schema["properties"]["memo_for"]
1123 .as_object()
1124 .unwrap()
1125 .contains_key("examples"));
1126
1127 let example_value = &json_schema["properties"]["memo_for"]["examples"][0];
1128 assert_eq!(example_value, &json!(["ORG1/SYMBOL", "ORG2/SYMBOL"]));
1129 }
1130
1131 #[test]
1132 fn test_build_schema_includes_default_in_properties() {
1133 let mut fields = HashMap::new();
1134 let mut schema = FieldSchema::new(
1135 "ice_cream".to_string(),
1136 FieldType::String,
1137 Some("favorite ice cream flavor".to_string()),
1138 );
1139 schema.default = Some(QuillValue::from_json(json!("taro")));
1140 fields.insert("ice_cream".to_string(), schema);
1141
1142 let json_schema = build_schema_from_fields(&fields).unwrap().as_json().clone();
1143
1144 assert!(json_schema["properties"]["ice_cream"]
1146 .as_object()
1147 .unwrap()
1148 .contains_key("default"));
1149
1150 assert_eq!(json_schema["properties"]["ice_cream"]["default"], "taro");
1152
1153 let required_fields = json_schema["required"].as_array().unwrap();
1155 assert!(!required_fields.contains(&json!("ice_cream")));
1156 }
1157
1158 #[test]
1159 fn test_extract_defaults_from_schema() {
1160 let schema = json!({
1162 "$schema": "https://json-schema.org/draft/2019-09/schema",
1163 "type": "object",
1164 "properties": {
1165 "title": {
1166 "type": "string",
1167 "description": "Document title"
1168 },
1169 "author": {
1170 "type": "string",
1171 "description": "Document author",
1172 "default": "Anonymous"
1173 },
1174 "status": {
1175 "type": "string",
1176 "description": "Document status",
1177 "default": "draft"
1178 },
1179 "count": {
1180 "type": "number",
1181 "default": 42
1182 }
1183 },
1184 "required": ["title"]
1185 });
1186
1187 let defaults = extract_defaults_from_schema(&QuillValue::from_json(schema));
1188
1189 assert_eq!(defaults.len(), 3);
1191 assert!(!defaults.contains_key("title")); assert!(defaults.contains_key("author"));
1193 assert!(defaults.contains_key("status"));
1194 assert!(defaults.contains_key("count"));
1195
1196 assert_eq!(defaults.get("author").unwrap().as_str(), Some("Anonymous"));
1198 assert_eq!(defaults.get("status").unwrap().as_str(), Some("draft"));
1199 assert_eq!(defaults.get("count").unwrap().as_json().as_i64(), Some(42));
1200 }
1201
1202 #[test]
1203 fn test_extract_defaults_from_schema_empty() {
1204 let schema = json!({
1206 "$schema": "https://json-schema.org/draft/2019-09/schema",
1207 "type": "object",
1208 "properties": {
1209 "title": {"type": "string"},
1210 "author": {"type": "string"}
1211 },
1212 "required": ["title"]
1213 });
1214
1215 let defaults = extract_defaults_from_schema(&QuillValue::from_json(schema));
1216 assert_eq!(defaults.len(), 0);
1217 }
1218
1219 #[test]
1220 fn test_extract_defaults_from_schema_no_properties() {
1221 let schema = json!({
1223 "$schema": "https://json-schema.org/draft/2019-09/schema",
1224 "type": "object"
1225 });
1226
1227 let defaults = extract_defaults_from_schema(&QuillValue::from_json(schema));
1228 assert_eq!(defaults.len(), 0);
1229 }
1230
1231 #[test]
1232 fn test_extract_examples_from_schema() {
1233 let schema = json!({
1235 "$schema": "https://json-schema.org/draft/2019-09/schema",
1236 "type": "object",
1237 "properties": {
1238 "title": {
1239 "type": "string",
1240 "description": "Document title"
1241 },
1242 "memo_for": {
1243 "type": "array",
1244 "description": "List of recipients",
1245 "examples": [
1246 ["ORG1/SYMBOL", "ORG2/SYMBOL"],
1247 ["DEPT/OFFICE"]
1248 ]
1249 },
1250 "author": {
1251 "type": "string",
1252 "description": "Document author",
1253 "examples": ["John Doe", "Jane Smith"]
1254 },
1255 "status": {
1256 "type": "string",
1257 "description": "Document status"
1258 }
1259 }
1260 });
1261
1262 let examples = extract_examples_from_schema(&QuillValue::from_json(schema));
1263
1264 assert_eq!(examples.len(), 2);
1266 assert!(!examples.contains_key("title")); assert!(examples.contains_key("memo_for"));
1268 assert!(examples.contains_key("author"));
1269 assert!(!examples.contains_key("status")); let memo_for_examples = examples.get("memo_for").unwrap();
1273 assert_eq!(memo_for_examples.len(), 2);
1274 assert_eq!(
1275 memo_for_examples[0].as_json(),
1276 &json!(["ORG1/SYMBOL", "ORG2/SYMBOL"])
1277 );
1278 assert_eq!(memo_for_examples[1].as_json(), &json!(["DEPT/OFFICE"]));
1279
1280 let author_examples = examples.get("author").unwrap();
1282 assert_eq!(author_examples.len(), 2);
1283 assert_eq!(author_examples[0].as_str(), Some("John Doe"));
1284 assert_eq!(author_examples[1].as_str(), Some("Jane Smith"));
1285 }
1286
1287 #[test]
1288 fn test_extract_examples_from_schema_empty() {
1289 let schema = json!({
1291 "$schema": "https://json-schema.org/draft/2019-09/schema",
1292 "type": "object",
1293 "properties": {
1294 "title": {"type": "string"},
1295 "author": {"type": "string"}
1296 }
1297 });
1298
1299 let examples = extract_examples_from_schema(&QuillValue::from_json(schema));
1300 assert_eq!(examples.len(), 0);
1301 }
1302
1303 #[test]
1304 fn test_extract_examples_from_schema_no_properties() {
1305 let schema = json!({
1307 "$schema": "https://json-schema.org/draft/2019-09/schema",
1308 "type": "object"
1309 });
1310
1311 let examples = extract_examples_from_schema(&QuillValue::from_json(schema));
1312 assert_eq!(examples.len(), 0);
1313 }
1314
1315 #[test]
1316 fn test_coerce_singular_to_array() {
1317 let schema = json!({
1318 "$schema": "https://json-schema.org/draft/2019-09/schema",
1319 "type": "object",
1320 "properties": {
1321 "tags": {"type": "array"}
1322 }
1323 });
1324
1325 let mut fields = HashMap::new();
1326 fields.insert(
1327 "tags".to_string(),
1328 QuillValue::from_json(json!("single-tag")),
1329 );
1330
1331 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
1332
1333 let tags = coerced.get("tags").unwrap();
1334 assert!(tags.as_array().is_some());
1335 let tags_array = tags.as_array().unwrap();
1336 assert_eq!(tags_array.len(), 1);
1337 assert_eq!(tags_array[0].as_str().unwrap(), "single-tag");
1338 }
1339
1340 #[test]
1341 fn test_coerce_array_unchanged() {
1342 let schema = json!({
1343 "$schema": "https://json-schema.org/draft/2019-09/schema",
1344 "type": "object",
1345 "properties": {
1346 "tags": {"type": "array"}
1347 }
1348 });
1349
1350 let mut fields = HashMap::new();
1351 fields.insert(
1352 "tags".to_string(),
1353 QuillValue::from_json(json!(["tag1", "tag2"])),
1354 );
1355
1356 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
1357
1358 let tags = coerced.get("tags").unwrap();
1359 let tags_array = tags.as_array().unwrap();
1360 assert_eq!(tags_array.len(), 2);
1361 }
1362
1363 #[test]
1364 fn test_coerce_string_to_boolean() {
1365 let schema = json!({
1366 "$schema": "https://json-schema.org/draft/2019-09/schema",
1367 "type": "object",
1368 "properties": {
1369 "active": {"type": "boolean"},
1370 "enabled": {"type": "boolean"}
1371 }
1372 });
1373
1374 let mut fields = HashMap::new();
1375 fields.insert("active".to_string(), QuillValue::from_json(json!("true")));
1376 fields.insert("enabled".to_string(), QuillValue::from_json(json!("FALSE")));
1377
1378 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
1379
1380 assert!(coerced.get("active").unwrap().as_bool().unwrap());
1381 assert!(!coerced.get("enabled").unwrap().as_bool().unwrap());
1382 }
1383
1384 #[test]
1385 fn test_coerce_number_to_boolean() {
1386 let schema = json!({
1387 "$schema": "https://json-schema.org/draft/2019-09/schema",
1388 "type": "object",
1389 "properties": {
1390 "flag1": {"type": "boolean"},
1391 "flag2": {"type": "boolean"},
1392 "flag3": {"type": "boolean"}
1393 }
1394 });
1395
1396 let mut fields = HashMap::new();
1397 fields.insert("flag1".to_string(), QuillValue::from_json(json!(0)));
1398 fields.insert("flag2".to_string(), QuillValue::from_json(json!(1)));
1399 fields.insert("flag3".to_string(), QuillValue::from_json(json!(42)));
1400
1401 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
1402
1403 assert!(!coerced.get("flag1").unwrap().as_bool().unwrap());
1404 assert!(coerced.get("flag2").unwrap().as_bool().unwrap());
1405 assert!(coerced.get("flag3").unwrap().as_bool().unwrap());
1406 }
1407
1408 #[test]
1409 fn test_coerce_float_to_boolean() {
1410 let schema = json!({
1411 "$schema": "https://json-schema.org/draft/2019-09/schema",
1412 "type": "object",
1413 "properties": {
1414 "flag1": {"type": "boolean"},
1415 "flag2": {"type": "boolean"},
1416 "flag3": {"type": "boolean"},
1417 "flag4": {"type": "boolean"}
1418 }
1419 });
1420
1421 let mut fields = HashMap::new();
1422 fields.insert("flag1".to_string(), QuillValue::from_json(json!(0.0)));
1423 fields.insert("flag2".to_string(), QuillValue::from_json(json!(0.5)));
1424 fields.insert("flag3".to_string(), QuillValue::from_json(json!(-1.5)));
1425 fields.insert("flag4".to_string(), QuillValue::from_json(json!(1e-100)));
1427
1428 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
1429
1430 assert!(!coerced.get("flag1").unwrap().as_bool().unwrap());
1431 assert!(coerced.get("flag2").unwrap().as_bool().unwrap());
1432 assert!(coerced.get("flag3").unwrap().as_bool().unwrap());
1433 assert!(!coerced.get("flag4").unwrap().as_bool().unwrap());
1435 }
1436
1437 #[test]
1438 fn test_coerce_string_to_number() {
1439 let schema = json!({
1440 "$schema": "https://json-schema.org/draft/2019-09/schema",
1441 "type": "object",
1442 "properties": {
1443 "count": {"type": "number"},
1444 "price": {"type": "number"}
1445 }
1446 });
1447
1448 let mut fields = HashMap::new();
1449 fields.insert("count".to_string(), QuillValue::from_json(json!("42")));
1450 fields.insert("price".to_string(), QuillValue::from_json(json!("19.99")));
1451
1452 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
1453
1454 assert_eq!(coerced.get("count").unwrap().as_i64().unwrap(), 42);
1455 assert_eq!(coerced.get("price").unwrap().as_f64().unwrap(), 19.99);
1456 }
1457
1458 #[test]
1459 fn test_coerce_boolean_to_number() {
1460 let schema = json!({
1461 "$schema": "https://json-schema.org/draft/2019-09/schema",
1462 "type": "object",
1463 "properties": {
1464 "active": {"type": "number"},
1465 "disabled": {"type": "number"}
1466 }
1467 });
1468
1469 let mut fields = HashMap::new();
1470 fields.insert("active".to_string(), QuillValue::from_json(json!(true)));
1471 fields.insert("disabled".to_string(), QuillValue::from_json(json!(false)));
1472
1473 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
1474
1475 assert_eq!(coerced.get("active").unwrap().as_i64().unwrap(), 1);
1476 assert_eq!(coerced.get("disabled").unwrap().as_i64().unwrap(), 0);
1477 }
1478
1479 #[test]
1480 fn test_coerce_no_schema_properties() {
1481 let schema = json!({
1482 "$schema": "https://json-schema.org/draft/2019-09/schema",
1483 "type": "object"
1484 });
1485
1486 let mut fields = HashMap::new();
1487 fields.insert("title".to_string(), QuillValue::from_json(json!("Test")));
1488
1489 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
1490
1491 assert_eq!(coerced.get("title").unwrap().as_str().unwrap(), "Test");
1493 }
1494
1495 #[test]
1496 fn test_coerce_field_without_type() {
1497 let schema = json!({
1498 "$schema": "https://json-schema.org/draft/2019-09/schema",
1499 "type": "object",
1500 "properties": {
1501 "title": {"description": "A title field"}
1502 }
1503 });
1504
1505 let mut fields = HashMap::new();
1506 fields.insert("title".to_string(), QuillValue::from_json(json!("Test")));
1507
1508 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
1509
1510 assert_eq!(coerced.get("title").unwrap().as_str().unwrap(), "Test");
1512 }
1513
1514 #[test]
1515 fn test_coerce_array_to_string() {
1516 let schema = json!({
1517 "$schema": "https://json-schema.org/draft/2019-09/schema",
1518 "type": "object",
1519 "properties": {
1520 "title": {"type": "string"},
1521 "tags": {"type": "string"} }
1523 });
1524
1525 let mut fields = HashMap::new();
1526 fields.insert(
1528 "title".to_string(),
1529 QuillValue::from_json(json!(["Wrapped Title"])),
1530 );
1531 fields.insert(
1533 "tags".to_string(),
1534 QuillValue::from_json(json!(["tag1", "tag2"])),
1535 );
1536
1537 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
1538
1539 assert_eq!(
1541 coerced.get("title").unwrap().as_str().unwrap(),
1542 "Wrapped Title"
1543 );
1544
1545 assert!(coerced.get("tags").unwrap().as_array().is_some());
1547 assert_eq!(coerced.get("tags").unwrap().as_array().unwrap().len(), 2);
1548 }
1549
1550 #[test]
1551 fn test_coerce_mixed_fields() {
1552 let schema = json!({
1553 "$schema": "https://json-schema.org/draft/2019-09/schema",
1554 "type": "object",
1555 "properties": {
1556 "tags": {"type": "array"},
1557 "active": {"type": "boolean"},
1558 "count": {"type": "number"},
1559 "title": {"type": "string"}
1560 }
1561 });
1562
1563 let mut fields = HashMap::new();
1564 fields.insert("tags".to_string(), QuillValue::from_json(json!("single")));
1565 fields.insert("active".to_string(), QuillValue::from_json(json!("true")));
1566 fields.insert("count".to_string(), QuillValue::from_json(json!("42")));
1567 fields.insert(
1568 "title".to_string(),
1569 QuillValue::from_json(json!("Test Title")),
1570 );
1571
1572 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
1573
1574 assert_eq!(coerced.get("tags").unwrap().as_array().unwrap().len(), 1);
1576 assert!(coerced.get("active").unwrap().as_bool().unwrap());
1577 assert_eq!(coerced.get("count").unwrap().as_i64().unwrap(), 42);
1578 assert_eq!(
1579 coerced.get("title").unwrap().as_str().unwrap(),
1580 "Test Title"
1581 );
1582 }
1583
1584 #[test]
1585 fn test_coerce_invalid_string_to_number() {
1586 let schema = json!({
1587 "$schema": "https://json-schema.org/draft/2019-09/schema",
1588 "type": "object",
1589 "properties": {
1590 "count": {"type": "number"}
1591 }
1592 });
1593
1594 let mut fields = HashMap::new();
1595 fields.insert(
1596 "count".to_string(),
1597 QuillValue::from_json(json!("not-a-number")),
1598 );
1599
1600 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
1601
1602 assert_eq!(
1604 coerced.get("count").unwrap().as_str().unwrap(),
1605 "not-a-number"
1606 );
1607 }
1608
1609 #[test]
1610 fn test_coerce_object_to_array() {
1611 let schema = json!({
1612 "$schema": "https://json-schema.org/draft/2019-09/schema",
1613 "type": "object",
1614 "properties": {
1615 "items": {"type": "array"}
1616 }
1617 });
1618
1619 let mut fields = HashMap::new();
1620 fields.insert(
1621 "items".to_string(),
1622 QuillValue::from_json(json!({"key": "value"})),
1623 );
1624
1625 let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
1626
1627 let items = coerced.get("items").unwrap();
1629 assert!(items.as_array().is_some());
1630 let items_array = items.as_array().unwrap();
1631 assert_eq!(items_array.len(), 1);
1632 assert!(items_array[0].as_object().is_some());
1633 }
1634
1635 #[test]
1636 fn test_schema_card_in_defs() {
1637 use crate::quill::CardSchema;
1639
1640 let fields = HashMap::new();
1641 let mut cards = HashMap::new();
1642
1643 let name_schema = FieldSchema::new(
1644 "name".to_string(),
1645 FieldType::String,
1646 Some("Name field".to_string()),
1647 );
1648
1649 let mut card_fields = HashMap::new();
1650 card_fields.insert("name".to_string(), name_schema);
1651
1652 let card = CardSchema {
1653 name: "endorsements".to_string(),
1654 title: Some("Endorsements".to_string()),
1655 description: Some("Chain of endorsements".to_string()),
1656 fields: card_fields,
1657 ui: None,
1658 };
1659 cards.insert("endorsements".to_string(), card);
1660
1661 let document = CardSchema {
1662 name: "root".to_string(),
1663 title: None,
1664 description: None,
1665 fields,
1666 ui: None,
1667 };
1668 let json_schema = build_schema(&document, &cards).unwrap().as_json().clone();
1669
1670 assert!(json_schema["$defs"].is_object());
1672 assert!(json_schema["$defs"]["endorsements_card"].is_object());
1673
1674 let card_def = &json_schema["$defs"]["endorsements_card"];
1676 assert_eq!(card_def["type"], "object");
1677 assert_eq!(card_def["title"], "Endorsements");
1678 assert_eq!(card_def["description"], "Chain of endorsements");
1679
1680 assert_eq!(card_def["properties"]["CARD"]["const"], "endorsements");
1682
1683 assert!(card_def["properties"]["name"].is_object());
1685 assert_eq!(card_def["properties"]["name"]["type"], "string");
1686
1687 let required = card_def["required"].as_array().unwrap();
1689 assert!(required.contains(&json!("CARD")));
1690 }
1691
1692 #[test]
1693 fn test_schema_cards_array() {
1694 use crate::quill::CardSchema;
1696
1697 let fields = HashMap::new();
1698 let mut cards = HashMap::new();
1699
1700 let mut name_schema = FieldSchema::new(
1701 "name".to_string(),
1702 FieldType::String,
1703 Some("Endorser name".to_string()),
1704 );
1705 name_schema.required = true;
1706
1707 let mut org_schema = FieldSchema::new(
1708 "org".to_string(),
1709 FieldType::String,
1710 Some("Organization".to_string()),
1711 );
1712 org_schema.default = Some(QuillValue::from_json(json!("Unknown")));
1713
1714 let mut card_fields = HashMap::new();
1715 card_fields.insert("name".to_string(), name_schema);
1716 card_fields.insert("org".to_string(), org_schema);
1717
1718 let card = CardSchema {
1719 name: "endorsements".to_string(),
1720 title: Some("Endorsements".to_string()),
1721 description: Some("Chain of endorsements".to_string()),
1722 fields: card_fields,
1723 ui: None,
1724 };
1725 cards.insert("endorsements".to_string(), card);
1726
1727 let document = CardSchema {
1728 name: "root".to_string(),
1729 title: None,
1730 description: None,
1731 fields,
1732 ui: None,
1733 };
1734 let json_schema = build_schema(&document, &cards).unwrap().as_json().clone();
1735
1736 let cards_prop = &json_schema["properties"]["CARDS"];
1738 assert_eq!(cards_prop["type"], "array");
1739
1740 let items = &cards_prop["items"];
1742 assert!(items["oneOf"].is_array());
1743 let one_of = items["oneOf"].as_array().unwrap();
1744 assert!(!one_of.is_empty());
1745 assert_eq!(one_of[0]["$ref"], "#/$defs/endorsements_card");
1746
1747 assert!(items.get("x-discriminator").is_none());
1749
1750 let card_def = &json_schema["$defs"]["endorsements_card"];
1752 assert_eq!(card_def["properties"]["name"]["type"], "string");
1753 assert_eq!(card_def["properties"]["org"]["default"], "Unknown");
1754
1755 let required = card_def["required"].as_array().unwrap();
1757 assert!(required.contains(&json!("CARD")));
1758 assert!(required.contains(&json!("name")));
1759 assert!(!required.contains(&json!("org")));
1760 }
1761
1762 #[test]
1763 fn test_extract_card_item_defaults() {
1764 let schema = json!({
1766 "$schema": "https://json-schema.org/draft/2019-09/schema",
1767 "type": "object",
1768 "properties": {
1769 "endorsements": {
1770 "type": "array",
1771 "items": {
1772 "type": "object",
1773 "properties": {
1774 "name": { "type": "string" },
1775 "org": { "type": "string", "default": "Unknown Org" },
1776 "rank": { "type": "string", "default": "N/A" }
1777 }
1778 }
1779 },
1780 "title": { "type": "string" }
1781 }
1782 });
1783
1784 let card_defaults = extract_card_item_defaults(&QuillValue::from_json(schema));
1785
1786 assert_eq!(card_defaults.len(), 1);
1788 assert!(card_defaults.contains_key("endorsements"));
1789
1790 let endorsements_defaults = card_defaults.get("endorsements").unwrap();
1791 assert_eq!(endorsements_defaults.len(), 2); assert!(!endorsements_defaults.contains_key("name")); assert_eq!(
1794 endorsements_defaults.get("org").unwrap().as_str(),
1795 Some("Unknown Org")
1796 );
1797 assert_eq!(
1798 endorsements_defaults.get("rank").unwrap().as_str(),
1799 Some("N/A")
1800 );
1801 }
1802
1803 #[test]
1804 fn test_extract_card_item_defaults_empty() {
1805 let schema = json!({
1807 "type": "object",
1808 "properties": {
1809 "title": { "type": "string" }
1810 }
1811 });
1812
1813 let card_defaults = extract_card_item_defaults(&QuillValue::from_json(schema));
1814 assert!(card_defaults.is_empty());
1815 }
1816
1817 #[test]
1818 fn test_extract_card_item_defaults_no_item_defaults() {
1819 let schema = json!({
1821 "type": "object",
1822 "properties": {
1823 "endorsements": {
1824 "type": "array",
1825 "items": {
1826 "type": "object",
1827 "properties": {
1828 "name": { "type": "string" },
1829 "org": { "type": "string" }
1830 }
1831 }
1832 }
1833 }
1834 });
1835
1836 let card_defaults = extract_card_item_defaults(&QuillValue::from_json(schema));
1837 assert!(card_defaults.is_empty()); }
1839
1840 #[test]
1841 fn test_apply_card_item_defaults() {
1842 let mut item_defaults = HashMap::new();
1844 item_defaults.insert(
1845 "org".to_string(),
1846 QuillValue::from_json(json!("Default Org")),
1847 );
1848
1849 let mut card_defaults = HashMap::new();
1850 card_defaults.insert("endorsements".to_string(), item_defaults);
1851
1852 let mut fields = HashMap::new();
1854 fields.insert(
1855 "endorsements".to_string(),
1856 QuillValue::from_json(json!([
1857 { "name": "John Doe" },
1858 { "name": "Jane Smith", "org": "Custom Org" }
1859 ])),
1860 );
1861
1862 let result = apply_card_item_defaults(&fields, &card_defaults);
1863
1864 let endorsements = result.get("endorsements").unwrap().as_array().unwrap();
1866 assert_eq!(endorsements.len(), 2);
1867
1868 assert_eq!(endorsements[0]["name"], "John Doe");
1870 assert_eq!(endorsements[0]["org"], "Default Org");
1871
1872 assert_eq!(endorsements[1]["name"], "Jane Smith");
1874 assert_eq!(endorsements[1]["org"], "Custom Org");
1875 }
1876
1877 #[test]
1878 fn test_apply_card_item_defaults_empty_card() {
1879 let mut item_defaults = HashMap::new();
1880 item_defaults.insert(
1881 "org".to_string(),
1882 QuillValue::from_json(json!("Default Org")),
1883 );
1884
1885 let mut card_defaults = HashMap::new();
1886 card_defaults.insert("endorsements".to_string(), item_defaults);
1887
1888 let mut fields = HashMap::new();
1890 fields.insert("endorsements".to_string(), QuillValue::from_json(json!([])));
1891
1892 let result = apply_card_item_defaults(&fields, &card_defaults);
1893
1894 let endorsements = result.get("endorsements").unwrap().as_array().unwrap();
1896 assert!(endorsements.is_empty());
1897 }
1898
1899 #[test]
1900 fn test_apply_card_item_defaults_no_matching_card() {
1901 let mut item_defaults = HashMap::new();
1902 item_defaults.insert(
1903 "org".to_string(),
1904 QuillValue::from_json(json!("Default Org")),
1905 );
1906
1907 let mut card_defaults = HashMap::new();
1908 card_defaults.insert("endorsements".to_string(), item_defaults);
1909
1910 let mut fields = HashMap::new();
1912 fields.insert(
1913 "reviews".to_string(),
1914 QuillValue::from_json(json!([{ "author": "Bob" }])),
1915 );
1916
1917 let result = apply_card_item_defaults(&fields, &card_defaults);
1918
1919 let reviews = result.get("reviews").unwrap().as_array().unwrap();
1921 assert_eq!(reviews.len(), 1);
1922 assert_eq!(reviews[0]["author"], "Bob");
1923 assert!(reviews[0].get("org").is_none());
1924 }
1925
1926 #[test]
1927 fn test_card_validation_with_required_fields() {
1928 let schema = json!({
1930 "$schema": "https://json-schema.org/draft/2019-09/schema",
1931 "type": "object",
1932 "properties": {
1933 "endorsements": {
1934 "type": "array",
1935 "items": {
1936 "type": "object",
1937 "properties": {
1938 "name": { "type": "string" },
1939 "org": { "type": "string", "default": "Unknown" }
1940 },
1941 "required": ["name"]
1942 }
1943 }
1944 }
1945 });
1946
1947 let mut valid_fields = HashMap::new();
1949 valid_fields.insert(
1950 "endorsements".to_string(),
1951 QuillValue::from_json(json!([{ "name": "John" }])),
1952 );
1953
1954 let result = validate_document(&QuillValue::from_json(schema.clone()), &valid_fields);
1955 assert!(result.is_ok());
1956
1957 let mut invalid_fields = HashMap::new();
1959 invalid_fields.insert(
1960 "endorsements".to_string(),
1961 QuillValue::from_json(json!([{ "org": "SomeOrg" }])),
1962 );
1963
1964 let result = validate_document(&QuillValue::from_json(schema), &invalid_fields);
1965 assert!(result.is_err());
1966 }
1967 #[test]
1968 fn test_validate_document_invalid_card_type() {
1969 use crate::quill::{CardSchema, FieldSchema};
1970
1971 let mut card_fields = HashMap::new();
1972 card_fields.insert(
1973 "field1".to_string(),
1974 FieldSchema::new(
1975 "f1".to_string(),
1976 FieldType::String,
1977 Some("desc".to_string()),
1978 ),
1979 );
1980 let mut card_schemas = HashMap::new();
1981 card_schemas.insert(
1982 "valid_card".to_string(),
1983 CardSchema {
1984 name: "valid_card".to_string(),
1985 title: None,
1986 description: None,
1987 fields: card_fields,
1988 ui: None,
1989 },
1990 );
1991
1992 let document = CardSchema {
1993 name: "root".to_string(),
1994 title: None,
1995 description: None,
1996 fields: HashMap::new(),
1997 ui: None,
1998 };
1999 let schema = build_schema(&document, &card_schemas).unwrap();
2000
2001 let mut fields = HashMap::new();
2002 let invalid_card = json!({
2004 "CARD": "invalid_type",
2005 "field1": "value" });
2007 fields.insert(
2008 "CARDS".to_string(),
2009 QuillValue::from_json(json!([invalid_card])),
2010 );
2011
2012 let result = validate_document(&QuillValue::from_json(schema.as_json().clone()), &fields);
2013 assert!(result.is_err());
2014 let errs = result.unwrap_err();
2015 let err_msg = &errs[0];
2017 assert!(err_msg.contains("Invalid card type 'invalid_type'"));
2018 assert!(err_msg.contains("Valid types are: [valid_card]"));
2019 }
2020
2021 #[test]
2022 fn test_coerce_document_cards() {
2023 let mut card_fields = HashMap::new();
2024 let count_schema = FieldSchema::new(
2025 "Count".to_string(),
2026 FieldType::Number,
2027 Some("A number".to_string()),
2028 );
2029 card_fields.insert("count".to_string(), count_schema);
2030
2031 let active_schema = FieldSchema::new(
2032 "Active".to_string(),
2033 FieldType::Boolean,
2034 Some("A boolean".to_string()),
2035 );
2036 card_fields.insert("active".to_string(), active_schema);
2037
2038 let mut card_schemas = HashMap::new();
2039 card_schemas.insert(
2040 "test_card".to_string(),
2041 CardSchema {
2042 name: "test_card".to_string(),
2043 title: None,
2044 description: Some("Test card".to_string()),
2045 fields: card_fields,
2046 ui: None,
2047 },
2048 );
2049
2050 let document = CardSchema {
2051 name: "root".to_string(),
2052 title: None,
2053 description: None,
2054 fields: HashMap::new(),
2055 ui: None,
2056 };
2057 let schema = build_schema(&document, &card_schemas).unwrap();
2058
2059 let mut fields = HashMap::new();
2060 let card_value = json!({
2061 "CARD": "test_card",
2062 "count": "42",
2063 "active": "true"
2064 });
2065 fields.insert(
2066 "CARDS".to_string(),
2067 QuillValue::from_json(json!([card_value])),
2068 );
2069
2070 let coerced_fields = coerce_document(&schema, &fields);
2071
2072 let cards_array = coerced_fields.get("CARDS").unwrap().as_array().unwrap();
2073 let coerced_card = cards_array[0].as_object().unwrap();
2074
2075 assert_eq!(coerced_card.get("count").unwrap().as_i64(), Some(42));
2076 assert_eq!(coerced_card.get("active").unwrap().as_bool(), Some(true));
2077 }
2078
2079 #[test]
2080 fn test_validate_document_card_fields() {
2081 let mut card_fields = HashMap::new();
2082 let count_schema = FieldSchema::new(
2083 "Count".to_string(),
2084 FieldType::Number,
2085 Some("A number".to_string()),
2086 );
2087 card_fields.insert("count".to_string(), count_schema);
2088
2089 let mut card_schemas = HashMap::new();
2090 card_schemas.insert(
2091 "test_card".to_string(),
2092 CardSchema {
2093 name: "test_card".to_string(),
2094 title: None,
2095 description: Some("Test card".to_string()),
2096 fields: card_fields,
2097 ui: None,
2098 },
2099 );
2100
2101 let document = CardSchema {
2102 name: "root".to_string(),
2103 title: None,
2104 description: None,
2105 fields: HashMap::new(),
2106 ui: None,
2107 };
2108 let schema = build_schema(&document, &card_schemas).unwrap();
2109
2110 let mut fields = HashMap::new();
2111 let card_value = json!({
2112 "CARD": "test_card",
2113 "count": "not a number" });
2115 fields.insert(
2116 "CARDS".to_string(),
2117 QuillValue::from_json(json!([card_value])),
2118 );
2119
2120 let result = validate_document(&QuillValue::from_json(schema.as_json().clone()), &fields);
2121 assert!(result.is_err());
2122 let errs = result.unwrap_err();
2123
2124 let found_specific_error = errs
2126 .iter()
2127 .any(|e| e.contains("/CARDS/0") && e.contains("not a number") && !e.contains("oneOf"));
2128
2129 assert!(
2130 found_specific_error,
2131 "Did not find specific error msg in: {:?}",
2132 errs
2133 );
2134 }
2135
2136 #[test]
2137 fn test_card_field_ui_metadata() {
2138 use crate::quill::{CardSchema, UiFieldSchema};
2140
2141 let mut field_schema = FieldSchema::new(
2142 "from".to_string(),
2143 FieldType::String,
2144 Some("Sender".to_string()),
2145 );
2146 field_schema.ui = Some(UiFieldSchema {
2147 group: Some("Header".to_string()),
2148 order: Some(0),
2149 });
2150
2151 let mut card_fields = HashMap::new();
2152 card_fields.insert("from".to_string(), field_schema);
2153
2154 let card = CardSchema {
2155 name: "indorsement".to_string(),
2156 title: Some("Indorsement".to_string()),
2157 description: Some("An indorsement".to_string()),
2158 fields: card_fields,
2159 ui: None,
2160 };
2161
2162 let mut cards = HashMap::new();
2163 cards.insert("indorsement".to_string(), card);
2164
2165 let document = CardSchema {
2167 name: "root".to_string(),
2168 title: None,
2169 description: None,
2170 fields: HashMap::new(),
2171 ui: None,
2172 };
2173
2174 let schema = build_schema(&document, &cards).unwrap();
2175 let card_def = &schema.as_json()["$defs"]["indorsement_card"];
2176 let from_field = &card_def["properties"]["from"];
2177
2178 assert_eq!(from_field["x-ui"]["group"], "Header");
2179 assert_eq!(from_field["x-ui"]["order"], 0);
2180 }
2181
2182 #[test]
2183 fn test_hide_body_schema() {
2184 use crate::quill::{CardSchema, UiContainerSchema};
2185
2186 let ui_schema = UiContainerSchema {
2188 hide_body: Some(true),
2189 };
2190
2191 let field_schema = FieldSchema::new(
2193 "name".to_string(),
2194 FieldType::String,
2195 Some("Name".to_string()),
2196 );
2197
2198 let mut card_fields = HashMap::new();
2199 card_fields.insert("name".to_string(), field_schema);
2200
2201 let card = CardSchema {
2202 name: "meta_card".to_string(),
2203 title: None,
2204 description: Some("Meta only card".to_string()),
2205 fields: card_fields,
2206 ui: Some(UiContainerSchema {
2207 hide_body: Some(true),
2208 }),
2209 };
2210
2211 let mut cards = HashMap::new();
2212 cards.insert("meta_card".to_string(), card);
2213
2214 let document = CardSchema {
2215 name: "root".to_string(),
2216 title: None,
2217 description: None,
2218 fields: HashMap::new(),
2219 ui: Some(ui_schema),
2220 };
2221
2222 let schema = build_schema(&document, &cards).unwrap();
2223 let json_schema = schema.as_json();
2224
2225 assert!(json_schema.get("x-ui").is_some());
2227 assert_eq!(json_schema["x-ui"]["hide_body"], true);
2228
2229 let card_def = &json_schema["$defs"]["meta_card_card"];
2231 assert!(card_def.get("x-ui").is_some(), "Card should have x-ui");
2232 assert_eq!(card_def["x-ui"]["hide_body"], true);
2233 }
2234}