goctl added

This commit is contained in:
kim
2020-07-29 17:11:41 +08:00
parent b1975d29a7
commit 121323b8c3
142 changed files with 10690 additions and 0 deletions

View File

@@ -0,0 +1,182 @@
package parser
import (
"bufio"
"fmt"
"strings"
)
const (
startState = iota
attrNameState
attrValueState
attrColonState
multilineState
)
type baseState struct {
r *bufio.Reader
lineNumber *int
}
func newBaseState(r *bufio.Reader, lineNumber *int) *baseState {
return &baseState{
r: r,
lineNumber: lineNumber,
}
}
func (s *baseState) parseProperties() (map[string]string, error) {
var r = s.r
var attributes = make(map[string]string)
var builder strings.Builder
var key string
var st = startState
for {
ch, err := s.read()
if err != nil {
return nil, err
}
switch st {
case startState:
switch {
case isNewline(ch):
return nil, fmt.Errorf("%q should be on the same line with %q", leftParenthesis, infoDirective)
case isSpace(ch):
continue
case ch == leftParenthesis:
st = attrNameState
default:
return nil, fmt.Errorf("unexpected char %q after %q", ch, infoDirective)
}
case attrNameState:
switch {
case isNewline(ch):
if builder.Len() > 0 {
return nil, fmt.Errorf("unexpected newline after %q", builder.String())
}
case isLetterDigit(ch):
builder.WriteRune(ch)
case isSpace(ch):
if builder.Len() > 0 {
key = builder.String()
builder.Reset()
st = attrColonState
}
case ch == colon:
if builder.Len() == 0 {
return nil, fmt.Errorf("unexpected leading %q", ch)
}
key = builder.String()
builder.Reset()
st = attrValueState
case ch == rightParenthesis:
return attributes, nil
}
case attrColonState:
switch {
case isSpace(ch):
continue
case ch == colon:
st = attrValueState
default:
return nil, fmt.Errorf("bad char %q after %q in %q", ch, key, infoDirective)
}
case attrValueState:
switch {
case ch == multilineBeginTag:
if builder.Len() > 0 {
return nil, fmt.Errorf("%q before %q", builder.String(), multilineBeginTag)
} else {
st = multilineState
}
case isSpace(ch):
if builder.Len() > 0 {
builder.WriteRune(ch)
}
case isNewline(ch):
attributes[key] = builder.String()
builder.Reset()
st = attrNameState
case ch == rightParenthesis:
attributes[key] = builder.String()
builder.Reset()
return attributes, nil
default:
builder.WriteRune(ch)
}
case multilineState:
switch {
case ch == multilineEndTag:
attributes[key] = builder.String()
builder.Reset()
st = attrNameState
case isNewline(ch):
var multipleNewlines bool
loopAfterNewline:
for {
next, err := read(r)
if err != nil {
return nil, err
}
switch {
case isSpace(next):
continue
case isNewline(next):
multipleNewlines = true
default:
if err := unread(r); err != nil {
return nil, err
}
break loopAfterNewline
}
}
if multipleNewlines {
fmt.Fprintln(&builder)
} else {
builder.WriteByte(' ')
}
case ch == rightParenthesis:
if builder.Len() > 0 {
attributes[key] = builder.String()
builder.Reset()
}
return attributes, nil
default:
builder.WriteRune(ch)
}
}
}
}
func (s *baseState) read() (rune, error) {
value, err := read(s.r)
if err != nil {
return 0, err
}
if isNewline(value) {
*s.lineNumber++
}
return value, nil
}
func (s *baseState) readLine() (string, error) {
line, _, err := s.r.ReadLine()
if err != nil {
return "", err
}
*s.lineNumber++
return string(line), nil
}
func (s *baseState) skipSpaces() error {
return skipSpaces(s.r)
}
func (s *baseState) unread() error {
return unread(s.r)
}

View File

@@ -0,0 +1,20 @@
package parser
import (
"bufio"
"bytes"
"testing"
"github.com/stretchr/testify/assert"
)
func TestProperties(t *testing.T) {
const text = `(summary: hello world)`
var builder bytes.Buffer
builder.WriteString(text)
var lineNumber = 1
var state = newBaseState(bufio.NewReader(&builder), &lineNumber)
m, err := state.parseProperties()
assert.Nil(t, err)
assert.Equal(t, "hello world", m["summary"])
}

View File

@@ -0,0 +1,132 @@
package parser
import (
"errors"
"fmt"
"strings"
"zero/tools/goctl/api/spec"
)
type (
entity struct {
state *baseState
api *spec.ApiSpec
parser entityParser
}
entityParser interface {
parseLine(line string, api *spec.ApiSpec, annos []spec.Annotation) error
setEntityName(name string)
}
)
func newEntity(state *baseState, api *spec.ApiSpec, parser entityParser) entity {
return entity{
state: state,
api: api,
parser: parser,
}
}
func (s *entity) process() error {
line, err := s.state.readLine()
if err != nil {
return err
}
fields := strings.Fields(line)
if len(fields) < 2 {
return fmt.Errorf("invalid type definition for %q",
strings.TrimSpace(strings.Trim(string(line), "{")))
}
if len(fields) == 2 {
if fields[1] != leftBrace {
return fmt.Errorf("bad string %q after type", fields[1])
}
} else if len(fields) == 3 {
if fields[1] != typeStruct {
return fmt.Errorf("bad string %q after type", fields[1])
}
if fields[2] != leftBrace {
return fmt.Errorf("bad string %q after type", fields[2])
}
}
s.parser.setEntityName(fields[0])
var annos []spec.Annotation
memberLoop:
for {
ch, err := s.state.read()
if err != nil {
return err
}
var annoName string
var builder strings.Builder
switch {
case ch == at:
annotationLoop:
for {
next, err := s.state.read()
if err != nil {
return err
}
switch {
case isSpace(next):
if builder.Len() > 0 {
annoName = builder.String()
builder.Reset()
}
case isNewline(next):
if builder.Len() == 0 {
return errors.New("invalid annotation format")
}
case next == leftParenthesis:
if builder.Len() == 0 {
return errors.New("invalid annotation format")
}
annoName = builder.String()
builder.Reset()
if err := s.state.unread(); err != nil {
return err
}
attrs, err := s.state.parseProperties()
if err != nil {
return err
}
annos = append(annos, spec.Annotation{
Name: annoName,
Properties: attrs,
})
break annotationLoop
default:
builder.WriteRune(next)
}
}
case ch == rightBrace:
break memberLoop
case isLetterDigit(ch):
if err := s.state.unread(); err != nil {
return err
}
var line string
line, err = s.state.readLine()
if err != nil {
return err
}
line = strings.TrimSpace(line)
if err := s.parser.parseLine(line, s.api, annos); err != nil {
return err
}
annos = nil
}
}
return nil
}

View File

@@ -0,0 +1,62 @@
package parser
import (
"fmt"
"strings"
"zero/tools/goctl/api/spec"
)
const (
titleTag = "title"
descTag = "desc"
versionTag = "version"
authorTag = "author"
emailTag = "email"
)
type infoState struct {
*baseState
innerState int
}
func newInfoState(st *baseState) state {
return &infoState{
baseState: st,
innerState: startState,
}
}
func (s *infoState) process(api *spec.ApiSpec) (state, error) {
attrs, err := s.parseProperties()
if err != nil {
return nil, err
}
if err := s.writeInfo(api, attrs); err != nil {
return nil, err
}
return newRootState(s.r, s.lineNumber), nil
}
func (s *infoState) writeInfo(api *spec.ApiSpec, attrs map[string]string) error {
for k, v := range attrs {
switch k {
case titleTag:
api.Info.Title = strings.TrimSpace(v)
case descTag:
api.Info.Desc = strings.TrimSpace(v)
case versionTag:
api.Info.Version = strings.TrimSpace(v)
case authorTag:
api.Info.Author = strings.TrimSpace(v)
case emailTag:
api.Info.Email = strings.TrimSpace(v)
default:
return fmt.Errorf("unknown directive %q in %q section", k, infoDirective)
}
}
return nil
}

View File

@@ -0,0 +1,57 @@
package parser
import (
"bufio"
"bytes"
"fmt"
"io"
"io/ioutil"
"zero/tools/goctl/api/spec"
)
type Parser struct {
r *bufio.Reader
st string
}
func NewParser(filename string) (*Parser, error) {
api, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
info, body, service, err := MatchStruct(string(api))
if err != nil {
return nil, err
}
var buffer = new(bytes.Buffer)
buffer.WriteString(info)
buffer.WriteString(service)
return &Parser{
r: bufio.NewReader(buffer),
st: body,
}, nil
}
func (p *Parser) Parse() (api *spec.ApiSpec, err error) {
api = new(spec.ApiSpec)
types, err := parseStructAst(p.st)
if err != nil {
return nil, err
}
api.Types = types
var lineNumber = 1
st := newRootState(p.r, &lineNumber)
for {
st, err = st.process(api)
if err == io.EOF {
return api, p.validate(api)
}
if err != nil {
return nil, fmt.Errorf("near line: %d, %s", lineNumber, err.Error())
}
if st == nil {
return api, p.validate(api)
}
}
}

View File

@@ -0,0 +1,109 @@
package parser
import (
"bufio"
"fmt"
"strings"
"zero/tools/goctl/api/spec"
)
type rootState struct {
*baseState
}
func newRootState(r *bufio.Reader, lineNumber *int) state {
var state = newBaseState(r, lineNumber)
return rootState{
baseState: state,
}
}
func (s rootState) process(api *spec.ApiSpec) (state, error) {
var annos []spec.Annotation
var builder strings.Builder
for {
ch, err := s.read()
if err != nil {
return nil, err
}
switch {
case isSpace(ch):
if builder.Len() == 0 {
continue
}
token := builder.String()
builder.Reset()
return s.processToken(token, annos)
case ch == at:
if builder.Len() > 0 {
return nil, fmt.Errorf("%q before %q", builder.String(), at)
}
var annoName string
annoLoop:
for {
next, err := s.read()
if err != nil {
return nil, err
}
switch {
case isSpace(next):
if builder.Len() > 0 {
annoName = builder.String()
builder.Reset()
}
case next == leftParenthesis:
if err := s.unread(); err != nil {
return nil, err
}
if builder.Len() > 0 {
annoName = builder.String()
builder.Reset()
}
attrs, err := s.parseProperties()
if err != nil {
return nil, err
}
annos = append(annos, spec.Annotation{
Name: annoName,
Properties: attrs,
})
break annoLoop
default:
builder.WriteRune(next)
}
}
case ch == leftParenthesis:
if builder.Len() == 0 {
return nil, fmt.Errorf("incorrect %q at the beginning of the line", leftParenthesis)
}
if err := s.unread(); err != nil {
return nil, err
}
token := builder.String()
builder.Reset()
return s.processToken(token, annos)
case isLetterDigit(ch):
builder.WriteRune(ch)
case isNewline(ch):
if builder.Len() > 0 {
return nil, fmt.Errorf("incorrect newline after %q", builder.String())
}
}
}
}
func (s rootState) processToken(token string, annos []spec.Annotation) (state, error) {
switch token {
case infoDirective:
return newInfoState(s.baseState), nil
//case typeDirective:
//return newTypeState(s.baseState, annos), nil
case serviceDirective:
return newServiceState(s.baseState, annos), nil
default:
return nil, fmt.Errorf("wrong directive %q", token)
}
}

View File

@@ -0,0 +1,97 @@
package parser
import (
"fmt"
"strings"
"zero/tools/goctl/api/spec"
)
type serviceState struct {
*baseState
annos []spec.Annotation
}
func newServiceState(state *baseState, annos []spec.Annotation) state {
return &serviceState{
baseState: state,
annos: annos,
}
}
func (s *serviceState) process(api *spec.ApiSpec) (state, error) {
var name string
var routes []spec.Route
parser := &serviceEntityParser{
acceptName: func(n string) {
name = n
},
acceptRoute: func(route spec.Route) {
routes = append(routes, route)
},
}
ent := newEntity(s.baseState, api, parser)
if err := ent.process(); err != nil {
return nil, err
}
api.Service = spec.Service{
Name: name,
Annotations: append(api.Service.Annotations, s.annos...),
Routes: append(api.Service.Routes, routes...),
Groups: append(api.Service.Groups, spec.Group{
Annotations: s.annos,
Routes: routes,
}),
}
return newRootState(s.r, s.lineNumber), nil
}
type serviceEntityParser struct {
acceptName func(name string)
acceptRoute func(route spec.Route)
}
func (p *serviceEntityParser) parseLine(line string, api *spec.ApiSpec, annos []spec.Annotation) error {
fields := strings.Fields(line)
if len(fields) < 2 {
return fmt.Errorf("wrong line %q", line)
}
method := fields[0]
pathAndRequest := fields[1]
pos := strings.Index(pathAndRequest, "(")
if pos < 0 {
return fmt.Errorf("wrong line %q", line)
}
path := strings.TrimSpace(pathAndRequest[:pos])
pathAndRequest = pathAndRequest[pos+1:]
pos = strings.Index(pathAndRequest, ")")
if pos < 0 {
return fmt.Errorf("wrong line %q", line)
}
req := pathAndRequest[:pos]
var returns string
if len(fields) > 2 {
returns = fields[2]
}
returns = strings.ReplaceAll(returns, "returns", "")
returns = strings.ReplaceAll(returns, "(", "")
returns = strings.ReplaceAll(returns, ")", "")
returns = strings.TrimSpace(returns)
p.acceptRoute(spec.Route{
Annotations: annos,
Method: method,
Path: path,
RequestType: GetType(api, req),
ResponseType: GetType(api, returns),
})
return nil
}
func (p *serviceEntityParser) setEntityName(name string) {
p.acceptName(name)
}

View File

@@ -0,0 +1,7 @@
package parser
import "zero/tools/goctl/api/spec"
type state interface {
process(api *spec.ApiSpec) (state, error)
}

View File

@@ -0,0 +1,329 @@
package parser
import (
"errors"
"fmt"
"go/ast"
"go/parser"
"go/token"
"sort"
"strings"
"zero/tools/goctl/api/spec"
)
var (
ErrStructNotFound = errors.New("struct not found")
ErrUnSupportType = errors.New("unsupport type")
ErrUnSupportInlineType = errors.New("unsupport inline type")
interfaceExpr = `interface{}`
objectM = make(map[string]*spec.Type)
)
const (
golangF = `package ast
%s
`
pkgPrefix = "package"
)
func parseStructAst(golang string) ([]spec.Type, error) {
if !strings.HasPrefix(golang, pkgPrefix) {
golang = fmt.Sprintf(golangF, golang)
}
fSet := token.NewFileSet()
f, err := parser.ParseFile(fSet, "", golang, parser.ParseComments)
if err != nil {
return nil, err
}
commentMap := ast.NewCommentMap(fSet, f, f.Comments)
f.Comments = commentMap.Filter(f).Comments()
scope := f.Scope
if scope == nil {
return nil, ErrStructNotFound
}
objects := scope.Objects
structs := make([]*spec.Type, 0)
for structName, obj := range objects {
st, err := parseObject(structName, obj)
if err != nil {
return nil, err
}
structs = append(structs, st)
}
sort.Slice(structs, func(i, j int) bool {
return structs[i].Name < structs[j].Name
})
resp := make([]spec.Type, 0)
for _, item := range structs {
resp = append(resp, *item)
}
return resp, nil
}
func parseObject(structName string, obj *ast.Object) (*spec.Type, error) {
if data, ok := objectM[structName]; ok {
return data, nil
}
var st spec.Type
st.Name = structName
if obj.Decl == nil {
objectM[structName] = &st
return &st, nil
}
decl, ok := obj.Decl.(*ast.TypeSpec)
if !ok {
objectM[structName] = &st
return &st, nil
}
if decl.Type == nil {
objectM[structName] = &st
return &st, nil
}
tp, ok := decl.Type.(*ast.StructType)
if !ok {
objectM[structName] = &st
return &st, nil
}
fields := tp.Fields
if fields == nil {
objectM[structName] = &st
return &st, nil
}
fieldList := fields.List
members, err := parseFields(fieldList)
if err != nil {
return nil, err
}
st.Members = members
objectM[structName] = &st
return &st, nil
}
func parseFields(fields []*ast.Field) ([]spec.Member, error) {
members := make([]spec.Member, 0)
for _, field := range fields {
docs := parseCommentOrDoc(field.Doc)
comments := parseCommentOrDoc(field.Comment)
name := parseName(field.Names)
tp, stringExpr, err := parseType(field.Type)
if err != nil {
return nil, err
}
tag := parseTag(field.Tag)
isInline := name == ""
if isInline {
var err error
name, err = getInlineName(tp)
if err != nil {
return nil, err
}
}
members = append(members, spec.Member{
Name: name,
Type: stringExpr,
Expr: tp,
Tag: tag,
Comments: comments,
Docs: docs,
IsInline: isInline,
})
}
return members, nil
}
func getInlineName(tp interface{}) (string, error) {
switch v := tp.(type) {
case *spec.Type:
return v.Name, nil
case *spec.PointerType:
return getInlineName(v.Star)
case *spec.StructType:
return v.StringExpr, nil
default:
return "", ErrUnSupportInlineType
}
}
func getInlineTypePrefix(tp interface{}) (string, error) {
if tp == nil {
return "", nil
}
switch tp.(type) {
case *ast.Ident:
return "", nil
case *ast.StarExpr:
return "*", nil
case *ast.TypeSpec:
return "", nil
default:
return "", ErrUnSupportInlineType
}
}
func parseTag(basicLit *ast.BasicLit) string {
if basicLit == nil {
return ""
}
return basicLit.Value
}
// returns
// resp1:type can convert to *spec.PointerType|*spec.BasicType|*spec.MapType|*spec.ArrayType|*spec.InterfaceType
// resp2:type's string expression,like int、string、[]int64、map[string]User、*User
// resp3:error
func parseType(expr ast.Expr) (interface{}, string, error) {
if expr == nil {
return nil, "", ErrUnSupportType
}
switch v := expr.(type) {
case *ast.StarExpr:
star, stringExpr, err := parseType(v.X)
if err != nil {
return nil, "", err
}
e := fmt.Sprintf("*%s", stringExpr)
return &spec.PointerType{Star: star, StringExpr: e}, e, nil
case *ast.Ident:
if isBasicType(v.Name) {
return &spec.BasicType{Name: v.Name, StringExpr: v.Name}, v.Name, nil
} else if v.Obj != nil {
obj := v.Obj
if obj.Name != v.Name { // 防止引用自己而无限递归
specType, err := parseObject(v.Name, v.Obj)
if err != nil {
return nil, "", err
} else {
return specType, v.Obj.Name, nil
}
} else {
inlineType, err := getInlineTypePrefix(obj.Decl)
if err != nil {
return nil, "", err
}
return &spec.StructType{
StringExpr: fmt.Sprintf("%s%s", inlineType, v.Name),
}, v.Name, nil
}
} else {
return nil, "", fmt.Errorf(" [%s] - member is not exist", v.Name)
}
case *ast.MapType:
key, keyStringExpr, err := parseType(v.Key)
if err != nil {
return nil, "", err
}
value, valueStringExpr, err := parseType(v.Value)
if err != nil {
return nil, "", err
}
keyType, ok := key.(*spec.BasicType)
if !ok {
return nil, "", fmt.Errorf("[%+v] - unsupport type of map key", v.Key)
}
e := fmt.Sprintf("map[%s]%s", keyStringExpr, valueStringExpr)
return &spec.MapType{
Key: keyType.Name,
Value: value,
StringExpr: e,
}, e, nil
case *ast.ArrayType:
arrayType, stringExpr, err := parseType(v.Elt)
if err != nil {
return nil, "", err
}
e := fmt.Sprintf("[]%s", stringExpr)
return &spec.ArrayType{ArrayType: arrayType, StringExpr: e}, e, nil
case *ast.InterfaceType:
return &spec.InterfaceType{StringExpr: interfaceExpr}, interfaceExpr, nil
case *ast.ChanType:
return nil, "", errors.New("[chan] - unsupport type")
case *ast.FuncType:
return nil, "", errors.New("[func] - unsupport type")
case *ast.StructType: // todo can optimize
return nil, "", errors.New("[struct] - unsupport inline struct type")
case *ast.SelectorExpr:
x := v.X
sel := v.Sel
xIdent, ok := x.(*ast.Ident)
if ok {
name := xIdent.Name
if name != "time" && sel.Name != "Time" {
return nil, "", fmt.Errorf("[outter package] - package:%s, unsupport type", name)
}
tm := fmt.Sprintf("time.Time")
return &spec.TimeType{
StringExpr: tm,
}, tm, nil
}
return nil, "", ErrUnSupportType
default:
return nil, "", ErrUnSupportType
}
}
func isBasicType(tp string) bool {
switch tp {
case
"bool",
"uint8",
"uint16",
"uint32",
"uint64",
"int8",
"int16",
"int32",
"int64",
"float32",
"float64",
"complex64",
"complex128",
"string",
"int",
"uint",
"uintptr",
"byte",
"rune",
"Type",
"Type1",
"IntegerType",
"FloatType",
"ComplexType":
return true
default:
return false
}
}
func parseName(names []*ast.Ident) string {
if len(names) == 0 {
return ""
}
name := names[0]
return parseIdent(name)
}
func parseIdent(ident *ast.Ident) string {
if ident == nil {
return ""
}
return ident.Name
}
func parseCommentOrDoc(cg *ast.CommentGroup) []string {
if cg == nil {
return nil
}
comments := make([]string, 0)
for _, comment := range cg.List {
if comment == nil {
continue
}
text := strings.TrimSpace(comment.Text)
if text == "" {
continue
}
comments = append(comments, text)
}
return comments
}

View File

@@ -0,0 +1,95 @@
package parser
import (
"fmt"
"strings"
"zero/tools/goctl/api/spec"
"zero/tools/goctl/util"
)
type typeState struct {
*baseState
annos []spec.Annotation
}
func newTypeState(state *baseState, annos []spec.Annotation) state {
return &typeState{
baseState: state,
annos: annos,
}
}
func (s *typeState) process(api *spec.ApiSpec) (state, error) {
var name string
var members []spec.Member
parser := &typeEntityParser{
acceptName: func(n string) {
name = n
},
acceptMember: func(member spec.Member) {
members = append(members, member)
},
}
ent := newEntity(s.baseState, api, parser)
if err := ent.process(); err != nil {
return nil, err
}
api.Types = append(api.Types, spec.Type{
Name: name,
Annotations: s.annos,
Members: members,
})
return newRootState(s.r, s.lineNumber), nil
}
type typeEntityParser struct {
acceptName func(name string)
acceptMember func(member spec.Member)
}
func (p *typeEntityParser) parseLine(line string, api *spec.ApiSpec, annos []spec.Annotation) error {
index := strings.Index(line, "//")
comment := ""
if index >= 0 {
comment = line[index+2:]
line = strings.TrimSpace(line[:index])
}
fields := strings.Fields(line)
if len(fields) == 0 {
return nil
}
if len(fields) == 1 {
p.acceptMember(spec.Member{
Annotations: annos,
Name: fields[0],
Type: fields[0],
IsInline: true,
})
return nil
}
name := fields[0]
tp := fields[1]
var tag string
if len(fields) > 2 {
tag = fields[2]
} else {
tag = fmt.Sprintf("`json:\"%s\"`", util.Untitle(name))
}
p.acceptMember(spec.Member{
Annotations: annos,
Name: name,
Type: tp,
Tag: tag,
Comment: comment,
IsInline: false,
})
return nil
}
func (p *typeEntityParser) setEntityName(name string) {
p.acceptName(name)
}

View File

@@ -0,0 +1,103 @@
package parser
import (
"bufio"
"errors"
"regexp"
"strings"
"zero/tools/goctl/api/spec"
)
const (
// struct匹配
typeRegex = `(?m)(?m)(^ *type\s+[a-zA-Z][a-zA-Z0-9_-]+\s+(((struct)\s*?\{[\w\W]*?[^\{]\})|([a-zA-Z][a-zA-Z0-9_-]+)))|(^ *type\s*?\([\w\W]+\}\s*\))`
)
var (
emptyStrcut = errors.New("struct body not found")
)
var emptyType spec.Type
func GetType(api *spec.ApiSpec, t string) spec.Type {
for _, tp := range api.Types {
if tp.Name == t {
return tp
}
}
return emptyType
}
func isLetterDigit(r rune) bool {
return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || ('0' <= r && r <= '9')
}
func isSpace(r rune) bool {
return r == ' ' || r == '\t'
}
func isNewline(r rune) bool {
return r == '\n' || r == '\r'
}
func read(r *bufio.Reader) (rune, error) {
ch, _, err := r.ReadRune()
return ch, err
}
func readLine(r *bufio.Reader) (string, error) {
line, _, err := r.ReadLine()
if err != nil {
return "", err
} else {
return string(line), nil
}
}
func skipSpaces(r *bufio.Reader) error {
for {
next, err := read(r)
if err != nil {
return err
}
if !isSpace(next) {
return unread(r)
}
}
}
func unread(r *bufio.Reader) error {
return r.UnreadRune()
}
func MatchStruct(api string) (info, structBody, service string, err error) {
r := regexp.MustCompile(typeRegex)
indexes := r.FindAllStringIndex(api, -1)
if len(indexes) == 0 {
return "", "", "", emptyStrcut
}
startIndexes := indexes[0]
endIndexes := indexes[len(indexes)-1]
info = api[:startIndexes[0]]
structBody = api[startIndexes[0]:endIndexes[len(endIndexes)-1]]
service = api[endIndexes[len(endIndexes)-1]:]
firstIIndex := strings.Index(info, "i")
if firstIIndex > 0 {
info = info[firstIIndex:]
}
lastServiceRightBraceIndex := strings.LastIndex(service, "}") + 1
var firstServiceIndex int
for index, char := range service {
if !isSpace(char) && !isNewline(char) {
firstServiceIndex = index
break
}
}
service = service[firstServiceIndex:lastServiceRightBraceIndex]
return
}

View File

@@ -0,0 +1,54 @@
package parser
import (
"errors"
"fmt"
"strings"
"zero/core/stringx"
"zero/tools/goctl/api/spec"
"zero/tools/goctl/api/util"
)
func (p *Parser) validate(api *spec.ApiSpec) (err error) {
var builder strings.Builder
for _, tp := range api.Types {
if ok, name := p.validateDuplicateProperty(tp); !ok {
fmt.Fprintf(&builder, `duplicate property "%s" of type "%s"`+"\n", name, tp.Name)
}
}
if ok, info := p.validateDuplicateRouteHandler(api); !ok {
fmt.Fprintf(&builder, info)
}
if len(builder.String()) > 0 {
return errors.New(builder.String())
}
return nil
}
func (p *Parser) validateDuplicateProperty(tp spec.Type) (bool, string) {
var names []string
for _, member := range tp.Members {
if stringx.Contains(names, member.Name) {
return false, member.Name
} else {
names = append(names, member.Name)
}
}
return true, ""
}
func (p *Parser) validateDuplicateRouteHandler(api *spec.ApiSpec) (bool, string) {
var names []string
for _, r := range api.Service.Routes {
handler, ok := util.GetAnnotationValue(r.Annotations, "server", "handler")
if !ok {
return false, fmt.Sprintf("missing handler annotation for %s", r.Path)
}
if stringx.Contains(names, handler) {
return false, fmt.Sprintf(`duplicated handler for name "%s"`, handler)
} else {
names = append(names, handler)
}
}
return true, ""
}

View File

@@ -0,0 +1,16 @@
package parser
const (
infoDirective = "info"
serviceDirective = "service"
typeDirective = "type"
typeStruct = "struct"
at = '@'
colon = ':'
leftParenthesis = '('
rightParenthesis = ')'
leftBrace = "{"
rightBrace = '}'
multilineBeginTag = '>'
multilineEndTag = '<'
)