@@ -437,3 +437,222 @@ func TestNestedTypes(t *testing.T) {
437437 })
438438 }
439439}
440+
441+ func TestOptionalPrimitive (t * testing.T ) {
442+ optionalString := cel .OpaqueType ("optional_type" , cel .StringType )
443+ optionalInt := cel .OpaqueType ("optional_type" , cel .IntType )
444+
445+ tests := []struct {
446+ name string
447+ output * cel.Type
448+ expected * cel.Type
449+ compatible bool
450+ errContains string
451+ }{
452+ {
453+ name : "optional<string> to string" ,
454+ output : optionalString ,
455+ expected : cel .StringType ,
456+ compatible : true ,
457+ },
458+ {
459+ name : "optional<string> to int" ,
460+ output : optionalString ,
461+ expected : cel .IntType ,
462+ compatible : false ,
463+ errContains : "kind mismatch" ,
464+ },
465+ {
466+ name : "optional<int> to string" ,
467+ output : optionalInt ,
468+ expected : cel .StringType ,
469+ compatible : false ,
470+ errContains : "kind mismatch" ,
471+ },
472+ {
473+ name : "optional(optional(string)) to string" ,
474+ output : cel .OpaqueType ("optional_type" , optionalString ),
475+ expected : cel .StringType ,
476+ compatible : true ,
477+ },
478+ }
479+
480+ for _ , tt := range tests {
481+ t .Run (tt .name , func (t * testing.T ) {
482+ compatible , err := AreTypesStructurallyCompatible (tt .output , tt .expected , nil )
483+ assert .Equal (t , tt .compatible , compatible )
484+ if tt .compatible {
485+ assert .NoError (t , err )
486+ } else {
487+ require .Error (t , err )
488+ assert .Contains (t , err .Error (), tt .errContains )
489+ }
490+ })
491+ }
492+ }
493+
494+ func TestOptionalListTypes (t * testing.T ) {
495+ optionalString := cel .OpaqueType ("optional_type" , cel .StringType )
496+ optionalListString := cel .OpaqueType ("optional_type" , cel .ListType (cel .StringType ))
497+
498+ tests := []struct {
499+ name string
500+ output * cel.Type
501+ expected * cel.Type
502+ compatible bool
503+ errContains string
504+ }{
505+ {
506+ name : "optional<string> list element → list<string>" ,
507+ output : cel .ListType (optionalString ),
508+ expected : cel .ListType (cel .StringType ),
509+ compatible : true ,
510+ },
511+ {
512+ name : "optional<list<string>> → list<string>" ,
513+ output : optionalListString ,
514+ expected : cel .ListType (cel .StringType ),
515+ compatible : true ,
516+ },
517+ {
518+ name : "list<optional<string>> → list<int>" ,
519+ output : cel .ListType (optionalString ),
520+ expected : cel .ListType (cel .IntType ),
521+ compatible : false ,
522+ errContains : "list element type incompatible" ,
523+ },
524+ }
525+
526+ for _ , tt := range tests {
527+ t .Run (tt .name , func (t * testing.T ) {
528+ compatible , err := AreTypesStructurallyCompatible (tt .output , tt .expected , nil )
529+ assert .Equal (t , tt .compatible , compatible )
530+ if tt .compatible {
531+ assert .NoError (t , err )
532+ } else {
533+ require .Error (t , err )
534+ assert .Contains (t , err .Error (), tt .errContains )
535+ }
536+ })
537+ }
538+ }
539+
540+ func TestOptionalMapTypes (t * testing.T ) {
541+ optionalString := cel .OpaqueType ("optional_type" , cel .StringType )
542+
543+ tests := []struct {
544+ name string
545+ output * cel.Type
546+ expected * cel.Type
547+ compatible bool
548+ errContains string
549+ }{
550+ {
551+ name : "map[string]optional<string> → map[string]string" ,
552+ output : cel .MapType (cel .StringType , optionalString ),
553+ expected : cel .MapType (cel .StringType , cel .StringType ),
554+ compatible : true ,
555+ },
556+ {
557+ name : "map[string]optional<string> → map[string]int" ,
558+ output : cel .MapType (cel .StringType , optionalString ),
559+ expected : cel .MapType (cel .StringType , cel .IntType ),
560+ compatible : false ,
561+ errContains : "map value type incompatible" ,
562+ },
563+ {
564+ name : "map[optional<string>]string → map[string]string (key)" ,
565+ output : cel .MapType (optionalString , cel .StringType ),
566+ expected : cel .MapType (cel .StringType , cel .StringType ),
567+ compatible : true , // optional<string> → string ok
568+ },
569+ {
570+ name : "map[optional<string>]string → map[int]string" ,
571+ output : cel .MapType (optionalString , cel .StringType ),
572+ expected : cel .MapType (cel .IntType , cel .StringType ),
573+ compatible : false ,
574+ errContains : "map key type incompatible" ,
575+ },
576+ }
577+
578+ for _ , tt := range tests {
579+ t .Run (tt .name , func (t * testing.T ) {
580+ compatible , err := AreTypesStructurallyCompatible (tt .output , tt .expected , nil )
581+ assert .Equal (t , tt .compatible , compatible )
582+ if tt .compatible {
583+ assert .NoError (t , err )
584+ } else {
585+ require .Error (t , err )
586+ assert .Contains (t , err .Error (), tt .errContains )
587+ }
588+ })
589+ }
590+ }
591+
592+ func TestOptionalStructTypes (t * testing.T ) {
593+ fields := map [string ]* apiservercel.DeclField {
594+ "name" : apiservercel .NewDeclField ("name" , apiservercel .StringType , true , nil , nil ),
595+ }
596+ personType := apiservercel .NewObjectType (TypeNamePrefix + "person" , fields )
597+ otherType := apiservercel .NewObjectType (TypeNamePrefix + "other" , fields )
598+
599+ unknownFieldsType := apiservercel .NewObjectType (TypeNamePrefix + "unknownFields" , map [string ]* apiservercel.DeclField {})
600+ unknownFieldsType .Metadata = map [string ]string {XKubernetesPreserveUnknownFields : "true" }
601+
602+ optionalString := cel .OptionalType (cel .StringType )
603+ optionalPerson := cel .OptionalType (personType .CelType ())
604+
605+ provider := NewDeclTypeProvider (personType , otherType , unknownFieldsType )
606+
607+ tests := []struct {
608+ name string
609+ output * cel.Type
610+ expected * cel.Type
611+ compatible bool
612+ errContains string
613+ }{
614+ {
615+ name : "optional<string> field inside struct" ,
616+ output : cel .ListType (optionalString ), // simulate a struct field → list<string>
617+ expected : cel .ListType (cel .StringType ),
618+ compatible : true ,
619+ },
620+ {
621+ name : "optional<person> → person" ,
622+ output : optionalPerson ,
623+ expected : personType .CelType (),
624+ compatible : true ,
625+ },
626+ {
627+ name : "optional<person> → empty struct ok (permissive right now, could be rejected as well)" ,
628+ output : optionalPerson ,
629+ expected : apiservercel .NewObjectType (TypeNamePrefix + "noFields" , nil ).CelType (),
630+ compatible : true ,
631+ },
632+ {
633+ name : "optional<person> → matching struct" ,
634+ output : optionalPerson ,
635+ expected : otherType .CelType (),
636+ compatible : true ,
637+ },
638+ {
639+ name : "optional<person> → unknown field allowing type" ,
640+ output : optionalPerson ,
641+ expected : unknownFieldsType .CelType (),
642+ compatible : true ,
643+ },
644+ }
645+
646+ for _ , tt := range tests {
647+ t .Run (tt .name , func (t * testing.T ) {
648+ compatible , err := AreTypesStructurallyCompatible (tt .output , tt .expected , provider )
649+ assert .Equal (t , tt .compatible , compatible )
650+ if tt .compatible {
651+ assert .NoError (t , err )
652+ } else {
653+ require .Error (t , err )
654+ assert .Contains (t , err .Error (), tt .errContains )
655+ }
656+ })
657+ }
658+ }
0 commit comments