blob: 32b8403292a8a30bedffc8f1ab337e7bfe180d5c [file]
// 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 {
// Max number of items to return.
Limit int `form:"limit"`
// Where to resume listing.
Token string `form:"token"`
// Include only items matching the regular expression filter.
Filter string `form:"filter"`
}
// PackageParams are query parameters for /v1beta/package/{path}.
type PackageParams struct {
// Module path.
Module string `form:"module"`
// Module version: semantic version, 'latest', or default branches 'master' or 'main'.
// (Latest if empty).
Version string `form:"version"`
// GOOS of documentation build context.
GOOS string `form:"goos"`
// GOARCH of documentation build context.
GOARCH string `form:"goarch"`
// Documentation format: text, html, md or markdown.
// If omitted, documentation is not returned.
Doc string `form:"doc"`
// Whether to include examples with the returned documentation.
Examples bool `form:"examples"`
// Whether to include the packages that this one imports.
Imports bool `form:"imports"`
// Whether to include licenses in the result.
Licenses bool `form:"licenses"`
}
// SymbolsParams are query parameters for /v1beta/symbols/{path}.
type SymbolsParams struct {
// Module path.
Module string `form:"module"`
// Module version: semantic version, 'latest', or default branches 'master' or 'main'.
// (Latest if empty).
Version string `form:"version"`
// GOOS of documentation build context.
GOOS string `form:"goos"`
// GOARCH of documentation build context.
GOARCH string `form:"goarch"`
ListParams
}
// ImportedByParams are query parameters for /v1beta/imported-by/{path}.
type ImportedByParams struct {
// Module path.
Module string `form:"module"`
// Module version: semantic version, 'latest', or default branches 'master' or 'main'.
// (Latest if empty).
Version string `form:"version"`
ListParams
}
// ModuleParams are query parameters for /v1beta/module/{path}.
type ModuleParams struct {
// Module version: semantic version, 'latest', or default branches 'master' or 'main'.
// (Latest if empty).
Version string `form:"version"`
// Whether to include licenses in the result.
Licenses bool `form:"licenses"`
// Whether to include the README in the result.
Readme bool `form:"readme"`
}
// VersionsParams are query parameters for /v1beta/versions/{path}.
type VersionsParams struct {
ListParams
}
// PackagesParams are query parameters for /v1beta/packages/{path}.
type PackagesParams struct {
// Module version: semantic version, 'latest', or default branches 'master' or 'main'.
// (Latest if empty).
Version string `form:"version"`
ListParams
}
// SearchParams are query parameters for /v1beta/search.
type SearchParams struct {
// Find packages matching this query.
Query string `form:"q"`
// If non-empty, find symbols matching this string.
// The query further restricts the search to matching packages.
Symbol string `form:"symbol"`
ListParams
}
// VulnParams are query parameters for /v1beta/vulns/{path}.
type VulnParams struct {
// Module path.
Module string `form:"module"`
// Module version: semantic version, 'latest', or default branches 'master' or 'main'.
// (Latest if empty).
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, bool, int types).
func ParseParams(v url.Values, dst any) error {
val := reflect.ValueOf(dst)
if val.Kind() != reflect.Pointer || val.Elem().Kind() != reflect.Struct {
return InternalServerError("dst must be a pointer to a struct")
}
if err := parseValue(v, val.Elem()); err != nil {
return BadRequest(err.Error(), "correct the query parameter and retry")
}
return nil
}
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.Pointer {
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.Pointer {
return setSingle(field, tag, val)
}
if field.IsNil() {
field.Set(reflect.New(field.Type().Elem()))
}
return setAny(field.Elem(), 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 integer value %q for %s", val, tag)
}
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", val, tag)
}
field.SetBool(bv)
default:
return fmt.Errorf("unsupported type %s for field %s", field.Type(), tag)
}
return nil
}
// pageParams resolves the limit and offset from ListParams.
// It returns an error if the token is invalid.
func (lp ListParams) pageParams(defaultLimit int) (limit, offset int, err error) {
limit = lp.Limit
if limit <= 0 {
limit = defaultLimit
}
if lp.Token != "" {
offset, err = decodePageToken(lp.Token)
if err != nil {
return 0, 0, fmt.Errorf("invalid next-page token: %v", err)
}
}
return limit, offset, nil
}