Skip to content

Commit 74e044f

Browse files
authored
Search: added --official flag to filter search results (#86)
1 parent 3805cc7 commit 74e044f

File tree

7 files changed

+217
-29
lines changed

7 files changed

+217
-29
lines changed

cmd/search.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type SearchCmd struct {
2525
License string
2626
Source string
2727
Format internalcmd.OutputFormat
28+
IsOfficial bool
2829
registryBuilder registry.Builder
2930
packagePrinter printer.Printer
3031
}
@@ -105,6 +106,13 @@ func NewSearchCmd(baseCmd *internalcmd.BaseCmd, opt ...cmdopts.CmdOption) (*cobr
105106
"Optional, specify a partial match for required categories (can be repeated)",
106107
)
107108

109+
cobraCommand.Flags().BoolVar(
110+
&c.IsOfficial,
111+
"official",
112+
false,
113+
"Optional, only official server packages are included in results",
114+
)
115+
108116
allowed := internalcmd.AllowedOutputFormats()
109117
cobraCommand.Flags().Var(
110118
&c.Format,
@@ -136,6 +144,9 @@ func (c *SearchCmd) filters() map[string]string {
136144
if c.License != "" {
137145
f["license"] = c.License
138146
}
147+
if c.IsOfficial {
148+
f["is_official"] = "true"
149+
}
139150

140151
return f
141152
}

cmd/search_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,27 @@ func TestSearchCmd_Filters(t *testing.T) {
6767
"license": "MIT",
6868
},
6969
},
70+
{
71+
name: "official",
72+
setters: func(c *SearchCmd) {
73+
c.IsOfficial = true
74+
c.License = "MIT"
75+
},
76+
wantFilter: map[string]string{
77+
"is_official": "true",
78+
"license": "MIT",
79+
},
80+
},
81+
{
82+
name: "official missing when not true",
83+
setters: func(c *SearchCmd) {
84+
c.IsOfficial = false
85+
c.License = "MIT"
86+
},
87+
wantFilter: map[string]string{
88+
"license": "MIT",
89+
},
90+
},
7091
}
7192

7293
for _, tc := range tests {

internal/filter/match.go

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"maps"
66
"slices"
77
"sort"
8+
"strconv"
89
"strings"
910
)
1011

@@ -15,7 +16,7 @@ type Predicate[T any] func(item T, filterValue string) bool
1516
type Options[T any] struct {
1617
matchers map[string]Predicate[T]
1718
unsupported map[string]struct{}
18-
logFunc func(key, val string)
19+
logFunc func(key string, val string)
1920
}
2021

2122
// Option configures filter Options.
@@ -61,11 +62,20 @@ func NewOptions[T any](opt ...Option[T]) (Options[T], error) {
6162
return opts, nil
6263
}
6364

64-
// ValueProvider extracts a single string value from an item of type T.
65-
type ValueProvider[T any] func(T) string
65+
// Provider is a generic function type that encapsulates the logic for extracting
66+
// a value of type V from an filterValue of type T. It provides a flexible way to
67+
// retrieve specific data from various types of structures or sources.
68+
type Provider[T any, V any] func(T) V
6669

67-
// ValuesProvider extracts a slice of string values from an item of type T.
68-
type ValuesProvider[T any] func(T) []string
70+
// BoolValueProvider extracts a single boolean value from an item of type T.
71+
type BoolValueProvider[T any] Provider[T, bool]
72+
73+
// StringValueProvider extracts a single string value from an item of type T.
74+
type StringValueProvider[T any] Provider[T, string]
75+
76+
// StringValuesProvider extracts a slice of string values from an item of type T.
77+
// type ValuesProvider[T any] func(T) []string
78+
type StringValuesProvider[T any] Provider[T, []string]
6979

7080
// Equals returns a Predicate that checks if the value extracted by the provider
7181
// exactly matches the filter value (case-insensitive, normalized).
@@ -74,22 +84,39 @@ type ValuesProvider[T any] func(T) []string
7484
//
7585
// predicate := Equals(options.SourceProvider),
7686
// result := predicate(pkg, "github") // true if pkg.Source equals "github"
77-
func Equals[T any](provider ValueProvider[T]) Predicate[T] {
87+
func Equals[T any](provider StringValueProvider[T]) Predicate[T] {
7888
return func(item T, val string) bool {
7989
actual := NormalizeString(provider(item))
8090
expected := NormalizeString(val)
8191
return actual == expected
8292
}
8393
}
8494

95+
// EqualsBool returns a Predicate that checks if the value extracted by the provider
96+
// matches the parsed boolean representation of the filter value.
97+
//
98+
// Example:
99+
//
100+
// predicate := EqualsBool(options.IsOfficialProvider),
101+
// result := predicate(pkg, "true") // true if pkg.IsOfficial is true
102+
func EqualsBool[T any](provider BoolValueProvider[T]) Predicate[T] {
103+
return func(item T, val string) bool {
104+
parsedVal, err := strconv.ParseBool(NormalizeString(val))
105+
if err != nil {
106+
return false
107+
}
108+
return provider(item) == parsedVal
109+
}
110+
}
111+
85112
// Partial returns a Predicate that checks if the value extracted by the provider
86113
// contains the filter value as a substring (case-insensitive, normalized).
87114
//
88115
// Example:
89116
//
90117
// predicate := Partial(options.VersionProvider),
91118
// result := predicate(pkg, "1.2") // true if pkg.Version contains "1.2"
92-
func Partial[T any](provider ValueProvider[T]) Predicate[T] {
119+
func Partial[T any](provider StringValueProvider[T]) Predicate[T] {
93120
return func(item T, val string) bool {
94121
actual := NormalizeString(provider(item))
95122
expected := NormalizeString(val)
@@ -105,7 +132,7 @@ func Partial[T any](provider ValueProvider[T]) Predicate[T] {
105132
//
106133
// predicate := PartialAll(options.ToolsProvider),
107134
// 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] {
135+
func PartialAll[T any](provider StringValuesProvider[T]) Predicate[T] {
109136
return func(item T, val string) bool {
110137
required := NormalizeSlice(strings.Split(val, ","))
111138
actual := NormalizeSlice(provider(item))
@@ -128,13 +155,13 @@ func PartialAll[T any](provider ValuesProvider[T]) Predicate[T] {
128155

129156
// EqualsAny returns a Predicate that checks if *ANY* of the values from the supplied providers are equal to the
130157
// filter value (case-insensitive, normalized).
131-
// Functionally similar to Equals, but operates on one or more ValueProvider.
158+
// Functionally similar to Equals, but operates on one or more StringValueProvider.
132159
//
133160
// Example:
134161
//
135162
// predicate := EqualsAny(options.ToolsProvider),
136163
// 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] {
164+
func EqualsAny[T any](providers ...StringValueProvider[T]) Predicate[T] {
138165
return func(item T, val string) bool {
139166
q := NormalizeString(val)
140167
for _, p := range providers {
@@ -155,7 +182,7 @@ func EqualsAny[T any](providers ...ValueProvider[T]) Predicate[T] {
155182
//
156183
// predicate := HasOnly(options.ToolsProvider),
157184
// 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] {
185+
func HasOnly[T any](provider StringValuesProvider[T]) Predicate[T] {
159186
return func(item T, val string) bool {
160187
required := strings.Split(val, ",")
161188
expected := make(map[string]struct{}, len(required))
@@ -181,7 +208,7 @@ func HasOnly[T any](provider ValuesProvider[T]) Predicate[T] {
181208
//
182209
// predicate := HasAll(options.ToolsProvider),
183210
// 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] {
211+
func HasAll[T any](provider StringValuesProvider[T]) Predicate[T] {
185212
return func(item T, val string) bool {
186213
required := NormalizeSlice(strings.Split(val, ","))
187214
actual := provider(item)
@@ -208,7 +235,7 @@ func HasAll[T any](provider ValuesProvider[T]) Predicate[T] {
208235
//
209236
// predicate := HasAny(options.ToolsProvider),
210237
// 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] {
238+
func HasAny[T any](provider StringValuesProvider[T]) Predicate[T] {
212239
return func(item T, val string) bool {
213240
required := NormalizeSlice(strings.Split(val, ","))
214241
allowed := make(map[string]struct{}, len(required))
@@ -256,7 +283,7 @@ func WithUnsupportedKeys[T any](keys ...string) Option[T] {
256283
}
257284

258285
// WithLogFunc sets a log function which will be used to log info if unsupported keys are encountered.
259-
func WithLogFunc[T any](logFunc func(key, val string)) Option[T] {
286+
func WithLogFunc[T any](logFunc func(key string, val string)) Option[T] {
260287
return func(o *Options[T]) error {
261288
if logFunc != nil {
262289
o.logFunc = logFunc

internal/filter/match_test.go

Lines changed: 109 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ import (
88
)
99

1010
type testItem struct {
11-
Name string
12-
Category string
13-
Tags []string
11+
Name string
12+
Category string
13+
Tags []string
14+
IsOfficial bool
1415
}
1516

1617
func TestNormalizeString(t *testing.T) {
@@ -133,6 +134,111 @@ func TestMatch_BasicEquals(t *testing.T) {
133134
assert.True(t, match)
134135
}
135136

137+
func TestMatch_EqualsBool(t *testing.T) {
138+
t.Parallel()
139+
140+
tests := []struct {
141+
name string
142+
itemVal bool
143+
filterValue string
144+
expectedMatch bool
145+
}{
146+
{
147+
name: "true",
148+
itemVal: true,
149+
filterValue: "true",
150+
expectedMatch: true,
151+
},
152+
{
153+
name: "true whitespace",
154+
itemVal: true,
155+
filterValue: " true ",
156+
expectedMatch: true,
157+
},
158+
{
159+
name: "true upper",
160+
itemVal: true,
161+
filterValue: "TRUE",
162+
expectedMatch: true,
163+
},
164+
{
165+
name: "true mixed",
166+
itemVal: true,
167+
filterValue: "TruE",
168+
expectedMatch: true,
169+
},
170+
{
171+
name: "true all",
172+
itemVal: true,
173+
filterValue: " trUE ",
174+
expectedMatch: true,
175+
},
176+
{
177+
name: "false",
178+
itemVal: false,
179+
filterValue: "false",
180+
expectedMatch: true,
181+
},
182+
{
183+
name: "false whitespace",
184+
itemVal: false,
185+
filterValue: " false ",
186+
expectedMatch: true,
187+
},
188+
{
189+
name: "false upper",
190+
itemVal: false,
191+
filterValue: "FALSE",
192+
expectedMatch: true,
193+
},
194+
{
195+
name: "false mixed",
196+
itemVal: false,
197+
filterValue: "FalsE",
198+
expectedMatch: true,
199+
},
200+
{
201+
name: "false all",
202+
itemVal: false,
203+
filterValue: " FAlse ",
204+
expectedMatch: true,
205+
},
206+
{
207+
name: "no match when filter is non-bool value",
208+
itemVal: false,
209+
filterValue: "hello",
210+
expectedMatch: false,
211+
},
212+
{
213+
name: "no match when filter true, but item false",
214+
itemVal: false,
215+
filterValue: "true",
216+
expectedMatch: false,
217+
},
218+
{
219+
name: "no match when filter false, but item true",
220+
itemVal: true,
221+
filterValue: "false",
222+
expectedMatch: false,
223+
},
224+
}
225+
226+
key := "is_official"
227+
matcher := WithMatcher(key, EqualsBool(func(i testItem) bool { return i.IsOfficial }))
228+
229+
for _, tc := range tests {
230+
t.Run(tc.name, func(t *testing.T) {
231+
t.Parallel()
232+
233+
item := testItem{IsOfficial: tc.itemVal}
234+
filters := map[string]string{key: tc.filterValue}
235+
match, err := Match(item, filters, matcher)
236+
require.NoError(t, err)
237+
assert.Equal(t, tc.expectedMatch, match)
238+
})
239+
}
240+
}
241+
136242
func TestMatch_FailsOnMismatch(t *testing.T) {
137243
matchers := map[string]Predicate[testItem]{
138244
"group": Equals(func(i testItem) string { return i.Category }),

internal/printer/package_printer.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ func (p *PackagePrinter) printDetails(pkg packages.Package) error {
100100
return err
101101
}
102102

103+
if _, err := fmt.Fprintf(p.out, " 🔒 Official: %s\n", map[bool]string{true: "✅", false: "❌"}[pkg.IsOfficial]); err != nil {
104+
return err
105+
}
106+
103107
if _, err := fmt.Fprintf(p.out, " 📁 Registry: %s\n", pkg.Source); err != nil {
104108
return err
105109
}

0 commit comments

Comments
 (0)