447 lines
12 KiB
Go
447 lines
12 KiB
Go
package test
|
|
|
|
import (
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// transport defines which struct tags are used to serialize request on the wire
|
|
const (
|
|
transportForm = "form"
|
|
transportJSON = "json"
|
|
)
|
|
|
|
// sentField describes one request parameter exactly as SDK puts it on the wire
|
|
type sentField struct {
|
|
// Name of the parameter on the wire
|
|
Name string
|
|
// Name of the go field (for messages)
|
|
GoName string
|
|
// Go type of the field, nil if it can not be resolved
|
|
Type reflect.Type
|
|
// Field is not sent when it has zero value
|
|
OmitEmpty bool
|
|
// Value of `validate` tag
|
|
Validate string
|
|
}
|
|
|
|
// sdkCall describes one DecortApiCall* invocation found in SDK sources
|
|
type sdkCall struct {
|
|
// Directory of package the call was found in
|
|
Dir string
|
|
// Name of the type passed as params (may be unexported wrapper), empty for nil params
|
|
TypeName string
|
|
Transport string
|
|
// Name of function the call was found in
|
|
FuncName string
|
|
}
|
|
|
|
// sdkIndex stores parsed SDK packages: struct declarations and API calls by url
|
|
type sdkIndex struct {
|
|
// dir -> type name -> type expression
|
|
types map[string]map[string]ast.Expr
|
|
// url (e.g. "/cloudapi/compute/start") -> calls
|
|
calls map[string][]sdkCall
|
|
}
|
|
|
|
var (
|
|
sdkIndexOnce sync.Once
|
|
sdkIndexCached *sdkIndex
|
|
sdkIndexErr error
|
|
)
|
|
|
|
// getSDKIndex parses SDK sources in ../../pkg once per test run
|
|
func getSDKIndex() (*sdkIndex, error) {
|
|
sdkIndexOnce.Do(func() {
|
|
sdkIndexCached, sdkIndexErr = buildSDKIndex("../../pkg")
|
|
})
|
|
return sdkIndexCached, sdkIndexErr
|
|
}
|
|
|
|
func buildSDKIndex(root string) (*sdkIndex, error) {
|
|
idx := newSDKIndex()
|
|
fset := token.NewFileSet()
|
|
err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
|
|
return nil
|
|
}
|
|
file, err := parser.ParseFile(fset, path, nil, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
idx.addFile(filepath.Dir(path), file)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return idx, nil
|
|
}
|
|
|
|
func newSDKIndex() *sdkIndex {
|
|
return &sdkIndex{
|
|
types: make(map[string]map[string]ast.Expr),
|
|
calls: make(map[string][]sdkCall),
|
|
}
|
|
}
|
|
|
|
// addFile collects type declarations and API calls of one file
|
|
func (idx *sdkIndex) addFile(dir string, file *ast.File) {
|
|
if idx.types[dir] == nil {
|
|
idx.types[dir] = make(map[string]ast.Expr)
|
|
}
|
|
|
|
for _, decl := range file.Decls {
|
|
switch d := decl.(type) {
|
|
case *ast.GenDecl:
|
|
for _, spec := range d.Specs {
|
|
if ts, ok := spec.(*ast.TypeSpec); ok {
|
|
idx.types[dir][ts.Name.Name] = ts.Type
|
|
}
|
|
}
|
|
case *ast.FuncDecl:
|
|
if d.Body != nil {
|
|
idx.addFuncCalls(dir, d)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// addFuncCalls finds DecortApiCall* invocations in function body and resolves url and type of params
|
|
func (idx *sdkIndex) addFuncCalls(dir string, fn *ast.FuncDecl) {
|
|
// variable name -> type name
|
|
varTypes := make(map[string]string)
|
|
for _, field := range fn.Type.Params.List {
|
|
for _, name := range field.Names {
|
|
varTypes[name.Name] = typeNameOf(field.Type)
|
|
}
|
|
}
|
|
// variable name -> last assigned string literal
|
|
strVars := make(map[string]string)
|
|
|
|
ast.Inspect(fn.Body, func(n ast.Node) bool {
|
|
switch node := n.(type) {
|
|
case *ast.AssignStmt:
|
|
for i, lhs := range node.Lhs {
|
|
ident, ok := lhs.(*ast.Ident)
|
|
if !ok || i >= len(node.Rhs) {
|
|
continue
|
|
}
|
|
if s, ok := stringLiteral(node.Rhs[i]); ok {
|
|
strVars[ident.Name] = s
|
|
}
|
|
if name := typeNameOfValue(node.Rhs[i]); name != "" {
|
|
varTypes[ident.Name] = name
|
|
}
|
|
}
|
|
case *ast.ValueSpec:
|
|
for i, name := range node.Names {
|
|
if node.Type != nil {
|
|
varTypes[name.Name] = typeNameOf(node.Type)
|
|
}
|
|
if i < len(node.Values) {
|
|
if s, ok := stringLiteral(node.Values[i]); ok {
|
|
strVars[name.Name] = s
|
|
}
|
|
if tn := typeNameOfValue(node.Values[i]); tn != "" {
|
|
varTypes[name.Name] = tn
|
|
}
|
|
}
|
|
}
|
|
case *ast.CallExpr:
|
|
sel, ok := node.Fun.(*ast.SelectorExpr)
|
|
if !ok {
|
|
return true
|
|
}
|
|
|
|
var urlArg, paramsArg ast.Expr
|
|
transport := transportForm
|
|
switch sel.Sel.Name {
|
|
case "DecortApiCall", "DecortApiCallMP":
|
|
if len(node.Args) != 4 {
|
|
return true
|
|
}
|
|
urlArg, paramsArg = node.Args[2], node.Args[3]
|
|
case "DecortApiCallCtype":
|
|
if len(node.Args) != 5 {
|
|
return true
|
|
}
|
|
urlArg, paramsArg = node.Args[2], node.Args[4]
|
|
if ctype, ok := node.Args[3].(*ast.SelectorExpr); ok && ctype.Sel.Name == "MIMEJSON" {
|
|
transport = transportJSON
|
|
}
|
|
default:
|
|
return true
|
|
}
|
|
|
|
url, ok := stringLiteral(urlArg)
|
|
if !ok {
|
|
ident, isIdent := urlArg.(*ast.Ident)
|
|
if !isIdent {
|
|
return true
|
|
}
|
|
if url, ok = strVars[ident.Name]; !ok {
|
|
return true
|
|
}
|
|
}
|
|
|
|
call := sdkCall{Dir: dir, Transport: transport, FuncName: fn.Name.Name}
|
|
switch p := paramsArg.(type) {
|
|
case *ast.Ident:
|
|
if p.Name != "nil" {
|
|
call.TypeName = varTypes[p.Name]
|
|
}
|
|
default:
|
|
call.TypeName = typeNameOfValue(paramsArg)
|
|
}
|
|
idx.calls[url] = append(idx.calls[url], call)
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
func stringLiteral(e ast.Expr) (string, bool) {
|
|
lit, ok := e.(*ast.BasicLit)
|
|
if !ok || lit.Kind != token.STRING {
|
|
return "", false
|
|
}
|
|
s, err := strconv.Unquote(lit.Value)
|
|
return s, err == nil
|
|
}
|
|
|
|
// typeNameOfValue returns type name of expressions like T{...} or &T{...}
|
|
func typeNameOfValue(e ast.Expr) string {
|
|
switch v := e.(type) {
|
|
case *ast.CompositeLit:
|
|
return typeNameOf(v.Type)
|
|
case *ast.UnaryExpr:
|
|
return typeNameOfValue(v.X)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// typeNameOf returns name of local named type (T or *T), empty string otherwise
|
|
func typeNameOf(e ast.Expr) string {
|
|
switch v := e.(type) {
|
|
case *ast.Ident:
|
|
return v.Name
|
|
case *ast.StarExpr:
|
|
return typeNameOf(v.X)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// callsForPath returns SDK calls for platform path like "/restmachine/cloudapi/compute/start"
|
|
func (idx *sdkIndex) callsForPath(path string) []sdkCall {
|
|
return idx.calls[strings.TrimPrefix(path, "/restmachine")]
|
|
}
|
|
|
|
// sentFields returns flattened list of parameters SDK puts on the wire for the call
|
|
func (idx *sdkIndex) sentFields(call sdkCall) ([]sentField, bool) {
|
|
if call.TypeName == "" {
|
|
return nil, true
|
|
}
|
|
expr, ok := idx.types[call.Dir][call.TypeName]
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
st, ok := expr.(*ast.StructType)
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
fields := idx.structFields(call.Dir, st, call.Transport, map[string]bool{call.TypeName: true})
|
|
|
|
// serialized wrapper field inherits `validate` of the skipped (url:"-") request field with the same json name
|
|
if call.Transport == transportForm {
|
|
skipped := idx.skippedFormFields(call.Dir, st, map[string]bool{call.TypeName: true})
|
|
for i := range fields {
|
|
if fields[i].Validate != "" {
|
|
continue
|
|
}
|
|
if validate, ok := skipped[strings.TrimSpace(fields[i].Name)]; ok {
|
|
fields[i].Validate = validate
|
|
}
|
|
}
|
|
}
|
|
|
|
return fields, true
|
|
}
|
|
|
|
// skippedFormFields returns json name -> validate tag of fields that are not sent in form requests (url:"-")
|
|
func (idx *sdkIndex) skippedFormFields(dir string, st *ast.StructType, visited map[string]bool) map[string]string {
|
|
result := make(map[string]string)
|
|
for _, field := range st.Fields.List {
|
|
var tag reflect.StructTag
|
|
if field.Tag != nil {
|
|
if s, err := strconv.Unquote(field.Tag.Value); err == nil {
|
|
tag = reflect.StructTag(s)
|
|
}
|
|
}
|
|
urlTag, hasURLTag := tag.Lookup("url")
|
|
|
|
if len(field.Names) == 0 && !hasURLTag {
|
|
name := typeNameOf(field.Type)
|
|
if embedded, ok := idx.types[dir][name].(*ast.StructType); ok && !visited[name] {
|
|
visited[name] = true
|
|
for k, v := range idx.skippedFormFields(dir, embedded, visited) {
|
|
result[k] = v
|
|
}
|
|
delete(visited, name)
|
|
}
|
|
continue
|
|
}
|
|
|
|
if urlTag != "-" {
|
|
continue
|
|
}
|
|
jsonName := strings.TrimSpace(strings.Split(tag.Get("json"), ",")[0])
|
|
if jsonName != "" && jsonName != "-" {
|
|
result[jsonName] = tag.Get("validate")
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (idx *sdkIndex) structFields(dir string, st *ast.StructType, transport string, visited map[string]bool) []sentField {
|
|
var result []sentField
|
|
for _, field := range st.Fields.List {
|
|
var tag reflect.StructTag
|
|
if field.Tag != nil {
|
|
if s, err := strconv.Unquote(field.Tag.Value); err == nil {
|
|
tag = reflect.StructTag(s)
|
|
}
|
|
}
|
|
wireTag, hasWireTag := tag.Lookup(transportTagKey(transport))
|
|
|
|
// embedded struct without tag is flattened by both encoding/json and go-querystring
|
|
if len(field.Names) == 0 && !hasWireTag {
|
|
name := typeNameOf(field.Type)
|
|
if embedded, ok := idx.types[dir][name].(*ast.StructType); ok && !visited[name] {
|
|
visited[name] = true
|
|
result = append(result, idx.structFields(dir, embedded, transport, visited)...)
|
|
delete(visited, name)
|
|
}
|
|
continue
|
|
}
|
|
|
|
goNames := make([]string, 0, len(field.Names))
|
|
for _, n := range field.Names {
|
|
goNames = append(goNames, n.Name)
|
|
}
|
|
if len(goNames) == 0 {
|
|
goNames = append(goNames, typeNameOf(field.Type))
|
|
}
|
|
|
|
for _, goName := range goNames {
|
|
if !ast.IsExported(goName) {
|
|
continue
|
|
}
|
|
name, omitEmpty, skip := parseWireTag(wireTag, goName)
|
|
if skip {
|
|
continue
|
|
}
|
|
result = append(result, sentField{
|
|
Name: name,
|
|
GoName: goName,
|
|
Type: idx.resolveType(dir, field.Type, map[string]bool{}),
|
|
OmitEmpty: omitEmpty,
|
|
Validate: tag.Get("validate"),
|
|
})
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func transportTagKey(transport string) string {
|
|
if transport == transportJSON {
|
|
return "json"
|
|
}
|
|
return "url"
|
|
}
|
|
|
|
// parseWireTag returns wire name of the field, omitempty flag and whether the field is skipped (tag "-")
|
|
func parseWireTag(tag, goName string) (string, bool, bool) {
|
|
parts := strings.Split(tag, ",")
|
|
// keep tag name as is (including spaces) so that typos in tags are reported
|
|
name := parts[0]
|
|
if name == "-" && len(parts) == 1 {
|
|
return "", false, true
|
|
}
|
|
if name == "" {
|
|
name = goName
|
|
}
|
|
var omitEmpty bool
|
|
for _, opt := range parts[1:] {
|
|
if strings.TrimSpace(opt) == "omitempty" {
|
|
omitEmpty = true
|
|
}
|
|
}
|
|
return name, omitEmpty, false
|
|
}
|
|
|
|
var (
|
|
emptyInterfaceType = reflect.TypeOf((*interface{})(nil)).Elem()
|
|
structPlaceholder = reflect.TypeOf(struct{}{})
|
|
basicTypes = map[string]reflect.Type{
|
|
"bool": reflect.TypeOf(false),
|
|
"string": reflect.TypeOf(""),
|
|
"int": reflect.TypeOf(int(0)),
|
|
"int8": reflect.TypeOf(int8(0)),
|
|
"int16": reflect.TypeOf(int16(0)),
|
|
"int32": reflect.TypeOf(int32(0)),
|
|
"int64": reflect.TypeOf(int64(0)),
|
|
"uint": reflect.TypeOf(uint(0)),
|
|
"uint8": reflect.TypeOf(uint8(0)),
|
|
"uint16": reflect.TypeOf(uint16(0)),
|
|
"uint32": reflect.TypeOf(uint32(0)),
|
|
"uint64": reflect.TypeOf(uint64(0)),
|
|
"float32": reflect.TypeOf(float32(0)),
|
|
"float64": reflect.TypeOf(float64(0)),
|
|
"any": emptyInterfaceType,
|
|
}
|
|
)
|
|
|
|
// resolveType converts type expression to reflect.Type for kind comparison (nil for types from other packages)
|
|
func (idx *sdkIndex) resolveType(dir string, e ast.Expr, visited map[string]bool) reflect.Type {
|
|
switch v := e.(type) {
|
|
case *ast.Ident:
|
|
if t, ok := basicTypes[v.Name]; ok {
|
|
return t
|
|
}
|
|
if visited[v.Name] {
|
|
return nil
|
|
}
|
|
underlying, ok := idx.types[dir][v.Name]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
visited[v.Name] = true
|
|
return idx.resolveType(dir, underlying, visited)
|
|
case *ast.StarExpr:
|
|
return idx.resolveType(dir, v.X, visited)
|
|
case *ast.StructType:
|
|
return structPlaceholder
|
|
case *ast.InterfaceType:
|
|
return emptyInterfaceType
|
|
case *ast.ArrayType:
|
|
elem := idx.resolveType(dir, v.Elt, visited)
|
|
if elem == nil {
|
|
elem = emptyInterfaceType
|
|
}
|
|
return reflect.SliceOf(elem)
|
|
case *ast.MapType:
|
|
return reflect.MapOf(reflect.TypeOf(""), emptyInterfaceType)
|
|
}
|
|
return nil
|
|
}
|