343 lines
9.8 KiB
Go
343 lines
9.8 KiB
Go
package configkit
|
|
|
|
import (
|
|
"errors"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type namedCount int32
|
|
|
|
type textValue struct {
|
|
value string
|
|
}
|
|
|
|
func (v textValue) MarshalText() ([]byte, error) {
|
|
return []byte(v.value), nil
|
|
}
|
|
|
|
func (v *textValue) UnmarshalText(data []byte) error {
|
|
v.value = string(data)
|
|
return nil
|
|
}
|
|
|
|
type marshalOnly string
|
|
|
|
func (v marshalOnly) MarshalText() ([]byte, error) {
|
|
return []byte(v), nil
|
|
}
|
|
|
|
type unmarshalOnly string
|
|
|
|
func (v *unmarshalOnly) UnmarshalText(data []byte) error {
|
|
*v = unmarshalOnly(data)
|
|
return nil
|
|
}
|
|
|
|
type recursiveConfig struct {
|
|
Name string `yaml:"name"`
|
|
Next *recursiveConfig `yaml:"next"`
|
|
Children map[string]recursiveConfig `yaml:"children"`
|
|
}
|
|
|
|
func TestBuildSchemaDescribesSupportedTypes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type Embedded struct {
|
|
InlineName string `yaml:"inline_name"`
|
|
}
|
|
type nested struct {
|
|
Enabled bool `yaml:"enabled"`
|
|
}
|
|
type config struct {
|
|
HTTPServer string `yaml:"server,omitempty" configkit:"secret"`
|
|
Count namedCount `yaml:"count"`
|
|
Delay time.Duration `yaml:"delay"`
|
|
Custom textValue `yaml:"custom"`
|
|
Optional *textValue `yaml:"optional"`
|
|
Nested nested `yaml:"nested"`
|
|
Objects map[string]nested `yaml:"objects"`
|
|
Items []nested `yaml:"items"`
|
|
Fixed [2]string `yaml:"fixed"`
|
|
Dynamic any `yaml:"dynamic"`
|
|
*Embedded `yaml:",inline"`
|
|
Extras map[string]int `yaml:",inline"`
|
|
UntaggedField bool
|
|
Ignored chan int `yaml:"-"`
|
|
private chan int
|
|
}
|
|
|
|
schema, issue := buildSchema(reflect.TypeFor[config]())
|
|
if issue != nil {
|
|
t.Fatalf("buildSchema() issue = %#v", issue)
|
|
}
|
|
root := schema.root
|
|
checks := map[string]descriptorKind{
|
|
"server": descriptorString,
|
|
"count": descriptorSigned,
|
|
"delay": descriptorDuration,
|
|
"custom": descriptorText,
|
|
"optional": descriptorPointer,
|
|
"nested": descriptorStruct,
|
|
"objects": descriptorMap,
|
|
"items": descriptorSlice,
|
|
"fixed": descriptorArray,
|
|
"dynamic": descriptorDynamic,
|
|
"inline_name": descriptorString,
|
|
"untaggedfield": descriptorBoolean,
|
|
}
|
|
for name, want := range checks {
|
|
position, ok := root.fieldByName[name]
|
|
if !ok {
|
|
t.Errorf("field %q missing", name)
|
|
continue
|
|
}
|
|
got := root.fields[position].descriptor.kind
|
|
if got != want {
|
|
t.Errorf("field %q kind = %v, want %v", name, got, want)
|
|
}
|
|
}
|
|
server := root.fields[root.fieldByName["server"]]
|
|
if !server.secret {
|
|
t.Fatal("secret tag was not retained")
|
|
}
|
|
if root.inlineMap == nil || root.inlineMap.descriptor.kind != descriptorMap {
|
|
t.Fatalf("inline map = %#v", root.inlineMap)
|
|
}
|
|
if mapped, mapIssue := buildSchema(reflect.TypeFor[map[string]nested]()); mapIssue != nil || mapped.root.kind != descriptorMap {
|
|
t.Fatalf("string-keyed root map = %#v, issue=%#v", mapped, mapIssue)
|
|
}
|
|
if _, exists := root.fieldByName["ignored"]; exists {
|
|
t.Fatal("ignored field was bound")
|
|
}
|
|
}
|
|
|
|
func TestBuildSchemaSupportsRecursiveDescriptorsWithoutSharingLoads(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
first, issue := buildSchema(reflect.TypeFor[recursiveConfig]())
|
|
if issue != nil {
|
|
t.Fatal(issue.message)
|
|
}
|
|
next := first.root.fields[first.root.fieldByName["next"]].descriptor
|
|
if next.kind != descriptorPointer || next.element != first.root {
|
|
t.Fatal("recursive pointer did not refer to the existing descriptor")
|
|
}
|
|
children := first.root.fields[first.root.fieldByName["children"]].descriptor
|
|
if children.kind != descriptorMap || children.element != first.root {
|
|
t.Fatal("recursive map did not refer to the existing descriptor")
|
|
}
|
|
second, issue := buildSchema(reflect.TypeFor[recursiveConfig]())
|
|
if issue != nil {
|
|
t.Fatal(issue.message)
|
|
}
|
|
if first.root == second.root {
|
|
t.Fatal("descriptor graph was cached across loads")
|
|
}
|
|
}
|
|
|
|
func TestBuildSchemaRejectsInvalidRootsTypesTagsAndBindings(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type InlineA struct {
|
|
Value string `yaml:"same"`
|
|
}
|
|
type InlineB struct {
|
|
Value string `yaml:"same"`
|
|
}
|
|
type ambiguous struct {
|
|
InlineA `yaml:",inline"`
|
|
InlineB `yaml:",inline"`
|
|
}
|
|
type directConflict struct {
|
|
Same string `yaml:"same"`
|
|
InlineA `yaml:",inline"`
|
|
}
|
|
type twoMaps struct {
|
|
First map[string]string `yaml:",inline"`
|
|
Second map[string]string `yaml:",inline"`
|
|
}
|
|
type invalidInline struct {
|
|
Value string `yaml:",inline"`
|
|
}
|
|
type invalidMap struct {
|
|
Values map[int]string `yaml:"values"`
|
|
}
|
|
type unsupported struct {
|
|
Value complex64 `yaml:"value"`
|
|
}
|
|
type badOption struct {
|
|
Value string `yaml:"value,flow"`
|
|
}
|
|
type badConfigTag struct {
|
|
Value string `configkit:"private"`
|
|
}
|
|
type missingMarshal struct {
|
|
Value unmarshalOnly `yaml:"value"`
|
|
}
|
|
type missingUnmarshal struct {
|
|
Value marshalOnly `yaml:"value"`
|
|
}
|
|
type RecursiveInline struct {
|
|
*RecursiveInline `yaml:",inline"`
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
typeOf reflect.Type
|
|
code Code
|
|
}{
|
|
{name: "nil root", typeOf: nil, code: CodeSchemaRoot},
|
|
{name: "pointer root", typeOf: reflect.TypeFor[*recursiveConfig](), code: CodeSchemaRoot},
|
|
{name: "slice root", typeOf: reflect.TypeFor[[]string](), code: CodeSchemaRoot},
|
|
{name: "non-string root map", typeOf: reflect.TypeFor[map[int]string](), code: CodeSchemaRoot},
|
|
{name: "text scalar root", typeOf: reflect.TypeFor[textValue](), code: CodeSchemaRoot},
|
|
{name: "ambiguous fields", typeOf: reflect.TypeFor[ambiguous](), code: CodeSchemaAmbiguousField},
|
|
{name: "direct inline conflict", typeOf: reflect.TypeFor[directConflict](), code: CodeSchemaAmbiguousField},
|
|
{name: "multiple inline maps", typeOf: reflect.TypeFor[twoMaps](), code: CodeSchemaInline},
|
|
{name: "invalid inline", typeOf: reflect.TypeFor[invalidInline](), code: CodeSchemaInline},
|
|
{name: "invalid nested map", typeOf: reflect.TypeFor[invalidMap](), code: CodeSchemaUnsupportedType},
|
|
{name: "unsupported kind", typeOf: reflect.TypeFor[unsupported](), code: CodeSchemaUnsupportedType},
|
|
{name: "unsupported YAML option", typeOf: reflect.TypeFor[badOption](), code: CodeSchemaTag},
|
|
{name: "unsupported ConfigKit tag", typeOf: reflect.TypeFor[badConfigTag](), code: CodeSchemaTag},
|
|
{name: "missing text marshal", typeOf: reflect.TypeFor[missingMarshal](), code: CodeSchemaTextCodec},
|
|
{name: "missing text unmarshal", typeOf: reflect.TypeFor[missingUnmarshal](), code: CodeSchemaTextCodec},
|
|
{name: "recursive inline", typeOf: reflect.TypeFor[RecursiveInline](), code: CodeSchemaInline},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
if _, issue := buildSchema(test.typeOf); issue == nil || issue.code != test.code {
|
|
t.Fatalf("issue = %#v, want %s", issue, test.code)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildSchemaDoesNotExecuteApplicationMethods(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type config struct {
|
|
Value panicText `yaml:"value"`
|
|
Structural yamlHookStruct `yaml:"structural"`
|
|
}
|
|
if _, issue := buildSchema(reflect.TypeFor[config]()); issue != nil {
|
|
t.Fatal(issue.message)
|
|
}
|
|
}
|
|
|
|
type panicText struct{}
|
|
|
|
type yamlHookStruct struct {
|
|
Value string `yaml:"value"`
|
|
}
|
|
|
|
func (*yamlHookStruct) UnmarshalYAML(any) error {
|
|
panic("descriptor construction called parser hook")
|
|
}
|
|
|
|
func (panicText) MarshalText() ([]byte, error) {
|
|
panic("descriptor construction called MarshalText")
|
|
}
|
|
|
|
func (*panicText) UnmarshalText([]byte) error {
|
|
panic("descriptor construction called UnmarshalText")
|
|
}
|
|
|
|
func (panicText) UnmarshalYAML(any) error {
|
|
panic("descriptor construction called parser hook")
|
|
}
|
|
|
|
func TestResolveSchemaPathUsesContainerSemantics(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type item struct {
|
|
Name string `yaml:"name"`
|
|
}
|
|
type config struct {
|
|
Items []item `yaml:"items"`
|
|
Values map[string]item `yaml:"values"`
|
|
Fixed [2]string `yaml:"fixed"`
|
|
Dynamic any `yaml:"dynamic"`
|
|
Extras map[string]string `yaml:",inline"`
|
|
}
|
|
schema, issue := buildSchema(reflect.TypeFor[config]())
|
|
if issue != nil {
|
|
t.Fatal(issue.message)
|
|
}
|
|
|
|
valid := []Path{
|
|
Keys("items").Index(0).Key("name"),
|
|
mustPath(t, "/items/0/name"),
|
|
Keys("values", "0", "name"),
|
|
mustPath(t, "/values/0/name"),
|
|
Keys("fixed").Index(1),
|
|
Keys("dynamic", "anything").Index(9),
|
|
Keys("application_key"),
|
|
}
|
|
for _, path := range valid {
|
|
if _, ok := resolveSchemaPath(schema, path); !ok {
|
|
t.Errorf("valid path %q rejected", path.String())
|
|
}
|
|
}
|
|
invalid := []Path{
|
|
Keys("missing", "field"),
|
|
Keys("items", "0", "name"),
|
|
mustPath(t, "/items/00/name"),
|
|
Keys("fixed").Index(2),
|
|
Keys("values").Index(0),
|
|
Path{}.Index(-1),
|
|
}
|
|
for _, path := range invalid {
|
|
if _, ok := resolveSchemaPath(schema, path); ok {
|
|
t.Errorf("invalid path %q accepted", path.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func mustPath(t *testing.T, pointer string) Path {
|
|
t.Helper()
|
|
path, err := ParsePath(pointer)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
func TestInlineMapPointersFailBeforeDefaults(t *testing.T) {
|
|
type cfg struct {
|
|
Extra *map[string]int `yaml:",inline"`
|
|
}
|
|
for _, nonNil := range []bool{false, true} {
|
|
called := false
|
|
_, err := Load(NoSource(), Options[cfg]{Definition: Definition[cfg]{Defaults: func() cfg {
|
|
called = true
|
|
if nonNil {
|
|
m := map[string]int{"value": 1}
|
|
return cfg{Extra: &m}
|
|
}
|
|
return cfg{}
|
|
}}})
|
|
var loadErr *LoadError
|
|
if !errors.As(err, &loadErr) || called {
|
|
t.Fatalf("invalid inline schema reached defaults: called=%v error=%v", called, err)
|
|
}
|
|
if len(loadErr.Diagnostics()) != 1 || loadErr.Diagnostics()[0].Code != CodeSchemaInline {
|
|
t.Fatal("missing inline schema diagnostic")
|
|
}
|
|
}
|
|
type pointer *pointer
|
|
type recursive struct {
|
|
Extra pointer `yaml:",inline"`
|
|
}
|
|
if _, err := Load[recursive](NoSource()); err == nil {
|
|
t.Fatal("recursive inline pointer was accepted")
|
|
}
|
|
type double struct {
|
|
Extra **map[string]int `yaml:",inline"`
|
|
}
|
|
if _, err := Load[double](NoSource()); err == nil {
|
|
t.Fatal("double inline map pointer was accepted")
|
|
}
|
|
}
|