160 lines
4.5 KiB
Go
160 lines
4.5 KiB
Go
package validate
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
|
|
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
|
|
|
|
"github.com/go-playground/validator/v10"
|
|
)
|
|
|
|
var structValidator = newValidator()
|
|
|
|
func newValidator() *validator.Validate {
|
|
instance := validator.New()
|
|
instance.SetTagName("binding")
|
|
return instance
|
|
}
|
|
|
|
// JSON binds and validates a JSON request body using standard-library HTTP
|
|
// types. It intentionally does not depend on Gin.
|
|
func JSON(request *http.Request, value any) error {
|
|
if request == nil || request.Body == nil {
|
|
return apierrors.BadRequest("INVALID_BODY", "request body is invalid")
|
|
}
|
|
if err := json.NewDecoder(request.Body).Decode(value); err != nil {
|
|
return apierrors.BadRequest("INVALID_BODY", "request body is invalid").WithCause(err)
|
|
}
|
|
return validateValue(value, "INVALID_BODY", "request body is invalid")
|
|
}
|
|
|
|
// Query binds query parameters using form tags and validates the result.
|
|
func Query(request *http.Request, value any) error {
|
|
if request == nil {
|
|
return apierrors.BadRequest("INVALID_QUERY", "query parameters are invalid")
|
|
}
|
|
if err := bindValues(request.URL.Query(), value, "form"); err != nil {
|
|
return apierrors.BadRequest("INVALID_QUERY", "query parameters are invalid").WithCause(err)
|
|
}
|
|
return validateValue(value, "INVALID_QUERY", "query parameters are invalid")
|
|
}
|
|
|
|
// URI binds path parameters supplied by a router adapter and validates them.
|
|
func URI(params map[string]string, value any) error {
|
|
if err := bindValues(singleValues(params), value, "uri"); err != nil {
|
|
return apierrors.BadRequest("INVALID_PATH", "path parameters are invalid").WithCause(err)
|
|
}
|
|
return validateValue(value, "INVALID_PATH", "path parameters are invalid")
|
|
}
|
|
|
|
func validateValue(value any, code, message string) error {
|
|
if err := structValidator.Struct(value); err != nil {
|
|
return apierrors.BadRequest(code, message).WithCause(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func bindValues(values map[string][]string, target any, tagName string) error {
|
|
value := reflect.ValueOf(target)
|
|
if value.Kind() != reflect.Pointer || value.IsNil() || value.Elem().Kind() != reflect.Struct {
|
|
return &bindError{"target must be a non-nil pointer to a struct"}
|
|
}
|
|
return bindStruct(values, value.Elem(), tagName)
|
|
}
|
|
|
|
func bindStruct(values map[string][]string, target reflect.Value, tagName string) error {
|
|
typeOfTarget := target.Type()
|
|
for index := 0; index < target.NumField(); index++ {
|
|
field := target.Field(index)
|
|
if !field.CanSet() {
|
|
continue
|
|
}
|
|
fieldInfo := typeOfTarget.Field(index)
|
|
tag := fieldInfo.Tag.Get(tagName)
|
|
if tag == "-" {
|
|
continue
|
|
}
|
|
name, defaultValue := parseTag(tag, fieldInfo.Name)
|
|
items, ok := values[name]
|
|
if !ok || len(items) == 0 || items[0] == "" {
|
|
if defaultValue != "" {
|
|
items = []string{defaultValue}
|
|
} else {
|
|
continue
|
|
}
|
|
}
|
|
if err := setValue(field, items[0]); err != nil {
|
|
return &bindError{fieldInfo.Name + ": " + err.Error()}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parseTag(tag, fallback string) (string, string) {
|
|
if tag == "" {
|
|
return fallback, ""
|
|
}
|
|
parts := strings.Split(tag, ",")
|
|
name := parts[0]
|
|
if name == "" {
|
|
name = fallback
|
|
}
|
|
var defaultValue string
|
|
for _, part := range parts[1:] {
|
|
if strings.HasPrefix(part, "default=") {
|
|
defaultValue = strings.TrimPrefix(part, "default=")
|
|
}
|
|
}
|
|
return name, defaultValue
|
|
}
|
|
|
|
func setValue(target reflect.Value, value string) error {
|
|
switch target.Kind() {
|
|
case reflect.String:
|
|
target.SetString(value)
|
|
case reflect.Bool:
|
|
parsed, err := strconv.ParseBool(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
target.SetBool(parsed)
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
parsed, err := strconv.ParseInt(value, 10, target.Type().Bits())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
target.SetInt(parsed)
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
parsed, err := strconv.ParseUint(value, 10, target.Type().Bits())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
target.SetUint(parsed)
|
|
case reflect.Float32, reflect.Float64:
|
|
parsed, err := strconv.ParseFloat(value, target.Type().Bits())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
target.SetFloat(parsed)
|
|
default:
|
|
return &bindError{"unsupported field type " + target.Type().String()}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func singleValues(values map[string]string) map[string][]string {
|
|
result := make(map[string][]string, len(values))
|
|
for key, value := range values {
|
|
result[key] = []string{value}
|
|
}
|
|
return result
|
|
}
|
|
|
|
type bindError struct{ message string }
|
|
|
|
func (e *bindError) Error() string { return e.message }
|