mirror of
https://github.com/arangodb/kube-arangodb.git
synced 2024-12-14 11:57:37 +00:00
398 lines
9.2 KiB
Go
398 lines
9.2 KiB
Go
//
|
|
// DISCLAIMER
|
|
//
|
|
// Copyright 2023-2024 ArangoDB GmbH, Cologne, Germany
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//
|
|
// Copyright holder is ArangoDB GmbH, Cologne, Germany
|
|
//
|
|
|
|
package internal
|
|
|
|
import (
|
|
"fmt"
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"io/fs"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
openapi "k8s.io/kube-openapi/pkg/common"
|
|
|
|
"github.com/arangodb/kube-arangodb/pkg/util"
|
|
"github.com/arangodb/kube-arangodb/pkg/util/errors"
|
|
)
|
|
|
|
const (
|
|
rootPackageName = "github.com/arangodb/kube-arangodb"
|
|
)
|
|
|
|
func parseDocDefinition(t *testing.T, root, path, typ string, field *ast.Field, fs *token.FileSet) DocDefinition {
|
|
def := DocDefinition{
|
|
Path: path,
|
|
Type: typ,
|
|
}
|
|
|
|
require.NotNil(t, field)
|
|
|
|
if links, ok := extract(field, "link"); ok {
|
|
def.Links = links
|
|
}
|
|
|
|
if d, ok := extract(field, "default"); ok {
|
|
def.Default = util.NewType[string](d[0])
|
|
}
|
|
|
|
if example, ok := extract(field, "example"); ok {
|
|
def.Example = example
|
|
}
|
|
|
|
if enum, ok := extract(field, "enum"); ok {
|
|
def.Enum = enum
|
|
}
|
|
|
|
if immutable, ok := extract(field, "immutable"); ok {
|
|
def.Immutable = util.NewType[string](immutable[0])
|
|
}
|
|
|
|
if important, ok := extract(field, "important"); ok {
|
|
def.Important = util.NewType[string](important[0])
|
|
}
|
|
|
|
if docs, dep, ok, err := extractNotTags(field); err != nil {
|
|
require.Fail(t, fmt.Sprintf("Error while getting tags for %s: %s", path, err.Error()))
|
|
} else if !ok {
|
|
println(def.Path, " is missing documentation!")
|
|
} else {
|
|
def.Docs = docs
|
|
def.Deprecated = dep
|
|
}
|
|
|
|
file := fs.File(field.Pos())
|
|
|
|
filePath, err := filepath.Rel(root, file.Name())
|
|
require.NoError(t, err)
|
|
|
|
def.File = filePath
|
|
def.Line = file.Line(field.Pos())
|
|
|
|
return def
|
|
}
|
|
|
|
type typeInfo struct {
|
|
path string
|
|
typ string
|
|
}
|
|
|
|
func iterateOverObject(t *testing.T, fields map[string]*ast.Field, name string, object reflect.Type, path string) map[typeInfo]*ast.Field {
|
|
r := map[typeInfo]*ast.Field{}
|
|
t.Run(name, func(t *testing.T) {
|
|
for k, v := range iterateOverObjectDirect(t, fields, name, object, path) {
|
|
r[k] = v
|
|
}
|
|
})
|
|
|
|
return r
|
|
}
|
|
|
|
func iterateOverObjectDirect(t *testing.T, fields map[string]*ast.Field, name string, object reflect.Type, path string) map[typeInfo]*ast.Field {
|
|
if n, _, simple := isSimpleType(object); simple {
|
|
return map[typeInfo]*ast.Field{
|
|
typeInfo{
|
|
path: fmt.Sprintf("%s.%s", path, name),
|
|
typ: n,
|
|
}: nil,
|
|
}
|
|
}
|
|
|
|
r := map[typeInfo]*ast.Field{}
|
|
|
|
switch object.Kind() {
|
|
case reflect.Array, reflect.Slice:
|
|
if _, _, simple := isSimpleType(object.Elem()); simple {
|
|
return map[typeInfo]*ast.Field{
|
|
typeInfo{
|
|
path: fmt.Sprintf("%s.%s", path, name),
|
|
typ: "array",
|
|
}: nil,
|
|
}
|
|
}
|
|
|
|
for k, v := range iterateOverObjectDirect(t, fields, fmt.Sprintf("%s\\[int\\]", name), object.Elem(), path) {
|
|
r[k] = v
|
|
}
|
|
case reflect.Map:
|
|
if _, _, simple := isSimpleType(object.Elem()); simple {
|
|
return map[typeInfo]*ast.Field{
|
|
typeInfo{
|
|
path: fmt.Sprintf("%s.%s", path, name),
|
|
typ: "object",
|
|
}: nil,
|
|
}
|
|
}
|
|
|
|
for k, v := range iterateOverObjectDirect(t, fields, fmt.Sprintf("%s.\\<%s\\>", name, object.Key().Kind().String()), object.Elem(), path) {
|
|
r[k] = v
|
|
}
|
|
case reflect.Struct:
|
|
for field := 0; field < object.NumField(); field++ {
|
|
f := object.Field(field)
|
|
|
|
if !f.IsExported() {
|
|
continue
|
|
}
|
|
|
|
tag, ok := f.Tag.Lookup("json")
|
|
if !ok {
|
|
if f.Anonymous {
|
|
tag = ",inline"
|
|
}
|
|
}
|
|
|
|
n, inline := extractTag(tag)
|
|
|
|
if n == "-" {
|
|
continue
|
|
}
|
|
|
|
fullFieldName := fmt.Sprintf("%s.%s.%s", object.PkgPath(), object.Name(), f.Name)
|
|
|
|
doc, ok := fields[fullFieldName]
|
|
if !ok && !f.Anonymous {
|
|
require.True(t, ok, "field %s was not parsed from source", fullFieldName)
|
|
}
|
|
|
|
if !f.Anonymous {
|
|
if t, ok := extractType(doc); ok {
|
|
info := typeInfo{
|
|
path: fmt.Sprintf("%s.%s.%s", path, name, n),
|
|
typ: t[0],
|
|
}
|
|
r[info] = doc
|
|
continue
|
|
}
|
|
}
|
|
|
|
// inline and anonymous field (embedded)
|
|
if inline && n == "" {
|
|
if doc != nil {
|
|
if t, ok := extractType(doc); ok {
|
|
info := typeInfo{
|
|
path: fmt.Sprintf("%s.%s", path, name),
|
|
typ: t[0],
|
|
}
|
|
r[info] = doc
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
if inline {
|
|
for k, v := range iterateOverObjectDirect(t, fields, name, f.Type, path) {
|
|
if v == nil {
|
|
v = doc
|
|
}
|
|
r[k] = v
|
|
}
|
|
} else {
|
|
|
|
for k, v := range iterateOverObject(t, fields, n, f.Type, fmt.Sprintf("%s.%s", path, name)) {
|
|
if v == nil {
|
|
v = doc
|
|
}
|
|
r[k] = v
|
|
}
|
|
}
|
|
}
|
|
case reflect.Pointer:
|
|
for k, v := range iterateOverObjectDirect(t, fields, name, object.Elem(), path) {
|
|
r[k] = v
|
|
}
|
|
default:
|
|
require.Failf(t, "unsupported type", "%s for %s at %s", object.String(), name, path)
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
func extractType(n *ast.Field) ([]string, bool) {
|
|
return extract(n, "type")
|
|
}
|
|
|
|
func extract(n *ast.Field, tag string) ([]string, bool) {
|
|
if n.Doc == nil {
|
|
return nil, false
|
|
}
|
|
|
|
var ret []string
|
|
|
|
for _, c := range n.Doc.List {
|
|
if strings.HasPrefix(c.Text, fmt.Sprintf("// +doc/%s: ", tag)) {
|
|
ret = append(ret, strings.TrimPrefix(c.Text, fmt.Sprintf("// +doc/%s: ", tag)))
|
|
}
|
|
}
|
|
|
|
return ret, len(ret) > 0
|
|
}
|
|
|
|
func extractNotTags(n *ast.Field) ([]string, []string, bool, error) {
|
|
if n.Doc == nil {
|
|
return nil, nil, false, nil
|
|
}
|
|
|
|
var ret, dep []string
|
|
|
|
var deprecated bool
|
|
|
|
for _, c := range n.Doc.List {
|
|
if strings.HasPrefix(c.Text, "// ") {
|
|
if strings.HasPrefix(c.Text, "// Deprecated") {
|
|
if !strings.HasPrefix(c.Text, "// Deprecated: ") {
|
|
return nil, nil, false, errors.Errorf("Invalid deprecated field")
|
|
}
|
|
}
|
|
if strings.HasPrefix(c.Text, "// Deprecated:") {
|
|
deprecated = true
|
|
dep = append(dep, strings.TrimSpace(strings.TrimPrefix(c.Text, "// Deprecated:")))
|
|
continue
|
|
}
|
|
|
|
if !strings.HasPrefix(c.Text, "// +doc/") {
|
|
v := strings.TrimSpace(strings.TrimPrefix(c.Text, "// "))
|
|
if deprecated {
|
|
dep = append(dep, v)
|
|
} else {
|
|
ret = append(ret, v)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return ret, dep, len(ret) > 0 || len(dep) > 0, nil
|
|
}
|
|
|
|
// isSimpleType returns the OpenAPI-compatible type name, type format and boolean indicating if this is simple type or not
|
|
func isSimpleType(obj reflect.Type) (string, string, bool) {
|
|
typ, frmt := openapi.OpenAPITypeFormat(obj.Kind().String())
|
|
return typ, frmt, typ != "" || frmt != ""
|
|
}
|
|
|
|
func extractTag(tag string) (string, bool) {
|
|
parts := strings.Split(tag, ",")
|
|
|
|
if len(parts) == 1 {
|
|
return parts[0], false
|
|
}
|
|
|
|
if parts[1] == "inline" {
|
|
return parts[0], true
|
|
}
|
|
|
|
return parts[0], false
|
|
}
|
|
|
|
// parseSourceFiles returns map of <path to field in structure> -> AST for structure Field and the token inspector for all files in package
|
|
func parseSourceFiles(t *testing.T, root string, fset *token.FileSet, path string) map[string]*ast.Field {
|
|
d := parseMultipleDirs(t, root, fset, parser.ParseComments, path)
|
|
|
|
r := map[string]*ast.Field{}
|
|
|
|
for k, f := range d {
|
|
var ct *ast.TypeSpec
|
|
var nt *ast.TypeSpec
|
|
|
|
ast.Inspect(f, func(n ast.Node) bool {
|
|
switch x := n.(type) {
|
|
case *ast.TypeSpec, *ast.FuncDecl, *ast.Field, *ast.Package, *ast.File, *ast.Ident, *ast.StructType:
|
|
default:
|
|
if x == nil {
|
|
return true
|
|
}
|
|
return true
|
|
}
|
|
|
|
switch x := n.(type) {
|
|
case *ast.TypeSpec:
|
|
ct = x
|
|
case *ast.StructType:
|
|
nt = ct
|
|
case *ast.FuncDecl:
|
|
nt = nil
|
|
case *ast.Field:
|
|
if nt != nil {
|
|
require.NotEmpty(t, nt.Name)
|
|
|
|
if len(x.Names) > 0 {
|
|
for _, name := range x.Names {
|
|
r[fmt.Sprintf("%s.%s.%s", k, nt.Name, name)] = x
|
|
}
|
|
} else {
|
|
// If x.Names is empty, it's an anonymous field
|
|
if len(x.Names) == 0 {
|
|
// first check if it's a pointer to a struct
|
|
typeName, ok := x.Type.(*ast.StarExpr)
|
|
if ok {
|
|
ident, ok := typeName.X.(*ast.SelectorExpr)
|
|
if ok {
|
|
fieldName := ident.Sel.Name
|
|
r[fmt.Sprintf("%s.%s.%s", k, nt.Name, fieldName)] = x
|
|
}
|
|
} else {
|
|
// if it's not a pointer
|
|
ident, ok := x.Type.(*ast.SelectorExpr)
|
|
if ok {
|
|
fieldName := ident.Sel.Name
|
|
r[fmt.Sprintf("%s.%s.%s", k, nt.Name, fieldName)] = x
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
return true
|
|
})
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
func parseMultipleDirs(t *testing.T, root string, fset *token.FileSet, mode parser.Mode, dirs ...string) map[string]*ast.Package {
|
|
// positions are relative to fset
|
|
|
|
r := map[string]*ast.Package{}
|
|
|
|
for _, dir := range dirs {
|
|
d, err := parser.ParseDir(fset, dir, func(info fs.FileInfo) bool {
|
|
return !strings.HasSuffix(info.Name(), "_test.go") &&
|
|
info.Name() != "zz_generated.deepcopy.go"
|
|
}, mode)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, d, 1)
|
|
k := strings.ReplaceAll(dir, root, rootPackageName)
|
|
|
|
for _, v := range d {
|
|
require.NotContains(t, r, k)
|
|
r[k] = v
|
|
}
|
|
}
|
|
|
|
return r
|
|
}
|