@@ -29,14 +29,15 @@ const (
2929 TypeNamePrefix = "__type_"
3030)
3131
32- // AreTypesStructurallyCompatible checks if two CEL types are structurally compatible,
33- // ignoring type names and using duck-typing semantics .
32+ // AreTypesStructurallyCompatible checks if an output type from an expected or executed CEL expression is compatible
33+ // with an expected type .
3434//
3535// This performs deep structural comparison:
3636// - For primitives: checks kind equality
3737// - For lists: recursively checks element type compatibility
3838// - For maps: recursively checks key and value type compatibility
3939// - For structs: uses DeclTypeProvider to introspect fields and check all required fields exist with compatible types
40+ // - For map → struct and struct → map compatibility if fields/keys are structurally compatible
4041//
4142// The provider is required for introspecting struct field information.
4243// Returns true if types are compatible, false otherwise. If false, the error describes why.
@@ -50,20 +51,28 @@ func AreTypesStructurallyCompatible(output, expected *cel.Type, provider *DeclTy
5051 return true , nil
5152 }
5253
53- // Kinds must match
54- if output .Kind () != expected . Kind () {
55- return false , fmt . Errorf ( "type kind mismatch: got %q, expected %q" , output .String () , expected . String () )
54+ // Unwrap optional output if available
55+ if output .Kind () == cel . OpaqueKind && output . TypeName () == "optional_type" {
56+ return AreTypesStructurallyCompatible ( output .Parameters ()[ 0 ] , expected , provider )
5657 }
5758
58- switch expected .Kind () {
59- case cel .ListKind :
59+ switch {
60+ case expected .Kind () == cel .StructKind && output .Kind () == cel .MapKind :
61+ return areMapTypesAssignableToStruct (output , expected , provider )
62+ case expected .Kind () == cel .MapKind && output .Kind () == cel .StructKind :
63+ return areStructTypesAssignableToMap (output , expected , provider )
64+ case expected .Kind () == cel .ListKind :
6065 return areListTypesCompatible (output , expected , provider )
61- case cel .MapKind :
66+ case expected . Kind () == cel .MapKind :
6267 return areMapTypesCompatible (output , expected , provider )
63- case cel .StructKind :
68+ case expected . Kind () == cel .StructKind :
6469 return areStructTypesCompatible (output , expected , provider )
6570 default :
66- // For primitives (int, string, bool, etc.), kind equality is enough
71+ // Kinds must match otherwise
72+ if output .Kind () != expected .Kind () {
73+ return false , fmt .Errorf ("type kind mismatch: got %q, expected %q" , output .String (), expected .String ())
74+ }
75+ // primitives: kind equality already checked
6776 return true , nil
6877 }
6978}
@@ -219,6 +228,10 @@ func areStructFieldsCompatible(output, expected *apiservercel.DeclType, provider
219228 if expected == nil {
220229 return true , nil
221230 }
231+ // PreserveUnknownFields is set on the expected type, so everything we would pass from output would be okay
232+ if expected .Metadata [XKubernetesPreserveUnknownFields ] == "true" {
233+ return true , nil
234+ }
222235
223236 if output == nil {
224237 return false , fmt .Errorf ("output type is nil" )
@@ -268,3 +281,70 @@ func areStructFieldsCompatible(output, expected *apiservercel.DeclType, provider
268281
269282 return true , nil
270283}
284+
285+ func areMapTypesAssignableToStruct (outputMap , expectedStruct * cel.Type , provider * DeclTypeProvider ) (bool , error ) {
286+ expectedDecl := resolveDeclTypeFromPath (expectedStruct .String (), provider )
287+ if expectedDecl == nil || expectedDecl .Fields == nil {
288+ return true , nil
289+ }
290+
291+ // map parameters are [keyType, valueType]
292+ params := outputMap .Parameters ()
293+ if len (params ) < 2 {
294+ return false , fmt .Errorf ("map must have key and value types" )
295+ }
296+
297+ keyType := params [0 ]
298+ valType := params [1 ]
299+
300+ // keys must be strings to match struct field names
301+ if keyType .Kind () != cel .StringKind {
302+ return false , fmt .Errorf ("map keys must be strings to assign to struct" )
303+ }
304+
305+ for fieldName , expectedField := range expectedDecl .Fields {
306+ expectedFieldCEL := expectedField .Type .CelType ()
307+ if expectedFieldCEL == nil {
308+ continue
309+ }
310+ compatible , err := AreTypesStructurallyCompatible (valType , expectedFieldCEL , provider )
311+ if ! compatible {
312+ return false , fmt .Errorf ("map value incompatible with struct field %q: %w" , fieldName , err )
313+ }
314+ }
315+
316+ return true , nil
317+ }
318+
319+ func areStructTypesAssignableToMap (outputStruct , expectedMap * cel.Type , provider * DeclTypeProvider ) (bool , error ) {
320+ outputDecl := resolveDeclTypeFromPath (outputStruct .String (), provider )
321+ if outputDecl == nil || outputDecl .Fields == nil {
322+ return true , nil
323+ }
324+
325+ params := expectedMap .Parameters ()
326+ if len (params ) < 2 {
327+ return false , fmt .Errorf ("expected map must have key and value types" )
328+ }
329+ keyType := params [0 ]
330+ valType := params [1 ]
331+
332+ // struct field names map to string keys
333+ if keyType .Kind () != cel .StringKind {
334+ return false , fmt .Errorf ("map key type must be string when assigning struct → map" )
335+ }
336+
337+ for fieldName , outputField := range outputDecl .Fields {
338+ outputCEL := outputField .Type .CelType ()
339+ if outputCEL == nil {
340+ continue
341+ }
342+
343+ compatible , err := AreTypesStructurallyCompatible (outputCEL , valType , provider )
344+ if ! compatible {
345+ return false , fmt .Errorf ("struct field %q incompatible with map value type: %w" , fieldName , err )
346+ }
347+ }
348+
349+ return true , nil
350+ }
0 commit comments