@@ -436,10 +436,25 @@ pub fn validate_document(
436436 } ;
437437
438438 // Validate the document and collect errors immediately
439+ let mut all_errors = Vec :: new ( ) ;
440+
441+ // 1. Recursive card validation
442+ if let Some ( cards) = doc_value. get ( "CARDS" ) . and_then ( |v| v. as_array ( ) ) {
443+ let card_errors = validate_cards_array ( schema, cards) ;
444+ all_errors. extend ( card_errors) ;
445+ }
446+
447+ // 2. Standard validation
439448 let validation_result = compiled. validate ( & doc_value) ;
440449
441450 match validation_result {
442- Ok ( _) => Ok ( ( ) ) ,
451+ Ok ( _) => {
452+ if all_errors. is_empty ( ) {
453+ Ok ( ( ) )
454+ } else {
455+ Err ( all_errors)
456+ }
457+ }
443458 Err ( error) => {
444459 let path = error. instance_path ( ) . to_string ( ) ;
445460 let path_display = if path. is_empty ( ) {
@@ -448,57 +463,137 @@ pub fn validate_document(
448463 path. clone ( )
449464 } ;
450465
451- // Check for potential invalid card type error
452- if path. starts_with ( "/CARDS/" ) && error. to_string ( ) . contains ( "oneOf" ) {
453- // Try to parse the index from path /CARDS/n
454- if let Some ( rest) = path. strip_prefix ( "/CARDS/" ) {
455- // path might be just "/CARDS/0" or "/CARDS/0/some/field"
456- // We only want to intervene if the error is about the card item itself failing oneOf
457- let is_item_error = !rest. contains ( '/' ) ;
458-
459- if is_item_error {
460- if let Ok ( idx) = rest. parse :: < usize > ( ) {
461- if let Some ( cards) = doc_value. get ( "CARDS" ) . and_then ( |v| v. as_array ( ) ) {
462- if let Some ( item) = cards. get ( idx) {
463- // Check if the item has a CARD field
464- if let Some ( card_type) =
465- item. get ( "CARD" ) . and_then ( |v| v. as_str ( ) )
466- {
467- // Collect valid card types from schema definitions
468- let mut valid_types = Vec :: new ( ) ;
469- if let Some ( defs) = schema
470- . as_json ( )
471- . get ( "$defs" )
472- . and_then ( |v| v. as_object ( ) )
466+ // If we have specific card errors, we might want to skip generic CARDS errors
467+ // from the main schema validation to avoid noise.
468+ // But for now, we'll include everything unless it's a "oneOf" error on a card we already diagnosed.
469+ let is_generic_card_error = path. starts_with ( "/CARDS/" )
470+ && error. to_string ( ) . contains ( "oneOf" )
471+ && !all_errors. is_empty ( ) ;
472+
473+ if !is_generic_card_error {
474+ // Check for potential invalid card type error (legacy check, but still useful)
475+ if path. starts_with ( "/CARDS/" ) && error. to_string ( ) . contains ( "oneOf" ) {
476+ // Try to parse the index from path /CARDS/n
477+ if let Some ( rest) = path. strip_prefix ( "/CARDS/" ) {
478+ // path might be just "/CARDS/0" or "/CARDS/0/some/field"
479+ // We only want to intervene if the error is about the card item itself failing oneOf
480+ let is_item_error = !rest. contains ( '/' ) ;
481+
482+ if is_item_error {
483+ if let Ok ( idx) = rest. parse :: < usize > ( ) {
484+ if let Some ( cards) =
485+ doc_value. get ( "CARDS" ) . and_then ( |v| v. as_array ( ) )
486+ {
487+ if let Some ( item) = cards. get ( idx) {
488+ // Check if the item has a CARD field
489+ if let Some ( card_type) =
490+ item. get ( "CARD" ) . and_then ( |v| v. as_str ( ) )
473491 {
474- for key in defs. keys ( ) {
475- if let Some ( name) = key. strip_suffix ( "_card" ) {
476- valid_types. push ( name. to_string ( ) ) ;
492+ // Collect valid card types from schema definitions
493+ let mut valid_types = Vec :: new ( ) ;
494+ if let Some ( defs) = schema
495+ . as_json ( )
496+ . get ( "$defs" )
497+ . and_then ( |v| v. as_object ( ) )
498+ {
499+ for key in defs. keys ( ) {
500+ if let Some ( name) = key. strip_suffix ( "_card" ) {
501+ valid_types. push ( name. to_string ( ) ) ;
502+ }
477503 }
478504 }
479- }
480505
481- // If we found valid types and the current type is NOT in the list
482- if !valid_types. is_empty ( )
483- && !valid_types. contains ( & card_type. to_string ( ) )
484- {
485- valid_types. sort ( ) ;
486- let valid_list = valid_types. join ( ", " ) ;
487- let message = format ! ( "Validation error at {}: Invalid card type '{}'. Valid types are: [{}]" , path_display, card_type, valid_list) ;
488- return Err ( vec ! [ message] ) ;
506+ // If we found valid types and the current type is NOT in the list
507+ if !valid_types. is_empty ( )
508+ && !valid_types. contains ( & card_type. to_string ( ) )
509+ {
510+ valid_types. sort ( ) ;
511+ let valid_list = valid_types. join ( ", " ) ;
512+ let message = format ! ( "Validation error at {}: Invalid card type '{}'. Valid types are: [{}]" , path_display, card_type, valid_list) ;
513+ all_errors. push ( message) ;
514+ return Err ( all_errors) ;
515+ }
489516 }
490517 }
491518 }
492519 }
493520 }
494521 }
495522 }
523+
524+ let message = format ! ( "Validation error at {}: {}" , path_display, error) ;
525+ all_errors. push ( message) ;
496526 }
497527
498- let message = format ! ( "Validation error at {}: {}" , path_display, error) ;
499- Err ( vec ! [ message] )
528+ Err ( all_errors)
529+ }
530+ }
531+ }
532+
533+ /// Helper to recursively validate an array of card objects
534+ fn validate_cards_array ( document_schema : & QuillValue , cards_array : & [ Value ] ) -> Vec < String > {
535+ let mut errors = Vec :: new ( ) ;
536+
537+ // Get definitions for card schemas
538+ let defs = document_schema
539+ . as_json ( )
540+ . get ( "$defs" )
541+ . and_then ( |v| v. as_object ( ) ) ;
542+
543+ for ( idx, card) in cards_array. iter ( ) . enumerate ( ) {
544+ // We only process objects that have a CARD discriminator
545+ if let Some ( card_obj) = card. as_object ( ) {
546+ if let Some ( card_type) = card_obj. get ( "CARD" ) . and_then ( |v| v. as_str ( ) ) {
547+ // Construct the definition name: {type}_card
548+ let def_name = format ! ( "{}_card" , card_type) ;
549+
550+ // Look up the schema for this card type
551+ if let Some ( card_schema_json) = defs. and_then ( |d| d. get ( & def_name) ) {
552+ // Convert the card object to HashMap<String, QuillValue> for recursion
553+ let mut card_fields = HashMap :: new ( ) ;
554+ for ( k, v) in card_obj {
555+ card_fields. insert ( k. clone ( ) , QuillValue :: from_json ( v. clone ( ) ) ) ;
556+ }
557+
558+ // Recursively validate this card's fields
559+ if let Err ( card_errors) = validate_document (
560+ & QuillValue :: from_json ( card_schema_json. clone ( ) ) ,
561+ & card_fields,
562+ ) {
563+ // Prefix errors with location
564+ for err in card_errors {
565+ // If the error already starts with "Validation error at ", insert the prefix
566+ // otherwise just prefix it.
567+ // Typical error: "Validation error at field: message"
568+ // We want: "Validation error at /CARDS/0/field: message"
569+
570+ let prefix = format ! ( "/CARDS/{}" , idx) ;
571+ let new_msg =
572+ if let Some ( rest) = err. strip_prefix ( "Validation error at " ) {
573+ if rest. starts_with ( "document" ) {
574+ // "Validation error at document: message" -> "Validation error at /CARDS/0: message"
575+ format ! (
576+ "Validation error at {}:{}" ,
577+ prefix,
578+ rest. strip_prefix( "document" ) . unwrap_or( rest)
579+ )
580+ } else {
581+ // "Validation error at /field: message" -> "Validation error at /CARDS/0/field: message"
582+ format ! ( "Validation error at {}{}" , prefix, rest)
583+ }
584+ } else {
585+ format ! ( "Validation error at {}: {}" , prefix, err)
586+ } ;
587+
588+ errors. push ( new_msg) ;
589+ }
590+ }
591+ }
592+ }
500593 }
501594 }
595+
596+ errors
502597}
503598
504599/// Coerce a single value to match the expected schema type
@@ -634,9 +729,70 @@ pub fn coerce_document(
634729 coerced_fields. insert ( field_name. clone ( ) , field_value. clone ( ) ) ;
635730 }
636731
732+ // Recursively coerce cards if the CARDS field is present
733+ if let Some ( cards_value) = coerced_fields. get ( "CARDS" ) {
734+ if let Some ( cards_array) = cards_value. as_array ( ) {
735+ let coerced_cards = coerce_cards_array ( schema, cards_array) ;
736+ coerced_fields. insert (
737+ "CARDS" . to_string ( ) ,
738+ QuillValue :: from_json ( Value :: Array ( coerced_cards) ) ,
739+ ) ;
740+ }
741+ }
742+
637743 coerced_fields
638744}
639745
746+ /// Helper to recursively coerce an array of card objects
747+ fn coerce_cards_array ( document_schema : & QuillValue , cards_array : & [ Value ] ) -> Vec < Value > {
748+ let mut coerced_cards = Vec :: new ( ) ;
749+
750+ // Get definitions for card schemas
751+ let defs = document_schema
752+ . as_json ( )
753+ . get ( "$defs" )
754+ . and_then ( |v| v. as_object ( ) ) ;
755+
756+ for card in cards_array {
757+ // We only process objects that have a CARD discriminator
758+ if let Some ( card_obj) = card. as_object ( ) {
759+ if let Some ( card_type) = card_obj. get ( "CARD" ) . and_then ( |v| v. as_str ( ) ) {
760+ // Construct the definition name: {type}_card
761+ let def_name = format ! ( "{}_card" , card_type) ;
762+
763+ // Look up the schema for this card type
764+ if let Some ( card_schema_json) = defs. and_then ( |d| d. get ( & def_name) ) {
765+ // Convert the card object to HashMap<String, QuillValue> for coerce_document
766+ let mut card_fields = HashMap :: new ( ) ;
767+ for ( k, v) in card_obj {
768+ card_fields. insert ( k. clone ( ) , QuillValue :: from_json ( v. clone ( ) ) ) ;
769+ }
770+
771+ // Recursively coerce this card's fields
772+ let coerced_card_fields = coerce_document (
773+ & QuillValue :: from_json ( card_schema_json. clone ( ) ) ,
774+ & card_fields,
775+ ) ;
776+
777+ // Convert back to JSON Value
778+ let mut coerced_card_obj = Map :: new ( ) ;
779+ for ( k, v) in coerced_card_fields {
780+ coerced_card_obj. insert ( k, v. into_json ( ) ) ;
781+ }
782+
783+ coerced_cards. push ( Value :: Object ( coerced_card_obj) ) ;
784+ continue ;
785+ }
786+ }
787+ }
788+
789+ // If not an object, no CARD type, or no matching schema, keep as-is
790+ coerced_cards. push ( card. clone ( ) ) ;
791+ }
792+
793+ coerced_cards
794+ }
795+
640796#[ cfg( test) ]
641797mod tests {
642798 use super :: * ;
@@ -1643,4 +1799,96 @@ mod tests {
16431799 assert ! ( err_msg. contains( "Invalid card type 'invalid_type'" ) ) ;
16441800 assert ! ( err_msg. contains( "Valid types are: [valid_card]" ) ) ;
16451801 }
1802+
1803+ #[ test]
1804+ fn test_coerce_document_cards ( ) {
1805+ let mut card_fields = HashMap :: new ( ) ;
1806+ let mut count_schema = FieldSchema :: new ( "Count" . to_string ( ) , "A number" . to_string ( ) ) ;
1807+ count_schema. r#type = Some ( "number" . to_string ( ) ) ;
1808+ card_fields. insert ( "count" . to_string ( ) , count_schema) ;
1809+
1810+ let mut active_schema = FieldSchema :: new ( "Active" . to_string ( ) , "A boolean" . to_string ( ) ) ;
1811+ active_schema. r#type = Some ( "boolean" . to_string ( ) ) ;
1812+ card_fields. insert ( "active" . to_string ( ) , active_schema) ;
1813+
1814+ let mut card_schemas = HashMap :: new ( ) ;
1815+ card_schemas. insert (
1816+ "test_card" . to_string ( ) ,
1817+ CardSchema {
1818+ name : "test_card" . to_string ( ) ,
1819+ title : None ,
1820+ ui : None ,
1821+ description : "Test card" . to_string ( ) ,
1822+ fields : card_fields,
1823+ } ,
1824+ ) ;
1825+
1826+ let schema = build_schema ( & HashMap :: new ( ) , & card_schemas) . unwrap ( ) ;
1827+
1828+ let mut fields = HashMap :: new ( ) ;
1829+ let card_value = json ! ( {
1830+ "CARD" : "test_card" ,
1831+ "count" : "42" ,
1832+ "active" : "true"
1833+ } ) ;
1834+ fields. insert (
1835+ "CARDS" . to_string ( ) ,
1836+ QuillValue :: from_json ( json ! ( [ card_value] ) ) ,
1837+ ) ;
1838+
1839+ let coerced_fields = coerce_document ( & schema, & fields) ;
1840+
1841+ let cards_array = coerced_fields. get ( "CARDS" ) . unwrap ( ) . as_array ( ) . unwrap ( ) ;
1842+ let coerced_card = cards_array[ 0 ] . as_object ( ) . unwrap ( ) ;
1843+
1844+ assert_eq ! ( coerced_card. get( "count" ) . unwrap( ) . as_i64( ) , Some ( 42 ) ) ;
1845+ assert_eq ! ( coerced_card. get( "active" ) . unwrap( ) . as_bool( ) , Some ( true ) ) ;
1846+ }
1847+
1848+ #[ test]
1849+ fn test_validate_document_card_fields ( ) {
1850+ let mut card_fields = HashMap :: new ( ) ;
1851+ let mut count_schema = FieldSchema :: new ( "Count" . to_string ( ) , "A number" . to_string ( ) ) ;
1852+ count_schema. r#type = Some ( "number" . to_string ( ) ) ;
1853+ card_fields. insert ( "count" . to_string ( ) , count_schema) ;
1854+
1855+ let mut card_schemas = HashMap :: new ( ) ;
1856+ card_schemas. insert (
1857+ "test_card" . to_string ( ) ,
1858+ CardSchema {
1859+ name : "test_card" . to_string ( ) ,
1860+ title : None ,
1861+ ui : None ,
1862+ description : "Test card" . to_string ( ) ,
1863+ fields : card_fields,
1864+ } ,
1865+ ) ;
1866+
1867+ let schema = build_schema ( & HashMap :: new ( ) , & card_schemas) . unwrap ( ) ;
1868+
1869+ let mut fields = HashMap :: new ( ) ;
1870+ let card_value = json ! ( {
1871+ "CARD" : "test_card" ,
1872+ "count" : "not a number" // Invalid type
1873+ } ) ;
1874+ fields. insert (
1875+ "CARDS" . to_string ( ) ,
1876+ QuillValue :: from_json ( json ! ( [ card_value] ) ) ,
1877+ ) ;
1878+
1879+ let result = validate_document ( & QuillValue :: from_json ( schema. as_json ( ) . clone ( ) ) , & fields) ;
1880+ assert ! ( result. is_err( ) ) ;
1881+ let errs = result. unwrap_err ( ) ;
1882+
1883+ // We expect a specific error from recursive validation
1884+ let found_specific_error = errs
1885+ . iter ( )
1886+ . any ( |e| e. contains ( "/CARDS/0" ) && e. contains ( "not a number" ) && !e. contains ( "oneOf" ) ) ;
1887+
1888+ assert ! (
1889+ found_specific_error,
1890+ "Did not find specific error msg in: {:?}" ,
1891+ errs
1892+ ) ;
1893+ }
16461894}
0 commit comments