You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
go-zero/tools/goctl/pkg/parser/api/parser/analyzer.go

479 lines
11 KiB
Go

package parser
import (
"fmt"
"sort"
"strings"
"github.com/zeromicro/go-zero/core/lang"
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
"github.com/zeromicro/go-zero/tools/goctl/pkg/parser/api/ast"
"github.com/zeromicro/go-zero/tools/goctl/pkg/parser/api/importstack"
"github.com/zeromicro/go-zero/tools/goctl/pkg/parser/api/placeholder"
"github.com/zeromicro/go-zero/tools/goctl/pkg/parser/api/token"
)
// Analyzer analyzes the ast and converts it to spec.
type Analyzer struct {
api *API
spec *spec.ApiSpec
}
func (a *Analyzer) astTypeToSpec(in ast.DataType) (spec.Type, error) {
isLiteralType := func(dt ast.DataType) bool {
if _, ok := dt.(*ast.BaseDataType); ok {
return true
}
_, ok := dt.(*ast.AnyDataType)
return ok
}
switch v := (in).(type) {
case *ast.BaseDataType:
raw := v.RawText()
if IsBaseType(raw) {
return spec.PrimitiveType{
RawName: raw,
}, nil
}
return spec.DefineStruct{RawName: raw}, nil
case *ast.AnyDataType:
return nil, ast.SyntaxError(v.Pos(), "unsupported any type")
case *ast.StructDataType:
// TODO(keson) feature: can be extended
case *ast.InterfaceDataType:
return spec.InterfaceType{RawName: v.RawText()}, nil
case *ast.MapDataType:
if !isLiteralType(v.Key) {
return nil, ast.SyntaxError(v.Pos(), "expected literal type, got <%T>", v)
}
if !v.Key.CanEqual() {
return nil, ast.SyntaxError(v.Pos(), "map key <%T> must be equal data type", v)
}
value, err := a.astTypeToSpec(v.Value)
if err != nil {
return nil, err
}
return spec.MapType{
RawName: v.RawText(),
Key: v.RawText(),
Value: value,
}, nil
case *ast.PointerDataType:
raw := v.DataType.RawText()
if IsBaseType(raw) {
return spec.PointerType{RawName: v.RawText(), Type: spec.PrimitiveType{RawName: raw}}, nil
}
value, err := a.astTypeToSpec(v.DataType)
if err != nil {
return nil, err
}
return spec.PointerType{
RawName: v.RawText(),
Type: value,
}, nil
case *ast.ArrayDataType:
if v.Length.Token.Type == token.ELLIPSIS {
return nil, ast.SyntaxError(v.Pos(), "Array: unsupported dynamic length")
}
value, err := a.astTypeToSpec(v.DataType)
if err != nil {
return nil, err
}
return spec.ArrayType{
RawName: v.RawText(),
Value: value,
}, nil
case *ast.SliceDataType:
value, err := a.astTypeToSpec(v.DataType)
if err != nil {
return nil, err
}
return spec.ArrayType{
RawName: v.RawText(),
Value: value,
}, nil
}
return nil, ast.SyntaxError(in.Pos(), "unsupported type <%T>", in)
}
func (a *Analyzer) convert2Spec() error {
a.fillInfo()
if err := a.fillTypes(); err != nil {
return err
}
if err := a.fillService(); err != nil {
return err
}
sort.SliceStable(a.spec.Types, func(i, j int) bool {
return a.spec.Types[i].Name() < a.spec.Types[j].Name()
})
groups := make([]spec.Group, 0, len(a.spec.Service.Groups))
for _, v := range a.spec.Service.Groups {
sort.SliceStable(v.Routes, func(i, j int) bool {
return v.Routes[i].Path < v.Routes[j].Path
})
groups = append(groups, v)
}
sort.SliceStable(groups, func(i, j int) bool {
return groups[i].Annotation.Properties[groupKeyText] < groups[j].Annotation.Properties[groupKeyText]
})
a.spec.Service.Groups = groups
return nil
}
func (a *Analyzer) convertAtDoc(atDoc ast.AtDocStmt) spec.AtDoc {
var ret spec.AtDoc
switch val := atDoc.(type) {
case *ast.AtDocLiteralStmt:
ret.Text = val.Value.Token.Text
case *ast.AtDocGroupStmt:
ret.Properties = a.convertKV(val.Values)
}
return ret
}
func (a *Analyzer) convertKV(kv []*ast.KVExpr) map[string]string {
var ret = map[string]string{}
for _, v := range kv {
key := strings.TrimSuffix(v.Key.Token.Text, ":")
if key == summaryKeyText {
ret[key] = v.Value.RawText()
} else {
ret[key] = v.Value.Token.Text
}
}
return ret
}
func (a *Analyzer) fieldToMember(field *ast.ElemExpr) (spec.Member, error) {
var name []string
for _, v := range field.Name {
name = append(name, v.Token.Text)
}
tp, err := a.astTypeToSpec(field.DataType)
if err != nil {
return spec.Member{}, err
}
head, leading := field.CommentGroup()
m := spec.Member{
Name: strings.Join(name, ", "),
Type: tp,
Docs: head.List(),
Comment: leading.String(),
IsInline: field.IsAnonymous(),
}
if field.Tag != nil {
m.Tag = field.Tag.Token.Text
}
return m, nil
}
func (a *Analyzer) fillRouteType(route *spec.Route) error {
if route.RequestType != nil {
switch route.RequestType.(type) {
case spec.DefineStruct:
tp, err := a.findDefinedType(route.RequestType.Name())
if err != nil {
return err
}
route.RequestType = tp
}
}
if route.ResponseType != nil {
switch route.ResponseType.(type) {
case spec.DefineStruct:
tp, err := a.findDefinedType(route.ResponseType.Name())
if err != nil {
return err
}
route.ResponseType = tp
}
}
return nil
}
func (a *Analyzer) fillService() error {
var groups []spec.Group
for _, item := range a.api.ServiceStmts {
var group spec.Group
if item.AtServerStmt != nil {
group.Annotation.Properties = a.convertKV(item.AtServerStmt.Values)
}
for _, astRoute := range item.Routes {
head, leading := astRoute.CommentGroup()
route := spec.Route{
Method: astRoute.Route.Method.Token.Text,
Path: astRoute.Route.Path.Format(""),
Doc: head.List(),
Comment: leading.List(),
}
if astRoute.AtDoc != nil {
route.AtDoc = a.convertAtDoc(astRoute.AtDoc)
}
if astRoute.AtHandler != nil {
route.AtDoc = a.convertAtDoc(astRoute.AtDoc)
route.Handler = astRoute.AtHandler.Name.Token.Text
head, leading := astRoute.AtHandler.CommentGroup()
route.HandlerDoc = head.List()
route.HandlerComment = leading.List()
}
if astRoute.Route.Request != nil && astRoute.Route.Request.Body != nil {
requestType, err := a.getType(astRoute.Route.Request)
if err != nil {
return err
}
route.RequestType = requestType
}
if astRoute.Route.Response != nil && astRoute.Route.Response.Body != nil {
responseType, err := a.getType(astRoute.Route.Response)
if err != nil {
return err
}
route.ResponseType = responseType
}
if err := a.fillRouteType(&route); err != nil {
return err
}
group.Routes = append(group.Routes, route)
name := item.Name.Format("")
if len(a.spec.Service.Name) > 0 && a.spec.Service.Name != name {
return ast.SyntaxError(item.Name.Pos(), "multiple service names defined <%s> and <%s>", name, a.spec.Service.Name)
}
a.spec.Service.Name = name
}
groups = append(groups, group)
}
a.spec.Service.Groups = groups
return nil
}
func (a *Analyzer) fillInfo() {
properties := make(map[string]string)
if a.api.info != nil {
for _, kv := range a.api.info.Values {
key := kv.Key.Token.Text
properties[strings.TrimSuffix(key, ":")] = kv.Value.RawText()
}
}
a.spec.Info.Properties = properties
infoKeyValue := make(map[string]string)
for key, value := range properties {
titleKey := strings.Title(strings.TrimSuffix(key, ":"))
infoKeyValue[titleKey] = value
}
a.spec.Info.Title = infoKeyValue[infoTitleKey]
a.spec.Info.Desc = infoKeyValue[infoDescKey]
a.spec.Info.Version = infoKeyValue[infoVersionKey]
a.spec.Info.Author = infoKeyValue[infoAuthorKey]
a.spec.Info.Email = infoKeyValue[infoEmailKey]
}
func (a *Analyzer) fillTypes() error {
for _, item := range a.api.TypeStmt {
switch v := (item).(type) {
case *ast.TypeLiteralStmt:
if err := a.fillTypeExpr(v.Expr); err != nil {
return err
}
case *ast.TypeGroupStmt:
for _, expr := range v.ExprList {
err := a.fillTypeExpr(expr)
if err != nil {
return err
}
}
}
}
var types []spec.Type
for _, item := range a.spec.Types {
switch v := (item).(type) {
case spec.DefineStruct:
var members []spec.Member
for _, member := range v.Members {
switch v := member.Type.(type) {
case spec.DefineStruct:
tp, err := a.findDefinedType(v.RawName)
if err != nil {
return err
}
member.Type = tp
}
members = append(members, member)
}
v.Members = members
types = append(types, v)
default:
return fmt.Errorf("unknown type %+v", v)
}
}
a.spec.Types = types
return nil
}
func (a *Analyzer) fillTypeExpr(expr *ast.TypeExpr) error {
head, _ := expr.CommentGroup()
switch val := expr.DataType.(type) {
case *ast.StructDataType:
var members []spec.Member
for _, item := range val.Elements {
m, err := a.fieldToMember(item)
if err != nil {
return err
}
members = append(members, m)
}
a.spec.Types = append(a.spec.Types, spec.DefineStruct{
RawName: expr.Name.Token.Text,
Members: members,
Docs: head.List(),
})
return nil
default:
return ast.SyntaxError(expr.Pos(), "expected <struct> expr, got <%T>", expr.DataType)
}
}
func (a *Analyzer) findDefinedType(name string) (spec.Type, error) {
for _, item := range a.spec.Types {
if _, ok := item.(spec.DefineStruct); ok {
if item.Name() == name {
return item, nil
}
}
}
return nil, fmt.Errorf("type %s not defined", name)
}
func (a *Analyzer) getType(expr *ast.BodyStmt) (spec.Type, error) {
body := expr.Body
var tp spec.Type
var err error
var rawText = body.Format("")
if IsBaseType(body.Value.Token.Text) {
tp = spec.PrimitiveType{RawName: body.Value.Token.Text}
} else {
tp, err = a.findDefinedType(body.Value.Token.Text)
if err != nil {
return nil, err
}
}
if body.LBrack != nil {
if body.Star != nil {
return spec.PointerType{
RawName: rawText,
Type: tp,
}, nil
}
return spec.ArrayType{
RawName: rawText,
Value: tp,
}, nil
}
if body.Star != nil {
return spec.PointerType{
RawName: rawText,
Type: tp,
}, nil
}
return tp, nil
}
// Parse parses the given file and returns the parsed spec.
func Parse(filename string, src interface{}) (*spec.ApiSpec, error) {
p := New(filename, src)
ast := p.Parse()
if err := p.CheckErrors(); err != nil {
return nil, err
}
is := importstack.New()
err := is.Push(ast.Filename)
if err != nil {
return nil, err
}
importSet := map[string]lang.PlaceholderType{}
api, err := convert2API(ast, importSet, is)
if err != nil {
return nil, err
}
if err := api.SelfCheck(); err != nil {
return nil, err
}
var result = new(spec.ApiSpec)
analyzer := Analyzer{
api: api,
spec: result,
}
err = analyzer.convert2Spec()
if err != nil {
return nil, err
}
return result, nil
}
var kind = map[string]placeholder.Type{
"bool": placeholder.PlaceHolder,
"int": placeholder.PlaceHolder,
"int8": placeholder.PlaceHolder,
"int16": placeholder.PlaceHolder,
"int32": placeholder.PlaceHolder,
"int64": placeholder.PlaceHolder,
"uint": placeholder.PlaceHolder,
"uint8": placeholder.PlaceHolder,
"uint16": placeholder.PlaceHolder,
"uint32": placeholder.PlaceHolder,
"uint64": placeholder.PlaceHolder,
"uintptr": placeholder.PlaceHolder,
"float32": placeholder.PlaceHolder,
"float64": placeholder.PlaceHolder,
"complex64": placeholder.PlaceHolder,
"complex128": placeholder.PlaceHolder,
"string": placeholder.PlaceHolder,
"byte": placeholder.PlaceHolder,
"rune": placeholder.PlaceHolder,
"any": placeholder.PlaceHolder,
}
// IsBaseType returns true if the given type is a base type.
func IsBaseType(text string) bool {
_, ok := kind[text]
return ok
}