@@ -438,6 +438,158 @@ func TestNestedTypes(t *testing.T) {
438438 }
439439}
440440
441+ func TestMapToStructCompatibility (t * testing.T ) {
442+ // homogeneous struct: {a:int, b:int}
443+ intStructFields := map [string ]* apiservercel.DeclField {
444+ "a" : apiservercel .NewDeclField ("a" , apiservercel .IntType , true , nil , nil ),
445+ "b" : apiservercel .NewDeclField ("b" , apiservercel .IntType , false , nil , nil ),
446+ }
447+ intStruct := apiservercel .NewObjectType (TypeNamePrefix + "intStruct" , intStructFields )
448+
449+ // homogeneous struct: {x:string, y:string}
450+ stringStructFields := map [string ]* apiservercel.DeclField {
451+ "x" : apiservercel .NewDeclField ("x" , apiservercel .StringType , true , nil , nil ),
452+ "y" : apiservercel .NewDeclField ("y" , apiservercel .StringType , true , nil , nil ),
453+ }
454+ stringStruct := apiservercel .NewObjectType (TypeNamePrefix + "stringStruct" , stringStructFields )
455+
456+ provider := NewDeclTypeProvider (intStruct , stringStruct )
457+
458+ tests := []struct {
459+ name string
460+ output * cel.Type
461+ expected * cel.Type
462+ compatible bool
463+ errContains string
464+ }{
465+ {
466+ name : "map[string]int → struct{a:int, b:int} OK" ,
467+ output : cel .MapType (cel .StringType , cel .IntType ),
468+ expected : intStruct .CelType (),
469+ compatible : true ,
470+ },
471+ {
472+ name : "map[string]optional<int> → struct{a:int, b:int} OK" ,
473+ output : cel .MapType (cel .StringType , cel .OptionalType (cel .IntType )),
474+ expected : intStruct .CelType (),
475+ compatible : true ,
476+ },
477+ {
478+ name : "map[string]string → struct{x:string, y:string} OK" ,
479+ output : cel .MapType (cel .StringType , cel .StringType ),
480+ expected : stringStruct .CelType (),
481+ compatible : true ,
482+ },
483+ {
484+ name : "map[string]dyn → struct{x:string, y:string} OK" ,
485+ output : cel .MapType (cel .StringType , cel .DynType ),
486+ expected : stringStruct .CelType (),
487+ compatible : true ,
488+ },
489+ {
490+ name : "map[int]string → struct{x:string,y:string} invalid (key type)" ,
491+ output : cel .MapType (cel .IntType , cel .StringType ),
492+ expected : stringStruct .CelType (),
493+ compatible : false ,
494+ errContains : "keys must be strings" ,
495+ },
496+ {
497+ name : "map[string]int → struct{x:string,y:string} incompatible" ,
498+ output : cel .MapType (cel .StringType , cel .IntType ),
499+ expected : stringStruct .CelType (),
500+ compatible : false ,
501+ errContains : "incompatible" ,
502+ },
503+ }
504+
505+ for _ , tt := range tests {
506+ t .Run (tt .name , func (t * testing.T ) {
507+ compatible , err := AreTypesStructurallyCompatible (tt .output , tt .expected , provider )
508+ assert .Equal (t , tt .compatible , compatible )
509+ if tt .compatible {
510+ assert .NoError (t , err )
511+ } else {
512+ require .Error (t , err )
513+ if tt .errContains != "" {
514+ assert .Contains (t , err .Error (), tt .errContains )
515+ }
516+ }
517+ })
518+ }
519+ }
520+
521+ func TestStructToMapCompatibility (t * testing.T ) {
522+ intStructFields := map [string ]* apiservercel.DeclField {
523+ "a" : apiservercel .NewDeclField ("a" , apiservercel .IntType , true , nil , nil ),
524+ "b" : apiservercel .NewDeclField ("b" , apiservercel .IntType , false , nil , nil ),
525+ }
526+ intStruct := apiservercel .NewObjectType (TypeNamePrefix + "intStruct" , intStructFields )
527+
528+ stringStructFields := map [string ]* apiservercel.DeclField {
529+ "x" : apiservercel .NewDeclField ("x" , apiservercel .StringType , true , nil , nil ),
530+ "y" : apiservercel .NewDeclField ("y" , apiservercel .StringType , true , nil , nil ),
531+ }
532+ stringStruct := apiservercel .NewObjectType (TypeNamePrefix + "stringStruct" , stringStructFields )
533+
534+ provider := NewDeclTypeProvider (intStruct , stringStruct )
535+
536+ tests := []struct {
537+ name string
538+ output * cel.Type
539+ expected * cel.Type
540+ compatible bool
541+ errContains string
542+ }{
543+ {
544+ name : "struct{a:int,b:int} → map[string]int OK" ,
545+ output : intStruct .CelType (),
546+ expected : cel .MapType (cel .StringType , cel .IntType ),
547+ compatible : true ,
548+ },
549+ {
550+ name : "struct{x:string,y:string} → map[string]string OK" ,
551+ output : stringStruct .CelType (),
552+ expected : cel .MapType (cel .StringType , cel .StringType ),
553+ compatible : true ,
554+ },
555+ {
556+ name : "struct{x:string,y:string} → map[string]dyn OK" ,
557+ output : stringStruct .CelType (),
558+ expected : cel .MapType (cel .StringType , cel .DynType ),
559+ compatible : true ,
560+ },
561+ {
562+ name : "struct{x:string,y:string} → map[int]string invalid (bad key)" ,
563+ output : stringStruct .CelType (),
564+ expected : cel .MapType (cel .IntType , cel .StringType ),
565+ compatible : false ,
566+ errContains : "key type must be string" ,
567+ },
568+ {
569+ name : "struct{x:string,y:string} → map[string]int incompatible" ,
570+ output : stringStruct .CelType (),
571+ expected : cel .MapType (cel .StringType , cel .IntType ),
572+ compatible : false ,
573+ errContains : "incompatible" ,
574+ },
575+ }
576+
577+ for _ , tt := range tests {
578+ t .Run (tt .name , func (t * testing.T ) {
579+ compatible , err := AreTypesStructurallyCompatible (tt .output , tt .expected , provider )
580+ assert .Equal (t , tt .compatible , compatible )
581+ if tt .compatible {
582+ assert .NoError (t , err )
583+ } else {
584+ require .Error (t , err )
585+ if tt .errContains != "" {
586+ assert .Contains (t , err .Error (), tt .errContains )
587+ }
588+ }
589+ })
590+ }
591+ }
592+
441593func TestOptionalPrimitive (t * testing.T ) {
442594 optionalString := cel .OpaqueType ("optional_type" , cel .StringType )
443595 optionalInt := cel .OpaqueType ("optional_type" , cel .IntType )
0 commit comments