| // 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 /v1/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 /v1/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 /v1/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 /v1/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 /v1/versions/{path}. | |
| type VersionsParams struct { | |
| ListParams | |
| // Whether to include pseudo-versions in the result. | |
| PseudoVersions bool `form:"pseudo"` | |
| } | |
| // PackagesParams are query parameters for /v1/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 /v1/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 /v1/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 | |
| } |