Skip to content

Commit 3a77375

Browse files
authored
feat(coderd/httpapi): add QueryParamParser.JSONStringMap (#16578)
This PR provides a convenience function for parsing a `map[string]string` from a query parameter. Context: #16558 (comment)
1 parent a17cf03 commit 3a77375

File tree

4 files changed

+84
-24
lines changed

4 files changed

+84
-24
lines changed

coderd/httpapi/queryparams.go

+18
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package httpapi
22

33
import (
44
"database/sql"
5+
"encoding/json"
56
"errors"
67
"fmt"
78
"net/url"
@@ -257,6 +258,23 @@ func (p *QueryParamParser) Strings(vals url.Values, def []string, queryParam str
257258
})
258259
}
259260

261+
func (p *QueryParamParser) JSONStringMap(vals url.Values, def map[string]string, queryParam string) map[string]string {
262+
v, err := parseQueryParam(p, vals, func(v string) (map[string]string, error) {
263+
var m map[string]string
264+
if err := json.NewDecoder(strings.NewReader(v)).Decode(&m); err != nil {
265+
return nil, err
266+
}
267+
return m, nil
268+
}, def, queryParam)
269+
if err != nil {
270+
p.Errors = append(p.Errors, codersdk.ValidationError{
271+
Field: queryParam,
272+
Detail: fmt.Sprintf("Query param %q must be a valid JSON object: %s", queryParam, err.Error()),
273+
})
274+
}
275+
return v
276+
}
277+
260278
// ValidEnum represents an enum that can be parsed and validated.
261279
type ValidEnum interface {
262280
// Add more types as needed (avoid importing large dependency trees).

coderd/httpapi/queryparams_test.go

+64
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,70 @@ func TestParseQueryParams(t *testing.T) {
473473
testQueryParams(t, expParams, parser, parser.UUIDs)
474474
})
475475

476+
t.Run("JSONStringMap", func(t *testing.T) {
477+
t.Parallel()
478+
479+
expParams := []queryParamTestCase[map[string]string]{
480+
{
481+
QueryParam: "valid_map",
482+
Value: `{"key1": "value1", "key2": "value2"}`,
483+
Expected: map[string]string{
484+
"key1": "value1",
485+
"key2": "value2",
486+
},
487+
},
488+
{
489+
QueryParam: "empty",
490+
Value: "{}",
491+
Default: map[string]string{},
492+
Expected: map[string]string{},
493+
},
494+
{
495+
QueryParam: "no_value",
496+
NoSet: true,
497+
Default: map[string]string{},
498+
Expected: map[string]string{},
499+
},
500+
{
501+
QueryParam: "default",
502+
NoSet: true,
503+
Default: map[string]string{"key": "value"},
504+
Expected: map[string]string{"key": "value"},
505+
},
506+
{
507+
QueryParam: "null",
508+
Value: "null",
509+
Expected: map[string]string(nil),
510+
},
511+
{
512+
QueryParam: "undefined",
513+
Value: "undefined",
514+
Expected: map[string]string(nil),
515+
},
516+
{
517+
QueryParam: "invalid_map",
518+
Value: `{"key1": "value1", "key2": "value2"`, // missing closing brace
519+
Expected: map[string]string(nil),
520+
Default: map[string]string{},
521+
ExpectedErrorContains: `Query param "invalid_map" must be a valid JSON object: unexpected EOF`,
522+
},
523+
{
524+
QueryParam: "incorrect_type",
525+
Value: `{"key1": 1, "key2": true}`,
526+
Expected: map[string]string(nil),
527+
ExpectedErrorContains: `Query param "incorrect_type" must be a valid JSON object: json: cannot unmarshal number into Go value of type string`,
528+
},
529+
{
530+
QueryParam: "multiple_keys",
531+
Values: []string{`{"key1": "value1"}`, `{"key2": "value2"}`},
532+
Expected: map[string]string(nil),
533+
ExpectedErrorContains: `Query param "multiple_keys" provided more than once, found 2 times.`,
534+
},
535+
}
536+
parser := httpapi.NewQueryParamParser()
537+
testQueryParams(t, expParams, parser, parser.JSONStringMap)
538+
})
539+
476540
t.Run("Required", func(t *testing.T) {
477541
t.Parallel()
478542

coderd/provisionerdaemons.go

+1-12
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func (api *API) provisionerDaemons(rw http.ResponseWriter, r *http.Request) {
4444
p := httpapi.NewQueryParamParser()
4545
limit := p.PositiveInt32(qp, 50, "limit")
4646
ids := p.UUIDs(qp, nil, "ids")
47-
tagsRaw := p.String(qp, "", "tags")
47+
tags := p.JSONStringMap(qp, database.StringMap{}, "tags")
4848
p.ErrorExcessParams(qp)
4949
if len(p.Errors) > 0 {
5050
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
@@ -54,17 +54,6 @@ func (api *API) provisionerDaemons(rw http.ResponseWriter, r *http.Request) {
5454
return
5555
}
5656

57-
tags := database.StringMap{}
58-
if tagsRaw != "" {
59-
if err := tags.Scan([]byte(tagsRaw)); err != nil {
60-
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
61-
Message: "Invalid tags query parameter",
62-
Detail: err.Error(),
63-
})
64-
return
65-
}
66-
}
67-
6857
daemons, err := api.Database.GetProvisionerDaemonsWithStatusByOrganization(
6958
ctx,
7059
database.GetProvisionerDaemonsWithStatusByOrganizationParams{

coderd/provisionerjobs.go

+1-12
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ func (api *API) handleAuthAndFetchProvisionerJobs(rw http.ResponseWriter, r *htt
108108
if ids == nil {
109109
ids = p.UUIDs(qp, nil, "ids")
110110
}
111-
tagsRaw := p.String(qp, "", "tags")
111+
tags := p.JSONStringMap(qp, database.StringMap{}, "tags")
112112
p.ErrorExcessParams(qp)
113113
if len(p.Errors) > 0 {
114114
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
@@ -118,17 +118,6 @@ func (api *API) handleAuthAndFetchProvisionerJobs(rw http.ResponseWriter, r *htt
118118
return nil, false
119119
}
120120

121-
tags := database.StringMap{}
122-
if tagsRaw != "" {
123-
if err := tags.Scan([]byte(tagsRaw)); err != nil {
124-
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
125-
Message: "Invalid tags query parameter",
126-
Detail: err.Error(),
127-
})
128-
return nil, false
129-
}
130-
}
131-
132121
jobs, err := api.Database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner(ctx, database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams{
133122
OrganizationID: org.ID,
134123
Status: slice.StringEnums[database.ProvisionerJobStatus](status),

0 commit comments

Comments
 (0)