Files
decort-golang-sdk/tests/platform_upgrade/sdk_calls.go
2026-09-25 16:32:09 +03:00

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
}