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
This commit is contained in:
2026-08-16 23:51:58 +08:00
commit 4bf991b3e7
24 changed files with 3017 additions and 0 deletions
+165
View File
@@ -0,0 +1,165 @@
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
}