feat(goctl): Add api parser (#2585)

This commit is contained in:
anqiansong
2023-03-28 23:45:26 +08:00
committed by GitHub
parent 455a6c8f97
commit 50bc361430
59 changed files with 11633 additions and 6 deletions

View File

@@ -0,0 +1,412 @@
package parser
import (
"fmt"
"strings"
"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/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 {
_, ok := dt.(*ast.BaseDataType)
if 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 {
if err := a.fillTypes(); err != nil {
return err
}
return a.fillService()
}
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, ":")
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) fillTypes() error {
for _, item := range a.api.TypeStmt {
switch v := (item).(type) {
case *ast.TypeLiteralStmt:
err := a.fillTypeExpr(v.Expr)
if 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
}
var importManager = make(map[string]placeholder.Type)
importManager[ast.Filename] = placeholder.PlaceHolder
api, err := convert2API(ast, importManager)
if 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
}

View File

@@ -0,0 +1,45 @@
package parser
import (
"bytes"
"fmt"
"io/ioutil"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/tools/goctl/pkg/parser/api/assertx"
)
func Test_Parse(t *testing.T) {
t.Run("valid", func(t *testing.T) {
_, err := Parse("./testdata/example.api", nil)
assert.Nil(t, err)
})
t.Run("invalid", func(t *testing.T) {
data, err := ioutil.ReadFile("./testdata/invalid.api")
assert.NoError(t, err)
splits := bytes.Split(data, []byte("-----"))
var testFile []string
for idx, split := range splits {
replacer := strings.NewReplacer(" ", "", "\t", "", "\n", "", "\r", "", "\f", "")
r := replacer.Replace(string(split))
if len(r) == 0 {
continue
}
filename := filepath.Join(t.TempDir(), fmt.Sprintf("invalid%d.api", idx))
err := ioutil.WriteFile(filename, split, 0666)
assert.NoError(t, err)
testFile = append(testFile, filename)
}
for _, v := range testFile {
_, err := Parse(v, nil)
assertx.Error(t, err)
}
})
t.Run("circleImport", func(t *testing.T) {
_, err := Parse("./testdata/base.api", nil)
assertx.Error(t, err)
})
}

View File

@@ -0,0 +1,311 @@
package parser
import (
"fmt"
"path/filepath"
"strings"
"github.com/zeromicro/go-zero/tools/goctl/pkg/parser/api/ast"
"github.com/zeromicro/go-zero/tools/goctl/pkg/parser/api/placeholder"
"github.com/zeromicro/go-zero/tools/goctl/pkg/parser/api/token"
)
// API is the parsed api file.
type API struct {
Filename string
Syntax *ast.SyntaxStmt
info *ast.InfoStmt // Info block does not participate in code generation.
importStmt []ast.ImportStmt // ImportStmt block does not participate in code generation.
TypeStmt []ast.TypeStmt
ServiceStmts []*ast.ServiceStmt
importManager map[string]placeholder.Type
}
func convert2API(a *ast.AST, importManager map[string]placeholder.Type) (*API, error) {
var api = new(API)
api.importManager = make(map[string]placeholder.Type)
api.Filename = a.Filename
for k, v := range importManager {
api.importManager[k] = v
}
one := a.Stmts[0]
syntax, ok := one.(*ast.SyntaxStmt)
if !ok {
return nil, ast.SyntaxError(one.Pos(), "expected syntax statement, got <%T>", one)
}
api.Syntax = syntax
for i := 1; i < len(a.Stmts); i++ {
one := a.Stmts[i]
switch val := one.(type) {
case *ast.SyntaxStmt:
return nil, ast.DuplicateStmtError(val.Pos(), "duplicate syntax statement")
case *ast.InfoStmt:
if api.info != nil {
return nil, ast.DuplicateStmtError(val.Pos(), "duplicate info statement")
}
api.info = val
case ast.ImportStmt:
api.importStmt = append(api.importStmt, val)
case ast.TypeStmt:
api.TypeStmt = append(api.TypeStmt, val)
case *ast.ServiceStmt:
api.ServiceStmts = append(api.ServiceStmts, val)
}
}
if err := api.SelfCheck(); err != nil {
return nil, err
}
return api, nil
}
func (api *API) checkImportStmt() error {
f := newFilter()
b := f.addCheckItem("import value expression")
for _, v := range api.importStmt {
switch val := v.(type) {
case *ast.ImportLiteralStmt:
b.check(val.Value)
case *ast.ImportGroupStmt:
b.check(val.Values...)
}
}
return f.error()
}
func (api *API) checkInfoStmt() error {
if api.info == nil {
return nil
}
f := newFilter()
b := f.addCheckItem("info key expression")
for _, v := range api.info.Values {
b.check(v.Key)
}
return f.error()
}
func (api *API) checkServiceStmt() error {
f := newFilter()
serviceNameChecker := f.addCheckItem("service name expression")
handlerChecker := f.addCheckItem("handler expression")
pathChecker := f.addCheckItem("path expression")
var serviceName = map[string]string{}
for _, v := range api.ServiceStmts {
name := strings.TrimSuffix(v.Name.Format(""), "-api")
if sn, ok := serviceName[name]; ok {
if sn != name {
serviceNameChecker.errorManager.add(ast.SyntaxError(v.Name.Pos(), "multiple service name"))
}
} else {
serviceName[name] = name
}
var group = api.getAtServerValue(v.AtServerStmt, "prefix")
for _, item := range v.Routes {
handlerChecker.check(item.AtHandler.Name)
path := fmt.Sprintf("[%s]:%s", group, item.Route.Format(""))
pathChecker.check(ast.NewTokenNode(token.Token{
Text: path,
Position: item.Route.Pos(),
}))
}
}
return f.error()
}
func (api *API) checkTypeStmt() error {
f := newFilter()
b := f.addCheckItem("type expression")
for _, v := range api.TypeStmt {
switch val := v.(type) {
case *ast.TypeLiteralStmt:
b.check(val.Expr.Name)
case *ast.TypeGroupStmt:
for _, expr := range val.ExprList {
b.check(expr.Name)
}
}
}
return f.error()
}
func (api *API) checkTypeDeclareContext() error {
var typeMap = map[string]placeholder.Type{}
for _, v := range api.TypeStmt {
switch tp := v.(type) {
case *ast.TypeLiteralStmt:
typeMap[tp.Expr.Name.Token.Text] = placeholder.PlaceHolder
case *ast.TypeGroupStmt:
for _, v := range tp.ExprList {
typeMap[v.Name.Token.Text] = placeholder.PlaceHolder
}
}
}
return api.checkTypeContext(typeMap)
}
func (api *API) checkTypeContext(declareContext map[string]placeholder.Type) error {
var em = newErrorManager()
for _, v := range api.TypeStmt {
switch tp := v.(type) {
case *ast.TypeLiteralStmt:
em.add(api.checkTypeExprContext(declareContext, tp.Expr.DataType))
case *ast.TypeGroupStmt:
for _, v := range tp.ExprList {
em.add(api.checkTypeExprContext(declareContext, v.DataType))
}
}
}
return em.error()
}
func (api *API) checkTypeExprContext(declareContext map[string]placeholder.Type, tp ast.DataType) error {
switch val := tp.(type) {
case *ast.ArrayDataType:
return api.checkTypeExprContext(declareContext, val.DataType)
case *ast.BaseDataType:
if IsBaseType(val.Base.Token.Text) {
return nil
}
_, ok := declareContext[val.Base.Token.Text]
if !ok {
return ast.SyntaxError(val.Base.Pos(), "unresolved type <%s>", val.Base.Token.Text)
}
return nil
case *ast.MapDataType:
var manager = newErrorManager()
manager.add(api.checkTypeExprContext(declareContext, val.Key))
manager.add(api.checkTypeExprContext(declareContext, val.Value))
return manager.error()
case *ast.PointerDataType:
return api.checkTypeExprContext(declareContext, val.DataType)
case *ast.SliceDataType:
return api.checkTypeExprContext(declareContext, val.DataType)
case *ast.StructDataType:
var manager = newErrorManager()
for _, e := range val.Elements {
manager.add(api.checkTypeExprContext(declareContext, e.DataType))
}
return manager.error()
}
return nil
}
func (api *API) getAtServerValue(atServer *ast.AtServerStmt, key string) string {
if atServer == nil {
return ""
}
for _, val := range atServer.Values {
if val.Key.Token.Text == key {
return val.Value.Token.Text
}
}
return ""
}
func (api *API) mergeAPI(in *API) error {
for k, v := range in.importManager {
api.importManager[k] = v
}
if api.Syntax.Value.Format() != in.Syntax.Value.Format() {
return ast.SyntaxError(in.Syntax.Value.Pos(),
"multiple syntax value expression, expected <%s>, got <%s>",
api.Syntax.Value.Format(),
in.Syntax.Value.Format(),
)
}
api.TypeStmt = append(api.TypeStmt, in.TypeStmt...)
api.ServiceStmts = append(api.ServiceStmts, in.ServiceStmts...)
return nil
}
func (api *API) parseImportedAPI(imports []ast.ImportStmt) ([]*API, error) {
var list []*API
if len(imports) == 0 {
return list, nil
}
var importValueSet = map[string]token.Token{}
for _, imp := range imports {
switch val := imp.(type) {
case *ast.ImportLiteralStmt:
importValueSet[strings.ReplaceAll(val.Value.Token.Text, `"`, "")] = val.Value.Token
case *ast.ImportGroupStmt:
for _, v := range val.Values {
importValueSet[strings.ReplaceAll(v.Token.Text, `"`, "")] = v.Token
}
}
}
dir := filepath.Dir(api.Filename)
for impPath, tok := range importValueSet {
if !filepath.IsAbs(impPath) {
impPath = filepath.Join(dir, impPath)
}
// import cycle check
if _, ok := api.importManager[impPath]; ok {
return nil, ast.SyntaxError(tok.Position, "import circle not allowed")
} else {
api.importManager[impPath] = placeholder.PlaceHolder
}
p := New(impPath, "")
ast := p.Parse()
if err := p.CheckErrors(); err != nil {
return nil, err
}
nestedApi, err := convert2API(ast, api.importManager)
if err != nil {
return nil, err
}
if err = nestedApi.parseReverse(); err != nil {
return nil, err
}
list = append(list, nestedApi)
if err != nil {
return nil, err
}
}
return list, nil
}
func (api *API) parseReverse() error {
list, err := api.parseImportedAPI(api.importStmt)
if err != nil {
return err
}
for _, e := range list {
if err = api.mergeAPI(e); err != nil {
return err
}
}
return nil
}
func (api *API) SelfCheck() error {
if err := api.parseReverse(); err != nil {
return err
}
if err := api.checkImportStmt(); err != nil {
return err
}
if err := api.checkInfoStmt(); err != nil {
return err
}
if err := api.checkTypeStmt(); err != nil {
return err
}
if err := api.checkServiceStmt(); err != nil {
return err
}
return api.checkTypeDeclareContext()
}

View File

@@ -0,0 +1,28 @@
package parser
import (
"fmt"
"strings"
)
type errorManager struct {
errors []string
}
func newErrorManager() *errorManager {
return &errorManager{}
}
func (e *errorManager) add(err error) {
if err == nil {
return
}
e.errors = append(e.errors, err.Error())
}
func (e *errorManager) error() error {
if len(e.errors)==0{
return nil
}
return fmt.Errorf(strings.Join(e.errors, "\n"))
}

View File

@@ -0,0 +1,55 @@
package parser
import (
"github.com/zeromicro/go-zero/tools/goctl/pkg/parser/api/ast"
"github.com/zeromicro/go-zero/tools/goctl/pkg/parser/api/placeholder"
)
type filterBuilder struct {
m map[string]placeholder.Type
checkExprName string
errorManager *errorManager
}
func (b *filterBuilder) check(nodes ...*ast.TokenNode) {
for _, node := range nodes {
if _, ok := b.m[node.Token.Text]; ok {
b.errorManager.add(ast.DuplicateStmtError(node.Pos(), "duplicate "+b.checkExprName))
} else {
b.m[node.Token.Text] = placeholder.PlaceHolder
}
}
}
func (b *filterBuilder) error() error {
return b.errorManager.error()
}
type filter struct {
builders []*filterBuilder
}
func newFilter() *filter {
return &filter{}
}
func (f *filter) addCheckItem(checkExprName string) *filterBuilder {
b := &filterBuilder{
m: make(map[string]placeholder.Type),
checkExprName: checkExprName,
errorManager: newErrorManager(),
}
f.builders = append(f.builders, b)
return b
}
func (f *filter) error() error {
if len(f.builders) == 0 {
return nil
}
var errorManager = newErrorManager()
for _, b := range f.builders {
errorManager.add(b.error())
}
return errorManager.error()
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,5 @@
@doc(
foo: "foo"
bar: "bar"
baz: ""
)

View File

@@ -0,0 +1,3 @@
@doc ""
@doc "foo"
@doc "bar"

View File

@@ -0,0 +1,3 @@
@handler foo
@handler foo1
@handler _bar

View File

@@ -0,0 +1,16 @@
@server(
foo: bar
bar: baz
baz: foo
qux: /v1
quux: /v1/v2
middleware: M1,M2
timeout1: 1h
timeout2: 10m
timeout3: 10s
timeout4: 10ms
timeout5: 10µs
timeout6: 10ns
timeout7: 1h10m10s10ms10µs10ns
maxBytes: 1024
)

View File

@@ -0,0 +1,11 @@
syntax = "v1"
import "base1.api"
info (
title: "type title here"
desc: "type desc here"
author: "type author here"
email: "type email here"
version: "type version here"
)

View File

@@ -0,0 +1,11 @@
syntax = "v1"
import "base2.api"
info (
title: "type title here"
desc: "type desc here"
author: "type author here"
email: "type email here"
version: "type version here"
)

View File

@@ -0,0 +1,11 @@
syntax = "v1"
import "base.api"
info (
title: "type title here"
desc: "type desc here"
author: "type author here"
email: "type email here"
version: "type version here"
)

View File

@@ -0,0 +1,4 @@
// foo
// bar
/*foo*/
/*bar*/ //baz

View File

@@ -0,0 +1,167 @@
syntax = "v1"
import "example_base1.api"
import (
"example_base2.api"
)
info (
title: "type title here"
desc: "type desc here"
author: "type author here"
email: "type email here"
version: "type version here"
)
type GetFormReq {
Name string `form:"name"`
Age int `form:"age"`
Hobbits []string `form:"hobbits"`
}
type GetFormREsp {
Name string `json:"name"`
Age int `json:"age"`
Hobbits []string `json:"hobbits"`
}
type (
PostFormReq {
Name string `form:"name"`
Age int `form:"age"`
Hobbits []string `form:"hobbits"`
}
PostFormResp {
Name string `json:"name"`
Age int `json:"age"`
Hobbits []string `json:"hobbits"`
}
)
type (
PostJsonReq {
Name string `json:"name"`
Age int `json:"age"`
Hobbits []string `json:"hobbits"`
}
PostJsonResp {
Name string `json:"name"`
Age int `json:"age"`
Hobbits []string `json:"hobbits"`
Extra map[string]string `json:"extra"`
Data interface{} `json:"data"`
}
)
type (
PostPathReq {
Id string `path:"id"`
}
PostPathResp {
Name string `json:"name"`
Age int `json:"age"`
Hobbits []string `json:"hobbits"`
Hobbits2 [2]string `json:"hobbits2"`
Extra map[string]string `json:"extra"`
Data interface{} `json:"data"`
}
)
type (
DemoOfArrayReq {
In string `json:"in"`
}
DemoOfArrayResp {
Out string `json:"out"`
}
)
type (
Nest {
Name string `json:"name"`
}
NestDemoReq1 {
Nest *Nest `json:"nest"`
}
NestDemoResp1 {
Nest []*Nest `json:"nest"`
}
NestDemoReq2 {
*Nest
}
NestDemoResp2 {
*Nest `json:"nest"`
}
)
@server (
group: form
timeout: 3s
)
service example {
@handler getForm
get /example/form (GetFormReq) returns (GetFormREsp)
@handler postForm
post /example/form (PostFormReq) returns (PostFormResp)
}
@server (
group: json
jwt: Auth
timeout: 3m
)
service example {
@doc "json demo"
@handler postJson
post /example/json (PostJsonReq) returns (PostJsonResp)
}
@server (
group: path
middleware: Path
prefix: /v1/v2
timeout: 100ms
)
service example {
@doc (
desc: "path demo"
)
@handler postPath
post /example/path (PostPathReq) returns (PostPathResp)
}
@server (
group: array
prefix: /array
maxBytes: 1024
)
service example {
@doc (
desc: "array response demo"
)
@handler getArray
post /example/array (DemoOfArrayReq) returns ([]DemoOfArrayResp)
@doc (
desc: "array pointer response demo"
)
@handler getArrayPointer
post /example/array/pointer (DemoOfArrayReq) returns ([]*DemoOfArrayResp)
@doc (
desc: "array base response demo"
)
@handler getArrayBase
post /example/array/base (DemoOfArrayReq) returns ([]string)
}
service example {
@handler nestDemo1
post /example/nest (NestDemoReq1) returns (NestDemoResp1)
@handler nestDemo2
post /example/nest2 (NestDemoReq2) returns (NestDemoResp2)
}

View File

@@ -0,0 +1,12 @@
syntax = "v1"
info(
title: "type title here"
desc: "type desc here"
author: "type author here"
email: "type email here"
version: "type version here"
)
type BaseReq1{}
type BaseResp1{}

View File

@@ -0,0 +1,12 @@
syntax = "v1"
info(
title: "type title here"
desc: "type desc here"
author: "type author here"
email: "type email here"
version: "type version here"
)
type BaseReq2{}
type BaseResp2{}

View File

@@ -0,0 +1,5 @@
import (
""
"foo"
"bar"
)

View File

@@ -0,0 +1,3 @@
import ""
import "foo"
import "bar"

View File

@@ -0,0 +1,7 @@
info(
title: "type title here"
desc: "type desc here"
author: "type author here"
email: "type email here"
version: "type version here"
)

View File

@@ -0,0 +1,136 @@
// test case: expected syntax statement
info ()
-----
// test case: duplicate syntax statement
syntax = "v1"
syntax = "v1"
-----
// test case: duplicate info statement
syntax = "v1"
info()
info()
-----
// test case: duplicate type
syntax = "v1"
type Foo{}
type Foo{}
-----
// test case: duplicate type
syntax = "v1"
type Baz{}
type (
Baz{}
)
-----
// test case: multiple service name
syntax = "v1"
service foo{
@handler foo
get /foo
}
service bar{
@handler foo
get /foo
}
-----
// test case: duplicate handler
syntax = "v1"
service foo{
@handler foo
get /foo
@handler foo
get /bar
}
-----
// test case: duplicate path
syntax = "v1"
service foo{
@handler foo
get /foo
@handler bar
get /foo
@handler qux
get /v1/baz
}
@server(
prefix: /v1
)
service foo{
@handler qux
get /baz
@handler quux
get /baz
}
-----
// test case: type declare context
syntax = "v1"
type Foo {
Bar Bar `json:"bar"`
}
-----
// test case: map key expected literal type
syntax = "v1"
type Foo {
Bar map[[]int]string `json:"bar"`
}
-----
// test case: map key expected literal type
syntax = "v1"
type Foo {
Bar map[[]int]string `json:"bar"`
}
-----
// test case: map key expected literal type
syntax = "v1"
type Foo {
Bar *map[[]int]string `json:"bar"`
}
-----
// test case: map valu expected literal type
syntax = "v1"
type Foo {
Bar *map[string]{} `json:"bar"`
}
-----
// test case: invalid slice
syntax = "v1"
type Foo {
Bar []map[[]int]string `json:"bar"`
}
-----
// test case: array
syntax = "v1"
type Foo {
Bar [...]int `json:"bar"`
}
-----
// test case: any
syntax = "v1"
type Foo {
Bar any `json:"bar"`
}
-----
// test case: unresolved type
syntax = "v1"
service example {
@handler nestDemo
post /example/nest (NestDemoReq) returns (NestDemoResp)
}

View File

@@ -0,0 +1,37 @@
service foo {
@handler bar
get /ping
@handler bar
get /ping;
}
service bar {
@doc "bar"
@handler foo
get /foo/:bar (Foo)
@doc "bar"
@handler foo
get /foo/:bar (Foo) returns ();
@handler foo
get /foo/:bar returns (Foo)
@handler foo
get /foo/:bar () returns (Foo);
}
service baz-api {
@handler foo
post /foo/:bar/foo-bar-baz (Foo) returns (*Bar)
@handler foo
post /foo/:bar/foo-bar-baz (Foo) returns (*Bar);
@handler bar
post /foo ([]Foo) returns ([]*Bar)
@handler bar
post /foo ([]Foo) returns ([]*Bar);
}

View File

@@ -0,0 +1,160 @@
// aaaa
/*bb*/ syntax /*cc*/ = /*dd*/ "v1" /*syntax doc*/ // syntax stmt
// bbb
info ( // info stmt
title: "type title here" // title expr
/*ee*/
desc: "type desc here"
author: "type author here"
email: "type email here"
version: "type version here"
)
type AliasInt int
type AliasString = string
type AliasArray [2]int8
type AliasArray2 [...]int8
type AliasSlice []int8
type AliasMap map[string]int
type Any interface{}
type AliasMapKeyStruct map[{
Name string `json:"name"`
Age int `json:"age"`
Bar {
Name string `json:"name"`
Age int `json:"age"`
Bar {
Name string `json:"name"`
Age int `json:"age"`
Bar {
Name string `json:"name"`
Age int `json:"age"`
}
}
}
}]int
type AliasMapValueStruct map[string]{
Name string `json:"name"`
Age int `json:"age"`
Bar {
Name string `json:"name"`
Age int `json:"age"`
Bar {
Name string `json:"name"`
Age int `json:"age"`
Bar {
Name string `json:"name"`
Age int `json:"age"`
}
}
}
}
type Foo {
Bar {
Name string `json:"name"`
Age int `json:"age"`
Bar {
Name string `json:"name"`
Age int `json:"age"`
Bar {
Name string `json:"name"`
Age int `json:"age"`
}
}
}
}
type Bar {
Base int `json:"base"`
Array1 [2]int `json:"array1"`
Array2 [...]int `json:"array2"`
Slice []int `json:"slice"`
Map1 map[string]int `json:"map1"`
Map2 map[string]*int `json:"map2"`
Map3 map[string][]int `json:"map3"`
Map4 map[string][]*int `json:"map4"`
Map5 map[string][2]int `json:"map5"`
Map6 map[string][...]int `json:"map6"`
Interface interface{} `json:"interface"`
Any any `json:"any"`
Foo Foo `json:"foo"`
Baz {
F1 int `json:"f1"`
F2 int `json:"f2"`
} `json:"baz"`
Qux *string `json:"qux"`
Quux bool `json:"quux"`
}
type (
GroupAliasInt int
GroupAliasString = string
GroupAliasArray [2]int8
GroupAliasArray2 [...]int8
GroupAliasSlice []int8
GroupAliasMap map[string]int
GroupAny interface{}
GroupFoo {}
GroupBar {
Base int `json:"base"`
Array1 [2]int `json:"array1"`
Array2 [...]int `json:"array2"`
Slice []int `json:"slice"`
Map1 map[string]int `json:"map1"`
Map2 map[string]*int `json:"map2"`
Map3 map[string][]int `json:"map3"`
Map4 map[string][]*int `json:"map4"`
Map5 map[string][2]int `json:"map5"`
Map6 map[string][...]int `json:"map6"`
Interface interface{} `json:"interface"`
Any any `json:"any"`
Foo Foo `json:"foo"`
Baz {
F1 int `json:"f1"`
F2 int `json:"f2"`
} `json:"baz"`
Qux *string `json:"qux"`
Quux bool `json:"quux"`
}
)
@server ()
service test {
@handler foo
get /test/foo
}
@server (
jwt: Auth
group: Group1
)
service test {
@doc "ping"
@handler foo
get /test/foo
@doc (
key1: "value1"
key2: "value2"
)
@handler bar
get /test/foo (Foo)
@handler baz
post /test/foo/baz returns (Bar)
@handler qux
post /test/foo/baz/:qux (Foo) returns (Bar)
@handler quux
post /test/foo/baz/:qux/qu-ux (Foo) returns (Bar)
@handler foobar
post /foo/bar (*Foo) returns ([]Bar)
@handler barbaz
post /bar/baz ([]*Foo) returns ([]int)
}
// terminal
// terminal2
/*
kkk
*/

View File

@@ -0,0 +1,50 @@
// format api demo
syntax ="v1" // dd
info()
info(foo:"")
info(foo:""
bar: ""
quux: "")
info(foo:""
bar: ""
quux: ""
)
// info statement
// info statement
info (// Info bloack
title: "type title here" // title comment
desc: "type desc here"
author: "type author here"
/*aaa*/
/*
bbb
*/
email: "type email here" // eamil comment
/*aaa*/version:/*bbb*/ "type version here"// version comment
)
import ""
import "aaa"
import"bb"
import "cc"
import()
import(
)
import (
)
import ("aa")
import ("aa" "bb")
import ("aa"
"bb"
)
import ("aa"
"bb")
import (
"aa"
"bb"
)