Skip to content

Commit 77e37cf

Browse files
committed
chore: support multiple key:value search query params
GitHub does this in their label search. Including this for an upcoming feature where the csv format is a bit unwieldy.
1 parent 2b773f9 commit 77e37cf

File tree

2 files changed

+61
-9
lines changed

2 files changed

+61
-9
lines changed

coderd/httpapi/queryparams.go

+26-6
Original file line numberDiff line numberDiff line change
@@ -248,13 +248,23 @@ func ParseCustom[T any](parser *QueryParamParser, vals url.Values, def T, queryP
248248
return v
249249
}
250250

251-
// ParseCustomList is a function that handles csv query params.
251+
// ParseCustomList is a function that handles csv query params or multiple values
252+
// for a query param.
253+
// Csv is supported as it is a common way to pass multiple values in a query param.
254+
// Multiple values is supported (key=value&key=value2) for feature parity with GitHub issue search.
252255
func ParseCustomList[T any](parser *QueryParamParser, vals url.Values, def []T, queryParam string, parseFunc func(v string) (T, error)) []T {
253-
v, err := parseQueryParam(parser, vals, func(v string) ([]T, error) {
254-
terms := strings.Split(v, ",")
256+
v, err := parseQueryParamSet(parser, vals, func(set []string) ([]T, error) {
257+
// Gather all terms.
258+
allTerms := make([]string, 0, len(set))
259+
for _, s := range set {
260+
// If a term is a csv, break it out into individual terms.
261+
terms := strings.Split(s, ",")
262+
allTerms = append(allTerms, terms...)
263+
}
264+
255265
var badValues []string
256266
var output []T
257-
for _, s := range terms {
267+
for _, s := range allTerms {
258268
good, err := parseFunc(s)
259269
if err != nil {
260270
badValues = append(badValues, s)
@@ -277,7 +287,18 @@ func ParseCustomList[T any](parser *QueryParamParser, vals url.Values, def []T,
277287
return v
278288
}
279289

290+
// parseQueryParam expects just 1 value set for the given query param.
280291
func parseQueryParam[T any](parser *QueryParamParser, vals url.Values, parse func(v string) (T, error), def T, queryParam string) (T, error) {
292+
setParse := func(set []string) (T, error) {
293+
if len(set) > 1 {
294+
return def, xerrors.Errorf("multiple values provided for the query param %q, only 1 is supported", queryParam)
295+
}
296+
return parse(set[0])
297+
}
298+
return parseQueryParamSet(parser, vals, setParse, def, queryParam)
299+
}
300+
301+
func parseQueryParamSet[T any](parser *QueryParamParser, vals url.Values, parse func(set []string) (T, error), def T, queryParam string) (T, error) {
281302
parser.addParsed(queryParam)
282303
// If the query param is required and not present, return an error.
283304
if parser.RequiredNotEmptyParams[queryParam] && (!vals.Has(queryParam) || vals.Get(queryParam) == "") {
@@ -293,6 +314,5 @@ func parseQueryParam[T any](parser *QueryParamParser, vals url.Values, parse fun
293314
return def, nil
294315
}
295316

296-
str := vals.Get(queryParam)
297-
return parse(str)
317+
return parse(vals[queryParam])
298318
}

coderd/httpapi/queryparams_test.go

+35-3
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,13 @@ import (
1717
type queryParamTestCase[T any] struct {
1818
QueryParam string
1919
// No set does not set the query param, rather than setting the empty value
20-
NoSet bool
21-
Value string
20+
NoSet bool
21+
// Value vs values is the difference between a single query param and multiple
22+
// to the same key.
23+
// -> key=value
24+
Value string
25+
// -> key=value1 key=value2
26+
Values []string
2227
Default T
2328
Expected T
2429
ExpectedErrorContains string
@@ -354,6 +359,23 @@ func TestParseQueryParams(t *testing.T) {
354359
Default: []uuid.UUID{},
355360
ExpectedErrorContains: "bogus",
356361
},
362+
{
363+
QueryParam: "multiple_keys",
364+
Values: []string{"6c8ef17d-5dd8-4b92-bac9-41944f90f237", "65fb05f3-12c8-4a0a-801f-40439cf9e681"},
365+
Expected: []uuid.UUID{
366+
uuid.MustParse("6c8ef17d-5dd8-4b92-bac9-41944f90f237"),
367+
uuid.MustParse("65fb05f3-12c8-4a0a-801f-40439cf9e681"),
368+
},
369+
},
370+
{
371+
QueryParam: "multiple_and_csv",
372+
Values: []string{"6c8ef17d-5dd8-4b92-bac9-41944f90f237", "65fb05f3-12c8-4a0a-801f-40439cf9e681, 01b94888-1eab-4bbf-aed0-dc7a8010da97"},
373+
Expected: []uuid.UUID{
374+
uuid.MustParse("6c8ef17d-5dd8-4b92-bac9-41944f90f237"),
375+
uuid.MustParse("65fb05f3-12c8-4a0a-801f-40439cf9e681"),
376+
uuid.MustParse("01b94888-1eab-4bbf-aed0-dc7a8010da97"),
377+
},
378+
},
357379
}
358380

359381
parser := httpapi.NewQueryParamParser()
@@ -381,7 +403,17 @@ func testQueryParams[T any](t *testing.T, testCases []queryParamTestCase[T], par
381403
if c.NoSet {
382404
continue
383405
}
384-
v.Set(c.QueryParam, c.Value)
406+
if len(c.Values) > 0 && c.Value != "" {
407+
t.Errorf("test case %q has both value and values, choose one, not both!", c.QueryParam)
408+
t.FailNow()
409+
}
410+
if c.Value != "" {
411+
c.Values = append(c.Values, c.Value)
412+
}
413+
414+
for _, value := range c.Values {
415+
v.Add(c.QueryParam, value)
416+
}
385417
}
386418

387419
for _, c := range testCases {

0 commit comments

Comments
 (0)