feat: bootstrap reusable GoOmni framework

This commit is contained in:
2026-09-20 22:06:24 +08:00
commit 8b19aa79ee
60 changed files with 4923 additions and 0 deletions

159
validate/validate.go Normal file
View 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 }