Skip to content

Commit

Permalink
Merge pull request #61 from gotomicro/feature/generic-response
Browse files Browse the repository at this point in the history
feat: 支持将配置的统一相应格式解析为泛型并生成代码; fix: 修复解析 map 值类型时解析失败的情况; 修复解析字面值失败的情况
  • Loading branch information
link-duan authored Feb 22, 2023
2 parents 78d779c + 6d42761 commit 73ae9f7
Show file tree
Hide file tree
Showing 17 changed files with 562 additions and 437 deletions.
21 changes: 15 additions & 6 deletions generators/axios/axios.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ func (p *Printer) request(path string, method string, item *spec.Operation) f.Do
} else {
params = append(params, f.Group(
f.Content("data: "),
ts.NewPrinter(p.schema).SetTypeFieldsInline(true).PrintTypeName(mediaType.Schema),
p.printType(mediaType.Schema),
))
}
}
Expand Down Expand Up @@ -270,7 +270,7 @@ func (p *Printer) paramsType(params []*spec.ParameterRef) f.Doc {
for _, param := range params {
fields = append(fields, f.Group(
f.Content(param.Value.Name+"?: "),
ts.NewPrinter(p.schema).SetTypeFieldsInline(true).PrintTypeName(param.Value.Schema),
p.printType(param.Value.Schema),
))
}

Expand Down Expand Up @@ -341,15 +341,24 @@ func (p *Printer) responseType(res *spec.Response) f.Doc {
if schema == nil {
continue
}
tsPrinter := ts.NewPrinter(p.schema).SetTypeFieldsInline(true)
ret := tsPrinter.PrintTypeName(schema)
p.importType(tsPrinter.ReferencedTypes...)
return ret
return p.printType(schema)
}

return f.Content("any")
}

func (p *Printer) printType(schema *spec.SchemaRef) f.Doc {
tsPrinter := ts.NewPrinter(p.schema).SetTypeFieldsInline(true)
var ret f.Doc
if schema.Ref != "" {
ret = tsPrinter.PrintTypeName(schema)
} else {
ret = tsPrinter.PrintTypeBody(schema)
}
p.importType(tsPrinter.ReferencedTypes...)
return ret
}

func (p *Printer) requestFnName(item *spec.Operation) string {
slices := strings.Split(item.OperationID, ".")
if len(slices) == 1 {
Expand Down
8 changes: 8 additions & 0 deletions generators/ts/ts.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ func (p *Printer) PrintTypeBody(definition *spec.SchemaRef) f.Doc {
return f.Content(ext.TypeParam.Name)
case spec.ExtendedTypeSpecific:
return p.printSpecific(ext)
case spec.ExtendedTypeNull:
return f.Content("null")
case spec.ExtendedTypeUnknown:
return f.Content("unknown")
case spec.ExtendedTypeObject:
// ignore
}
Expand Down Expand Up @@ -201,6 +205,10 @@ func (p *Printer) PrintTypeName(definition *spec.SchemaRef) f.Doc {
return f.Content(ext.TypeParam.Name)
case spec.ExtendedTypeSpecific:
return p.printSpecific(ext)
case spec.ExtendedTypeNull:
return f.Content("null")
case spec.ExtendedTypeUnknown:
return f.Content("unknown")
case spec.ExtendedTypeObject:
// ignore
}
Expand Down
38 changes: 18 additions & 20 deletions generators/umi/umi.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,16 +145,10 @@ func (p *Printer) request(path string, method string, item *spec.Operation) f.Do
if item.RequestBody != nil {
_, mediaType := p.getRequestMediaType(item)
if mediaType != nil {
if mediaType.Schema.Ref != "" {
s := spec.Unref(p.schema, mediaType.Schema)
p.importType(s.Value.Title)
params = append(params, f.Content("data: "+s.Value.Title))
} else {
params = append(params, f.Group(
f.Content("data: "),
ts.NewPrinter(p.schema).SetTypeFieldsInline(true).PrintTypeName(mediaType.Schema),
))
}
params = append(params, f.Group(
f.Content("data: "),
p.printType(mediaType.Schema),
))
}
}

Expand Down Expand Up @@ -250,7 +244,7 @@ func (p *Printer) paramsType(params []*spec.ParameterRef) f.Doc {
for _, param := range params {
fields = append(fields, f.Group(
f.Content(param.Value.Name+"?: "),
ts.NewPrinter(p.schema).SetTypeFieldsInline(true).PrintTypeName(param.Value.Schema),
p.printType(param.Value.Schema),
))
}

Expand Down Expand Up @@ -315,21 +309,25 @@ func (p *Printer) jsDoc(item *spec.Operation) f.Doc {
return res
}

func (p *Printer) printType(schema *spec.SchemaRef) f.Doc {
tsPrinter := ts.NewPrinter(p.schema).SetTypeFieldsInline(true)
var ret f.Doc
if schema.Ref != "" {
ret = tsPrinter.PrintTypeName(schema)
} else {
ret = tsPrinter.PrintTypeBody(schema)
}
p.importType(tsPrinter.ReferencedTypes...)
return ret
}

func (p *Printer) responseType(res *spec.Response) f.Doc {
for _, mediaType := range res.Content {
schema := mediaType.Schema
if schema == nil {
continue
}
tsPrinter := ts.NewPrinter(p.schema).SetTypeFieldsInline(true)
var ret f.Doc
if schema.Ref != "" {
ret = tsPrinter.PrintTypeName(schema)
} else {
ret = tsPrinter.PrintTypeBody(schema)
}
p.importType(tsPrinter.ReferencedTypes...)
return ret
return p.printType(schema)
}

return f.Content("any")
Expand Down
240 changes: 240 additions & 0 deletions plugins/common/custom_rule_analyzer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
package common

import (
"fmt"
"go/ast"
"log"
"net/http"

analyzer "github.com/gotomicro/eapi"
"github.com/gotomicro/eapi/spec"
"github.com/robertkrimen/otto"
"github.com/samber/lo"
)

type CustomRuleAnalyzer struct {
ctx *analyzer.Context
spec *analyzer.APISpec
api *analyzer.API
c *Config
}

func NewCustomRuleAnalyzer(ctx *analyzer.Context, spec *analyzer.APISpec, api *analyzer.API, c *Config) *CustomRuleAnalyzer {
return &CustomRuleAnalyzer{ctx: ctx, spec: spec, api: api, c: c}
}

func (p *CustomRuleAnalyzer) MatchCustomResponseRule(node ast.Node) (matched bool) {
if p.c == nil || len(p.c.Response) == 0 {
return false
}

for idx, rule := range p.c.Response {
var responseTypeTitle = "CustomResponseType"
if idx > 0 {
responseTypeTitle = fmt.Sprintf("CustomResponseType%d", idx)
}

genericTypeRef := spec.RefComponentSchemas(responseTypeTitle)
genericType, ok := p.ctx.Doc().Components.Schemas[responseTypeTitle]
if !ok {
genericType = NewDataSchemaTransformer(rule.Return.Data).TransformToGeneric()
genericType.Value.Title = responseTypeTitle
p.ctx.Doc().Components.Schemas[responseTypeTitle] = genericType
}

p.ctx.MatchCall(
node,
analyzer.NewCallRule().WithRule(rule.Type, rule.Method),
func(call *ast.CallExpr, typeName, fnName string) {
matched = true
var contentType = rule.Return.ContentType
res := spec.NewResponse()
comment := p.ctx.ParseComment(p.ctx.GetHeadingCommentOf(call.Pos()))
res.Description = comment.TextPointer()
schema := p.parseDataType(call, rule.Return.Data, contentType, genericTypeRef)
res.WithContent(spec.NewContentWithSchemaRef(schema, []string{contentType}))
statusCode := p.parseStatusCodeInCall(call, rule.Return.Status)
p.spec.AddResponse(statusCode, res)
},
)

if genericType.IsTypeAlias() {
delete(p.ctx.Doc().Components.Schemas, responseTypeTitle)
}
}

return
}

func (p *CustomRuleAnalyzer) MatchCustomRequestRule(node ast.Node) (matched bool) {
if p.c == nil || len(p.c.Request) == 0 {
return false
}

for idx, rule := range p.c.Request {
var requestTypeTitle = "CustomRequestType"
if idx > 0 {
requestTypeTitle = fmt.Sprintf("CustomRequestType%d", idx)
}
genericTypeRef := spec.RefComponentSchemas(requestTypeTitle)
genericType, ok := p.ctx.Doc().Components.Schemas[requestTypeTitle]
if !ok {
genericType = NewDataSchemaTransformer(rule.Return.Data).TransformToGeneric()
genericType.Value.Title = requestTypeTitle
p.ctx.Doc().Components.Schemas[requestTypeTitle] = genericType
}

p.ctx.MatchCall(
node,
analyzer.NewCallRule().WithRule(rule.Type, rule.Method),
func(call *ast.CallExpr, typeName, fnName string) {
matched = true

switch p.api.Method {
case http.MethodGet, http.MethodHead, http.MethodOptions, http.MethodDelete:
params := p.parseParamsInCall(call, rule.Return.Data, analyzer.MimeTypeFormData)
for _, param := range params {
p.spec.AddParameter(param)
}

default:
contentType := p.getRequestContentType(rule.Return.ContentType)
schema := p.parseDataType(call, rule.Return.Data, contentType, genericTypeRef)
if schema == nil {
return
}
reqBody := spec.NewRequestBody()
reqBody.Required = true
commentGroup := p.ctx.GetHeadingCommentOf(call.Pos())
if commentGroup != nil {
comment := p.ctx.ParseComment(commentGroup)
reqBody.Description = comment.Text()
}
reqBody.WithSchemaRef(schema, []string{contentType})
p.spec.RequestBody = &spec.RequestBodyRef{Value: reqBody}
}

},
)

if genericType.IsTypeAlias() {
delete(p.ctx.Doc().Components.Schemas, requestTypeTitle)
}
}

return
}

func (p *CustomRuleAnalyzer) parseDataType(call *ast.CallExpr, dataType *DataSchema, contentType string, genericType *spec.SchemaRef) (schema *spec.SchemaRef) {
res := NewDataSchemaTransformer(dataType).TransformToSpecific(genericType, func(dataType *DataSchema) *spec.SchemaRef {
output := p.evaluate(call, string(dataType.Type))
if output == nil {
return spec.NewObjectSchema().WithExtendedType(spec.NewNullExtType()).NewRef()
}
expr, ok := output.(ast.Expr)
if !ok {
fmt.Printf("invalid data type '%s' in configuration file\n", dataType.Type)
return nil
}
return p.ctx.GetSchemaByExpr(expr, contentType)
})

resUnref := spec.Unref(p.ctx.Doc(), res)
ext := resUnref.Value.ExtendedTypeInfo
if ext != nil && ext.Type == spec.ExtendedTypeSpecific {
generic := spec.Unref(p.ctx.Doc(), ext.SpecificType.Type)
if len(ext.SpecificType.Args) == 1 && generic.IsTypeAlias() {
return ext.SpecificType.Args[0]
}
}

return res
}

func (p *CustomRuleAnalyzer) parseParamsInCall(call *ast.CallExpr, dataType *DataSchema, contentType string) (params []*spec.Parameter) {
switch dataType.Type {
case DataTypeString, DataTypeNumber, DataTypeInteger, DataTypeBoolean, DataTypeFile, DataTypeArray:
param := &spec.Parameter{}
schema := spec.NewSchema()
schema.Type = string(dataType.Type)
schema.Format = dataType.Format
param.Schema = spec.NewSchemaRef("", schema)
return append(params, param)

case DataTypeObject: // unsupported in form data
fmt.Printf("object is unsupported in form data\n")
return

default:
output := p.evaluate(call, string(dataType.Type))
expr, ok := output.(ast.Expr)
if !ok {
fmt.Printf("invalid data type '%s' in configuration file\n", dataType.Type)
return nil
}
return analyzer.NewParamParser(p.ctx, p.paramNameParser).Parse(expr)
}
}

// 获取一个尽可能正确的 request payload contentType
func (p *CustomRuleAnalyzer) getRequestContentType(contentType string) string {
if contentType != "" {
if !lo.Contains(p.spec.Consumes, contentType) {
p.spec.Consumes = append(p.spec.Consumes, contentType)
}
return contentType
}
if len(p.spec.Consumes) != 0 {
return p.spec.Consumes[0]
}

// fallback
switch p.api.Method {
case http.MethodGet, http.MethodHead:
return analyzer.MimeTypeFormData
default:
return analyzer.MimeTypeJson
}
}

func (p *CustomRuleAnalyzer) paramNameParser(fieldName string, tags map[string]string) (name, in string) {
name, ok := tags["form"]
if ok {
return name, "query"
}
return fieldName, "query"
}

func (p *CustomRuleAnalyzer) parseStatusCodeInCall(call *ast.CallExpr, statusCode string) (code int) {
if statusCode == "" {
return 200 // default to 200
}

output := p.evaluate(call, statusCode)
switch value := output.(type) {
case int64:
code = int(value)
case int:
code = value
case ast.Expr:
code = p.ctx.ParseStatusCode(value)
}

return
}

func (p *CustomRuleAnalyzer) evaluate(call *ast.CallExpr, code string) interface{} {
env := otto.New()
_ = env.Set("args", call.Args)
output, err := env.Run(code)
if err != nil {
log.Fatalln("evaluate failed", err)
}

value, err := output.Export()
if err != nil {
log.Fatalln("evaluate failed", err)
}

return value
}
Loading

0 comments on commit 73ae9f7

Please sign in to comment.