Skip to content

Commit 219caf6

Browse files
authored
Improve registry filter options naming (#58)
* Improve registry filter options naming * Update --tag and --category to use partial matches
1 parent ca6c94f commit 219caf6

File tree

3 files changed

+133
-44
lines changed

3 files changed

+133
-44
lines changed

internal/filter/match.go

Lines changed: 101 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,19 @@ func NewOptions[T any](opt ...Option[T]) (Options[T], error) {
6161
return opts, nil
6262
}
6363

64+
// ValueProvider extracts a single string value from an item of type T.
6465
type ValueProvider[T any] func(T) string
6566

67+
// ValuesProvider extracts a slice of string values from an item of type T.
6668
type ValuesProvider[T any] func(T) []string
6769

70+
// Equals returns a Predicate that checks if the value extracted by the provider
71+
// exactly matches the filter value (case-insensitive, normalized).
72+
//
73+
// Example:
74+
//
75+
// predicate := Equals(options.SourceProvider),
76+
// result := predicate(pkg, "github") // true if pkg.Source equals "github"
6877
func Equals[T any](provider ValueProvider[T]) Predicate[T] {
6978
return func(item T, val string) bool {
7079
actual := NormalizeString(provider(item))
@@ -73,18 +82,84 @@ func Equals[T any](provider ValueProvider[T]) Predicate[T] {
7382
}
7483
}
7584

76-
func Contains[T any](provider ValueProvider[T]) Predicate[T] {
85+
// Partial returns a Predicate that checks if the value extracted by the provider
86+
// contains the filter value as a substring (case-insensitive, normalized).
87+
//
88+
// Example:
89+
//
90+
// predicate := Partial(options.VersionProvider),
91+
// result := predicate(pkg, "1.2") // true if pkg.Version contains "1.2"
92+
func Partial[T any](provider ValueProvider[T]) Predicate[T] {
7793
return func(item T, val string) bool {
7894
actual := NormalizeString(provider(item))
7995
expected := NormalizeString(val)
8096
return strings.Contains(actual, expected)
8197
}
8298
}
8399

84-
func ContainsOnly[T any](provider ValuesProvider[T]) Predicate[T] {
100+
// PartialAll returns a Predicate that checks if *ALL* comma-separated values in the filter string are found
101+
// as substrings within provided values (case-insensitive, normalized).
102+
// Functionally similar to Partial, but operates on a ValuesProvider, and expects the filter to be comma-separated.
103+
//
104+
// Example:
105+
//
106+
// predicate := PartialAll(options.ToolsProvider),
107+
// result := predicate(pkg, "get_current_time,convert_time") // true if pkg.Tools contains values with "get_current_time" and "convert_time" as substrings
108+
func PartialAll[T any](provider ValuesProvider[T]) Predicate[T] {
109+
return func(item T, val string) bool {
110+
required := NormalizeSlice(strings.Split(val, ","))
111+
actual := NormalizeSlice(provider(item))
112+
113+
for _, v := range required {
114+
found := false
115+
for _, a := range actual {
116+
if strings.Contains(a, v) {
117+
found = true
118+
break
119+
}
120+
}
121+
if !found {
122+
return false
123+
}
124+
}
125+
return true
126+
}
127+
}
128+
129+
// EqualsAny returns a Predicate that checks if *ANY* of the values from the supplied providers are equal to the
130+
// filter value (case-insensitive, normalized).
131+
// Functionally similar to Equals, but operates on one or more ValueProvider.
132+
//
133+
// Example:
134+
//
135+
// predicate := EqualsAny(options.ToolsProvider),
136+
// result := predicate(pkg, "get_current_time,convert_time") // true if pkg.Tools contains values "get_current_time" or "convert_time"
137+
func EqualsAny[T any](providers ...ValueProvider[T]) Predicate[T] {
138+
return func(item T, val string) bool {
139+
q := NormalizeString(val)
140+
for _, p := range providers {
141+
actual := NormalizeString(p(item))
142+
if strings.Contains(actual, q) {
143+
return true
144+
}
145+
}
146+
return false
147+
}
148+
}
149+
150+
// HasOnly returns a Predicate that checks if the values extracted by the provider are a subset of
151+
// the comma-separated values in the filter string (case-insensitive, normalized).
152+
// Returns true only if *ALL* extracted values are present in the filter list.
153+
//
154+
// Example:
155+
//
156+
// predicate := HasOnly(options.ToolsProvider),
157+
// result := predicate(pkg, "get_current_time,convert_time") // true if pkg.Tools only contains tools from the list
158+
func HasOnly[T any](provider ValuesProvider[T]) Predicate[T] {
85159
return func(item T, val string) bool {
86160
required := strings.Split(val, ",")
87161
expected := make(map[string]struct{}, len(required))
162+
88163
for _, v := range required {
89164
expected[NormalizeString(v)] = struct{}{}
90165
}
@@ -98,31 +173,48 @@ func ContainsOnly[T any](provider ValuesProvider[T]) Predicate[T] {
98173
}
99174
}
100175

101-
func ContainsAll[T any](provider ValuesProvider[T]) Predicate[T] {
176+
// HasAll returns a Predicate that checks if the values extracted by the provider include *ALL*
177+
// of the comma-separated values in the filter string (case-insensitive, normalized)..
178+
// Returns true only if *ALL* required values are present in the extracted values.
179+
//
180+
// Example:
181+
//
182+
// predicate := HasAll(options.ToolsProvider),
183+
// result := predicate(pkg, "get_current_time,convert_time") // true if pkg.Tools contains both "get_current_time" and "convert_time"
184+
func HasAll[T any](provider ValuesProvider[T]) Predicate[T] {
102185
return func(item T, val string) bool {
103186
required := NormalizeSlice(strings.Split(val, ","))
104187
actual := provider(item)
188+
allowed := make(map[string]struct{}, len(actual))
105189

106-
actualSet := make(map[string]struct{}, len(actual))
107190
for _, v := range actual {
108-
actualSet[NormalizeString(v)] = struct{}{}
191+
allowed[NormalizeString(v)] = struct{}{}
109192
}
110193

111194
for _, r := range required {
112-
if _, ok := actualSet[r]; !ok {
195+
if _, ok := allowed[r]; !ok {
113196
return false
114197
}
115198
}
116199
return true
117200
}
118201
}
119202

120-
func ContainsAny[T any](provider ValuesProvider[T]) Predicate[T] {
203+
// HasAny returns a Predicate that checks if the values extracted by the provider include *ANY* of
204+
// the comma-separated values in the filter string (case-insensitive, normalized).
205+
// Returns true if at least one required value is present in the extracted values.
206+
//
207+
// Example:
208+
//
209+
// predicate := HasAny(options.ToolsProvider),
210+
// result := predicate(pkg, "get_current_time,convert_time") // true if pkg.Tools contains either "get_current_time" or "convert_time"
211+
func HasAny[T any](provider ValuesProvider[T]) Predicate[T] {
121212
return func(item T, val string) bool {
122-
required := strings.Split(val, ",")
213+
required := NormalizeSlice(strings.Split(val, ","))
123214
allowed := make(map[string]struct{}, len(required))
215+
124216
for _, v := range required {
125-
allowed[NormalizeString(v)] = struct{}{}
217+
allowed[v] = struct{}{}
126218
}
127219

128220
for _, v := range provider(item) {
@@ -134,19 +226,6 @@ func ContainsAny[T any](provider ValuesProvider[T]) Predicate[T] {
134226
}
135227
}
136228

137-
func OrContains[T any](providers ...ValueProvider[T]) Predicate[T] {
138-
return func(item T, val string) bool {
139-
q := NormalizeString(val)
140-
for _, p := range providers {
141-
actual := NormalizeString(p(item))
142-
if strings.Contains(actual, q) {
143-
return true
144-
}
145-
}
146-
return false
147-
}
148-
}
149-
150229
// WithMatchers adds or overrides matchers.
151230
func WithMatchers[T any](m map[string]Predicate[T]) Option[T] {
152231
return func(o *Options[T]) error {

internal/filter/match_test.go

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,31 +32,41 @@ func TestEquals(t *testing.T) {
3232
}
3333

3434
func TestContains(t *testing.T) {
35-
p := Contains(func(m testItem) string { return m.Category })
35+
p := Partial(func(m testItem) string { return m.Category })
3636
assert.True(t, p(testItem{Category: "devtools"}, "tool"))
3737
assert.False(t, p(testItem{Category: "runtime"}, "tool"))
3838
}
3939

40-
func TestContainsOnly(t *testing.T) {
41-
p := ContainsOnly(func(m testItem) []string { return m.Tags })
40+
func TestHasOnly(t *testing.T) {
41+
p := HasOnly(func(m testItem) []string { return m.Tags })
4242
assert.True(t, p(testItem{Tags: []string{"A", "B"}}, "a,b"))
4343
assert.False(t, p(testItem{Tags: []string{"A", "C"}}, "a,b"))
4444
}
4545

46-
func TestContainsAll(t *testing.T) {
47-
p := ContainsAll(func(m testItem) []string { return m.Tags })
46+
func TestHasAll(t *testing.T) {
47+
p := HasAll(func(m testItem) []string { return m.Tags })
4848
assert.True(t, p(testItem{Tags: []string{"X", "Y", "Z"}}, "x,y"))
4949
assert.False(t, p(testItem{Tags: []string{"X", "Y"}}, "x,y,z"))
5050
}
5151

52-
func TestContainsAny(t *testing.T) {
53-
p := ContainsAny(func(m testItem) []string { return m.Tags })
52+
func TestHasAny(t *testing.T) {
53+
p := HasAny(func(m testItem) []string { return m.Tags })
5454
assert.True(t, p(testItem{Tags: []string{"alpha", "beta"}}, "beta,gamma"))
5555
assert.False(t, p(testItem{Tags: []string{"alpha"}}, "beta,gamma"))
5656
}
5757

58-
func TestOrContains(t *testing.T) {
59-
p := OrContains(
58+
func TestPartialAll(t *testing.T) {
59+
p := PartialAll(func(m testItem) []string { return m.Tags })
60+
// foo is a substring of foo2, bar is a substring of bar3, so both (all) match.
61+
assert.True(t, p(testItem{Name: "a", Tags: []string{"foo2", "bar3"}}, "foo,bar"))
62+
// foo is a substring of foo2, filter was only a single value so all have matched.
63+
assert.True(t, p(testItem{Name: "a", Tags: []string{"foo2", "bar3"}}, "foo"))
64+
// foo doesn't match any of the values even as substrings.
65+
assert.False(t, p(testItem{Name: "b", Tags: []string{"baz", "bar"}}, "foo"))
66+
}
67+
68+
func TestEqualsAny(t *testing.T) {
69+
p := EqualsAny(
6070
func(m testItem) string { return m.Name },
6171
func(m testItem) string { return m.Category },
6272
)

internal/registry/options/filter.go

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -107,31 +107,31 @@ func WithNameMatcher() Option {
107107
// The matcher is applied during Match only if the runtime filter key is present in the filters map.
108108
// Matching is case-insensitive and uses normalized values.
109109
func WithRuntimeMatcher(provider ValuesProvider) Option {
110-
return filter.WithMatcher(FilterKeyRuntime, filter.ContainsAny(provider))
110+
return filter.WithMatcher(FilterKeyRuntime, filter.HasAny(provider))
111111
}
112112

113113
// WithToolsMatcher returns a filter.Option with a matcher configured for the "tools" filter key.
114114
// The matcher is applied during Match only if the tools filter key is present in the filters map.
115115
// This matcher returns true only if all filter values are found in the package's tools.
116116
// Matching is case-insensitive and uses normalized values.
117117
func WithToolsMatcher(provider ValuesProvider) Option {
118-
return filter.WithMatcher(FilterKeyTools, filter.ContainsAll(provider))
118+
return filter.WithMatcher(FilterKeyTools, filter.HasAll(provider))
119119
}
120120

121121
// WithTagsMatcher returns a filter.Option with a matcher configured for the "tags" filter key.
122122
// The matcher is applied during Match only if the tags filter key is present in the filters map.
123-
// This matcher returns true if all of the filter values are found in the package's tags.
123+
// This matcher returns true if all the filter values are found in the package's tag as substrings.
124124
// Matching is case-insensitive and uses normalized values.
125125
func WithTagsMatcher(provider ValuesProvider) Option {
126-
return filter.WithMatcher(FilterKeyTags, filter.ContainsAll(provider))
126+
return filter.WithMatcher(FilterKeyTags, filter.PartialAll(provider))
127127
}
128128

129129
// WithCategoriesMatcher returns a filter.Option with a matcher configured for the "categories" filter key.
130130
// The matcher is applied during Match only if the categories filter key is present in the filters map.
131-
// This matcher returns true if all of the filter values are found in the package's categories.
131+
// This matcher returns true if all the filter values are found in the package's categories as substrings.
132132
// Matching is case-insensitive and uses normalized values.
133133
func WithCategoriesMatcher(provider ValuesProvider) Option {
134-
return filter.WithMatcher(FilterKeyCategories, filter.ContainsAll(provider))
134+
return filter.WithMatcher(FilterKeyCategories, filter.PartialAll(provider))
135135
}
136136

137137
// WithVersionMatcher returns a filter.Option with a matcher configured for the "version" filter key.
@@ -145,7 +145,7 @@ func WithVersionMatcher(provider ValueProvider) Option {
145145
// The matcher is applied during Match only if the license filter key is present in the filters map.
146146
// This matcher performs case-insensitive substring matching on the license field.
147147
func WithLicenseMatcher(provider ValueProvider) Option {
148-
return filter.WithMatcher(FilterKeyLicense, filter.Contains(provider))
148+
return filter.WithMatcher(FilterKeyLicense, filter.Partial(provider))
149149
}
150150

151151
// WithSourceMatcher returns a filter.Option with a matcher configured for the "source" filter key.
@@ -167,19 +167,19 @@ func withWildcardMatcher(providers ...ValueProvider) Predicate {
167167
if q == WildcardCharacter {
168168
return true
169169
}
170-
return filter.OrContains(providers...)(pkg, q)
170+
return filter.EqualsAny(providers...)(pkg, q)
171171
}
172172
}
173173

174174
func DefaultMatchers() map[string]Predicate {
175175
return map[string]Predicate{
176176
FilterKeyName: withWildcardMatcher(NameProvider, DisplayNameProvider, IDProvider),
177-
FilterKeyRuntime: filter.ContainsAny(RuntimesProvider),
178-
FilterKeyTools: filter.ContainsAll(ToolsProvider),
179-
FilterKeyTags: filter.ContainsAll(TagsProvider),
180-
FilterKeyCategories: filter.ContainsAll(CategoriesProvider),
177+
FilterKeyRuntime: filter.HasAny(RuntimesProvider),
178+
FilterKeyTools: filter.HasAll(ToolsProvider),
179+
FilterKeyTags: filter.PartialAll(TagsProvider),
180+
FilterKeyCategories: filter.PartialAll(CategoriesProvider),
181181
FilterKeyVersion: filter.Equals(VersionProvider),
182-
FilterKeyLicense: filter.Contains(LicenseProvider),
182+
FilterKeyLicense: filter.Partial(LicenseProvider),
183183
FilterKeySource: filter.Equals(SourceProvider),
184184
}
185185
}

0 commit comments

Comments
 (0)