feat: bootstrap reusable GoOmni framework
This commit is contained in:
159
validate/validate.go
Normal file
159
validate/validate.go
Normal file
@@ -0,0 +1,159 @@
|
||||
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 }
|
||||
Reference in New Issue
Block a user