@@ -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 SearchMetadataItem.
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 SearchMetadataItem ) ExtractValue () (any , error ) {
42+ switch item .Type {
43+ case SearchMetadataTypeString :
44+ return item .extractStringValue ()
45+ case SearchMetadataTypeListString :
46+ return item .extractListStringValue ()
47+ case SearchMetadataTypeMapStringBoolean :
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 SearchMetadataItem.
55+ // This is an internal method used by ExtractValue.
56+ func (item SearchMetadataItem ) 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 SearchMetadataItem.
68+ // This is an internal method used by ExtractValue.
69+ func (item SearchMetadataItem ) 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 SearchMetadataItem.
96+ // This is an internal method used by ExtractValue.
97+ func (item SearchMetadataItem ) 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+ // validateSearchMetadataItem validates a single SearchMetadataItem.
125+ // This is an internal helper function used during JSON unmarshaling.
126+ func validateSearchMetadataItem (item SearchMetadataItem ) 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+
38143type 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+ // SearchMetadataItem represents a single search metadata item with a name, type, and value.
197+ // Supported types are defined by the SearchMetadataType* constants.
198+ type SearchMetadataItem struct {
199+ Name string `json:"name"` // The name/key of the search 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+ // SearchMetadata represents a collection of search metadata items.
205+ // It validates that all items are valid and that there are no duplicate names.
206+ type SearchMetadata []SearchMetadataItem
207+
208+ // UnmarshalJSON implements custom JSON unmarshaling for SearchMetadata.
209+ // It validates each item and ensures there are no duplicate names.
210+ func (sm * SearchMetadata ) UnmarshalJSON (data []byte ) error {
211+ // First unmarshal into a slice of SearchMetadataItem
212+ var items []SearchMetadataItem
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 := validateSearchMetadataItem (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+ * sm = SearchMetadata (items )
233+ return nil
234+ }
235+
91236type 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+ SearchMetadatas []SearchMetadata `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+ TypeSearchMetadata = "olm.search.metadata"
110257 TypeConstraint = "olm.constraint"
111258 TypeChannel = "olm.channel"
112259)
113260
261+ // Search metadata item type constants define the supported types for SearchMetadataItem values.
262+ const (
263+ SearchMetadataTypeString = "String"
264+ SearchMetadataTypeListString = "ListString"
265+ SearchMetadataTypeMapStringBoolean = "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+
114279func 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+ TypeSearchMetadata : func (p Property ) error { return appendParsed (& out .SearchMetadatas , 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+
174348func 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+ // MustBuildSearchMetadata creates a search metadata property from a SearchMetadata.
457+ // It panics if the items are invalid or if there are duplicate names.
458+ func MustBuildSearchMetadata (searchMetadata SearchMetadata ) Property {
459+ return MustBuild (& searchMetadata )
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