From d0e8c5a01b6feb337dfba7597fe61d976808f2ef Mon Sep 17 00:00:00 2001 From: Leon Clark Date: Wed, 19 Nov 2025 13:03:05 -0800 Subject: [PATCH 1/2] params: support repeated fields --- params/params.go | 160 ++++++++++++++++++++++++++++-------------- params/params_test.go | 1 + 2 files changed, 110 insertions(+), 51 deletions(-) diff --git a/params/params.go b/params/params.go index 80c134b..d71f0d8 100644 --- a/params/params.go +++ b/params/params.go @@ -14,63 +14,68 @@ import ( var ErrNotFound = errors.New("not found") func GetString(r *http.Request, name string, required bool) (string, bool, error) { - param := r.PathValue(name) - - if param == "" { - param = r.URL.Query().Get(name) - } - - // fallback to a header lookup - if param == "" { - param = r.Header.Get(name) + values, ok, err := GetStringArray(r, name, required) + if err != nil { + return "", false, err } - if required && len(param) == 0 { - return "", false, fmt.Errorf("%s: %w", name, ErrNotFound) + if !ok || len(values) == 0 { + return "", false, nil } - return param, param != "", nil + return values[0], true, nil } func GetStringPath(r *http.Request, name string, required bool) (string, bool, error) { - param := r.PathValue(name) - if required && len(param) == 0 { - return "", false, fmt.Errorf("%s: %w", name, ErrNotFound) + values, ok, err := GetStringArrayPath(r, name, required) + if err != nil { + return "", false, err + } + + if !ok || len(values) == 0 { + return "", false, nil } - return param, param != "", nil + return values[0], true, nil } func GetStringQuery(r *http.Request, name string, required bool) (string, bool, error) { - param := r.URL.Query().Get(name) - if required && len(param) == 0 { - return "", false, fmt.Errorf("%s: %w", name, ErrNotFound) + values, ok, err := GetStringArrayQuery(r, name, required) + if err != nil { + return "", false, err + } + + if !ok || len(values) == 0 { + return "", false, nil } - return param, param != "", nil + return values[0], true, nil } func GetStringHeader(r *http.Request, name string, required bool) (string, bool, error) { - param := r.Header.Get(name) - if required && len(param) == 0 { - return "", false, fmt.Errorf("%s: %w", name, ErrNotFound) + values, ok, err := GetStringArrayHeader(r, name, required) + if err != nil { + return "", false, err } - return param, param != "", nil + if !ok || len(values) == 0 { + return "", false, nil + } + + return values[0], true, nil } func GetStringCookie(r *http.Request, name string, required bool) (string, bool, error) { - cookie, err := r.Cookie(name) - if err == nil && cookie != nil { - // Return found even if cookie is empty, because it _is_ present! - return cookie.Value, true, nil + values, ok, err := GetStringArrayCookie(r, name, required) + if err != nil { + return "", false, err } - if required { - return "", false, fmt.Errorf("%s: %w", name, ErrNotFound) + if !ok || len(values) == 0 { + return "", false, nil } - return "", false, nil + return values[0], true, nil } func GetInt32(r *http.Request, name string, required bool) (int32, bool, error) { @@ -397,49 +402,102 @@ func convertUUID(s string) (uuid.UUID, bool, error) { return id, true, nil } +func splitAndAppend(dest []string, source string) []string { + if strings.Contains(source, ",") { + for v := range strings.SplitSeq(source, ",") { + if v != "" { + dest = append(dest, v) + } + } + + return dest + } + + if source != "" { + return append(dest, source) + } + + return dest +} + func GetStringArray(r *http.Request, name string, required bool) ([]string, bool, error) { - s, ok, err := GetString(r, name, required) - if err != nil || len(s) == 0 || !ok { - return nil, false, err + out, _, _ := GetStringArrayPath(r, name, false) + + if len(out) == 0 { + out, _, _ = GetStringArrayQuery(r, name, false) } - return strings.Split(s, ","), true, nil + // fallback to a header lookup + if len(out) == 0 { + out, _, _ = GetStringArrayHeader(r, name, false) + } + + if len(out) == 0 { + out, _, _ = GetStringArrayCookie(r, name, false) + } + + if required && len(out) == 0 { + return nil, false, fmt.Errorf("%s: %w", name, ErrNotFound) + } + + return out, len(out) > 0, nil } func GetStringArrayPath(r *http.Request, name string, required bool) ([]string, bool, error) { - s, ok, err := GetStringPath(r, name, required) - if err != nil || len(s) == 0 || !ok { - return nil, false, err + out := splitAndAppend(nil, r.PathValue(name)) + + if required && len(out) == 0 { + return nil, false, fmt.Errorf("%s: %w", name, ErrNotFound) } - return strings.Split(s, ","), true, nil + return out, len(out) > 0, nil } func GetStringArrayQuery(r *http.Request, name string, required bool) ([]string, bool, error) { - s, ok, err := GetStringQuery(r, name, required) - if err != nil || len(s) == 0 || !ok { - return nil, false, err + var out []string + + values := r.URL.Query()[name] + for _, v := range values { + out = splitAndAppend(out, v) + } + + if required && len(out) == 0 { + return nil, false, fmt.Errorf("%s: %w", name, ErrNotFound) } - return strings.Split(s, ","), true, nil + return out, len(out) > 0, nil } func GetStringArrayHeader(r *http.Request, name string, required bool) ([]string, bool, error) { - s, ok, err := GetStringHeader(r, name, required) - if err != nil || len(s) == 0 || !ok { - return nil, false, err + var out []string + + values := r.Header.Values(name) + for _, v := range values { + out = splitAndAppend(out, v) + } + + if required && len(out) == 0 { + return nil, false, fmt.Errorf("%s: %w", name, ErrNotFound) } - return strings.Split(s, ","), true, nil + return out, len(out) > 0, nil } func GetStringArrayCookie(r *http.Request, name string, required bool) ([]string, bool, error) { - s, ok, err := GetStringCookie(r, name, required) - if err != nil || len(s) == 0 || !ok { - return nil, false, err + var out []string + + cookies := r.CookiesNamed(name) + for _, c := range cookies { + if c != nil { + out = splitAndAppend(out, c.Value) + } + } + + if required && len(out) == 0 { + return nil, false, fmt.Errorf("%s: %w", name, ErrNotFound) } - return strings.Split(s, ","), true, nil + return out, len(out) > 0, nil } func GetInt32Array(r *http.Request, name string, required bool) ([]int32, bool, error) { diff --git a/params/params_test.go b/params/params_test.go index b07838a..a425aad 100644 --- a/params/params_test.go +++ b/params/params_test.go @@ -229,6 +229,7 @@ func TestGetUUIDArray(t *testing.T) { {"not required missing", httptest.NewRequest("GET", "/BAR?", nil), "foo", false, nil, false, false}, {"bad format", httptest.NewRequest("GET", "/BAR?foo=a123", nil), "foo", true, nil, true, false}, {"large", httptest.NewRequest("GET", fmt.Sprintf("/BAR?foo=%s,%s,%s", id1.String(), id2.String(), id3.String()), nil), "foo", true, []uuid.UUID{id1, id2, id3}, false, true}, + {"large repeated", httptest.NewRequest("GET", fmt.Sprintf("/BAR?foo=%s&foo=%s,,,%s", id1.String(), id2.String(), id3.String()), nil), "foo", true, []uuid.UUID{id1, id2, id3}, false, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From cd6e1b06bbe45c4f63928767c426bb296601a6c8 Mon Sep 17 00:00:00 2001 From: Leon Clark Date: Wed, 19 Nov 2025 13:13:11 -0800 Subject: [PATCH 2/2] simplify splitAndAppend --- params/params.go | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/params/params.go b/params/params.go index d71f0d8..2611481 100644 --- a/params/params.go +++ b/params/params.go @@ -403,18 +403,10 @@ func convertUUID(s string) (uuid.UUID, bool, error) { } func splitAndAppend(dest []string, source string) []string { - if strings.Contains(source, ",") { - for v := range strings.SplitSeq(source, ",") { - if v != "" { - dest = append(dest, v) - } + for v := range strings.SplitSeq(source, ",") { + if v != "" { + dest = append(dest, v) } - - return dest - } - - if source != "" { - return append(dest, source) } return dest