1 Star 0 Fork 1

go-genie / httptransport

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
operator_scanner.go 13.19 KB
一键复制 编辑 原始数据 按行查看 历史
文兄 提交于 2024-02-20 16:35 . 1
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
package openapi
import (
"context"
"fmt"
"go/ast"
"go/constant"
"go/types"
"net/http"
"reflect"
"runtime/debug"
"sort"
"strconv"
"strings"
"gitee.com/go-genie/httptransport"
"gitee.com/go-genie/httptransport/httpx"
"gitee.com/go-genie/httptransport/transformers"
"gitee.com/go-genie/logr"
"gitee.com/go-genie/oas"
"gitee.com/go-genie/packagesx"
"gitee.com/go-genie/statuserror"
typesutil "gitee.com/go-genie/xx/types"
"github.com/pkg/errors"
)
func NewOperatorScanner(pkg *packagesx.Package) *OperatorScanner {
return &OperatorScanner{
pkg: pkg,
DefinitionScanner: NewDefinitionScanner(pkg),
StatusErrScanner: NewStatusErrScanner(pkg),
}
}
type OperatorScanner struct {
*DefinitionScanner
*StatusErrScanner
pkg *packagesx.Package
operators map[*types.TypeName]*Operator
}
func (scanner *OperatorScanner) Operator(ctx context.Context, typeName *types.TypeName) *Operator {
if typeName == nil {
return nil
}
if typeName.Pkg().Path() == pkgImportPathHttpTransport {
if typeName.Name() == "GroupOperator" {
// old version fallback
return &Operator{}
}
if typeName.Name() == "MetaOperator" {
return &Operator{}
}
}
if operator, ok := scanner.operators[typeName]; ok {
return operator
}
logr.FromContext(ctx).Debug("scanning Operator `%s.%s`", typeName.Pkg().Path(), typeName.Name())
defer func() {
if e := recover(); e != nil {
panic(
errors.Errorf(
"scan Operator `%s` failed, panic: %s; calltrace: %s",
fullTypeName(typeName),
fmt.Sprint(e),
string(debug.Stack()),
),
)
}
}()
if typeStruct, ok := typeName.Type().Underlying().(*types.Struct); ok {
operator := &Operator{}
operator.Tag = scanner.tagFrom(typeName.Pkg().Path())
scanner.scanRouteMeta(operator, typeName)
scanner.scanParameterOrRequestBody(ctx, operator, typeStruct)
scanner.scanReturns(ctx, operator, typeName)
// cached scanned
if scanner.operators == nil {
scanner.operators = map[*types.TypeName]*Operator{}
}
scanner.operators[typeName] = operator
return operator
}
return nil
}
func (scanner *OperatorScanner) singleReturnOf(typeName *types.TypeName, name string) (string, bool) {
if typeName == nil {
return "", false
}
for _, typ := range []types.Type{
typeName.Type(),
types.NewPointer(typeName.Type()),
} {
method, ok := typesutil.FromTType(typ).MethodByName(name)
if ok {
results, n := scanner.pkg.FuncResultsOf(method.(*typesutil.TMethod).Func)
if n == 1 {
for _, v := range results[0] {
if v.Value != nil {
s, err := strconv.Unquote(v.Value.ExactString())
if err != nil {
panic(errors.Errorf("%s: %s", err, v.Value))
}
return s, true
}
}
}
}
}
return "", false
}
func (scanner *OperatorScanner) tagFrom(pkgPath string) string {
tag := strings.TrimPrefix(pkgPath, scanner.pkg.PkgPath)
return strings.TrimPrefix(tag, "/")
}
func (scanner *OperatorScanner) scanRouteMeta(op *Operator, typeName *types.TypeName) {
typeStruct := typeName.Type().Underlying().(*types.Struct)
op.ID = typeName.Name()
for i := 0; i < typeStruct.NumFields(); i++ {
f := typeStruct.Field(i)
tags := reflect.StructTag(typeStruct.Tag(i))
if f.Anonymous() && strings.Contains(f.Type().String(), pkgImportPathHttpx+".Method") {
if path, ok := tags.Lookup("path"); ok {
vs := strings.Split(path, ",")
op.Path = vs[0]
if len(vs) > 0 {
for i := range vs {
switch vs[i] {
case "deprecated":
op.Deprecated = true
}
}
}
}
if basePath, ok := tags.Lookup("basePath"); ok {
op.BasePath = basePath
}
if summary, ok := tags.Lookup("summary"); ok {
op.Summary = summary
}
break
}
}
lines := scanner.pkg.CommentsOf(scanner.pkg.IdentOf(typeName))
comments := strings.Split(lines, "\n")
for i := range comments {
if strings.Contains(comments[i], "@deprecated") {
op.Deprecated = true
}
}
if op.Summary == "" {
comments = filterMarkedLines(comments)
if comments[0] != "" {
op.Summary = comments[0]
if len(comments) > 1 {
op.Description = strings.Join(comments[1:], "\n")
}
}
}
if method, ok := scanner.singleReturnOf(typeName, "Method"); ok {
op.Method = method
}
if path, ok := scanner.singleReturnOf(typeName, "Path"); ok {
op.Path = path
}
if bathPath, ok := scanner.singleReturnOf(typeName, "BasePath"); ok {
op.BasePath = bathPath
}
}
func (scanner *OperatorScanner) scanReturns(ctx context.Context, op *Operator, typeName *types.TypeName) {
for _, typ := range []types.Type{
typeName.Type(),
types.NewPointer(typeName.Type()),
} {
method, ok := typesutil.FromTType(typ).MethodByName("Output")
if ok {
results, n := scanner.pkg.FuncResultsOf(method.(*typesutil.TMethod).Func)
if n == 2 {
for _, v := range results[0] {
if v.Type != nil {
if v.Type.String() != types.Typ[types.UntypedNil].String() {
if op.SuccessType != nil && op.SuccessType.String() != v.Type.String() {
logr.FromContext(ctx).Warn(
errors.Errorf(
"%s success result must be same struct, but got %v, already set %v",
op.ID,
v.Type,
op.SuccessType,
),
)
}
op.SuccessType = v.Type
op.SuccessStatus, op.SuccessResponse = scanner.getResponse(ctx, v.Type, v.Expr)
}
}
}
}
if scanner.StatusErrScanner.StatusErrType != nil {
op.StatusErrors = scanner.StatusErrScanner.StatusErrorsInFunc(method.(*typesutil.TMethod).Func)
op.StatusErrorSchema = scanner.DefinitionScanner.GetSchemaByType(
ctx,
scanner.StatusErrScanner.StatusErrType,
)
}
}
}
}
func (scanner *OperatorScanner) firstValueOfFunc(named *types.Named, name string) (interface{}, bool) {
method, ok := typesutil.FromTType(types.NewPointer(named)).MethodByName(name)
if ok {
results, n := scanner.pkg.FuncResultsOf(method.(*typesutil.TMethod).Func)
if n == 1 {
for _, r := range results[0] {
if r.IsValue() {
if v := valueOf(r.Value); v != nil {
return v, true
}
}
}
return nil, true
}
}
return nil, false
}
func (scanner *OperatorScanner) getResponse(ctx context.Context, tpe types.Type, expr ast.Expr) (
statusCode int,
response *oas.Response,
) {
response = &oas.Response{}
if tpe.String() == "error" {
statusCode = http.StatusNoContent
return
}
contentType := ""
if isHttpxResponse(tpe) {
scanResponseWrapper := func(expr ast.Expr) {
firstCallExpr := true
ast.Inspect(
expr, func(node ast.Node) bool {
switch callExpr := node.(type) {
case *ast.CallExpr:
if firstCallExpr {
firstCallExpr = false
v, _ := scanner.pkg.Eval(callExpr.Args[0])
tpe = v.Type
}
switch e := callExpr.Fun.(type) {
case *ast.SelectorExpr:
switch e.Sel.Name {
case "WithSchema":
v, _ := scanner.pkg.Eval(callExpr.Args[0])
tpe = v.Type
case "WithStatusCode":
v, _ := scanner.pkg.Eval(callExpr.Args[0])
if code, ok := valueOf(v.Value).(int); ok {
statusCode = code
}
return false
case "WithContentType":
v, _ := scanner.pkg.Eval(callExpr.Args[0])
if code, ok := valueOf(v.Value).(string); ok {
contentType = code
}
return false
}
}
}
return true
},
)
}
if ident, ok := expr.(*ast.Ident); ok && ident.Obj != nil {
if stmt, ok := ident.Obj.Decl.(*ast.AssignStmt); ok {
for _, e := range stmt.Rhs {
scanResponseWrapper(e)
}
}
} else {
scanResponseWrapper(expr)
}
}
if pointer, ok := tpe.(*types.Pointer); ok {
tpe = pointer.Elem()
}
if named, ok := tpe.(*types.Named); ok {
if v, ok := scanner.firstValueOfFunc(named, "ContentType"); ok {
if s, ok := v.(string); ok {
contentType = s
}
if contentType == "" {
contentType = "*"
}
}
if v, ok := scanner.firstValueOfFunc(named, "StatusCode"); ok {
if i, ok := v.(int64); ok {
statusCode = int(i)
}
}
}
if contentType == "" {
contentType = httpx.MIME_JSON
}
response.AddContent(contentType, oas.NewMediaTypeWithSchema(scanner.DefinitionScanner.GetSchemaByType(ctx, tpe)))
return
}
func (scanner *OperatorScanner) scanParameterOrRequestBody(
ctx context.Context,
op *Operator,
typeStruct *types.Struct,
) {
typesutil.EachField(
typesutil.FromTType(typeStruct),
"name",
func(field typesutil.StructField, fieldDisplayName string, omitempty bool) bool {
location, _ := tagValueAndFlagsByTagString(field.Tag().Get("in"))
if location == "" {
panic(errors.Errorf("missing tag `in` for %s of %s", field.Name(), op.ID))
}
name, flags := tagValueAndFlagsByTagString(field.Tag().Get("name"))
schema := scanner.DefinitionScanner.propSchemaByField(
ctx,
field.Name(),
field.Type().(*typesutil.TType).Type,
field.Tag(),
name,
flags,
scanner.pkg.CommentsOf(scanner.pkg.IdentOf(field.(*typesutil.TStructField).Var)),
)
transformer, err := transformers.TransformerMgrDefault.NewTransformer(
context.Background(), field.Type(), transformers.TransformerOption{
MIME: field.Tag().Get("mime"),
},
)
if err != nil {
panic(err)
}
switch location {
case "body":
reqBody := oas.NewRequestBody("", true)
reqBody.AddContent(transformer.Names()[0], oas.NewMediaTypeWithSchema(schema))
op.SetRequestBody(reqBody)
case "query":
op.AddNonBodyParameter(oas.QueryParameter(fieldDisplayName, schema, !omitempty))
case "cookie":
op.AddNonBodyParameter(oas.CookieParameter(fieldDisplayName, schema, !omitempty))
case "header":
op.AddNonBodyParameter(oas.HeaderParameter(fieldDisplayName, schema, !omitempty))
case "path":
op.AddNonBodyParameter(oas.PathParameter(fieldDisplayName, schema))
}
return true
},
"in",
)
}
type Operator struct {
httptransport.RouteMeta
Tag string
Description string
NonBodyParameters map[string]*oas.Parameter
RequestBody *oas.RequestBody
StatusErrors []*statuserror.StatusErr
StatusErrorSchema *oas.Schema
SuccessStatus int
SuccessType types.Type
SuccessResponse *oas.Response
}
func (operator *Operator) AddNonBodyParameter(parameter *oas.Parameter) {
if operator.NonBodyParameters == nil {
operator.NonBodyParameters = map[string]*oas.Parameter{}
}
operator.NonBodyParameters[parameter.Name] = parameter
}
func (operator *Operator) SetRequestBody(requestBody *oas.RequestBody) {
operator.RequestBody = requestBody
}
func (operator *Operator) BindOperation(method string, operation *oas.Operation, last bool) {
parameterNames := map[string]bool{}
for _, parameter := range operation.Parameters {
parameterNames[parameter.Name] = true
}
for _, parameter := range operator.NonBodyParameters {
if !parameterNames[parameter.Name] {
operation.Parameters = append(operation.Parameters, parameter)
}
}
if operator.RequestBody != nil {
operation.SetRequestBody(operator.RequestBody)
}
for _, statusError := range operator.StatusErrors {
statusErrorList := make([]string, 0)
if operation.Responses.Responses != nil {
if resp, ok := operation.Responses.Responses[statusError.StatusCode()]; ok {
if resp.Extensions != nil {
if v, ok := resp.Extensions[XStatusErrs]; ok {
if list, ok := v.([]string); ok {
statusErrorList = append(statusErrorList, list...)
}
}
}
}
}
statusErrorList = append(statusErrorList, statusError.Summary())
sort.Strings(statusErrorList)
resp := oas.NewResponse("")
resp.AddExtension(XStatusErrs, statusErrorList)
resp.AddContent(httpx.MIME_JSON, oas.NewMediaTypeWithSchema(operator.StatusErrorSchema))
operation.AddResponse(statusError.StatusCode(), resp)
}
if last {
operation.OperationId = operator.ID
operation.Deprecated = operator.Deprecated
operation.Summary = operator.Summary
operation.Description = operator.Description
if operator.Tag != "" {
operation.Tags = []string{operator.Tag}
}
if operator.SuccessType == nil {
operation.Responses.AddResponse(http.StatusNoContent, &oas.Response{})
} else {
status := operator.SuccessStatus
if status == 0 {
status = http.StatusOK
if method == http.MethodPost {
status = http.StatusCreated
}
}
if status >= http.StatusMultipleChoices && status < http.StatusBadRequest {
operator.SuccessResponse = oas.NewResponse(operator.SuccessResponse.Description)
}
operation.Responses.AddResponse(status, operator.SuccessResponse)
}
}
// sort all parameters by postion and name
if len(operation.Parameters) > 0 {
sort.Slice(
operation.Parameters, func(i, j int) bool {
return positionOrders[operation.Parameters[i].In]+operation.Parameters[i].Name <
positionOrders[operation.Parameters[j].In]+operation.Parameters[j].Name
},
)
}
}
var positionOrders = map[oas.Position]string{
"path": "1",
"header": "2",
"query": "3",
"cookie": "4",
}
func valueOf(v constant.Value) interface{} {
if v == nil {
return nil
}
switch v.Kind() {
case constant.Float:
v, _ := strconv.ParseFloat(v.String(), 64)
return v
case constant.Bool:
v, _ := strconv.ParseBool(v.String())
return v
case constant.String:
v, _ := strconv.Unquote(v.String())
return v
case constant.Int:
v, _ := strconv.ParseInt(v.String(), 10, 64)
return v
}
return nil
}
1
https://gitee.com/go-genie/httptransport.git
git@gitee.com:go-genie/httptransport.git
go-genie
httptransport
httptransport
v1.0.7

搜索帮助