goctl added
This commit is contained in:
182
tools/goctl/api/parser/basestate.go
Normal file
182
tools/goctl/api/parser/basestate.go
Normal 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)
|
||||
}
|
||||
20
tools/goctl/api/parser/basestate_test.go
Normal file
20
tools/goctl/api/parser/basestate_test.go
Normal 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"])
|
||||
}
|
||||
132
tools/goctl/api/parser/entity.go
Normal file
132
tools/goctl/api/parser/entity.go
Normal 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
|
||||
}
|
||||
62
tools/goctl/api/parser/infostate.go
Normal file
62
tools/goctl/api/parser/infostate.go
Normal 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
|
||||
}
|
||||
57
tools/goctl/api/parser/parser.go
Normal file
57
tools/goctl/api/parser/parser.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
109
tools/goctl/api/parser/rootstate.go
Normal file
109
tools/goctl/api/parser/rootstate.go
Normal 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)
|
||||
}
|
||||
}
|
||||
97
tools/goctl/api/parser/servicestate.go
Normal file
97
tools/goctl/api/parser/servicestate.go
Normal 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)
|
||||
}
|
||||
7
tools/goctl/api/parser/state.go
Normal file
7
tools/goctl/api/parser/state.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package parser
|
||||
|
||||
import "zero/tools/goctl/api/spec"
|
||||
|
||||
type state interface {
|
||||
process(api *spec.ApiSpec) (state, error)
|
||||
}
|
||||
329
tools/goctl/api/parser/typeparser.go
Normal file
329
tools/goctl/api/parser/typeparser.go
Normal 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
|
||||
}
|
||||
95
tools/goctl/api/parser/typestate.go
Normal file
95
tools/goctl/api/parser/typestate.go
Normal 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)
|
||||
}
|
||||
103
tools/goctl/api/parser/util.go
Normal file
103
tools/goctl/api/parser/util.go
Normal 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
|
||||
}
|
||||
54
tools/goctl/api/parser/validator.go
Normal file
54
tools/goctl/api/parser/validator.go
Normal 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, ""
|
||||
}
|
||||
16
tools/goctl/api/parser/vars.go
Normal file
16
tools/goctl/api/parser/vars.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package parser
|
||||
|
||||
const (
|
||||
infoDirective = "info"
|
||||
serviceDirective = "service"
|
||||
typeDirective = "type"
|
||||
typeStruct = "struct"
|
||||
at = '@'
|
||||
colon = ':'
|
||||
leftParenthesis = '('
|
||||
rightParenthesis = ')'
|
||||
leftBrace = "{"
|
||||
rightBrace = '}'
|
||||
multilineBeginTag = '>'
|
||||
multilineEndTag = '<'
|
||||
)
|
||||
Reference in New Issue
Block a user