Skip to content

Commit d974c05

Browse files
committed
alpha/declcfg/filter: implement filtering logic based on new olm.filterMetadata property
Signed-off-by: Joe Lanford <joe.lanford@gmail.com>
1 parent de6e61d commit d974c05

File tree

4 files changed

+912
-0
lines changed

4 files changed

+912
-0
lines changed

alpha/declcfg/filter/filter.go

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
// Package filter provides functionality for filtering File-Based Catalog metadata
2+
// based on search metadata properties. It supports filtering by string, list, and map
3+
// metadata types with flexible matching criteria and combination logic.
4+
package filter
5+
6+
import (
7+
"encoding/json"
8+
"fmt"
9+
10+
"github.com/operator-framework/operator-registry/alpha/declcfg"
11+
"github.com/operator-framework/operator-registry/alpha/property"
12+
)
13+
14+
// Result represents the result of evaluating a single filter criterion
15+
type Result struct {
16+
Name string // The name of the filter criterion
17+
Matched bool // Whether the criterion matched
18+
}
19+
20+
// MatchFunc defines how multiple filter criteria should be combined
21+
type MatchFunc func(results []Result) bool
22+
23+
// All returns true only if all criteria match (AND logic)
24+
func All(results []Result) bool {
25+
for _, result := range results {
26+
if !result.Matched {
27+
return false
28+
}
29+
}
30+
return true
31+
}
32+
33+
// Any returns true if any criteria matches (OR logic)
34+
func Any(results []Result) bool {
35+
for _, result := range results {
36+
if result.Matched {
37+
return true
38+
}
39+
}
40+
return false
41+
}
42+
43+
// ValueMatchFunc defines how values within a single criterion should be matched
44+
type ValueMatchFunc func(metadataValues, filterValues []string) bool
45+
46+
// anyValue returns true if metadata contains any of the filter values.
47+
// This is an internal value matching function used by HasAny criteria.
48+
func anyValue(metadataValues, filterValues []string) bool {
49+
metadataSet := make(map[string]bool)
50+
for _, v := range metadataValues {
51+
metadataSet[v] = true
52+
}
53+
54+
for _, filterValue := range filterValues {
55+
if metadataSet[filterValue] {
56+
return true
57+
}
58+
}
59+
return false
60+
}
61+
62+
// allValues returns true if metadata contains all of the filter values.
63+
// This is an internal value matching function used by HasAll criteria.
64+
func allValues(metadataValues, filterValues []string) bool {
65+
metadataSet := make(map[string]bool)
66+
for _, v := range metadataValues {
67+
metadataSet[v] = true
68+
}
69+
70+
for _, filterValue := range filterValues {
71+
if !metadataSet[filterValue] {
72+
return false
73+
}
74+
}
75+
return true
76+
}
77+
78+
// Filter holds the configuration for filtering metadata based on filter criteria.
79+
// It can be used to evaluate whether metadata objects match specified conditions.
80+
type Filter struct {
81+
// criteria are the individual filter criteria
82+
criteria []criterion
83+
// matchFunc determines how multiple filter criteria should be combined
84+
matchFunc MatchFunc
85+
}
86+
87+
// criterion represents a single filter criterion
88+
type criterion struct {
89+
name string
90+
values []string
91+
matchFunc ValueMatchFunc
92+
}
93+
94+
// New creates a new Filter with the specified match function.
95+
// The match function determines how multiple filter criteria are combined (e.g., All, Any).
96+
func New(matchFunc MatchFunc) *Filter {
97+
return &Filter{
98+
matchFunc: matchFunc,
99+
}
100+
}
101+
102+
// HasAny adds a filter criterion that matches if the metadata contains any of the specified values.
103+
// For string metadata, it checks if the value matches any of the provided values.
104+
// For list metadata, it checks if any list element matches any of the provided values.
105+
// For map metadata, it checks if any key with a true value matches any of the provided values.
106+
func (f *Filter) HasAny(name string, values ...string) *Filter {
107+
f.criteria = append(f.criteria, criterion{
108+
name: name,
109+
values: values,
110+
matchFunc: anyValue,
111+
})
112+
return f
113+
}
114+
115+
// HasAll adds a filter criterion that matches if the metadata contains all of the specified values.
116+
// For string metadata, it checks if the value matches all of the provided values (typically used with a single value).
117+
// For list metadata, it checks if the list contains all of the provided values.
118+
// For map metadata, it checks if all of the provided values exist as keys with true values.
119+
func (f *Filter) HasAll(name string, values ...string) *Filter {
120+
f.criteria = append(f.criteria, criterion{
121+
name: name,
122+
values: values,
123+
matchFunc: allValues,
124+
})
125+
return f
126+
}
127+
128+
// matchSearchMetadata evaluates filter criteria against a single SearchMetadata instance.
129+
// This is an internal helper method used by matchProperties.
130+
func (f *Filter) matchSearchMetadata(searchMetadata property.SearchMetadata) (bool, error) {
131+
// Create a map of search metadata for quick lookup
132+
metadataMap := make(map[string]property.SearchMetadataItem)
133+
for _, item := range searchMetadata {
134+
metadataMap[item.Name] = item
135+
}
136+
137+
// Evaluate each filter criterion
138+
results := make([]Result, 0, len(f.criteria))
139+
for _, filter := range f.criteria {
140+
metadata, exists := metadataMap[filter.name]
141+
142+
// If the filter criterion is not defined in the search metadata, it doesn't match
143+
if !exists {
144+
results = append(results, Result{
145+
Name: filter.name,
146+
Matched: false,
147+
})
148+
continue
149+
}
150+
151+
criterionMatch, err := applyCriterion(metadata, filter)
152+
if err != nil {
153+
return false, err
154+
}
155+
156+
results = append(results, Result{
157+
Name: filter.name,
158+
Matched: criterionMatch,
159+
})
160+
}
161+
162+
// Apply the match function to combine all criteria results
163+
return f.matchFunc(results), nil
164+
}
165+
166+
// matchProperties evaluates whether the given properties match the filter criteria.
167+
// This is an internal method used by MatchMeta.
168+
func (f *Filter) matchProperties(properties []property.Property) (bool, error) {
169+
// If no filter criteria, everything matches
170+
if len(f.criteria) == 0 {
171+
return true, nil
172+
}
173+
174+
var searchMetadatas []property.SearchMetadata
175+
for _, prop := range properties {
176+
if prop.Type == property.TypeSearchMetadata {
177+
sm, err := property.ParseOne[property.SearchMetadata](prop)
178+
if err != nil {
179+
return false, fmt.Errorf("failed to parse search metadata: %v", err)
180+
}
181+
searchMetadatas = append(searchMetadatas, sm)
182+
}
183+
}
184+
185+
// If no search metadata, it doesn't match any filter
186+
if len(searchMetadatas) == 0 {
187+
return false, nil
188+
}
189+
190+
if len(searchMetadatas) > 1 {
191+
return false, fmt.Errorf("multiple search metadata properties cannot be defined")
192+
}
193+
194+
return f.matchSearchMetadata(searchMetadatas[0])
195+
}
196+
197+
// MatchMeta evaluates whether the given Meta object matches the filter criteria.
198+
// It extracts the properties from the Meta's blob and applies the configured filter criteria.
199+
// Returns true if the metadata matches according to the configured match function, false otherwise.
200+
func (f *Filter) MatchMeta(m declcfg.Meta) (bool, error) {
201+
// metaBlob represents the structure of a Meta blob for extracting properties
202+
type propertiesBlob struct {
203+
Properties []property.Property `json:"properties,omitempty"`
204+
}
205+
206+
// Parse the blob to extract properties
207+
var blob propertiesBlob
208+
if err := json.Unmarshal(m.Blob, &blob); err != nil {
209+
return false, fmt.Errorf("failed to unmarshal meta blob: %v", err)
210+
}
211+
212+
return f.matchProperties(blob.Properties)
213+
}
214+
215+
// applyCriterion applies the filter criterion to the metadata based on the metadata's type.
216+
// This is an internal helper function that handles the type-specific logic for matching.
217+
func applyCriterion(metadata property.SearchMetadataItem, filter criterion) (bool, error) {
218+
metadataValue, err := metadata.ExtractValue()
219+
if err != nil {
220+
return false, err
221+
}
222+
values, err := metadataValueAsSlice(metadataValue)
223+
if err != nil {
224+
return false, err
225+
}
226+
return filter.matchFunc(values, filter.values), nil
227+
}
228+
229+
// metadataValueAsSlice converts metadata values to a string slice for uniform processing.
230+
// This is an internal helper function that normalizes different metadata types.
231+
func metadataValueAsSlice(metadataValue any) ([]string, error) {
232+
switch v := metadataValue.(type) {
233+
case string:
234+
return []string{v}, nil
235+
case []string:
236+
return v, nil
237+
case map[string]bool:
238+
var keys []string
239+
for key, value := range v {
240+
if value {
241+
keys = append(keys, key)
242+
}
243+
}
244+
return keys, nil
245+
default:
246+
return nil, fmt.Errorf("unsupported metadata value type: %T", metadataValue)
247+
}
248+
}

0 commit comments

Comments
 (0)