1
0
Fork 0
mirror of https://github.com/arangodb/kube-arangodb.git synced 2024-12-14 11:57:37 +00:00
kube-arangodb/internal/schema_builder_test.go
2024-07-26 10:32:36 +02:00

236 lines
6.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/token"
"reflect"
"testing"
"github.com/stretchr/testify/require"
apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
openapi "k8s.io/kube-openapi/pkg/common"
"github.com/arangodb/kube-arangodb/pkg/util"
)
type schemaBuilder struct {
root string
fields map[string]*ast.Field
fs *token.FileSet
}
type allowAnyType struct{}
func newSchemaBuilder(root string, fields map[string]*ast.Field, fs *token.FileSet) *schemaBuilder {
return &schemaBuilder{
root: root,
fields: fields,
fs: fs,
}
}
func (b *schemaBuilder) tryGetKubeOpenAPIDefinitions(t *testing.T, obj reflect.Type) *apiextensions.JSONSchemaProps {
if o, ok := reflect.New(obj).Interface().(openapi.OpenAPIV3DefinitionGetter); ok {
return b.openAPIDefToSchemaPros(t, o.OpenAPIV3Definition())
}
if o, ok := reflect.New(obj).Interface().(openapi.OpenAPIDefinitionGetter); ok {
return b.openAPIDefToSchemaPros(t, o.OpenAPIDefinition())
}
if obj := b.tryGetKubeOpenAPIV2Definitions(t, reflect.New(obj).Interface()); obj != nil {
return obj
}
return nil
}
func (b *schemaBuilder) tryGetKubeOpenAPIV2Definitions(t *testing.T, obj interface{}) *apiextensions.JSONSchemaProps {
type openAPISchemaTypeGetter interface {
OpenAPISchemaType() []string
}
type openAPISchemaFormatGetter interface {
OpenAPISchemaFormat() string
}
var typ, frmt string
if o, ok := obj.(openAPISchemaTypeGetter); ok {
strs := o.OpenAPISchemaType()
require.Len(t, strs, 1)
typ = strs[0]
}
if o, ok := obj.(openAPISchemaFormatGetter); ok {
frmt = o.OpenAPISchemaFormat()
}
if typ != "" || frmt != "" {
if frmt == "int-or-string" && typ == "string" {
return &apiextensions.JSONSchemaProps{
Type: typ,
XIntOrString: true,
}
}
return &apiextensions.JSONSchemaProps{
Type: typ,
Format: frmt,
}
}
return nil
}
func (b *schemaBuilder) openAPIDefToSchemaPros(t *testing.T, _ *openapi.OpenAPIDefinition) *apiextensions.JSONSchemaProps {
require.Fail(t, "openAPIDefToSchemaPros is not implemented because there were no calls to this function. Add the impl if needed.")
return nil
}
func (b *schemaBuilder) TypeToSchema(t *testing.T, obj reflect.Type, path string) (schema *apiextensions.JSONSchemaProps) {
// first check if type already implements a method to get OpenAPI schema:
schema = b.tryGetKubeOpenAPIDefinitions(t, obj)
if schema != nil {
return
}
// fallback to our impl:
switch obj.Kind() {
case reflect.Pointer:
schema = b.TypeToSchema(t, obj.Elem(), path)
case reflect.Struct:
if obj == reflect.TypeOf(allowAnyType{}) || obj == reflect.TypeOf(&allowAnyType{}) {
schema = &apiextensions.JSONSchemaProps{
Type: "object",
Description: "Object with preserved fields for backward compatibility",
XPreserveUnknownFields: util.NewType(true),
}
return
}
schema = b.StructToSchema(t, obj, path)
case reflect.Array, reflect.Slice:
schema = b.ArrayToSchema(t, obj.Elem(), path)
case reflect.Map:
schema = b.MapToSchema(t, obj, path)
default:
if typ, frmt, simple := isSimpleType(obj); simple {
schema = &apiextensions.JSONSchemaProps{
Type: typ,
Format: frmt,
}
} else {
t.Fatalf("Unsupported obj kind: %s", obj.Kind())
return
}
}
return schema
}
func (b *schemaBuilder) lookupDefinition(t *testing.T, fullName, path string) *DocDefinition {
f := b.fields[fullName]
if f == nil {
return nil
}
d := parseDocDefinition(t, b.root, path, "", f, b.fs)
return &d
}
func (b *schemaBuilder) ArrayToSchema(t *testing.T, elemObj reflect.Type, path string) *apiextensions.JSONSchemaProps {
isByteArray := elemObj.Kind() == reflect.Uint8
if isByteArray {
return &apiextensions.JSONSchemaProps{
Type: "string",
Format: "byte",
}
}
return &apiextensions.JSONSchemaProps{
Type: "array",
Items: &apiextensions.JSONSchemaPropsOrArray{
Schema: b.TypeToSchema(t, elemObj, path),
},
}
}
func (b *schemaBuilder) MapToSchema(t *testing.T, mapObj reflect.Type, path string) *apiextensions.JSONSchemaProps {
require.Equal(t, reflect.String, mapObj.Key().Kind(), "only string keys for map are supported %s", path)
return &apiextensions.JSONSchemaProps{
Type: "object",
AdditionalProperties: &apiextensions.JSONSchemaPropsOrBool{
Schema: b.TypeToSchema(t, mapObj.Elem(), path),
Allows: true, /* set automatically by serialization, but useful for testing */
},
}
}
func (b *schemaBuilder) StructToSchema(t *testing.T, structObj reflect.Type, path string) *apiextensions.JSONSchemaProps {
schema := &apiextensions.JSONSchemaProps{
Type: "object",
Properties: make(map[string]apiextensions.JSONSchemaProps),
}
for field := 0; field < structObj.NumField(); field++ {
f := structObj.Field(field)
if !f.IsExported() {
continue
}
tag, ok := f.Tag.Lookup("json")
if !ok {
if f.Anonymous {
tag = ",inline"
} else {
require.Failf(t, "field %s.%s has no valid json tag: can't build schema", path, f.Name)
}
}
n, inline := extractTag(tag)
if n == "-" {
continue
}
p := path
if !inline {
p = fmt.Sprintf("%s.%s", path, n)
}
s := b.TypeToSchema(t, f.Type, p)
require.NotNil(t, s, p)
fullFieldName := fmt.Sprintf("%s.%s.%s", structObj.PkgPath(), structObj.Name(), f.Name)
def := b.lookupDefinition(t, fullFieldName, p)
if def != nil {
def.ApplyToSchema(s)
}
if inline {
// merge into parent
for k, v := range s.Properties {
schema.Properties[k] = v
}
} else {
require.NotEmpty(t, n, fullFieldName, inline)
schema.Properties[n] = *s
}
}
return schema
}