aboutsummaryrefslogtreecommitdiff
path: root/internal/api/params.go
diff options
context:
space:
mode:
authorEthan Lee <ethanalee@google.com>2026-03-11 16:03:34 +0000
committerGopher Robot <gobot@golang.org>2026-03-18 10:11:42 -0700
commit9b7f749dab1cca49376cabf86673e35a3faff8fb (patch)
tree601fdec9c02fd5cd3dc15f4c72af61619d853f3a /internal/api/params.go
parent1b7f032dee11d6534119541bc3e03f0fcbcc4995 (diff)
downloadgo-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.go192
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
+}