- Service binding via reflection with 4 method signatures - Dependency injection (Wired/auto tags) and guards - Streamable HTTP (NDJSON) streaming responses - Go/TypeScript client code generation (self-contained, with interceptors) - Enum support, structured Result, logging - 16 regression tests covering all fixed bugs
166 lines
4.8 KiB
Go
166 lines
4.8 KiB
Go
package fun
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"reflect"
|
||
"strings"
|
||
"unicode"
|
||
)
|
||
|
||
func isPrivate(value string) bool {
|
||
return !unicode.IsUpper([]rune(value)[0])
|
||
}
|
||
|
||
// checkType 注册期递归校验类型是否受支持:
|
||
// int/uint/string/bool/struct/slice/enum;不支持匿名结构体、私有类型、空结构体;
|
||
// 枚举要求 Names/DisplayNames 长度一致
|
||
func checkType(t reflect.Type) {
|
||
if t.Kind() == reflect.Ptr {
|
||
t = t.Elem()
|
||
}
|
||
if strings.Contains(t.String(), "{}") {
|
||
panic(fmt.Sprintf("fun: %s generic types containing 'any' or interface{} are not supported", t.Name()))
|
||
}
|
||
switch t.Kind() {
|
||
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||
reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
|
||
reflect.String, reflect.Bool:
|
||
if t.Kind() == reflect.Uint8 && (t.Implements(displayEnumType) || t.Implements(enumType)) && isPrivate(t.Name()) {
|
||
panic("fun:" + t.Name() + " cannot be Private")
|
||
}
|
||
if t.Kind() == reflect.Uint8 && t.Implements(displayEnumType) {
|
||
enumValue := reflect.New(t).Elem().Interface().(displayEnum)
|
||
if len(enumValue.DisplayNames()) != len(enumValue.Names()) {
|
||
panic("fun: " + t.Name() + " enum names and display names must be the same length")
|
||
}
|
||
}
|
||
case reflect.Struct:
|
||
if t.NumField() == 0 {
|
||
panic("fun: " + t.Name() + " must have at least one field")
|
||
}
|
||
for i := 0; i < t.NumField(); i++ {
|
||
f := t.Field(i)
|
||
if isPrivate(f.Name) {
|
||
panic("fun:" + f.Name + " cannot be Private")
|
||
}
|
||
checkType(f.Type)
|
||
}
|
||
case reflect.Slice:
|
||
checkType(t.Elem())
|
||
default:
|
||
panic("fun:Unsupported types " + t.Name())
|
||
}
|
||
}
|
||
|
||
// checkDto 运行时校验请求数据:
|
||
// 非指针字段必须出现在请求中且非 nil;嵌套 struct/slice 递归;枚举值必须在范围内
|
||
func checkDto(dtoType reflect.Type, dtoMap any, methodName string) error {
|
||
t := dtoType
|
||
if t.Kind() == reflect.Ptr {
|
||
t = t.Elem()
|
||
}
|
||
switch t.Kind() {
|
||
case reflect.Struct:
|
||
obj, ok := dtoMap.(map[string]any)
|
||
if !ok {
|
||
return callError(fmt.Errorf("fun: method %s DTO %s must be an object", methodName, t.Name()))
|
||
}
|
||
for i := 0; i < t.NumField(); i++ {
|
||
f := t.Field(i)
|
||
value, ok := obj[firstLetterToLower(f.Name)]
|
||
if !ok {
|
||
// 兼容按原始字段名(首字母大写)传参的客户端
|
||
value, ok = obj[f.Name]
|
||
}
|
||
// 非指针且非切片字段必须存在且非 null;切片字段允许 null(反序列化为 nil slice)
|
||
if f.Type.Kind() != reflect.Ptr && f.Type.Kind() != reflect.Slice && (!ok || value == nil) {
|
||
return callError(fmt.Errorf("fun: %s Dto must be a pointer or have a corresponding field in the map", f.Name))
|
||
}
|
||
ft := f.Type
|
||
if ft.Kind() == reflect.Ptr {
|
||
ft = ft.Elem()
|
||
}
|
||
if (ft.Kind() == reflect.Struct || ft.Kind() == reflect.Slice) && value != nil {
|
||
if err := checkDto(ft, value, methodName); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
if ft.Kind() == reflect.Uint8 && value != nil && (ft.Implements(displayEnumType) || ft.Implements(enumType)) {
|
||
if err := checkEnumValue(ft, value, f.Name); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
}
|
||
case reflect.Slice:
|
||
list, ok := dtoMap.([]any)
|
||
if !ok {
|
||
return callError(fmt.Errorf("fun: Dto must be an array"))
|
||
}
|
||
for _, value := range list {
|
||
et0 := t.Elem()
|
||
et := et0
|
||
if et.Kind() == reflect.Ptr {
|
||
et = et.Elem()
|
||
}
|
||
// 指针元素允许 null,值元素必须非空
|
||
if et0.Kind() != reflect.Ptr && value == nil {
|
||
return callError(fmt.Errorf("fun:%s Dto must be a pointer or have a corresponding field in the map", et0.Name()))
|
||
}
|
||
if (et.Kind() == reflect.Struct || et.Kind() == reflect.Slice) && value != nil {
|
||
if err := checkDto(et, value, methodName); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
if et.Kind() == reflect.Uint8 && value != nil && (et.Implements(displayEnumType) || et.Implements(enumType)) {
|
||
if err := checkEnumValue(et, value, et.Name()); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// checkEnumValue 运行时校验枚举值是否在范围内
|
||
func checkEnumValue(t reflect.Type, value any, name string) error {
|
||
var max uint8
|
||
enumValue := reflect.New(t).Elem()
|
||
if t.Implements(displayEnumType) {
|
||
max = uint8(len(enumValue.Interface().(displayEnum).Names()))
|
||
} else {
|
||
max = uint8(len(enumValue.Interface().(enum).Names()))
|
||
}
|
||
var num uint8
|
||
switch v := value.(type) {
|
||
case float64:
|
||
num = uint8(v)
|
||
case float32:
|
||
num = uint8(v)
|
||
case uint8:
|
||
num = v
|
||
case uint16:
|
||
num = uint8(v)
|
||
case uint32:
|
||
num = uint8(v)
|
||
case uint64:
|
||
num = uint8(v)
|
||
case int:
|
||
num = uint8(v)
|
||
case int8:
|
||
num = uint8(v)
|
||
case int16:
|
||
num = uint8(v)
|
||
case int32:
|
||
num = uint8(v)
|
||
case int64:
|
||
num = uint8(v)
|
||
default:
|
||
return callError(errors.New("Fun:" + name + " Dto enum value type is not supported"))
|
||
}
|
||
if num >= max {
|
||
return callError(errors.New("Fun:" + name + " Dto value out of range"))
|
||
}
|
||
return nil
|
||
}
|