Files
fun/check.go
T
chiyi 4bf991b3e7 fun: HTTP RPC framework with DI, guards, streams and codegen
- 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
2026-08-16 23:51:58 +08:00

166 lines
4.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}