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:
@@ -0,0 +1,176 @@
|
||||
package fun
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type Gen interface {
|
||||
typeToTemplateType(t reflect.Type) string
|
||||
genService(svc *genSvc, serviceContext *genServiceType)
|
||||
genDefaultService()
|
||||
genStruct(t reflect.Type) *genImportType
|
||||
getEnum(t reflect.Type) *genImportType
|
||||
getName() string
|
||||
}
|
||||
|
||||
// genSvc 生成器视图下的服务
|
||||
type genSvc struct {
|
||||
name string
|
||||
methods []*genMethod
|
||||
}
|
||||
|
||||
// genMethod 生成器视图下的方法
|
||||
type genMethod struct {
|
||||
name string
|
||||
sig reflect.Type // 方法签名类型(不含接收者),Out(0) 为返回类型
|
||||
dtoType reflect.Type
|
||||
isStream bool
|
||||
}
|
||||
|
||||
// serviceGroups 按服务名分组已注册方法
|
||||
func (f *Fun) serviceGroups() map[string][]*genMethod {
|
||||
groups := map[string][]*genMethod{}
|
||||
for key, m := range f.methods {
|
||||
parts := strings.SplitN(key, ".", 2)
|
||||
svc, name := parts[0], parts[1]
|
||||
sig := reflect.New(m.serviceType).Method(m.methodIndex).Type()
|
||||
groups[svc] = append(groups[svc], &genMethod{
|
||||
name: name,
|
||||
sig: sig,
|
||||
dtoType: m.dtoType,
|
||||
isStream: m.isStream,
|
||||
})
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
type genType struct {
|
||||
GenServiceList []*genServiceType
|
||||
}
|
||||
|
||||
type genMethodType struct {
|
||||
MethodName string
|
||||
ReturnValueText string
|
||||
DtoText string
|
||||
ArgsText string
|
||||
GenericTypeText string
|
||||
IsProxy bool
|
||||
IsStream bool
|
||||
}
|
||||
|
||||
type genEnumType struct {
|
||||
Names []string
|
||||
DisplayNames []string
|
||||
Name string
|
||||
}
|
||||
|
||||
type genImportType struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
type genServiceType struct {
|
||||
ServiceName string
|
||||
GenMethodTypeList []*genMethodType
|
||||
GenImport []*genImportType
|
||||
IsIncludeProxy bool
|
||||
IsIncludeStream bool
|
||||
}
|
||||
|
||||
type genClassType struct {
|
||||
Name string
|
||||
GenImport []*genImportType
|
||||
GenClassFieldType []*genClassFieldType
|
||||
}
|
||||
|
||||
type genClassFieldType struct {
|
||||
Name string
|
||||
Type string
|
||||
Tag string
|
||||
}
|
||||
|
||||
func deduplicateServiceImports(imports []*genImportType) []*genImportType {
|
||||
seen := make(map[string]bool)
|
||||
var result []*genImportType
|
||||
for _, imp := range imports {
|
||||
if !seen[imp.Name] {
|
||||
seen[imp.Name] = true
|
||||
result = append(result, imp)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func parseGenericTypeParams(typeName string) string {
|
||||
start := strings.Index(typeName, "[")
|
||||
end := strings.LastIndex(typeName, "]")
|
||||
paramsStr := typeName[start+1 : end]
|
||||
params := strings.Split(paramsStr, ",")
|
||||
for i, param := range params {
|
||||
LL := strings.Split(strings.TrimSpace(param), ".")
|
||||
params[i] = firstLetterToUpper(LL[len(LL)-1])
|
||||
}
|
||||
return strings.Join(params, "")
|
||||
}
|
||||
|
||||
func getGenericTypeName(typeName string) string {
|
||||
start := strings.Index(typeName, "[")
|
||||
return typeName[0:start]
|
||||
}
|
||||
|
||||
func genCode(templateContent string, outputFileName string, templateData any, languageName string) {
|
||||
tmpl, err := template.New(languageName).Parse(templateContent)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err = tmpl.Execute(&buf, templateData)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
code := buf.Bytes()
|
||||
fullPath := filepath.Join(getDirectory(), languageName)
|
||||
|
||||
_, err = os.Stat(fullPath)
|
||||
if os.IsNotExist(err) {
|
||||
err = os.MkdirAll(fullPath, os.ModePerm)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
}
|
||||
err = os.WriteFile(filepath.Join(fullPath, outputFileName+"."+languageName), code, 0644)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// GenCode 执行代码生成:清空输出目录后按顺序运行每个生成器
|
||||
func GenCode(genList ...Gen) {
|
||||
if err := os.RemoveAll(getDirectory()); err != nil && !os.IsNotExist(err) {
|
||||
panic(err.Error())
|
||||
}
|
||||
for _, gen := range genList {
|
||||
gen.genDefaultService()
|
||||
}
|
||||
}
|
||||
|
||||
var directory = "./gen"
|
||||
|
||||
func SetOutput(path string) {
|
||||
directory = path
|
||||
}
|
||||
|
||||
func getDirectory() string {
|
||||
return directory
|
||||
}
|
||||
|
||||
func camelToSnake(s string) string {
|
||||
re := regexp.MustCompile(`([a-z0-9])([A-Z])`)
|
||||
snake := re.ReplaceAllString(s, `${1}_${2}`)
|
||||
return strings.ToLower(snake)
|
||||
}
|
||||
Reference in New Issue
Block a user