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.
diff --git a/internal/api/params.go b/internal/api/params.go
new file mode 100644
index 0000000..821f448
--- /dev/null
+++ b/internal/api/params.go
@@ -0,0 +1,135 @@
+// 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"
+ "strings"
+)
+
+// ListParams represents 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.
+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 structField.Anonymous && field.Kind() == reflect.Struct {
+ if err := parseValue(v, field); err != nil {
+ return err
+ }
+ continue
+ }
+
+ tag := structField.Tag.Get("form")
+ if tag == "" {
+ continue
+ }
+
+ if !v.Has(tag) {
+ continue
+ }
+
+ rawVal := v.Get(tag)
+
+ switch field.Kind() {
+ case reflect.String:
+ field.SetString(rawVal)
+ case reflect.Int:
+ if rawVal != "" {
+ iv, err := strconv.Atoi(rawVal)
+ if err != nil {
+ return fmt.Errorf("invalid value for %s: %w", tag, err)
+ }
+ field.SetInt(int64(iv))
+ }
+ case reflect.Bool:
+ if rawVal == "" || strings.ToLower(rawVal) == "true" || rawVal == "1" || strings.ToLower(rawVal) == "on" {
+ field.SetBool(true)
+ } else {
+ field.SetBool(false)
+ }
+ }
+ }
+ return nil
+}
diff --git a/internal/api/params_test.go b/internal/api/params_test.go
new file mode 100644
index 0000000..ad5abb8
--- /dev/null
+++ b/internal/api/params_test.go
@@ -0,0 +1,92 @@
+// 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 (
+ "net/url"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+)
+
+func TestParseParams(t *testing.T) {
+ for _, test := range []struct {
+ name string
+ values url.Values
+ dst any
+ want any
+ wantErr bool
+ }{
+ {
+ name: "PackageParams",
+ values: url.Values{"module": {"m"}, "version": {"v1.0.0"}, "goos": {"linux"}, "examples": {"true"}},
+ dst: &PackageParams{},
+ want: &PackageParams{
+ Module: "m",
+ Version: "v1.0.0",
+ GOOS: "linux",
+ Examples: true,
+ },
+ },
+ {
+ name: "SymbolsParams",
+ values: url.Values{"module": {"m"}, "limit": {"50"}, "filter": {"f"}},
+ dst: &SymbolsParams{},
+ want: &SymbolsParams{
+ Module: "m",
+ ListParams: ListParams{
+ Limit: 50,
+ Filter: "f",
+ },
+ },
+ },
+ {
+ name: "SearchParams",
+ values: url.Values{"q": {"query"}, "filter": {"^net/"}, "limit": {"10"}},
+ dst: &SearchParams{},
+ want: &SearchParams{
+ Query: "query",
+ ListParams: ListParams{
+ Filter: "^net/",
+ Limit: 10,
+ },
+ },
+ },
+ {
+ name: "Boolean presence",
+ values: url.Values{"examples": {""}, "licenses": {"on"}},
+ dst: &PackageParams{},
+ want: &PackageParams{
+ Examples: true,
+ Licenses: true,
+ },
+ },
+ {
+ name: "Invalid int",
+ values: url.Values{"limit": {"not-an-int"}},
+ dst: &SymbolsParams{},
+ wantErr: true,
+ },
+ {
+ name: "Not a pointer",
+ values: url.Values{},
+ dst: PackageParams{},
+ wantErr: true,
+ },
+ } {
+ t.Run(test.name, func(t *testing.T) {
+ err := ParseParams(test.values, test.dst)
+ if (err != nil) != test.wantErr {
+ t.Fatalf("ParseParams() error = %v, wantErr %v", err, test.wantErr)
+ }
+ if test.wantErr {
+ return
+ }
+ if diff := cmp.Diff(test.want, test.dst); diff != "" {
+ t.Errorf("ParseParams() mismatch (-want +got):\n%s", diff)
+ }
+ })
+ }
+}
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Kokoro presubmit build finished with status: SUCCESS
Logs at: https://source.cloud.google.com/results/invocations/6f1ef4c7-a15f-48a1-934a-3349e54b70dd
| kokoro-CI | +1 |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Kokoro presubmit build finished with status: SUCCESS
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Kokoro presubmit build finished with status: FAILURE
Logs at: https://source.cloud.google.com/results/invocations/397f6641-46a4-4df1-a832-556cf121a0e6
| kokoro-CI | -1 |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
func ParseParams(v url.Values, dst any) error {given that we have a predefined set of a few params to use, can we go with generics instead of reflection?
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
// ListParams represents common pagination and filtering parameters.is (or are)
func ParseParams(v url.Values, dst any) error {given that we have a predefined set of a few params to use, can we go with generics instead of reflection?
You need reflection to loop over the fields and get the tags.
if val == "" {Why is the default true? It should be false, so you don't get extra stuff unless you specifically ask for it.
if val == "on" {
field.SetBool(true)why this special case? I think we should just support what strconv.ParseBool supports.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
// ListParams represents common pagination and filtering parameters.Ethan Leeis (or are)
Done
func ParseParams(v url.Values, dst any) error {Jonathan Amsterdamgiven that we have a predefined set of a few params to use, can we go with generics instead of reflection?
You need reflection to loop over the fields and get the tags.
Yes, but I think it depends on if we want a dynamic approach to support more param types in the future. Using reflection means that any struct with form tags will work. However, given that it seems that the query parameters for API will likely be stable, it's probably best to use generics to accommodate the predefined set of structs. We can avoid the overhead of reflection which would be incurred on every API call.
Why is the default true? It should be false, so you don't get extra stuff unless you specifically ask for it.
Good point, addressed this in the change with generics.
why this special case? I think we should just support what strconv.ParseBool supports.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Kokoro presubmit build finished with status: SUCCESS
Logs at: https://source.cloud.google.com/results/invocations/2155f6ed-f9ba-4a32-b771-f31bca0b9ac3
| kokoro-CI | +1 |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Kokoro presubmit build finished with status: FAILURE
Logs at: https://source.cloud.google.com/results/invocations/7e4f7b24-ee3b-4372-b963-171f77c37fac
| kokoro-CI | -1 |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
var res TI don't think you're really using generics here. This is just a type switch over a finite list of types. A more natural way to express this in Go is to add a Parse method to each struct.
func ParseParams(v url.Values, dst any) error {Jonathan Amsterdamgiven that we have a predefined set of a few params to use, can we go with generics instead of reflection?
Ethan LeeYou need reflection to loop over the fields and get the tags.
Yes, but I think it depends on if we want a dynamic approach to support more param types in the future. Using reflection means that any struct with form tags will work. However, given that it seems that the query parameters for API will likely be stable, it's probably best to use generics to accommodate the predefined set of structs. We can avoid the overhead of reflection which would be incurred on every API call.
Acknowledged
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
I don't think you're really using generics here. This is just a type switch over a finite list of types. A more natural way to express this in Go is to add a Parse method to each struct.
Discussed offline, let's go with the previous implementation given that it's more extensible and thus easier to maintain in the future.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Kokoro presubmit build finished with status: SUCCESS
Logs at: https://source.cloud.google.com/results/invocations/52cc468c-7dd9-48be-8724-b0b5956648b7
| kokoro-CI | +1 |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Code-Review | +2 |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
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.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |