v1.16.5
This commit is contained in:
446
tests/platform_upgrade/sdk_calls.go
Normal file
446
tests/platform_upgrade/sdk_calls.go
Normal file
@@ -0,0 +1,446 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user