BREAKING CHANGES: - Guard interface: Guard(ctx Ctx) error; returning error short-circuits (subsequent guards and the method no longer execute) - Wired[T]() (*T, error); New() may return error; failed wiring is sticky - BindService/BindGuard/BindRoute return error; routes accept guards (guards receive merged query/form params as Ctx.State) - registration panics after Start; Ctx.Ip honors X-Forwarded-For/X-Real-IP - internal errors sanitized to fixed client messages FIXES: - int64 precision loss: /cell data decoded from raw JSON bytes and responses serialized with json.Number (no float64 round-trip) - enum values range-checked before uint8 conversion (256 no longer truncates to 0 and slips through) - logger: files failing name parsing are no longer deleted; log channel never blocks request goroutines; ConfigLogger is race-free; logWriterWorker unlock bug fixed - stream writer panics recovered (process no longer crashes); streamDone closed exactly once - generated Go client emits definitions for pointer-to-enum/struct fields - anonymous structs rejected at registration; isPrivate safe on empty names - removed dead Result.Id and RequestInfo.Type ADDITIONS: - graceful shutdown (Shutdown), StartOn, server timeouts by default (Read 60s/Idle 120s/Write off), SetTimeouts/SetMaxConcurrency - big-int-safe JSON in the generated TS client (>2^53 as BigInt) - example/ demo services and cmd/genexample artifact generator
211 lines
6.0 KiB
Go
211 lines
6.0 KiB
Go
package fun
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"math"
|
||
"reflect"
|
||
"strings"
|
||
"unicode"
|
||
)
|
||
|
||
func isPrivate(value string) bool {
|
||
if value == "" {
|
||
return false // 匿名类型名:交由各处的具名校验给出明确报错,不在此越界 panic
|
||
}
|
||
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.Name() == "" {
|
||
panic("fun: anonymous struct types are not supported, define a named type")
|
||
}
|
||
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:
|
||
name := t.Name()
|
||
if name == "" {
|
||
name = t.String() // 匿名/内建组合类型(如 interface{})给出可读的名字
|
||
}
|
||
panic("fun:Unsupported types " + 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 运行时校验枚举值是否在范围内。
|
||
// 必须在原始数值上先判范围再转 uint8:否则 256/512 等越界值先被截断成
|
||
// 合法小值(256→0),绕过范围检查后静默落到错误的枚举项上
|
||
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()))
|
||
}
|
||
outOfRange := callError(errors.New("Fun:" + name + " Dto value out of range"))
|
||
var num uint8
|
||
switch v := value.(type) {
|
||
case float64:
|
||
if v < 0 || v != math.Trunc(v) || v >= float64(max) {
|
||
return outOfRange
|
||
}
|
||
num = uint8(v)
|
||
case float32:
|
||
f := float64(v)
|
||
if f < 0 || f != math.Trunc(f) || f >= float64(max) {
|
||
return outOfRange
|
||
}
|
||
num = uint8(f)
|
||
case uint8:
|
||
num = v
|
||
case uint16:
|
||
if v >= uint16(max) {
|
||
return outOfRange
|
||
}
|
||
num = uint8(v)
|
||
case uint32:
|
||
if v >= uint32(max) {
|
||
return outOfRange
|
||
}
|
||
num = uint8(v)
|
||
case uint64:
|
||
if v >= uint64(max) {
|
||
return outOfRange
|
||
}
|
||
num = uint8(v)
|
||
case int:
|
||
if v < 0 || v >= int(max) {
|
||
return outOfRange
|
||
}
|
||
num = uint8(v)
|
||
case int8:
|
||
if v < 0 || int(v) >= int(max) {
|
||
return outOfRange
|
||
}
|
||
num = uint8(v)
|
||
case int16:
|
||
if v < 0 || int(v) >= int(max) {
|
||
return outOfRange
|
||
}
|
||
num = uint8(v)
|
||
case int32:
|
||
if v < 0 || int64(v) >= int64(max) {
|
||
return outOfRange
|
||
}
|
||
num = uint8(v)
|
||
case int64:
|
||
if v < 0 || v >= int64(max) {
|
||
return outOfRange
|
||
}
|
||
num = uint8(v)
|
||
default:
|
||
return callError(errors.New("Fun:" + name + " Dto enum value type is not supported"))
|
||
}
|
||
if num >= max {
|
||
return outOfRange
|
||
}
|
||
return nil
|
||
}
|