Skip to content

Commit db1e6c3

Browse files
committed
add olm.filterMetadata property; property parsing improvements
Signed-off-by: Joe Lanford <joe.lanford@gmail.com>
1 parent ef3bfdf commit db1e6c3

File tree

3 files changed

+806
-46
lines changed

3 files changed

+806
-46
lines changed

alpha/property/property.go

Lines changed: 225 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,111 @@ func (p Property) String() string {
3535
return fmt.Sprintf("type: %q, value: %q", p.Type, p.Value)
3636
}
3737

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+
38143
type Package struct {
39144
PackageName string `json:"packageName"`
40145
Version string `json:"version"`
@@ -88,6 +193,46 @@ type CSVMetadata struct {
88193
Provider v1alpha1.AppLink `json:"provider,omitempty"`
89194
}
90195

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+
91236
type Properties struct {
92237
Packages []Package `hash:"set"`
93238
PackagesRequired []PackageRequired `hash:"set"`
@@ -96,6 +241,7 @@ type Properties struct {
96241
BundleObjects []BundleObject `hash:"set"`
97242
Channels []Channel `hash:"set"`
98243
CSVMetadatas []CSVMetadata `hash:"set"`
244+
FilterMetadatas []FilterMetadata `hash:"set"`
99245

100246
Others []Property `hash:"set"`
101247
}
@@ -107,70 +253,98 @@ const (
107253
TypeGVKRequired = "olm.gvk.required"
108254
TypeBundleObject = "olm.bundle.object"
109255
TypeCSVMetadata = "olm.csv.metadata"
256+
TypeFilterMetadata = "olm.filterMetadata"
110257
TypeConstraint = "olm.constraint"
111258
TypeChannel = "olm.channel"
112259
)
113260

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+
114279
func Parse(in []Property) (*Properties, error) {
115280
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
116295
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 {
121298
return nil, ParseError{Idx: i, Typ: prop.Type, Err: err}
122299
}
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
164302
var p json.RawMessage
165303
if err := json.Unmarshal(prop.Value, &p); err != nil {
166304
return nil, ParseError{Idx: i, Typ: prop.Type, Err: err}
167305
}
168306
out.Others = append(out.Others, prop)
169307
}
170308
}
309+
171310
return &out, nil
172311
}
173312

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+
174348
func Deduplicate(in []Property) []Property {
175349
type key struct {
176350
typ string
@@ -279,6 +453,12 @@ func MustBuildCSVMetadata(csv v1alpha1.ClusterServiceVersion) Property {
279453
})
280454
}
281455

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+
282462
// NOTICE: The Channel properties are for internal use only.
283463
//
284464
// DO NOT use it for any public-facing functionalities.

0 commit comments

Comments
 (0)