add plaintext template
Kubernetes-commit: e394364cbd1d5ace0f9cfcf10fa2bc458076d1a4
This commit is contained in:
parent
8ae416a4e0
commit
ee7b47da7d
|
@ -35,6 +35,193 @@ func WithBuiltinTemplateFuncs(tmpl *template.Template) *template.Template {
|
|||
res, err := json.Marshal(obj)
|
||||
return string(res), err
|
||||
},
|
||||
"toPrettyJson": func(obj any) (string, error) {
|
||||
res, err := json.MarshalIndent(obj, "", " ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(res), err
|
||||
},
|
||||
"fail": func(message string) (string, error) {
|
||||
return "", errors.New(message)
|
||||
},
|
||||
"wrap": func(l int, s string) (string, error) {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
writer := term.NewWordWrapWriter(buf, uint(l))
|
||||
_, err := writer.Write([]byte(s))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
},
|
||||
"split": func(s string, sep string) []string {
|
||||
return strings.Split(s, sep)
|
||||
},
|
||||
"join": func(sep string, strs ...string) string {
|
||||
return strings.Join(strs, sep)
|
||||
},
|
||||
"include": func(name string, data interface{}) (string, error) {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
if err := tmpl.ExecuteTemplate(buf, name, data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
},
|
||||
"ternary": func(a, b any, condition bool) any {
|
||||
if condition {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
},
|
||||
"first": func(list any) (any, error) {
|
||||
if list == nil {
|
||||
return nil, errors.New("list is empty")
|
||||
}
|
||||
|
||||
tp := reflect.TypeOf(list).Kind()
|
||||
switch tp {
|
||||
case reflect.Slice, reflect.Array:
|
||||
l2 := reflect.ValueOf(list)
|
||||
|
||||
l := l2.Len()
|
||||
if l == 0 {
|
||||
return nil, errors.New("list is empty")
|
||||
}
|
||||
|
||||
return l2.Index(0).Interface(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("first cannot be used on type: %T", list)
|
||||
}
|
||||
},
|
||||
"last": func(list any) (any, error) {
|
||||
if list == nil {
|
||||
return nil, errors.New("list is empty")
|
||||
}
|
||||
|
||||
tp := reflect.TypeOf(list).Kind()
|
||||
switch tp {
|
||||
case reflect.Slice, reflect.Array:
|
||||
l2 := reflect.ValueOf(list)
|
||||
|
||||
l := l2.Len()
|
||||
if l == 0 {
|
||||
return nil, errors.New("list is empty")
|
||||
}
|
||||
|
||||
return l2.Index(l - 1).Interface(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("last cannot be used on type: %T", list)
|
||||
}
|
||||
},
|
||||
"indent": func(amount int, str string) string {
|
||||
pad := strings.Repeat(" ", amount)
|
||||
return pad + strings.Replace(str, "\n", "\n"+pad, -1)
|
||||
},
|
||||
"dict": func(keysAndValues ...any) (map[string]any, error) {
|
||||
if len(keysAndValues)%2 != 0 {
|
||||
return nil, errors.New("expected even # of arguments")
|
||||
}
|
||||
|
||||
res := map[string]any{}
|
||||
for i := 0; i+1 < len(keysAndValues); i = i + 2 {
|
||||
if key, ok := keysAndValues[i].(string); ok {
|
||||
res[key] = keysAndValues[i+1]
|
||||
} else {
|
||||
return nil, fmt.Errorf("key of type %T is not a string as expected", key)
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
},
|
||||
"contains": func(list any, value any) bool {
|
||||
if list == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
val := reflect.ValueOf(list)
|
||||
switch val.Kind() {
|
||||
case reflect.Array:
|
||||
case reflect.Slice:
|
||||
for i := 0; i < val.Len(); i++ {
|
||||
cur := val.Index(i)
|
||||
if cur.CanInterface() && reflect.DeepEqual(cur.Interface(), value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return false
|
||||
},
|
||||
"set": func(dict map[string]any, keysAndValues ...any) (any, error) {
|
||||
if len(keysAndValues)%2 != 0 {
|
||||
return nil, errors.New("expected even number of arguments")
|
||||
}
|
||||
|
||||
copyDict := make(map[string]any, len(dict))
|
||||
for k, v := range dict {
|
||||
copyDict[k] = v
|
||||
}
|
||||
|
||||
for i := 0; i < len(keysAndValues); i += 2 {
|
||||
key, ok := keysAndValues[i].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("keys must be strings")
|
||||
}
|
||||
|
||||
copyDict[key] = keysAndValues[i+1]
|
||||
}
|
||||
|
||||
return copyDict, nil
|
||||
},
|
||||
"add": func(value, operand int) int {
|
||||
return value + operand
|
||||
},
|
||||
"sub": func(value, operand int) int {
|
||||
return value - operand
|
||||
},
|
||||
"mul": func(value, operand int) int {
|
||||
return value * operand
|
||||
},
|
||||
"resolveRef": func(refAny any, document map[string]any) map[string]any {
|
||||
refString, ok := refAny.(string)
|
||||
if !ok {
|
||||
// if passed nil, or wrong type just treat the same
|
||||
// way as unresolved reference (makes for easier templates)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resolve field path encoded by the ref
|
||||
ref, err := jsonreference.New(refString)
|
||||
if err != nil {
|
||||
// Unrecognized ref format.
|
||||
return nil
|
||||
}
|
||||
|
||||
if !ref.HasFragmentOnly {
|
||||
// Downloading is not supported. Treat as not found
|
||||
return nil
|
||||
}
|
||||
|
||||
fragment := ref.GetURL().Fragment
|
||||
components := strings.Split(fragment, "/")
|
||||
cur := document
|
||||
|
||||
for _, k := range components {
|
||||
if len(k) == 0 {
|
||||
// first component is usually empty (#/components/) , etc
|
||||
continue
|
||||
}
|
||||
|
||||
next, ok := cur[k].(map[string]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
cur = next
|
||||
}
|
||||
return cur
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,358 @@
|
|||
/*
|
||||
Copyright 2022 The Kubernetes Authors.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package v2_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
v2 "k8s.io/kubectl/pkg/explain/v2"
|
||||
)
|
||||
|
||||
func TestFuncs(t *testing.T) {
|
||||
testcases := []struct {
|
||||
Name string
|
||||
FuncName string
|
||||
Source string
|
||||
Context any
|
||||
Expect string
|
||||
Error string
|
||||
}{
|
||||
{
|
||||
Name: "err",
|
||||
FuncName: "fail",
|
||||
Source: `{{fail .}}`,
|
||||
Context: "this is a test",
|
||||
Error: "this is a test",
|
||||
},
|
||||
{
|
||||
Name: "basic",
|
||||
FuncName: "wrap",
|
||||
Source: `{{wrap 3 .}}`,
|
||||
Context: "this is a really good test",
|
||||
Expect: "this\nis\na\nreally\ngood\ntest",
|
||||
},
|
||||
{
|
||||
Name: "basic",
|
||||
FuncName: "split",
|
||||
Source: `{{split . "/"}}`,
|
||||
Context: "this/is/a/slash/separated/thing",
|
||||
Expect: "[this is a slash separated thing]",
|
||||
},
|
||||
{
|
||||
Name: "basic",
|
||||
FuncName: "join",
|
||||
Source: `{{join "/" "this" "is" "a" "slash" "separated" "thing"}}`,
|
||||
Expect: "this/is/a/slash/separated/thing",
|
||||
},
|
||||
{
|
||||
Name: "basic",
|
||||
FuncName: "include",
|
||||
Source: `{{define "myTemplate"}}{{.}}{{end}}{{$var := include "myTemplate" .}}{{$var}}`,
|
||||
Context: "hello, world!",
|
||||
Expect: "hello, world!",
|
||||
},
|
||||
{
|
||||
Name: "nil",
|
||||
FuncName: "first",
|
||||
Source: `{{first .}}`,
|
||||
Context: nil,
|
||||
Error: "list is empty",
|
||||
},
|
||||
{
|
||||
Name: "empty",
|
||||
FuncName: "first",
|
||||
Source: `{{first .}}`,
|
||||
Context: []string{},
|
||||
Error: "list is empty",
|
||||
},
|
||||
{
|
||||
Name: "basic",
|
||||
FuncName: "first",
|
||||
Source: `{{first .}}`,
|
||||
Context: []string{"first", "second", "third"},
|
||||
Expect: "first",
|
||||
},
|
||||
{
|
||||
Name: "wrongtype",
|
||||
FuncName: "first",
|
||||
Source: `{{first .}}`,
|
||||
Context: "test",
|
||||
Error: "first cannot be used on type: string",
|
||||
},
|
||||
{
|
||||
Name: "nil",
|
||||
FuncName: "last",
|
||||
Source: `{{last .}}`,
|
||||
Context: nil,
|
||||
Error: "list is empty",
|
||||
},
|
||||
{
|
||||
Name: "empty",
|
||||
FuncName: "last",
|
||||
Source: `{{last .}}`,
|
||||
Context: []string{},
|
||||
Error: "list is empty",
|
||||
},
|
||||
{
|
||||
Name: "basic",
|
||||
FuncName: "last",
|
||||
Source: `{{last .}}`,
|
||||
Context: []string{"first", "second", "third"},
|
||||
Expect: "third",
|
||||
},
|
||||
{
|
||||
Name: "wrongtype",
|
||||
FuncName: "last",
|
||||
Source: `{{last .}}`,
|
||||
Context: "test",
|
||||
Error: "last cannot be used on type: string",
|
||||
},
|
||||
{
|
||||
Name: "none",
|
||||
FuncName: "indent",
|
||||
Source: `{{indent 0 .}}`,
|
||||
Context: "this is a string",
|
||||
Expect: "this is a string",
|
||||
},
|
||||
{
|
||||
Name: "some",
|
||||
FuncName: "indent",
|
||||
Source: `{{indent 2 .}}`,
|
||||
Context: "this is a string",
|
||||
Expect: " this is a string",
|
||||
},
|
||||
{
|
||||
Name: "empty",
|
||||
FuncName: "dict",
|
||||
Source: `{{dict | toJson}}`,
|
||||
Expect: "{}",
|
||||
},
|
||||
{
|
||||
Name: "single value",
|
||||
FuncName: "dict",
|
||||
Source: `{{dict "key" "value" | toJson}}`,
|
||||
Expect: `{"key":"value"}`,
|
||||
},
|
||||
{
|
||||
Name: "twoValues",
|
||||
FuncName: "dict",
|
||||
Source: `{{dict "key1" "val1" "key2" "val2" | toJson}}`,
|
||||
Expect: `{"key1":"val1","key2":"val2"}`,
|
||||
},
|
||||
{
|
||||
Name: "oddNumberArgs",
|
||||
FuncName: "dict",
|
||||
Source: `{{dict "key1" 1 "key2" | toJson}}`,
|
||||
Error: "error calling dict: expected even # of arguments",
|
||||
},
|
||||
{
|
||||
Name: "IntegerValue",
|
||||
FuncName: "dict",
|
||||
Source: `{{dict "key1" 1 | toJson}}`,
|
||||
Expect: `{"key1":1}`,
|
||||
},
|
||||
{
|
||||
Name: "MixedValues",
|
||||
FuncName: "dict",
|
||||
Source: `{{dict "key1" 1 "key2" "val2" "key3" (dict "key1" "val1") | toJson}}`,
|
||||
Expect: `{"key1":1,"key2":"val2","key3":{"key1":"val1"}}`,
|
||||
},
|
||||
{
|
||||
Name: "nil",
|
||||
FuncName: "contains",
|
||||
Source: `{{contains . "value"}}`,
|
||||
Context: nil,
|
||||
Expect: `false`,
|
||||
},
|
||||
{
|
||||
Name: "empty",
|
||||
FuncName: "contains",
|
||||
Source: `{{contains . "value"}}`,
|
||||
Context: []string{},
|
||||
Expect: `false`,
|
||||
},
|
||||
{
|
||||
Name: "basic",
|
||||
FuncName: "contains",
|
||||
Source: `{{contains . "value"}}`,
|
||||
Context: []string{"value"},
|
||||
Expect: `true`,
|
||||
},
|
||||
{
|
||||
Name: "struct",
|
||||
FuncName: "contains",
|
||||
Source: `{{contains $.haystack $.needle}}`,
|
||||
Context: map[string]any{
|
||||
"needle": schema.GroupVersionKind{"testgroup.k8s.io", "v1", "Kind"},
|
||||
"haystack": []schema.GroupVersionKind{
|
||||
{"randomgroup.k8s.io", "v1", "OtherKind"},
|
||||
{"testgroup.k8s.io", "v1", "OtherKind"},
|
||||
{"testgroup.k8s.io", "v1", "Kind"},
|
||||
},
|
||||
},
|
||||
Expect: `true`,
|
||||
},
|
||||
{
|
||||
Name: "nil",
|
||||
FuncName: "set",
|
||||
Source: `{{set nil "key" "value" | toJson}}`,
|
||||
Expect: `{"key":"value"}`,
|
||||
},
|
||||
{
|
||||
Name: "empty",
|
||||
FuncName: "set",
|
||||
Source: `{{set (dict) "key" "value" | toJson}}`,
|
||||
Expect: `{"key":"value"}`,
|
||||
},
|
||||
{
|
||||
Name: "OddArgs",
|
||||
FuncName: "set",
|
||||
Source: `{{set (dict) "key" "value" "key2" | toJson}}`,
|
||||
Error: `expected even number of arguments`,
|
||||
},
|
||||
{
|
||||
Name: "NonStringKey",
|
||||
FuncName: "set",
|
||||
Source: `{{set (dict) 1 "value" | toJson}}`,
|
||||
Error: `keys must be strings`,
|
||||
},
|
||||
{
|
||||
Name: "NilKey",
|
||||
FuncName: "set",
|
||||
Source: `{{set (dict) nil "value" | toJson}}`,
|
||||
Error: `keys must be strings`,
|
||||
},
|
||||
{
|
||||
Name: "NilValue",
|
||||
FuncName: "set",
|
||||
Source: `{{set (dict) "key" nil | toJson}}`,
|
||||
Expect: `{"key":null}`,
|
||||
},
|
||||
{
|
||||
Name: "OverwriteKey",
|
||||
FuncName: "set",
|
||||
Source: `{{set (dict "key1" "val1" "key2" "val2") "key1" nil | toJson}}`,
|
||||
Expect: `{"key1":null,"key2":"val2"}`,
|
||||
},
|
||||
{
|
||||
Name: "OverwriteKeyWithLefover",
|
||||
FuncName: "set",
|
||||
Source: `{{set (dict "key1" "val1" "key2" "val2" "key3" "val3") "key1" nil | toJson}}`,
|
||||
Expect: `{"key1":null,"key2":"val2","key3":"val3"}`,
|
||||
},
|
||||
{
|
||||
Name: "basic",
|
||||
FuncName: "add",
|
||||
Source: `{{add 1 2}}`,
|
||||
Expect: `3`,
|
||||
},
|
||||
{
|
||||
Name: "basic",
|
||||
FuncName: "sub",
|
||||
Source: `{{sub 1 2}}`,
|
||||
Expect: `-1`,
|
||||
},
|
||||
{
|
||||
Name: "basic",
|
||||
FuncName: "mul",
|
||||
Source: `{{mul 2 3}}`,
|
||||
Expect: `6`,
|
||||
},
|
||||
{
|
||||
Name: "basic",
|
||||
FuncName: "resolveRef",
|
||||
Source: `{{resolveRef "#/components/schemas/myTypeName" . | toJson}}`,
|
||||
Context: map[string]any{
|
||||
"components": map[string]any{
|
||||
"schemas": map[string]any{
|
||||
"myTypeName": map[string]any{
|
||||
"key": "val",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Expect: `{"key":"val"}`,
|
||||
},
|
||||
{
|
||||
Name: "basicNameWithDots",
|
||||
FuncName: "resolveRef",
|
||||
Source: `{{resolveRef "#/components/schemas/myTypeName.with.dots" . | toJson}}`,
|
||||
Context: map[string]any{
|
||||
"components": map[string]any{
|
||||
"schemas": map[string]any{
|
||||
"myTypeName.with.dots": map[string]any{
|
||||
"key": "val",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Expect: `{"key":"val"}`,
|
||||
},
|
||||
{
|
||||
Name: "notFound",
|
||||
FuncName: "resolveRef",
|
||||
Source: `{{resolveRef "#/components/schemas/otherTypeName" . | toJson}}`,
|
||||
Context: map[string]any{
|
||||
"components": map[string]any{
|
||||
"schemas": map[string]any{
|
||||
"myTypeName": map[string]any{
|
||||
"key": "val",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Expect: `null`,
|
||||
},
|
||||
{
|
||||
Name: "url",
|
||||
FuncName: "resolveRef",
|
||||
Source: `{{resolveRef "http://swagger.com/swagger.json#/components/schemas/myTypeName" . | toJson}}`,
|
||||
Context: map[string]any{
|
||||
"components": map[string]any{
|
||||
"schemas": map[string]any{
|
||||
"myTypeName": map[string]any{
|
||||
"key": "val",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Expect: `null`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tcase := range testcases {
|
||||
t.Run(tcase.FuncName+"/"+tcase.Name, func(t *testing.T) {
|
||||
|
||||
tmpl, err := v2.WithBuiltinTemplateFuncs(template.New("me")).Parse(tcase.Source)
|
||||
require.NoError(t, err)
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
err = tmpl.Execute(buf, tcase.Context)
|
||||
|
||||
if len(tcase.Error) > 0 {
|
||||
require.ErrorContains(t, err, tcase.Error)
|
||||
} else if output := buf.String(); len(tcase.Expect) > 0 {
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, output, tcase.Expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,250 @@
|
|||
{{- /* Determine if Path for requested GVR is at /api or /apis based on emptiness of group */ -}}
|
||||
{{- $prefix := (ternary "/api" (join "" "/apis/" $.GVR.Group) (not $.GVR.Group)) -}}
|
||||
|
||||
{{- /* Search both cluster-scoped and namespaced-scoped paths for the GVR to find its GVK */ -}}
|
||||
{{- /* Looks for path /apis/<group>/<version>/<resource> or /apis/<group>/<version>/<version>/namespaces/{namespace}/<resource> */ -}}
|
||||
{{- $clusterScopedSearchPath := join "/" $prefix $.GVR.Version $.GVR.Resource -}}
|
||||
{{- $namespaceScopedSearchPath := join "/" $prefix $.GVR.Version "namespaces" "\\{namespace\\}" $.GVR.Resource -}}
|
||||
{{- $path := or (index $.Document "paths" $clusterScopedSearchPath) (index $.Document "paths" $clusterScopedSearchPath) -}}
|
||||
|
||||
{{- /* Pull GVK from operation */ -}}
|
||||
{{- with $gvk := and $path (index $path "get" "x-kubernetes-group-version-kind") -}}
|
||||
{{- if $gvk.group -}}
|
||||
GROUP: {{ $gvk.group }}{{"\n" -}}
|
||||
{{- end -}}
|
||||
KIND: {{ $gvk.kind}}{{"\n" -}}
|
||||
VERSION: {{ $gvk.version }}{{"\n" -}}
|
||||
{{- "\n" -}}
|
||||
|
||||
{{- with include "schema" (dict "gvk" $gvk "Document" $.Document "FieldPath" $.FieldPath "Recursive" $.Recursive) -}}
|
||||
{{- . -}}
|
||||
{{- else -}}
|
||||
error: GVK {{$gvk}} not found in OpenAPI schema
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
error: GVR ({{$.GVR.String}}) not found in OpenAPI schema
|
||||
{{- end -}}
|
||||
{{- "\n" -}}
|
||||
|
||||
{{- /*
|
||||
Finds a schema with the given GVK and prints its explain output or empty string
|
||||
if GVK was not found
|
||||
|
||||
Takes dictionary as argument with keys:
|
||||
gvk: openapiv3 JSON schema
|
||||
Document: entire doc
|
||||
FieldPath: field path to follow
|
||||
Recursive: print recursive
|
||||
*/ -}}
|
||||
{{- define "schema" -}}
|
||||
{{- /* Find definition with this GVK by filtering out the components/schema with the given x-kubernetes-group-version-kind */ -}}
|
||||
{{- range index $.Document "components" "schemas" -}}
|
||||
{{- if contains (index . "x-kubernetes-group-version-kind") $.gvk -}}
|
||||
{{- with include "output" (set $ "schema" .) -}}
|
||||
{{- . -}}
|
||||
{{- else -}}
|
||||
error: field "{{$.FieldPath}}" does not exist
|
||||
{{- end -}}
|
||||
{{- break -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- /*
|
||||
Follows FieldPath until the FieldPath is empty. Then prints field name and field
|
||||
list of resultant schema. If field path is not found. Prints nothing.
|
||||
Example output:
|
||||
|
||||
FIELD: spec
|
||||
|
||||
DESCRIPTION:
|
||||
<template "description">
|
||||
|
||||
FIELDS:
|
||||
<template "fieldList">
|
||||
|
||||
Takes dictionary as argument with keys:
|
||||
schema: openapiv3 JSON schema
|
||||
history: map[string]int
|
||||
Document: entire doc
|
||||
FieldPath: field path to follow
|
||||
Recursive: print recursive
|
||||
*/ -}}
|
||||
{{- define "output" -}}
|
||||
{{- $refString := or (index $.schema "$ref") "" -}}
|
||||
{{- $nextContext := set $ "history" (set $.history $refString 1) -}}
|
||||
{{- $resolved := or (resolveRef $refString $.Document) $.schema -}}
|
||||
{{- if not $.FieldPath -}}
|
||||
DESCRIPTION:{{- "\n" -}}
|
||||
{{- or (include "description" (dict "schema" $resolved "Document" $.Document)) "<empty>" | wrap 76 | indent 4 -}}{{- "\n" -}}
|
||||
{{- with include "fieldList" (dict "schema" $resolved "level" 1 "Document" $.Document "Recursive" $.Recursive) -}}
|
||||
FIELDS:{{- "\n" -}}
|
||||
{{- . -}}
|
||||
{{- end -}}
|
||||
{{- else if and $refString (index $.history $refString) -}}
|
||||
{{- /* Stop and do nothing. Hit a cycle */ -}}
|
||||
{{- else if and $resolved.properties (index $resolved.properties (first $.FieldPath)) -}}
|
||||
{{- /* Schema has this property directly. Traverse to next schema */ -}}
|
||||
{{- $subschema := index $resolved.properties (first $.FieldPath) -}}
|
||||
{{- if eq 1 (len $.FieldPath) -}}
|
||||
{{- /* TODO: The original explain would say RESOURCE instead of FIELD here under some circumstances */ -}}
|
||||
FIELD: {{first $.FieldPath}} <{{ template "typeGuess" (dict "schema" $subschema) }}>{{"\n"}}
|
||||
{{- "\n" -}}
|
||||
{{- end -}}
|
||||
{{- template "output" (set $nextContext "history" (dict) "FieldPath" (slice $.FieldPath 1) "schema" $subschema ) -}}
|
||||
{{- else if $resolved.items -}}
|
||||
{{- /* If the schema is an array then traverse the array item fields */ -}}
|
||||
{{- template "output" (set $nextContext "schema" $resolved.items) -}}
|
||||
{{- else if $resolved.additionalProperties -}}
|
||||
{{- /* If the schema is a map then traverse the map item fields */ -}}
|
||||
{{- template "output" (set $nextContext "schema" $resolved.additionalProperties) -}}
|
||||
{{- else -}}
|
||||
{{- /* Last thing to try is all the alternatives in the allOf case */ -}}
|
||||
{{- /* Stop when one of the alternatives has an output at all */ -}}
|
||||
{{- range $index, $subschema := $resolved.allOf -}}
|
||||
{{- with include "output" (set $nextContext "schema" $subschema) -}}
|
||||
{{- . -}}
|
||||
{{- break -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- /*
|
||||
Prints list of fields of a given open api schema in following form:
|
||||
|
||||
field1 <type> -required-
|
||||
DESCRIPTION
|
||||
|
||||
field2 <type> -required-
|
||||
DESCRIPTION
|
||||
|
||||
or if Recursive is enabled:
|
||||
field1 <type> -required-
|
||||
subfield1 <type>
|
||||
subsubfield1 <type>
|
||||
subsubfield2 <type>
|
||||
subfield2 <type>
|
||||
field2 <type> -required-
|
||||
subfield1 <type>
|
||||
subfield2 <type>
|
||||
|
||||
Follows refs for field traversal. If there are cycles in the schema, they are
|
||||
detected and traversal ends.
|
||||
|
||||
Takes dictionary as argument with keys:
|
||||
schema: openapiv3 JSON schema
|
||||
level: indentation level
|
||||
history: map[string]int containing all ref names so far
|
||||
Document: entire doc
|
||||
Recursive: print recursive
|
||||
*/ -}}
|
||||
{{- define "fieldList" -}}
|
||||
{{- /* Resolve schema if it is a ref */}}
|
||||
{{- /* If this is a ref seen before, then ignore it */}}
|
||||
{{- $refString := or (index $.schema "$ref") "" }}
|
||||
{{- if and $refString (index (or $.history (dict)) $refString) -}}
|
||||
{{- /* Do nothing for cycle */}}
|
||||
{{- else -}}
|
||||
{{- $nextContext := set $ "history" (set $.history $refString 1) -}}
|
||||
{{- $resolved := or (resolveRef $refString $.Document) $.schema -}}
|
||||
{{- range $k, $v := $resolved.properties -}}
|
||||
{{- template "fieldDetail" (dict "name" $k "schema" $resolved "short" $.Recursive "level" $.level) -}}
|
||||
{{- if $.Recursive -}}
|
||||
{{- /* Check if we already know about this element */}}
|
||||
{{- template "fieldList" (set $nextContext "schema" $v "level" (add $.level 1)) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- range $resolved.allOf -}}
|
||||
{{- template "fieldList" (set $nextContext "schema" .)}}
|
||||
{{- end -}}
|
||||
{{- if $resolved.items}}{{- template "fieldList" (set $nextContext "schema" $resolved.items)}}{{end}}
|
||||
{{- if $resolved.additionalProperties}}{{- template "fieldList" (set $nextContext "schema" $resolved.additionalProperties)}}{{end}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
|
||||
{{- /*
|
||||
|
||||
Prints a single field of the given schema
|
||||
Optionally prints in a one-line style
|
||||
|
||||
Takes dictionary as argument with keys:
|
||||
schema: openapiv3 JSON schema which contains the field
|
||||
name: name of field
|
||||
short: limit printing to a single-line summary
|
||||
level: indentation amount
|
||||
*/ -}}
|
||||
{{- define "fieldDetail" -}}
|
||||
{{- $level := or $.level 0 -}}
|
||||
{{- $indentAmount := mul $level 2 -}}
|
||||
{{- $fieldSchema := index $.schema.properties $.name -}}
|
||||
{{- $.name | indent $indentAmount -}}{{"\t"}}<{{ template "typeGuess" (dict "schema" $fieldSchema) }}>
|
||||
{{- if contains $.schema.required $.name}} -required-{{- end -}}
|
||||
{{- "\n" -}}
|
||||
{{- if not $.short -}}
|
||||
{{- or $fieldSchema.description "<no description>" | wrap (sub 78 $indentAmount) | indent (add $indentAmount 2) -}}{{- "\n" -}}
|
||||
{{- "\n" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- /*
|
||||
|
||||
Prints the description of the given OpenAPI v3 schema. Also walks through all
|
||||
sibling schemas to the provided schema and prints the description of those schemas
|
||||
too
|
||||
|
||||
Takes dictionary as argument with keys:
|
||||
schema: openapiv3 JSON schema
|
||||
Document: document to resolve refs within
|
||||
*/ -}}
|
||||
{{- define "description" -}}
|
||||
{{- with or (resolveRef (index $.schema "$ref") $.Document) $.schema -}}
|
||||
{{- if .description -}}
|
||||
{{- .description -}}
|
||||
{{- "\n" -}}
|
||||
{{- end -}}
|
||||
{{- range .allOf -}}
|
||||
{{- template "description" (set $ "schema" .)}}
|
||||
{{- end -}}
|
||||
{{- if .items -}}
|
||||
{{- template "description" (set $ "schema" .items) -}}
|
||||
{{- end -}}
|
||||
{{- if .additionalProperties -}}
|
||||
{{- template "description" (set $ "schema" .additionalProperties) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- /* Renders a shortstring representing an interpretation of what is the "type"
|
||||
of a subschema e.g.:
|
||||
|
||||
`string` `number`, `Object`, `[]Object`, `map[string]string`, etc.
|
||||
|
||||
Serves as a more helpful type hint than raw typical openapi `type` field
|
||||
|
||||
Takes dictionary as argument with keys:
|
||||
schema: openapiv3 JSON schema
|
||||
*/ -}}
|
||||
{{- define "typeGuess" -}}
|
||||
{{- with $.schema -}}
|
||||
{{- if .items -}}
|
||||
[]{{template "typeGuess" (dict "schema" .items)}}
|
||||
{{- else if .additionalProperties -}}
|
||||
map[string]{{template "typeGuess" (dict "schema" .additionalProperties)}}
|
||||
{{- else if and .allOf (not .properties) (eq (len .allOf) 1) -}}
|
||||
{{- /* If allOf has a single element and there are no direct
|
||||
properties on the schema, defer to the allOf */ -}}
|
||||
{{- template "typeGuess" (dict "schema" (first .allOf)) -}}
|
||||
{{- else if index . "$ref"}}
|
||||
{{- /* Parse the #!/components/schemas/io.k8s.Kind string into just the Kind name */ -}}
|
||||
{{- $ref := index . "$ref" -}}
|
||||
{{- $name := split $ref "/" | last -}}
|
||||
{{- or (split $name "." | last) "Object" -}}
|
||||
{{- else -}}
|
||||
{{- or .type "Object" -}}
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
{{- fail "expected schema argument to subtemplate 'typeguess'" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
|
@ -0,0 +1,547 @@
|
|||
/*
|
||||
Copyright 2022 The Kubernetes Authors.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package templates_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/kube-openapi/pkg/spec3"
|
||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||
v2 "k8s.io/kubectl/pkg/explain/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed plaintext.tmpl
|
||||
plaintextSource string
|
||||
|
||||
//go:embed apiextensions.k8s.io_v1.json
|
||||
apiextensionsJSON string
|
||||
|
||||
apiExtensionsV1OpenAPI map[string]interface{} = func() map[string]interface{} {
|
||||
var res map[string]interface{}
|
||||
utilruntime.Must(json.Unmarshal([]byte(apiextensionsJSON), &res))
|
||||
return res
|
||||
}()
|
||||
|
||||
apiExtensionsV1OpenAPISpec spec3.OpenAPI = func() spec3.OpenAPI {
|
||||
var res spec3.OpenAPI
|
||||
utilruntime.Must(json.Unmarshal([]byte(apiextensionsJSON), &res))
|
||||
return res
|
||||
}()
|
||||
)
|
||||
|
||||
type testCase struct {
|
||||
// test case name
|
||||
Name string
|
||||
// if empty uses main template
|
||||
Subtemplate string
|
||||
// context to render withi
|
||||
Context any
|
||||
// checks to perform on rendered output
|
||||
Checks []check
|
||||
}
|
||||
|
||||
type check interface {
|
||||
doCheck(output string) error
|
||||
}
|
||||
|
||||
type checkError string
|
||||
|
||||
func (c checkError) doCheck(output string) error {
|
||||
if !strings.Contains(output, "error: "+string(c)) {
|
||||
return fmt.Errorf("expected error: '%v' in string:\n%v", string(c), output)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type checkContains string
|
||||
|
||||
func (c checkContains) doCheck(output string) error {
|
||||
if !strings.Contains(output, string(c)) {
|
||||
return fmt.Errorf("expected substring: '%v' in string:\n%v", string(c), output)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type checkEquals string
|
||||
|
||||
func (c checkEquals) doCheck(output string) error {
|
||||
if output != string(c) {
|
||||
return fmt.Errorf("output is not equal to expectation:\n%v", cmp.Diff(string(c), output))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func MapDict[K comparable, V any, N any](accum map[K]V, mapper func(V) N) map[K]N {
|
||||
res := make(map[K]N, len(accum))
|
||||
for k, v := range accum {
|
||||
res[k] = mapper(v)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func ReduceDict[K comparable, V any, N any](val map[K]V, accum N, mapper func(N, K, V) N) N {
|
||||
for k, v := range val {
|
||||
accum = mapper(accum, k, v)
|
||||
}
|
||||
return accum
|
||||
}
|
||||
|
||||
func TestPlaintext(t *testing.T) {
|
||||
testcases := []testCase{
|
||||
{
|
||||
// Test case where resource being rendered is not found in OpenAPI schema
|
||||
Name: "ResourceNotFound",
|
||||
Context: v2.TemplateContext{
|
||||
Document: apiExtensionsV1OpenAPI,
|
||||
GVR: schema.GroupVersionResource{},
|
||||
FieldPath: nil,
|
||||
Recursive: false,
|
||||
},
|
||||
Checks: []check{
|
||||
checkError("GVR (/, Resource=) not found in OpenAPI schema\n"),
|
||||
},
|
||||
},
|
||||
{
|
||||
// Test basic ability to find a GVR and print its description
|
||||
Name: "SchemaFound",
|
||||
Context: v2.TemplateContext{
|
||||
Document: apiExtensionsV1OpenAPI,
|
||||
GVR: schema.GroupVersionResource{
|
||||
Group: "apiextensions.k8s.io",
|
||||
Version: "v1",
|
||||
Resource: "customresourcedefinitions",
|
||||
},
|
||||
FieldPath: nil,
|
||||
Recursive: false,
|
||||
},
|
||||
Checks: []check{
|
||||
checkContains("CustomResourceDefinition represents a resource that should be exposed"),
|
||||
},
|
||||
},
|
||||
{
|
||||
// Test that shows trying to find a non-existent field path of an existing
|
||||
// schema
|
||||
Name: "SchemaFieldPathNotFound",
|
||||
Context: v2.TemplateContext{
|
||||
Document: apiExtensionsV1OpenAPI,
|
||||
GVR: schema.GroupVersionResource{
|
||||
Group: "apiextensions.k8s.io",
|
||||
Version: "v1",
|
||||
Resource: "customresourcedefinitions",
|
||||
},
|
||||
FieldPath: []string{"does", "not", "exist"},
|
||||
Recursive: false,
|
||||
},
|
||||
Checks: []check{
|
||||
checkError(`field "[does not exist]" does not exist`),
|
||||
},
|
||||
},
|
||||
{
|
||||
// Test traversing a single level for scalar field path
|
||||
Name: "SchemaFieldPathShallow",
|
||||
Context: v2.TemplateContext{
|
||||
Document: apiExtensionsV1OpenAPI,
|
||||
GVR: schema.GroupVersionResource{
|
||||
Group: "apiextensions.k8s.io",
|
||||
Version: "v1",
|
||||
Resource: "customresourcedefinitions",
|
||||
},
|
||||
FieldPath: []string{"kind"},
|
||||
Recursive: false,
|
||||
},
|
||||
Checks: []check{
|
||||
checkContains("FIELD: kind <string>"),
|
||||
},
|
||||
},
|
||||
{
|
||||
// Test traversing a multiple levels for scalar field path
|
||||
Name: "SchemaFieldPathDeep",
|
||||
Context: v2.TemplateContext{
|
||||
Document: apiExtensionsV1OpenAPI,
|
||||
GVR: schema.GroupVersionResource{
|
||||
Group: "apiextensions.k8s.io",
|
||||
Version: "v1",
|
||||
Resource: "customresourcedefinitions",
|
||||
},
|
||||
FieldPath: []string{"spec", "names", "singular"},
|
||||
Recursive: false,
|
||||
},
|
||||
Checks: []check{
|
||||
checkContains("FIELD: singular <string>"),
|
||||
},
|
||||
},
|
||||
{
|
||||
// Test traversing a multiple levels for scalar field path
|
||||
// through an array field
|
||||
Name: "SchemaFieldPathViaList",
|
||||
Context: v2.TemplateContext{
|
||||
Document: apiExtensionsV1OpenAPI,
|
||||
GVR: schema.GroupVersionResource{
|
||||
Group: "apiextensions.k8s.io",
|
||||
Version: "v1",
|
||||
Resource: "customresourcedefinitions",
|
||||
},
|
||||
FieldPath: []string{"spec", "versions", "name"},
|
||||
Recursive: false,
|
||||
},
|
||||
Checks: []check{
|
||||
checkContains("FIELD: name <string>"),
|
||||
},
|
||||
},
|
||||
{
|
||||
// Test traversing a multiple levels for scalar field path
|
||||
// through a map[string]T field.
|
||||
Name: "SchemaFieldPathViaMap",
|
||||
Context: v2.TemplateContext{
|
||||
Document: apiExtensionsV1OpenAPI,
|
||||
GVR: schema.GroupVersionResource{
|
||||
Group: "apiextensions.k8s.io",
|
||||
Version: "v1",
|
||||
Resource: "customresourcedefinitions",
|
||||
},
|
||||
FieldPath: []string{"spec", "versions", "schema", "openAPIV3Schema", "properties", "default"},
|
||||
Recursive: false,
|
||||
},
|
||||
Checks: []check{
|
||||
checkContains("default is a default value for undefined object fields"),
|
||||
},
|
||||
},
|
||||
{
|
||||
// Shows that walking through a recursively specified schema is A-OK!
|
||||
Name: "SchemaFieldPathRecursive",
|
||||
Context: v2.TemplateContext{
|
||||
Document: apiExtensionsV1OpenAPI,
|
||||
GVR: schema.GroupVersionResource{
|
||||
Group: "apiextensions.k8s.io",
|
||||
Version: "v1",
|
||||
Resource: "customresourcedefinitions",
|
||||
},
|
||||
FieldPath: []string{"spec", "versions", "schema", "openAPIV3Schema", "properties", "properties", "properties", "properties", "properties", "default"},
|
||||
Recursive: false,
|
||||
},
|
||||
Checks: []check{
|
||||
checkContains("default is a default value for undefined object fields"),
|
||||
},
|
||||
},
|
||||
{
|
||||
// Shows that all fields are included
|
||||
Name: "SchemaAllFields",
|
||||
Context: v2.TemplateContext{
|
||||
Document: apiExtensionsV1OpenAPI,
|
||||
GVR: schema.GroupVersionResource{
|
||||
Group: "apiextensions.k8s.io",
|
||||
Version: "v1",
|
||||
Resource: "customresourcedefinitions",
|
||||
},
|
||||
FieldPath: []string{"spec", "versions", "schema"},
|
||||
Recursive: false,
|
||||
},
|
||||
Checks: ReduceDict(apiExtensionsV1OpenAPISpec.Components.Schemas["io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceValidation"].Properties, []check{}, func(checks []check, k string, v spec.Schema) []check {
|
||||
return append(checks, checkContains(k), checkContains(v.Description))
|
||||
}),
|
||||
},
|
||||
{
|
||||
// Shows that all fields are included
|
||||
Name: "SchemaAllFieldsRecursive",
|
||||
Context: v2.TemplateContext{
|
||||
Document: apiExtensionsV1OpenAPI,
|
||||
GVR: schema.GroupVersionResource{
|
||||
Group: "apiextensions.k8s.io",
|
||||
Version: "v1",
|
||||
Resource: "customresourcedefinitions",
|
||||
},
|
||||
FieldPath: []string{"spec", "versions", "schema"},
|
||||
Recursive: true,
|
||||
},
|
||||
Checks: ReduceDict(apiExtensionsV1OpenAPISpec.Components.Schemas["io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceValidation"].Properties, []check{}, func(checks []check, k string, v spec.Schema) []check {
|
||||
return append(checks, checkContains(k))
|
||||
}),
|
||||
},
|
||||
{
|
||||
// Shows that the typeguess template works with scalars
|
||||
Name: "Scalar",
|
||||
Subtemplate: "typeGuess",
|
||||
Context: map[string]any{
|
||||
"schema": map[string]any{
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
Checks: []check{
|
||||
checkEquals("string"),
|
||||
},
|
||||
},
|
||||
{
|
||||
// Shows that the typeguess template behaves correctly given an
|
||||
// array with unknown items
|
||||
Name: "ArrayUnknown",
|
||||
Subtemplate: "typeGuess",
|
||||
Context: map[string]any{
|
||||
"schema": map[string]any{
|
||||
"description": "a cool field",
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
Checks: []check{
|
||||
checkEquals("array"),
|
||||
},
|
||||
},
|
||||
{
|
||||
// Shows that the typeguess template works with scalars
|
||||
Name: "ArrayOfScalar",
|
||||
Subtemplate: "typeGuess",
|
||||
Context: map[string]any{
|
||||
"schema": map[string]any{
|
||||
"description": "a cool field",
|
||||
"type": "array",
|
||||
"items": map[string]any{
|
||||
"type": "number",
|
||||
},
|
||||
},
|
||||
},
|
||||
Checks: []check{
|
||||
checkEquals("[]number"),
|
||||
},
|
||||
},
|
||||
{
|
||||
// Shows that the typeguess template works with arrays containing
|
||||
// a items which are a schema specified by a single-element allOf
|
||||
// pointing to a $ref
|
||||
Name: "ArrayOfAllOfRef",
|
||||
Subtemplate: "typeGuess",
|
||||
Context: map[string]any{
|
||||
"schema": map[string]any{
|
||||
"description": "a cool field",
|
||||
"type": "array",
|
||||
"items": map[string]any{
|
||||
"type": "object",
|
||||
"allOf": []map[string]any{
|
||||
{
|
||||
"$ref": "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceValidation",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Checks: []check{
|
||||
checkEquals("[]CustomResourceValidation"),
|
||||
},
|
||||
},
|
||||
{
|
||||
// Shows that the typeguess template works with arrays containing
|
||||
// a items which are a schema pointing to a $ref
|
||||
Name: "ArrayOfRef",
|
||||
Subtemplate: "typeGuess",
|
||||
Context: map[string]any{
|
||||
"schema": map[string]any{
|
||||
"description": "a cool field",
|
||||
"type": "array",
|
||||
"items": map[string]any{
|
||||
"type": "object",
|
||||
"$ref": "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceValidation",
|
||||
},
|
||||
},
|
||||
},
|
||||
Checks: []check{
|
||||
checkEquals("[]CustomResourceValidation"),
|
||||
},
|
||||
},
|
||||
{
|
||||
// Shows that the typeguess template works with arrays of maps of scalars
|
||||
Name: "ArrayOfMap",
|
||||
Subtemplate: "typeGuess",
|
||||
Context: map[string]any{
|
||||
"schema": map[string]any{
|
||||
"description": "a cool field",
|
||||
"type": "array",
|
||||
"items": map[string]any{
|
||||
"type": "object",
|
||||
"additionalProperties": map[string]any{
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Checks: []check{
|
||||
checkEquals("[]map[string]string"),
|
||||
},
|
||||
},
|
||||
{
|
||||
// Shows that the typeguess template works with maps of arrays of scalars
|
||||
Name: "MapOfArrayOfScalar",
|
||||
Subtemplate: "typeGuess",
|
||||
Context: map[string]any{
|
||||
"schema": map[string]any{
|
||||
"description": "a cool field",
|
||||
"type": "object",
|
||||
"additionalProperties": map[string]any{
|
||||
"type": "array",
|
||||
"items": map[string]any{
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Checks: []check{
|
||||
checkEquals("map[string][]string"),
|
||||
},
|
||||
},
|
||||
{
|
||||
// Shows that the typeguess template works with maps of ref types
|
||||
Name: "MapOfRef",
|
||||
Subtemplate: "typeGuess",
|
||||
Context: map[string]any{
|
||||
"schema": map[string]any{
|
||||
"description": "a cool field",
|
||||
"type": "object",
|
||||
"additionalProperties": map[string]any{
|
||||
"type": "string",
|
||||
"allOf": []map[string]any{
|
||||
{
|
||||
"$ref": "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceValidation",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Checks: []check{
|
||||
checkEquals("map[string]CustomResourceValidation"),
|
||||
},
|
||||
},
|
||||
{
|
||||
// Shows that the typeguess template prints `Object` if there
|
||||
// is absolutely no type information
|
||||
Name: "Unknown",
|
||||
Subtemplate: "typeGuess",
|
||||
Context: map[string]any{
|
||||
"schema": map[string]any{
|
||||
"description": "a cool field",
|
||||
},
|
||||
},
|
||||
Checks: []check{
|
||||
checkEquals("Object"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Required",
|
||||
Subtemplate: "fieldDetail",
|
||||
Context: map[string]any{
|
||||
"schema": map[string]any{
|
||||
"type": "object",
|
||||
"description": "a description that should not be printed",
|
||||
"properties": map[string]any{
|
||||
"thefield": map[string]any{
|
||||
"type": "string",
|
||||
"description": "a description that should not be printed",
|
||||
},
|
||||
},
|
||||
"required": []string{"thefield"},
|
||||
},
|
||||
"name": "thefield",
|
||||
"short": true,
|
||||
},
|
||||
Checks: []check{
|
||||
checkEquals("thefield\t<string> -required-\n"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Description",
|
||||
Subtemplate: "fieldDetail",
|
||||
Context: map[string]any{
|
||||
"schema": map[string]any{
|
||||
"type": "object",
|
||||
"description": "a description that should not be printed",
|
||||
"properties": map[string]any{
|
||||
"thefield": map[string]any{
|
||||
"type": "string",
|
||||
"description": "a description that should be printed",
|
||||
},
|
||||
},
|
||||
"required": []string{"thefield"},
|
||||
},
|
||||
"name": "thefield",
|
||||
"short": false,
|
||||
},
|
||||
Checks: []check{
|
||||
checkEquals("thefield\t<string> -required-\n a description that should be printed\n\n"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Indent",
|
||||
Subtemplate: "fieldDetail",
|
||||
Context: map[string]any{
|
||||
"schema": map[string]any{
|
||||
"type": "object",
|
||||
"description": "a description that should not be printed",
|
||||
"properties": map[string]any{
|
||||
"thefield": map[string]any{
|
||||
"type": "string",
|
||||
"description": "a description that should not be printed",
|
||||
},
|
||||
},
|
||||
"required": []string{"thefield"},
|
||||
},
|
||||
"name": "thefield",
|
||||
"short": true,
|
||||
"level": 5,
|
||||
},
|
||||
Checks: []check{
|
||||
checkEquals(" thefield\t<string> -required-\n"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tmpl, err := v2.WithBuiltinTemplateFuncs(template.New("")).Parse(plaintextSource)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, tcase := range testcases {
|
||||
testName := tcase.Name
|
||||
if len(tcase.Subtemplate) > 0 {
|
||||
testName = tcase.Subtemplate + "/" + testName
|
||||
}
|
||||
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
if len(tcase.Subtemplate) == 0 {
|
||||
tmpl.Execute(buf, tcase.Context)
|
||||
} else {
|
||||
tmpl.ExecuteTemplate(buf, tcase.Subtemplate, tcase.Context)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
output := buf.String()
|
||||
for _, check := range tcase.Checks {
|
||||
err = check.doCheck(output)
|
||||
|
||||
if err != nil {
|
||||
t.Log("test failed on output:\n" + output)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue