diff options
| author | Ethan Lee <ethanalee@google.com> | 2026-03-11 16:03:34 +0000 |
|---|---|---|
| committer | Gopher Robot <gobot@golang.org> | 2026-03-18 10:11:42 -0700 |
| commit | 9b7f749dab1cca49376cabf86673e35a3faff8fb (patch) | |
| tree | 601fdec9c02fd5cd3dc15f4c72af61619d853f3a /internal/api/params.go | |
| parent | 1b7f032dee11d6534119541bc3e03f0fcbcc4995 (diff) | |
| download | go-x-pkgsite-9b7f749dab1cca49376cabf86673e35a3faff8fb.tar.xz | |
internal/api: implement query parameter parsing for api
- Introduce structs that will be used to parse query parameters.
- Implement parsing method and create relevant tests.
Change-Id: Ib54a57a7eb8d2dbaab0edf705a6cb9e5bc8288a9
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/754240
Auto-Submit: Ethan Lee <ethanalee@google.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
kokoro-CI: kokoro <noreply+kokoro@google.com>
Diffstat (limited to 'internal/api/params.go')
| -rw-r--r-- | internal/api/params.go | 192 |
1 files changed, 192 insertions, 0 deletions
diff --git a/internal/api/params.go b/internal/api/params.go new file mode 100644 index 00000000..cdc5f4c5 --- /dev/null +++ b/internal/api/params.go @@ -0,0 +1,192 @@ +// Copyright 2026 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package api + +import ( + "fmt" + "net/url" + "reflect" + "strconv" +) + +// ListParams are common pagination and filtering parameters. +type ListParams struct { + Limit int `form:"limit"` + Token string `form:"token"` + Filter string `form:"filter"` +} + +// PackageParams represents query parameters for /v1/package/{path}. +type PackageParams struct { + Module string `form:"module"` + Version string `form:"version"` + GOOS string `form:"goos"` + GOARCH string `form:"goarch"` + Doc string `form:"doc"` + Examples bool `form:"examples"` + Licenses bool `form:"licenses"` +} + +// SymbolsParams represents query parameters for /v1/symbols/{path}. +type SymbolsParams struct { + Module string `form:"module"` + Version string `form:"version"` + GOOS string `form:"goos"` + GOARCH string `form:"goarch"` + ListParams + Examples bool `form:"examples"` +} + +// ImportedByParams represents query parameters for /v1/imported-by/{path}. +type ImportedByParams struct { + Module string `form:"module"` + Version string `form:"version"` + ListParams +} + +// ModuleParams represents query parameters for /v1/module/{path}. +type ModuleParams struct { + Version string `form:"version"` + Licenses bool `form:"licenses"` + Readme bool `form:"readme"` +} + +// VersionsParams represents query parameters for /v1/versions/{path}. +type VersionsParams struct { + ListParams +} + +// PackagesParams represents query parameters for /v1/packages/{path}. +type PackagesParams struct { + Version string `form:"version"` + ListParams +} + +// SearchParams represents query parameters for /v1/search. +type SearchParams struct { + Query string `form:"q"` + Symbol string `form:"symbol"` + ListParams +} + +// VulnParams represents query parameters for /v1/vulns/{module}. +type VulnParams struct { + Version string `form:"version"` + ListParams +} + +// ParseParams populates a struct from url.Values using 'form' tags. +// dst must be a pointer to a struct. It supports embedded structs recursively, +// pointers, slices, and basic types (string, int, int64, bool). +func ParseParams(v url.Values, dst any) error { + val := reflect.ValueOf(dst) + if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Struct { + return fmt.Errorf("dst must be a pointer to a struct") + } + return parseValue(v, val.Elem()) +} + +func parseValue(v url.Values, val reflect.Value) error { + typ := val.Type() + for i := 0; i < val.NumField(); i++ { + field := val.Field(i) + structField := typ.Field(i) + + if !field.CanSet() { + continue + } + + if structField.Anonymous { + f := field + if f.Kind() == reflect.Ptr { + if f.IsNil() { + if !f.CanSet() { + continue + } + f.Set(reflect.New(f.Type().Elem())) + } + f = f.Elem() + } + if f.Kind() == reflect.Struct { + if err := parseValue(v, f); err != nil { + return err + } + continue + } + } + + tag := structField.Tag.Get("form") + if tag == "" { + continue + } + + if !v.Has(tag) { + continue + } + + if err := setField(field, tag, v[tag]); err != nil { + return err + } + } + return nil +} + +func setField(field reflect.Value, tag string, vals []string) error { + if len(vals) == 0 { + return nil + } + + if field.Kind() == reflect.Slice { + slice := reflect.MakeSlice(field.Type(), len(vals), len(vals)) + for i, v := range vals { + if err := setAny(slice.Index(i), tag, v); err != nil { + return err + } + } + field.Set(slice) + return nil + } + + return setAny(field, tag, vals[0]) +} + +func setAny(field reflect.Value, tag, val string) error { + if field.Kind() == reflect.Ptr { + if field.IsNil() { + field.Set(reflect.New(field.Type().Elem())) + } + return setAny(field.Elem(), tag, val) + } + return setSingle(field, tag, val) +} + +func setSingle(field reflect.Value, tag, val string) error { + switch field.Kind() { + case reflect.String: + field.SetString(val) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if val == "" { + return fmt.Errorf("empty value for %s", tag) + } + iv, err := strconv.ParseInt(val, 10, field.Type().Bits()) + if err != nil { + return fmt.Errorf("invalid value %q for %s: %w", val, tag, err) + } + field.SetInt(iv) + case reflect.Bool: + if val == "" { + field.SetBool(false) + return nil + } + bv, err := strconv.ParseBool(val) + if err != nil { + return fmt.Errorf("invalid boolean value %q for %s: %w", val, tag, err) + } + field.SetBool(bv) + default: + return fmt.Errorf("unsupported type %s for field %s", field.Type(), tag) + } + return nil +} |
