439 lines
13 KiB
Go
439 lines
13 KiB
Go
package engine
|
|
|
|
import (
|
|
"encoding"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type descriptorKind uint8
|
|
|
|
const (
|
|
descriptorInvalid descriptorKind = iota
|
|
descriptorStruct
|
|
descriptorMap
|
|
descriptorSlice
|
|
descriptorArray
|
|
descriptorPointer
|
|
descriptorDynamic
|
|
descriptorString
|
|
descriptorBoolean
|
|
descriptorSigned
|
|
descriptorUnsigned
|
|
descriptorFloat
|
|
descriptorDuration
|
|
descriptorSecret
|
|
descriptorText
|
|
)
|
|
|
|
type schemaDescriptor struct {
|
|
root *typeDescriptor
|
|
types map[reflect.Type]*typeDescriptor
|
|
}
|
|
|
|
type typeDescriptor struct {
|
|
typeOf reflect.Type
|
|
kind descriptorKind
|
|
element *typeDescriptor
|
|
length int
|
|
members []structMember
|
|
fields []fieldDescriptor
|
|
fieldByName map[string]int
|
|
inlineMap *fieldDescriptor
|
|
}
|
|
|
|
type structMember struct {
|
|
index int
|
|
name string
|
|
descriptor *typeDescriptor
|
|
inline bool
|
|
}
|
|
|
|
type fieldDescriptor struct {
|
|
name string
|
|
index []int
|
|
descriptor *typeDescriptor
|
|
secret bool
|
|
inlineMap bool
|
|
}
|
|
|
|
type schemaIssue struct {
|
|
code Code
|
|
path Path
|
|
message string
|
|
}
|
|
|
|
var (
|
|
durationType = reflect.TypeFor[time.Duration]()
|
|
secretType = reflect.TypeFor[Secret]()
|
|
textMarshalerType = reflect.TypeFor[encoding.TextMarshaler]()
|
|
textUnmarshalerType = reflect.TypeFor[encoding.TextUnmarshaler]()
|
|
)
|
|
|
|
func buildSchema(root reflect.Type) (*schemaDescriptor, *schemaIssue) {
|
|
if root == nil || (root.Kind() != reflect.Struct && !isStringMap(root)) {
|
|
return nil, &schemaIssue{code: CodeSchemaRoot, message: "configuration root must be a struct or string-keyed map"}
|
|
}
|
|
builder := schemaBuilder{
|
|
cache: make(map[reflect.Type]*typeDescriptor),
|
|
building: make(map[reflect.Type]bool),
|
|
}
|
|
descriptor, issue := builder.describe(root, Path{})
|
|
if issue != nil {
|
|
return nil, issue
|
|
}
|
|
if descriptor.kind != descriptorStruct && descriptor.kind != descriptorMap {
|
|
return nil, &schemaIssue{code: CodeSchemaRoot, message: "configuration root must be a structural struct or string-keyed map"}
|
|
}
|
|
return &schemaDescriptor{root: descriptor, types: builder.cache}, nil
|
|
}
|
|
|
|
type schemaBuilder struct {
|
|
cache map[reflect.Type]*typeDescriptor
|
|
building map[reflect.Type]bool
|
|
}
|
|
|
|
func (b *schemaBuilder) describe(current reflect.Type, path Path) (*typeDescriptor, *schemaIssue) {
|
|
if descriptor, ok := b.cache[current]; ok {
|
|
return descriptor, nil
|
|
}
|
|
descriptor := &typeDescriptor{typeOf: current}
|
|
b.cache[current] = descriptor
|
|
b.building[current] = true
|
|
defer delete(b.building, current)
|
|
|
|
fail := func(code Code, message string) (*typeDescriptor, *schemaIssue) {
|
|
delete(b.cache, current)
|
|
return nil, &schemaIssue{code: code, path: path.clone(), message: message}
|
|
}
|
|
|
|
if current.Kind() == reflect.Pointer {
|
|
descriptor.kind = descriptorPointer
|
|
element, issue := b.describe(current.Elem(), path)
|
|
if issue != nil {
|
|
return nil, issue
|
|
}
|
|
descriptor.element = element
|
|
return descriptor, nil
|
|
}
|
|
if current == durationType {
|
|
descriptor.kind = descriptorDuration
|
|
return descriptor, nil
|
|
}
|
|
if current == secretType {
|
|
descriptor.kind = descriptorSecret
|
|
return descriptor, nil
|
|
}
|
|
marshals, unmarshals := textCodecSupport(current)
|
|
if marshals || unmarshals {
|
|
if !marshals || !unmarshals {
|
|
return fail(CodeSchemaTextCodec, "custom text scalar must implement both text marshal and unmarshal")
|
|
}
|
|
descriptor.kind = descriptorText
|
|
return descriptor, nil
|
|
}
|
|
|
|
switch current.Kind() {
|
|
case reflect.Struct:
|
|
descriptor.kind = descriptorStruct
|
|
descriptor.fieldByName = make(map[string]int)
|
|
if issue := b.describeFields(descriptor, current, path); issue != nil {
|
|
delete(b.cache, current)
|
|
return nil, issue
|
|
}
|
|
case reflect.Map:
|
|
if current.Key().Kind() != reflect.String {
|
|
return fail(CodeSchemaUnsupportedType, "configuration maps must have string keys")
|
|
}
|
|
descriptor.kind = descriptorMap
|
|
element, issue := b.describe(current.Elem(), path)
|
|
if issue != nil {
|
|
return nil, issue
|
|
}
|
|
descriptor.element = element
|
|
case reflect.Slice:
|
|
descriptor.kind = descriptorSlice
|
|
element, issue := b.describe(current.Elem(), path.Index(0))
|
|
if issue != nil {
|
|
return nil, issue
|
|
}
|
|
descriptor.element = element
|
|
case reflect.Array:
|
|
descriptor.kind = descriptorArray
|
|
descriptor.length = current.Len()
|
|
element, issue := b.describe(current.Elem(), path.Index(0))
|
|
if issue != nil {
|
|
return nil, issue
|
|
}
|
|
descriptor.element = element
|
|
case reflect.Interface:
|
|
if current.NumMethod() != 0 {
|
|
return fail(CodeSchemaUnsupportedType, "only the empty interface is supported for dynamic values")
|
|
}
|
|
descriptor.kind = descriptorDynamic
|
|
case reflect.String:
|
|
descriptor.kind = descriptorString
|
|
case reflect.Bool:
|
|
descriptor.kind = descriptorBoolean
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
descriptor.kind = descriptorSigned
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
descriptor.kind = descriptorUnsigned
|
|
case reflect.Float32, reflect.Float64:
|
|
descriptor.kind = descriptorFloat
|
|
default:
|
|
return fail(CodeSchemaUnsupportedType, "configuration type is not supported")
|
|
}
|
|
return descriptor, nil
|
|
}
|
|
|
|
func (b *schemaBuilder) describeFields(descriptor *typeDescriptor, current reflect.Type, path Path) *schemaIssue {
|
|
for i := 0; i < current.NumField(); i++ {
|
|
field := current.Field(i)
|
|
if !field.IsExported() {
|
|
continue
|
|
}
|
|
name, inline, ignored, issue := parseYAMLField(field, path)
|
|
if issue != nil {
|
|
return issue
|
|
}
|
|
if ignored {
|
|
continue
|
|
}
|
|
secret, issue := parseConfigKitField(field, path)
|
|
if issue != nil {
|
|
return issue
|
|
}
|
|
|
|
if inline {
|
|
if name != "" {
|
|
return &schemaIssue{code: CodeSchemaTag, path: path.clone(), message: "inline field cannot declare a YAML name"}
|
|
}
|
|
inlineType := field.Type
|
|
seenPointers := make(map[reflect.Type]bool)
|
|
for inlineType.Kind() == reflect.Pointer {
|
|
if seenPointers[inlineType] {
|
|
return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "inline pointer chain must terminate in a struct"}
|
|
}
|
|
seenPointers[inlineType] = true
|
|
inlineType = inlineType.Elem()
|
|
}
|
|
if inlineType.Kind() == reflect.Map && field.Type.Kind() == reflect.Pointer {
|
|
return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "inline maps cannot be pointers"}
|
|
}
|
|
switch inlineType.Kind() {
|
|
case reflect.Struct:
|
|
if b.building[inlineType] {
|
|
return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "recursive inline struct is not supported"}
|
|
}
|
|
child, childIssue := b.describe(inlineType, path)
|
|
if childIssue != nil {
|
|
return childIssue
|
|
}
|
|
if child.kind != descriptorStruct {
|
|
return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "inline field must be a structural struct or string-keyed map"}
|
|
}
|
|
for _, nested := range child.fields {
|
|
nested.index = prependIndex(i, nested.index)
|
|
nested.secret = secret || nested.secret
|
|
if issue := addField(descriptor, nested, path); issue != nil {
|
|
return issue
|
|
}
|
|
}
|
|
if child.inlineMap != nil {
|
|
nested := *child.inlineMap
|
|
nested.index = prependIndex(i, nested.index)
|
|
nested.secret = secret || nested.secret
|
|
if descriptor.inlineMap != nil {
|
|
return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "configuration object has more than one inline map"}
|
|
}
|
|
descriptor.inlineMap = &nested
|
|
}
|
|
memberType, memberIssue := b.describe(field.Type, path)
|
|
if memberIssue != nil {
|
|
return memberIssue
|
|
}
|
|
descriptor.members = append(descriptor.members, structMember{index: i, descriptor: memberType, inline: true})
|
|
case reflect.Map:
|
|
if inlineType.Key().Kind() != reflect.String {
|
|
return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "inline map must have string keys"}
|
|
}
|
|
if descriptor.inlineMap != nil {
|
|
return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "configuration object has more than one inline map"}
|
|
}
|
|
fieldPath := path.Key("*")
|
|
fieldType, childIssue := b.describe(inlineType, fieldPath)
|
|
if childIssue != nil {
|
|
return childIssue
|
|
}
|
|
descriptor.inlineMap = &fieldDescriptor{index: []int{i}, descriptor: fieldType, secret: secret, inlineMap: true}
|
|
descriptor.members = append(descriptor.members, structMember{index: i, descriptor: fieldType, inline: true})
|
|
default:
|
|
return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "inline field must be a struct, pointer to struct, or string-keyed map"}
|
|
}
|
|
continue
|
|
}
|
|
|
|
fieldPath := path.Key(name)
|
|
fieldType, childIssue := b.describe(field.Type, fieldPath)
|
|
if childIssue != nil {
|
|
return childIssue
|
|
}
|
|
if issue := addField(descriptor, fieldDescriptor{name: name, index: []int{i}, descriptor: fieldType, secret: secret}, path); issue != nil {
|
|
return issue
|
|
}
|
|
descriptor.members = append(descriptor.members, structMember{index: i, name: name, descriptor: fieldType})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func addField(owner *typeDescriptor, field fieldDescriptor, path Path) *schemaIssue {
|
|
if _, exists := owner.fieldByName[field.name]; exists {
|
|
return &schemaIssue{code: CodeSchemaAmbiguousField, path: path.Key(field.name), message: "configuration field name is ambiguous"}
|
|
}
|
|
owner.fieldByName[field.name] = len(owner.fields)
|
|
owner.fields = append(owner.fields, field)
|
|
return nil
|
|
}
|
|
|
|
func parseYAMLField(field reflect.StructField, path Path) (name string, inline, ignored bool, issue *schemaIssue) {
|
|
name = strings.ToLower(field.Name)
|
|
tag, present := field.Tag.Lookup("yaml")
|
|
if !present {
|
|
return name, false, false, nil
|
|
}
|
|
parts := strings.Split(tag, ",")
|
|
if parts[0] == "-" {
|
|
if len(parts) != 1 {
|
|
return "", false, false, &schemaIssue{code: CodeSchemaTag, path: path.clone(), message: "ignored YAML field cannot declare options"}
|
|
}
|
|
return "", false, true, nil
|
|
}
|
|
if parts[0] != "" {
|
|
name = parts[0]
|
|
}
|
|
seen := make(map[string]bool, len(parts)-1)
|
|
for _, option := range parts[1:] {
|
|
if option == "" || seen[option] {
|
|
return "", false, false, &schemaIssue{code: CodeSchemaTag, path: path.Key(name), message: "YAML field tag is malformed"}
|
|
}
|
|
seen[option] = true
|
|
switch option {
|
|
case "inline":
|
|
inline = true
|
|
case "omitempty":
|
|
// ConfigKit accepts the spelling but never omits projected values.
|
|
default:
|
|
return "", false, false, &schemaIssue{code: CodeSchemaTag, path: path.Key(name), message: "YAML field tag option is not supported"}
|
|
}
|
|
}
|
|
if inline && parts[0] == "" {
|
|
name = ""
|
|
}
|
|
return name, inline, false, nil
|
|
}
|
|
|
|
func parseConfigKitField(field reflect.StructField, path Path) (bool, *schemaIssue) {
|
|
tag, present := field.Tag.Lookup("configkit")
|
|
if !present || tag == "" {
|
|
return false, nil
|
|
}
|
|
if tag != "secret" {
|
|
return false, &schemaIssue{code: CodeSchemaTag, path: path.clone(), message: "ConfigKit field tag is not supported"}
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func textCodecSupport(current reflect.Type) (marshals, unmarshals bool) {
|
|
marshals = current.Implements(textMarshalerType)
|
|
unmarshals = current.Implements(textUnmarshalerType)
|
|
if current.Kind() != reflect.Pointer {
|
|
pointer := reflect.PointerTo(current)
|
|
marshals = marshals || pointer.Implements(textMarshalerType)
|
|
unmarshals = unmarshals || pointer.Implements(textUnmarshalerType)
|
|
}
|
|
return marshals, unmarshals
|
|
}
|
|
|
|
func isStringMap(current reflect.Type) bool {
|
|
return current.Kind() == reflect.Map && current.Key().Kind() == reflect.String
|
|
}
|
|
|
|
func prependIndex(index int, nested []int) []int {
|
|
result := make([]int, 1, len(nested)+1)
|
|
result[0] = index
|
|
return append(result, nested...)
|
|
}
|
|
|
|
type schemaPath struct {
|
|
descriptor *typeDescriptor
|
|
field *fieldDescriptor
|
|
dynamic bool
|
|
}
|
|
|
|
func resolveSchemaPath(schema *schemaDescriptor, path Path) (schemaPath, bool) {
|
|
if schema == nil || schema.root == nil || !path.valid() {
|
|
return schemaPath{}, false
|
|
}
|
|
current := schema.root
|
|
var field *fieldDescriptor
|
|
for _, segment := range path.segments {
|
|
for current.kind == descriptorPointer {
|
|
current = current.element
|
|
}
|
|
switch current.kind {
|
|
case descriptorStruct:
|
|
if segment.kind == pathSegmentIndex {
|
|
return schemaPath{}, false
|
|
}
|
|
position, ok := current.fieldByName[segment.key]
|
|
if ok {
|
|
field = ¤t.fields[position]
|
|
current = field.descriptor
|
|
continue
|
|
}
|
|
if current.inlineMap != nil {
|
|
field = current.inlineMap
|
|
current = current.inlineMap.descriptor.element
|
|
continue
|
|
}
|
|
return schemaPath{}, false
|
|
case descriptorMap:
|
|
if segment.kind == pathSegmentIndex {
|
|
return schemaPath{}, false
|
|
}
|
|
field = nil
|
|
current = current.element
|
|
case descriptorSlice, descriptorArray:
|
|
index, ok := schemaIndex(segment)
|
|
if !ok || (current.kind == descriptorArray && index >= current.length) {
|
|
return schemaPath{}, false
|
|
}
|
|
field = nil
|
|
current = current.element
|
|
case descriptorDynamic:
|
|
return schemaPath{descriptor: current, field: field, dynamic: true}, true
|
|
default:
|
|
return schemaPath{}, false
|
|
}
|
|
}
|
|
dynamic := current
|
|
for dynamic.kind == descriptorPointer {
|
|
dynamic = dynamic.element
|
|
}
|
|
return schemaPath{descriptor: current, field: field, dynamic: dynamic.kind == descriptorDynamic}, true
|
|
}
|
|
|
|
func schemaIndex(segment pathSegment) (int, bool) {
|
|
if segment.kind == pathSegmentIndex {
|
|
return segment.index, segment.index >= 0
|
|
}
|
|
if segment.kind != pathSegmentParsed || segment.key == "" || (len(segment.key) > 1 && segment.key[0] == '0') {
|
|
return 0, false
|
|
}
|
|
index, err := strconv.Atoi(segment.key)
|
|
return index, err == nil && index >= 0 && strconv.Itoa(index) == segment.key
|
|
}
|