mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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).
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user