Compare commits

..

2 Commits

2 changed files with 187 additions and 96 deletions

View File

@@ -1,122 +1,123 @@
package route
import (
"fmt"
"sort"
"fmt"
"sort"
"github.com/iancoleman/strcase"
"github.com/samber/lo"
"github.com/iancoleman/strcase"
"github.com/samber/lo"
)
type RenderBuildOpts struct {
PackageName string
ProjectPackage string
Routes []RouteDefinition
PackageName string
ProjectPackage string
Routes []RouteDefinition
}
func buildRenderData(opts RenderBuildOpts) (RenderData, error) {
rd := RenderData{
PackageName: opts.PackageName,
ProjectPackage: opts.ProjectPackage,
Imports: []string{},
Controllers: []string{},
Routes: make(map[string][]Router),
RouteGroups: []string{},
}
rd := RenderData{
PackageName: opts.PackageName,
ProjectPackage: opts.ProjectPackage,
Imports: []string{},
Controllers: []string{},
Routes: make(map[string][]Router),
RouteGroups: []string{},
}
imports := []string{}
controllers := []string{}
imports := []string{}
controllers := []string{}
for _, route := range opts.Routes {
imports = append(imports, route.Imports...)
controllers = append(controllers, fmt.Sprintf("%s *%s", strcase.ToLowerCamel(route.Name), route.Name))
for _, route := range opts.Routes {
imports = append(imports, route.Imports...)
controllers = append(controllers, fmt.Sprintf("%s *%s", strcase.ToLowerCamel(route.Name), route.Name))
for _, action := range route.Actions {
funcName := fmt.Sprintf("Func%d", len(action.Params))
if action.HasData {
funcName = "Data" + funcName
}
for _, action := range route.Actions {
funcName := fmt.Sprintf("Func%d", len(action.Params))
if action.HasData {
funcName = "Data" + funcName
}
params := lo.FilterMap(action.Params, func(item ParamDefinition, _ int) (string, bool) {
tok := buildParamToken(item)
if tok == "" {
return "", false
}
return tok, true
})
params := lo.FilterMap(action.Params, func(item ParamDefinition, _ int) (string, bool) {
tok := buildParamToken(item)
if tok == "" {
return "", false
}
return tok, true
})
rd.Routes[route.Name] = append(rd.Routes[route.Name], Router{
Method: strcase.ToCamel(action.Method),
Route: action.Route,
Controller: strcase.ToLowerCamel(route.Name),
Action: action.Name,
Func: funcName,
Params: params,
})
}
}
rd.Routes[route.Name] = append(rd.Routes[route.Name], Router{
Method: strcase.ToCamel(action.Method),
Route: action.Route,
Controller: strcase.ToLowerCamel(route.Name),
Action: action.Name,
Func: funcName,
Params: params,
})
}
}
// de-dup and sort imports/controllers for stable output
rd.Imports = lo.Uniq(imports)
sort.Strings(rd.Imports)
rd.Controllers = lo.Uniq(controllers)
sort.Strings(rd.Controllers)
// de-dup and sort imports/controllers for stable output
rd.Imports = lo.Uniq(imports)
sort.Strings(rd.Imports)
rd.Controllers = lo.Uniq(controllers)
sort.Strings(rd.Controllers)
// stable order for route groups and entries
for k := range rd.Routes {
rd.RouteGroups = append(rd.RouteGroups, k)
}
sort.Strings(rd.RouteGroups)
for _, k := range rd.RouteGroups {
items := rd.Routes[k]
sort.Slice(items, func(i, j int) bool {
if items[i].Method != items[j].Method {
return items[i].Method < items[j].Method
}
if items[i].Route != items[j].Route {
return items[i].Route < items[j].Route
}
return items[i].Action < items[j].Action
})
rd.Routes[k] = items
}
// stable order for route groups and entries
for k := range rd.Routes {
rd.RouteGroups = append(rd.RouteGroups, k)
}
sort.Strings(rd.RouteGroups)
for _, k := range rd.RouteGroups {
items := rd.Routes[k]
sort.Slice(items, func(i, j int) bool {
if items[i].Method != items[j].Method {
return items[i].Method < items[j].Method
}
if items[i].Route != items[j].Route {
return items[i].Route < items[j].Route
}
return items[i].Action < items[j].Action
})
rd.Routes[k] = items
}
return rd, nil
return rd, nil
}
func buildParamToken(item ParamDefinition) string {
key := item.Name
if item.Key != "" {
key = item.Key
}
key := item.Name
if item.Key != "" {
key = item.Key
}
switch item.Position {
case PositionQuery:
return fmt.Sprintf(`Query%s[%s]("%s")`, scalarSuffix(item.Type), item.Type, key)
case PositionHeader:
return fmt.Sprintf(`Header[%s]("%s")`, item.Type, key)
case PositionFile:
return fmt.Sprintf(`File[multipart.FileHeader]("%s")`, key)
case PositionCookie:
if item.Type == "string" {
return fmt.Sprintf(`CookieParam("%s")`, key)
}
return fmt.Sprintf(`Cookie[%s]("%s")`, item.Type, key)
case PositionBody:
return fmt.Sprintf(`Body[%s]("%s")`, item.Type, key)
case PositionPath:
return fmt.Sprintf(`Path%s[%s]("%s")`, scalarSuffix(item.Type), item.Type, key)
case PositionLocal:
return fmt.Sprintf(`Local[%s]("%s")`, item.Type, key)
}
return ""
switch item.Position {
case PositionQuery:
return fmt.Sprintf(`Query%s[%s]("%s")`, scalarSuffix(item.Type), item.Type, key)
case PositionHeader:
return fmt.Sprintf(`Header[%s]("%s")`, item.Type, key)
case PositionFile:
return fmt.Sprintf(`File[multipart.FileHeader]("%s")`, key)
case PositionCookie:
if item.Type == "string" {
return fmt.Sprintf(`CookieParam("%s")`, key)
}
return fmt.Sprintf(`Cookie[%s]("%s")`, item.Type, key)
case PositionBody:
return fmt.Sprintf(`Body[%s]("%s")`, item.Type, key)
case PositionPath:
return fmt.Sprintf(`Path%s[%s]("%s")`, scalarSuffix(item.Type), item.Type, key)
case PositionLocal:
return fmt.Sprintf(`Local[%s]("%s")`, item.Type, key)
}
return ""
}
func scalarSuffix(t string) string {
switch t {
case "string", "int", "int32", "int64", "float32", "float64", "bool":
return "Param"
}
return ""
switch t {
case "string", "int", "int8", "int16", "int32", "int64",
"uint", "uint8", "uint16", "uint32", "uint64",
"float32", "float64", "bool":
return "Param"
}
return ""
}

View File

@@ -0,0 +1,90 @@
## 路由定义
路由由控制器方法的注释进行声明式定义,解析器会从 Go AST 中读取注释、方法签名与参数列表,生成对应的路由注册与参数绑定代码。
- 关键标签:`@Router` 定义路径与方法;`@Bind` 定义方法参数与其来源位置及键名。
- 生成行为:根据注释规则生成 `router.<METHOD>(path, FuncN(...))` 或 `DataFuncN(...)` 包装调用,并自动汇聚所需 imports 与控制器注入字段。
### 注释语法
- `@Router <path> [<method>]`
- 例如:`@Router /users/:id [get]`
- `@Bind <paramName> <position> [key(<key>)] [table(<table>)] [model(<model>)]`
- `paramName` 必须与方法参数名一致(大小写敏感)
- `position` 取值:`path`、`query`、`body`、`header`、`cookie`、`local`、`file`
- 可选:`key()` 为覆盖默认键名;`table()`、`model()` 解析并保留,当前不参与渲染(为后续扩展预留)
### 参数来源与绑定生成
根据 `position` 与参数类型(是否标量)生成绑定器调用:
- query
- 标量类型:`QueryParam[T]("key")`
- 非标量类型:`Query[T]("key")`
- path
- 标量类型:`PathParam[T]("key")`
- 非标量类型:`Path[T]("key")`
- header`Header[T]("key")`
- body`Body[T]("key")`
- cookie
- `string``CookieParam("key")`
- 其他:`Cookie[T]("key")`
- file`File[multipart.FileHeader]("key")`
- local`Local[T]("key")`
说明:
- 标量类型集合(影响是否使用 `Param` 后缀):`string`、`int`、`int32`、`int64`、`float32`、`float64`、`bool`。
- `key` 默认与 `paramName` 相同;若设置 `key(...)` 则使用其值。
- `file` 使用固定类型参数 `multipart.FileHeader`(请确保依赖可用)。
### 类型与指针处理
- 支持 `T`、`*T`、`pkg.T`、`*pkg.T` 形式;会正确记录选择子表达式对应的 import。
- 忽略结尾为 `Context` 或 `Ctx` 的参数(视为框架上下文)。
- 指针处理:除 `local` 外,会去掉前导 `*` 作为泛型实参;`local` 保留指针(便于写回)。
### 解析与匹配规则
- 先收集注释中的多条 `@Bind`,再按“方法参数列表顺序”匹配并输出绑定器,确保调用顺序与方法签名一致。
- 未在方法参数中的 `@Bind` 会被忽略;缺失 `@Router` 或方法无注释将跳过该方法。
- import 自动收集去重;控制器注入字段名为类型名的小驼峰形式,例如:`userController *UserController`。
### 返回值与包装函数名
- 若方法返回值个数 > 1使用 `DataFuncN`(包含数据与错误等返回)。
- 否则使用 `FuncN`。
- 其中 `N` 为参与绑定的参数个数。
### 示例
注释与方法签名:
```go
// @Router /users/:id [get]
// @Bind id path key(id)
// @Bind fields query
// @Bind token header key(Authorization)
// @Bind sess cookie key(session_id)
// @Bind cfg local
func (uc *UserController) GetUser(ctx context.Context, id int64, fields []string, token string, sess string, cfg *AppConfig) (*User, error)
```
生成的路由注册(示意):
```go
router.Get("/users/:id", DataFunc4(
r.userController.GetUser,
PathParam[int64]("id"),
Query[[]string]("fields"),
Header[string]("Authorization"),
CookieParam("session_id"),
))
```
### 错误与限制
- 无效的 `@Router` 语法会报错;无效的 `position` 会在解析阶段触发错误。
- `file` 目前仅支持单文件头;`table()`、`model()` 尚未参与代码生成。
- 请与实际路由段保持一致(特别是 `path` 的 `key` 与路径变量名)。