| // 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 are 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 are 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 are query parameters for /v1/imported-by/{path}. |
| type ImportedByParams struct { |
| Module string `form:"module"` |
| Version string `form:"version"` |
| ListParams |
| } |
| |
| // ModuleParams are query parameters for /v1/module/{path}. |
| type ModuleParams struct { |
| Version string `form:"version"` |
| Licenses bool `form:"licenses"` |
| Readme bool `form:"readme"` |
| } |
| |
| // VersionsParams are query parameters for /v1/versions/{path}. |
| type VersionsParams struct { |
| ListParams |
| } |
| |
| // PackagesParams are query parameters for /v1/packages/{path}. |
| type PackagesParams struct { |
| Version string `form:"version"` |
| ListParams |
| } |
| |
| // SearchParams are query parameters for /v1/search. |
| type SearchParams struct { |
| Query string `form:"q"` |
| Symbol string `form:"symbol"` |
| ListParams |
| } |
| |
| // VulnParams are 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, 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 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.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 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 |
| } |