feat(coderd/httpapi): add QueryParamParser.JSONStringMap (#16578)

This PR provides a convenience function for parsing a
`map[string]string` from a query parameter.

Context:
https://github.com/coder/coder/pull/16558#discussion_r1956190615
This commit is contained in:
Cian Johnston
2025-02-18 14:14:30 +00:00
committed by GitHub
parent a17cf03980
commit 3a773758a2
4 changed files with 84 additions and 24 deletions
+18
View File
@@ -2,6 +2,7 @@ package httpapi
import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/url"
@@ -257,6 +258,23 @@ func (p *QueryParamParser) Strings(vals url.Values, def []string, queryParam str
})
}
func (p *QueryParamParser) JSONStringMap(vals url.Values, def map[string]string, queryParam string) map[string]string {
v, err := parseQueryParam(p, vals, func(v string) (map[string]string, error) {
var m map[string]string
if err := json.NewDecoder(strings.NewReader(v)).Decode(&m); err != nil {
return nil, err
}
return m, nil
}, def, queryParam)
if err != nil {
p.Errors = append(p.Errors, codersdk.ValidationError{
Field: queryParam,
Detail: fmt.Sprintf("Query param %q must be a valid JSON object: %s", queryParam, err.Error()),
})
}
return v
}
// ValidEnum represents an enum that can be parsed and validated.
type ValidEnum interface {
// Add more types as needed (avoid importing large dependency trees).
+64
View File
@@ -473,6 +473,70 @@ func TestParseQueryParams(t *testing.T) {
testQueryParams(t, expParams, parser, parser.UUIDs)
})
t.Run("JSONStringMap", func(t *testing.T) {
t.Parallel()
expParams := []queryParamTestCase[map[string]string]{
{
QueryParam: "valid_map",
Value: `{"key1": "value1", "key2": "value2"}`,
Expected: map[string]string{
"key1": "value1",
"key2": "value2",
},
},
{
QueryParam: "empty",
Value: "{}",
Default: map[string]string{},
Expected: map[string]string{},
},
{
QueryParam: "no_value",
NoSet: true,
Default: map[string]string{},
Expected: map[string]string{},
},
{
QueryParam: "default",
NoSet: true,
Default: map[string]string{"key": "value"},
Expected: map[string]string{"key": "value"},
},
{
QueryParam: "null",
Value: "null",
Expected: map[string]string(nil),
},
{
QueryParam: "undefined",
Value: "undefined",
Expected: map[string]string(nil),
},
{
QueryParam: "invalid_map",
Value: `{"key1": "value1", "key2": "value2"`, // missing closing brace
Expected: map[string]string(nil),
Default: map[string]string{},
ExpectedErrorContains: `Query param "invalid_map" must be a valid JSON object: unexpected EOF`,
},
{
QueryParam: "incorrect_type",
Value: `{"key1": 1, "key2": true}`,
Expected: map[string]string(nil),
ExpectedErrorContains: `Query param "incorrect_type" must be a valid JSON object: json: cannot unmarshal number into Go value of type string`,
},
{
QueryParam: "multiple_keys",
Values: []string{`{"key1": "value1"}`, `{"key2": "value2"}`},
Expected: map[string]string(nil),
ExpectedErrorContains: `Query param "multiple_keys" provided more than once, found 2 times.`,
},
}
parser := httpapi.NewQueryParamParser()
testQueryParams(t, expParams, parser, parser.JSONStringMap)
})
t.Run("Required", func(t *testing.T) {
t.Parallel()
+1 -12
View File
@@ -44,7 +44,7 @@ func (api *API) provisionerDaemons(rw http.ResponseWriter, r *http.Request) {
p := httpapi.NewQueryParamParser()
limit := p.PositiveInt32(qp, 50, "limit")
ids := p.UUIDs(qp, nil, "ids")
tagsRaw := p.String(qp, "", "tags")
tags := p.JSONStringMap(qp, database.StringMap{}, "tags")
p.ErrorExcessParams(qp)
if len(p.Errors) > 0 {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
@@ -54,17 +54,6 @@ func (api *API) provisionerDaemons(rw http.ResponseWriter, r *http.Request) {
return
}
tags := database.StringMap{}
if tagsRaw != "" {
if err := tags.Scan([]byte(tagsRaw)); err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid tags query parameter",
Detail: err.Error(),
})
return
}
}
daemons, err := api.Database.GetProvisionerDaemonsWithStatusByOrganization(
ctx,
database.GetProvisionerDaemonsWithStatusByOrganizationParams{
+1 -12
View File
@@ -108,7 +108,7 @@ func (api *API) handleAuthAndFetchProvisionerJobs(rw http.ResponseWriter, r *htt
if ids == nil {
ids = p.UUIDs(qp, nil, "ids")
}
tagsRaw := p.String(qp, "", "tags")
tags := p.JSONStringMap(qp, database.StringMap{}, "tags")
p.ErrorExcessParams(qp)
if len(p.Errors) > 0 {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
@@ -118,17 +118,6 @@ func (api *API) handleAuthAndFetchProvisionerJobs(rw http.ResponseWriter, r *htt
return nil, false
}
tags := database.StringMap{}
if tagsRaw != "" {
if err := tags.Scan([]byte(tagsRaw)); err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid tags query parameter",
Detail: err.Error(),
})
return nil, false
}
}
jobs, err := api.Database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner(ctx, database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams{
OrganizationID: org.ID,
Status: slice.StringEnums[database.ProvisionerJobStatus](status),