@@ -35,6 +35,111 @@ func (p Property) String() string {
35
35
return fmt .Sprintf ("type: %q, value: %q" , p .Type , p .Value )
36
36
}
37
37
38
+ // ExtractValue extracts and validates the value from a FilterMetadataItem.
39
+ // It returns the properly typed value (string, []string, or map[string]bool) as an interface{}.
40
+ // The returned value is guaranteed to be valid according to the item's Type field.
41
+ func (item FilterMetadataItem ) ExtractValue () (any , error ) {
42
+ switch item .Type {
43
+ case FilterMetadataTypeString :
44
+ return item .extractStringValue ()
45
+ case FilterMetadataTypeListString :
46
+ return item .extractListStringValue ()
47
+ case FilterMetadataTypeMapStringBoolean :
48
+ return item .extractMapStringBooleanValue ()
49
+ default :
50
+ return nil , fmt .Errorf ("unsupported type: %s" , item .Type )
51
+ }
52
+ }
53
+
54
+ // extractStringValue extracts and validates a string value from a FilterMetadataItem.
55
+ // This is an internal method used by ExtractValue.
56
+ func (item FilterMetadataItem ) extractStringValue () (string , error ) {
57
+ str , ok := item .Value .(string )
58
+ if ! ok {
59
+ return "" , fmt .Errorf ("type is 'String' but value is not a string: %T" , item .Value )
60
+ }
61
+ if len (str ) == 0 {
62
+ return "" , errors .New ("string value must have length >= 1" )
63
+ }
64
+ return str , nil
65
+ }
66
+
67
+ // extractListStringValue extracts and validates a []string value from a FilterMetadataItem.
68
+ // This is an internal method used by ExtractValue.
69
+ func (item FilterMetadataItem ) extractListStringValue () ([]string , error ) {
70
+ switch v := item .Value .(type ) {
71
+ case []string :
72
+ for i , str := range v {
73
+ if len (str ) == 0 {
74
+ return nil , fmt .Errorf ("ListString item[%d] must have length >= 1" , i )
75
+ }
76
+ }
77
+ return v , nil
78
+ case []interface {}:
79
+ result := make ([]string , len (v ))
80
+ for i , val := range v {
81
+ if str , ok := val .(string ); ! ok {
82
+ return nil , fmt .Errorf ("ListString item[%d] is not a string: %T" , i , val )
83
+ } else if len (str ) == 0 {
84
+ return nil , fmt .Errorf ("ListString item[%d] must have length >= 1" , i )
85
+ } else {
86
+ result [i ] = str
87
+ }
88
+ }
89
+ return result , nil
90
+ default :
91
+ return nil , fmt .Errorf ("type is 'ListString' but value is not a string list: %T" , item .Value )
92
+ }
93
+ }
94
+
95
+ // extractMapStringBooleanValue extracts and validates a map[string]bool value from a FilterMetadataItem.
96
+ // This is an internal method used by ExtractValue.
97
+ func (item FilterMetadataItem ) extractMapStringBooleanValue () (map [string ]bool , error ) {
98
+ switch v := item .Value .(type ) {
99
+ case map [string ]bool :
100
+ for key := range v {
101
+ if len (key ) == 0 {
102
+ return nil , errors .New ("MapStringBoolean keys must have length >= 1" )
103
+ }
104
+ }
105
+ return v , nil
106
+ case map [string ]interface {}:
107
+ result := make (map [string ]bool )
108
+ for key , val := range v {
109
+ if len (key ) == 0 {
110
+ return nil , errors .New ("MapStringBoolean keys must have length >= 1" )
111
+ }
112
+ if boolVal , ok := val .(bool ); ! ok {
113
+ return nil , fmt .Errorf ("MapStringBoolean value for key '%s' is not a boolean: %T" , key , val )
114
+ } else {
115
+ result [key ] = boolVal
116
+ }
117
+ }
118
+ return result , nil
119
+ default :
120
+ return nil , fmt .Errorf ("type is 'MapStringBoolean' but value is not a string-to-boolean map: %T" , item .Value )
121
+ }
122
+ }
123
+
124
+ // validateFilterMetadataItem validates a single FilterMetadataItem.
125
+ // This is an internal helper function used during JSON unmarshaling.
126
+ func validateFilterMetadataItem (item FilterMetadataItem ) error {
127
+ if item .Name == "" {
128
+ return errors .New ("name must be set" )
129
+ }
130
+ if item .Type == "" {
131
+ return errors .New ("type must be set" )
132
+ }
133
+ if item .Value == nil {
134
+ return errors .New ("value must be set" )
135
+ }
136
+
137
+ if _ , err := item .ExtractValue (); err != nil {
138
+ return err
139
+ }
140
+ return nil
141
+ }
142
+
38
143
type Package struct {
39
144
PackageName string `json:"packageName"`
40
145
Version string `json:"version"`
@@ -88,6 +193,46 @@ type CSVMetadata struct {
88
193
Provider v1alpha1.AppLink `json:"provider,omitempty"`
89
194
}
90
195
196
+ // FilterMetadataItem represents a single filter metadata item with a name, type, and value.
197
+ // Supported types are defined by the FilterMetadataType* constants.
198
+ type FilterMetadataItem struct {
199
+ Name string `json:"name"` // The name/key of the filter metadata
200
+ Type string `json:"type"` // The type of the value (String, ListString, MapStringBoolean)
201
+ Value interface {} `json:"value"` // The actual value, validated according to Type
202
+ }
203
+
204
+ // FilterMetadata represents a collection of filter metadata items.
205
+ // It validates that all items are valid and that there are no duplicate names.
206
+ type FilterMetadata []FilterMetadataItem
207
+
208
+ // UnmarshalJSON implements custom JSON unmarshaling for FilterMetadata.
209
+ // It validates each item and ensures there are no duplicate names.
210
+ func (fm * FilterMetadata ) UnmarshalJSON (data []byte ) error {
211
+ // First unmarshal into a slice of FilterMetadataItem
212
+ var items []FilterMetadataItem
213
+ if err := json .Unmarshal (data , & items ); err != nil {
214
+ return err
215
+ }
216
+
217
+ // Validate each item and check for duplicate names
218
+ namesSeen := make (map [string ]bool )
219
+ for i , item := range items {
220
+ if err := validateFilterMetadataItem (item ); err != nil {
221
+ return fmt .Errorf ("item[%d]: %v" , i , err )
222
+ }
223
+
224
+ // Check for duplicate names
225
+ if namesSeen [item .Name ] {
226
+ return fmt .Errorf ("item[%d]: duplicate name '%s'" , i , item .Name )
227
+ }
228
+ namesSeen [item .Name ] = true
229
+ }
230
+
231
+ // Set the validated items
232
+ * fm = FilterMetadata (items )
233
+ return nil
234
+ }
235
+
91
236
type Properties struct {
92
237
Packages []Package `hash:"set"`
93
238
PackagesRequired []PackageRequired `hash:"set"`
@@ -96,6 +241,7 @@ type Properties struct {
96
241
BundleObjects []BundleObject `hash:"set"`
97
242
Channels []Channel `hash:"set"`
98
243
CSVMetadatas []CSVMetadata `hash:"set"`
244
+ FilterMetadatas []FilterMetadata `hash:"set"`
99
245
100
246
Others []Property `hash:"set"`
101
247
}
@@ -107,70 +253,98 @@ const (
107
253
TypeGVKRequired = "olm.gvk.required"
108
254
TypeBundleObject = "olm.bundle.object"
109
255
TypeCSVMetadata = "olm.csv.metadata"
256
+ TypeFilterMetadata = "olm.filterMetadata"
110
257
TypeConstraint = "olm.constraint"
111
258
TypeChannel = "olm.channel"
112
259
)
113
260
261
+ // Filter metadata item type constants define the supported types for FilterMetadataItem values.
262
+ const (
263
+ FilterMetadataTypeString = "String"
264
+ FilterMetadataTypeListString = "ListString"
265
+ FilterMetadataTypeMapStringBoolean = "MapStringBoolean"
266
+ )
267
+
268
+ // appendParsed is a generic helper function that parses a property and appends it to a slice.
269
+ // This is an internal helper used by the Parse function to reduce code duplication.
270
+ func appendParsed [T any ](slice * []T , prop Property ) error {
271
+ parsed , err := ParseOne [T ](prop )
272
+ if err != nil {
273
+ return err
274
+ }
275
+ * slice = append (* slice , parsed )
276
+ return nil
277
+ }
278
+
114
279
func Parse (in []Property ) (* Properties , error ) {
115
280
var out Properties
281
+
282
+ // Map of property types to their parsing functions that directly append to output slices
283
+ parsers := map [string ]func (Property ) error {
284
+ TypePackage : func (p Property ) error { return appendParsed (& out .Packages , p ) },
285
+ TypePackageRequired : func (p Property ) error { return appendParsed (& out .PackagesRequired , p ) },
286
+ TypeGVK : func (p Property ) error { return appendParsed (& out .GVKs , p ) },
287
+ TypeGVKRequired : func (p Property ) error { return appendParsed (& out .GVKsRequired , p ) },
288
+ TypeBundleObject : func (p Property ) error { return appendParsed (& out .BundleObjects , p ) },
289
+ TypeCSVMetadata : func (p Property ) error { return appendParsed (& out .CSVMetadatas , p ) },
290
+ TypeFilterMetadata : func (p Property ) error { return appendParsed (& out .FilterMetadatas , p ) },
291
+ TypeChannel : func (p Property ) error { return appendParsed (& out .Channels , p ) },
292
+ }
293
+
294
+ // Parse each property using the appropriate parser
116
295
for i , prop := range in {
117
- switch prop .Type {
118
- case TypePackage :
119
- var p Package
120
- if err := json .Unmarshal (prop .Value , & p ); err != nil {
296
+ if parser , exists := parsers [prop .Type ]; exists {
297
+ if err := parser (prop ); err != nil {
121
298
return nil , ParseError {Idx : i , Typ : prop .Type , Err : err }
122
299
}
123
- out .Packages = append (out .Packages , p )
124
- case TypePackageRequired :
125
- var p PackageRequired
126
- if err := json .Unmarshal (prop .Value , & p ); err != nil {
127
- return nil , ParseError {Idx : i , Typ : prop .Type , Err : err }
128
- }
129
- out .PackagesRequired = append (out .PackagesRequired , p )
130
- case TypeGVK :
131
- var p GVK
132
- if err := json .Unmarshal (prop .Value , & p ); err != nil {
133
- return nil , ParseError {Idx : i , Typ : prop .Type , Err : err }
134
- }
135
- out .GVKs = append (out .GVKs , p )
136
- case TypeGVKRequired :
137
- var p GVKRequired
138
- if err := json .Unmarshal (prop .Value , & p ); err != nil {
139
- return nil , ParseError {Idx : i , Typ : prop .Type , Err : err }
140
- }
141
- out .GVKsRequired = append (out .GVKsRequired , p )
142
- case TypeBundleObject :
143
- var p BundleObject
144
- if err := json .Unmarshal (prop .Value , & p ); err != nil {
145
- return nil , ParseError {Idx : i , Typ : prop .Type , Err : err }
146
- }
147
- out .BundleObjects = append (out .BundleObjects , p )
148
- case TypeCSVMetadata :
149
- var p CSVMetadata
150
- if err := json .Unmarshal (prop .Value , & p ); err != nil {
151
- return nil , ParseError {Idx : i , Typ : prop .Type , Err : err }
152
- }
153
- out .CSVMetadatas = append (out .CSVMetadatas , p )
154
- // NOTICE: The Channel properties are for internal use only.
155
- // DO NOT use it for any public-facing functionalities.
156
- // This API is in alpha stage and it is subject to change.
157
- case TypeChannel :
158
- var p Channel
159
- if err := json .Unmarshal (prop .Value , & p ); err != nil {
160
- return nil , ParseError {Idx : i , Typ : prop .Type , Err : err }
161
- }
162
- out .Channels = append (out .Channels , p )
163
- default :
300
+ } else {
301
+ // For unknown types, use direct unmarshaling to preserve existing behavior
164
302
var p json.RawMessage
165
303
if err := json .Unmarshal (prop .Value , & p ); err != nil {
166
304
return nil , ParseError {Idx : i , Typ : prop .Type , Err : err }
167
305
}
168
306
out .Others = append (out .Others , prop )
169
307
}
170
308
}
309
+
171
310
return & out , nil
172
311
}
173
312
313
+ // ParseOne parses a single property into the specified type T.
314
+ // It validates that the property's Type field matches what the scheme expects for type T,
315
+ // ensuring type safety between the property metadata and the generic type parameter.
316
+ func ParseOne [T any ](p Property ) (T , error ) {
317
+ var zero T
318
+
319
+ // Get the type of T
320
+ targetType := reflect .TypeOf ((* T )(nil )).Elem ()
321
+
322
+ // Check if T is a pointer type, if so get the element type
323
+ if targetType .Kind () == reflect .Ptr {
324
+ targetType = targetType .Elem ()
325
+ }
326
+
327
+ // Look up the expected property type for this Go type
328
+ expectedPropertyType , ok := scheme [reflect .PointerTo (targetType )]
329
+ if ! ok {
330
+ return zero , fmt .Errorf ("type %s is not registered in the scheme" , targetType )
331
+ }
332
+
333
+ // Verify the property type matches what we expect
334
+ if p .Type != expectedPropertyType {
335
+ return zero , fmt .Errorf ("property type %q does not match expected type %q for %s" , p .Type , expectedPropertyType , targetType )
336
+ }
337
+
338
+ // Unmarshal the property value into the target type
339
+ // Any validation will happen automatically via custom UnmarshalJSON methods
340
+ var result T
341
+ if err := json .Unmarshal (p .Value , & result ); err != nil {
342
+ return zero , fmt .Errorf ("failed to unmarshal property value: %v" , err )
343
+ }
344
+
345
+ return result , nil
346
+ }
347
+
174
348
func Deduplicate (in []Property ) []Property {
175
349
type key struct {
176
350
typ string
@@ -279,6 +453,12 @@ func MustBuildCSVMetadata(csv v1alpha1.ClusterServiceVersion) Property {
279
453
})
280
454
}
281
455
456
+ // MustBuildFilterMetadata creates a filter metadata property from a FilterMetadata.
457
+ // It panics if the items are invalid or if there are duplicate names.
458
+ func MustBuildFilterMetadata (filterMetadata FilterMetadata ) Property {
459
+ return MustBuild (& filterMetadata )
460
+ }
461
+
282
462
// NOTICE: The Channel properties are for internal use only.
283
463
//
284
464
// DO NOT use it for any public-facing functionalities.
0 commit comments