456 lines
16 KiB
Go
456 lines
16 KiB
Go
package configkit
|
|
|
|
import (
|
|
"encoding"
|
|
"math"
|
|
"reflect"
|
|
"regexp"
|
|
"strconv"
|
|
"time"
|
|
|
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
|
)
|
|
|
|
var integerPattern = regexp.MustCompile(`^-?(0|[1-9][0-9]*)$`)
|
|
var unsignedIntegerPattern = regexp.MustCompile(`^(0|[1-9][0-9]*)$`)
|
|
|
|
type decodeIssue struct {
|
|
code Code
|
|
path Path
|
|
message string
|
|
location document.Location
|
|
}
|
|
|
|
type inputPresence struct {
|
|
null bool
|
|
location document.Location
|
|
}
|
|
|
|
type decodedConfiguration struct {
|
|
value reflect.Value
|
|
presence map[string]inputPresence
|
|
origins map[string]Origin
|
|
baseline *document.Node
|
|
final *document.Node
|
|
}
|
|
|
|
func decodeConfiguration(schema *schemaDescriptor, defaults reflect.Value, input *document.Node) (decodedConfiguration, []decodeIssue) {
|
|
if schema == nil || schema.root == nil || !defaults.IsValid() || defaults.Type() != schema.root.typeOf {
|
|
return decodedConfiguration{}, []decodeIssue{{code: CodeSchemaUnsupportedType, message: "configuration defaults do not match the schema"}}
|
|
}
|
|
baseline, projectionIssue := projectConfiguration(schema, defaults, defaultProjectionLimits())
|
|
if projectionIssue != nil {
|
|
return decodedConfiguration{}, []decodeIssue{decodeIssueFromProjection(projectionIssue)}
|
|
}
|
|
working, cloneIssue := cloneConfiguration(schema, defaults, defaultProjectionLimits())
|
|
if cloneIssue != nil {
|
|
return decodedConfiguration{}, []decodeIssue{decodeIssueFromProjection(cloneIssue)}
|
|
}
|
|
|
|
presence := make(map[string]inputPresence)
|
|
if input != nil {
|
|
collectPresence(input, Path{}, presence)
|
|
decoded, issues := decodeValue(input, working, schema.root, Path{}, schema)
|
|
if len(issues) != 0 {
|
|
return decodedConfiguration{}, issues
|
|
}
|
|
working = decoded
|
|
}
|
|
final, projectionIssue := projectConfiguration(schema, working, defaultProjectionLimits())
|
|
if projectionIssue != nil {
|
|
return decodedConfiguration{}, []decodeIssue{decodeIssueFromProjection(projectionIssue)}
|
|
}
|
|
origins := make(map[string]Origin)
|
|
collectOrigins(final, Path{}, presence, origins)
|
|
return decodedConfiguration{
|
|
value: working,
|
|
presence: presence,
|
|
origins: origins,
|
|
baseline: baseline,
|
|
final: final,
|
|
}, nil
|
|
}
|
|
|
|
func decodeValue(node *document.Node, base reflect.Value, descriptor *typeDescriptor, path Path, schema *schemaDescriptor) (reflect.Value, []decodeIssue) {
|
|
if node.Kind == document.Null {
|
|
switch descriptor.kind {
|
|
case descriptorPointer, descriptorMap, descriptorSlice, descriptorDynamic:
|
|
return reflect.Zero(descriptor.typeOf), nil
|
|
default:
|
|
return reflect.Value{}, []decodeIssue{newDecodeIssue(CodeDecodeNull, path, node, "null is not allowed for this configuration type")}
|
|
}
|
|
}
|
|
if descriptor.kind == descriptorPointer {
|
|
var elementBase reflect.Value
|
|
if base.IsNil() {
|
|
elementBase = reflect.Zero(descriptor.typeOf.Elem())
|
|
} else {
|
|
elementBase = base.Elem()
|
|
}
|
|
decoded, issues := decodeValue(node, elementBase, descriptor.element, path, schema)
|
|
if len(issues) != 0 {
|
|
return reflect.Value{}, issues
|
|
}
|
|
result := reflect.New(descriptor.typeOf.Elem())
|
|
result.Elem().Set(decoded)
|
|
return result, nil
|
|
}
|
|
|
|
switch descriptor.kind {
|
|
case descriptorStruct:
|
|
return decodeStruct(node, base, descriptor, path, schema)
|
|
case descriptorMap:
|
|
return decodeMap(node, base, descriptor, path, schema)
|
|
case descriptorSlice:
|
|
return decodeSlice(node, descriptor, path, schema)
|
|
case descriptorArray:
|
|
return decodeArray(node, descriptor, path, schema)
|
|
case descriptorDynamic:
|
|
return decodeDynamic(node, base, path, schema)
|
|
case descriptorString:
|
|
if node.Kind != document.String {
|
|
return typeMismatch(path, node, "string")
|
|
}
|
|
result := reflect.New(descriptor.typeOf).Elem()
|
|
result.SetString(node.Text)
|
|
return result, nil
|
|
case descriptorBoolean:
|
|
if node.Kind != document.Boolean {
|
|
return typeMismatch(path, node, "boolean")
|
|
}
|
|
result := reflect.New(descriptor.typeOf).Elem()
|
|
result.SetBool(node.Text == "true")
|
|
return result, nil
|
|
case descriptorSigned:
|
|
if node.Kind != document.Number || !integerPattern.MatchString(node.Text) {
|
|
return typeMismatch(path, node, "integer")
|
|
}
|
|
value, err := strconv.ParseInt(node.Text, 10, descriptor.typeOf.Bits())
|
|
if err != nil {
|
|
return rangeFailure(path, node, "integer")
|
|
}
|
|
result := reflect.New(descriptor.typeOf).Elem()
|
|
result.SetInt(value)
|
|
return result, nil
|
|
case descriptorUnsigned:
|
|
if node.Kind != document.Number || !unsignedIntegerPattern.MatchString(node.Text) {
|
|
return typeMismatch(path, node, "unsigned integer")
|
|
}
|
|
value, err := strconv.ParseUint(node.Text, 10, descriptor.typeOf.Bits())
|
|
if err != nil {
|
|
return rangeFailure(path, node, "unsigned integer")
|
|
}
|
|
result := reflect.New(descriptor.typeOf).Elem()
|
|
result.SetUint(value)
|
|
return result, nil
|
|
case descriptorFloat:
|
|
if node.Kind != document.Number {
|
|
return typeMismatch(path, node, "number")
|
|
}
|
|
value, err := strconv.ParseFloat(node.Text, descriptor.typeOf.Bits())
|
|
if err != nil || math.IsNaN(value) || math.IsInf(value, 0) {
|
|
return rangeFailure(path, node, "number")
|
|
}
|
|
result := reflect.New(descriptor.typeOf).Elem()
|
|
result.SetFloat(value)
|
|
return result, nil
|
|
case descriptorDuration:
|
|
if node.Kind != document.String {
|
|
return typeMismatch(path, node, "duration string")
|
|
}
|
|
value, err := time.ParseDuration(node.Text)
|
|
if err != nil {
|
|
return reflect.Value{}, []decodeIssue{newDecodeIssue(CodeDecodeDuration, path, node, "duration string is invalid")}
|
|
}
|
|
result := reflect.New(descriptor.typeOf).Elem()
|
|
result.SetInt(int64(value))
|
|
return result, nil
|
|
case descriptorText:
|
|
if node.Kind != document.String {
|
|
return typeMismatch(path, node, "text string")
|
|
}
|
|
result := reflect.New(descriptor.typeOf).Elem()
|
|
unmarshaler := result.Addr().Interface().(encoding.TextUnmarshaler)
|
|
if err := unmarshaler.UnmarshalText([]byte(node.Text)); err != nil {
|
|
return reflect.Value{}, []decodeIssue{newDecodeIssue(CodeDecodeText, path, node, "text value is invalid for its configuration type")}
|
|
}
|
|
return result, nil
|
|
default:
|
|
return reflect.Value{}, []decodeIssue{newDecodeIssue(CodeSchemaUnsupportedType, path, node, "configuration type is not supported")}
|
|
}
|
|
}
|
|
|
|
func decodeStruct(node *document.Node, base reflect.Value, descriptor *typeDescriptor, path Path, schema *schemaDescriptor) (reflect.Value, []decodeIssue) {
|
|
if node.Kind != document.Mapping {
|
|
return typeMismatch(path, node, "mapping")
|
|
}
|
|
result := reflect.New(descriptor.typeOf).Elem()
|
|
result.Set(base)
|
|
var issues []decodeIssue
|
|
for _, entry := range node.Mapping {
|
|
childPath := path.Key(entry.Key)
|
|
if position, ok := descriptor.fieldByName[entry.Key]; ok {
|
|
field := descriptor.fields[position]
|
|
target, ok := fieldForDecode(result, field.index, true)
|
|
if !ok {
|
|
issues = append(issues, decodeIssue{code: CodeSchemaUnsupportedType, path: childPath, message: "configuration field could not be addressed", location: entry.ValueLocation})
|
|
continue
|
|
}
|
|
decoded, fieldIssues := decodeValue(entry.Value, target, field.descriptor, childPath, schema)
|
|
if len(fieldIssues) != 0 {
|
|
issues = append(issues, fieldIssues...)
|
|
continue
|
|
}
|
|
target.Set(decoded)
|
|
continue
|
|
}
|
|
if descriptor.inlineMap != nil {
|
|
if issue := decodeInlineMapEntry(result, *descriptor.inlineMap, entry, childPath, schema); issue != nil {
|
|
issues = append(issues, issue...)
|
|
}
|
|
continue
|
|
}
|
|
issues = append(issues, decodeIssue{code: CodeDecodeUnknownField, path: childPath, message: "configuration field is not declared", location: entry.KeyLocation})
|
|
}
|
|
return result, issues
|
|
}
|
|
|
|
func decodeInlineMapEntry(target reflect.Value, field fieldDescriptor, entry document.Entry, path Path, schema *schemaDescriptor) []decodeIssue {
|
|
mapValue, ok := fieldForDecode(target, field.index, true)
|
|
if !ok || mapValue.Kind() != reflect.Map {
|
|
return []decodeIssue{{code: CodeSchemaInline, path: path, message: "inline map could not be addressed", location: entry.KeyLocation}}
|
|
}
|
|
if mapValue.IsNil() {
|
|
mapValue.Set(reflect.MakeMap(mapValue.Type()))
|
|
}
|
|
key := reflect.New(mapValue.Type().Key()).Elem()
|
|
key.SetString(entry.Key)
|
|
base := mapValue.MapIndex(key)
|
|
if !base.IsValid() {
|
|
base = reflect.Zero(mapValue.Type().Elem())
|
|
}
|
|
decoded, issues := decodeValue(entry.Value, base, field.descriptor.element, path, schema)
|
|
if len(issues) == 0 {
|
|
mapValue.SetMapIndex(key, decoded)
|
|
}
|
|
return issues
|
|
}
|
|
|
|
func decodeMap(node *document.Node, base reflect.Value, descriptor *typeDescriptor, path Path, schema *schemaDescriptor) (reflect.Value, []decodeIssue) {
|
|
if node.Kind != document.Mapping {
|
|
return typeMismatch(path, node, "mapping")
|
|
}
|
|
result := base
|
|
if result.IsNil() {
|
|
result = reflect.MakeMap(descriptor.typeOf)
|
|
}
|
|
var issues []decodeIssue
|
|
for _, entry := range node.Mapping {
|
|
key := reflect.New(descriptor.typeOf.Key()).Elem()
|
|
key.SetString(entry.Key)
|
|
entryBase := result.MapIndex(key)
|
|
if !entryBase.IsValid() {
|
|
entryBase = reflect.Zero(descriptor.typeOf.Elem())
|
|
}
|
|
decoded, entryIssues := decodeValue(entry.Value, entryBase, descriptor.element, path.Key(entry.Key), schema)
|
|
if len(entryIssues) != 0 {
|
|
issues = append(issues, entryIssues...)
|
|
continue
|
|
}
|
|
result.SetMapIndex(key, decoded)
|
|
}
|
|
return result, issues
|
|
}
|
|
|
|
func decodeSlice(node *document.Node, descriptor *typeDescriptor, path Path, schema *schemaDescriptor) (reflect.Value, []decodeIssue) {
|
|
if node.Kind != document.Sequence {
|
|
return typeMismatch(path, node, "sequence")
|
|
}
|
|
result := reflect.MakeSlice(descriptor.typeOf, len(node.Sequence), len(node.Sequence))
|
|
var issues []decodeIssue
|
|
for i, child := range node.Sequence {
|
|
decoded, childIssues := decodeValue(child, reflect.Zero(descriptor.typeOf.Elem()), descriptor.element, path.Index(i), schema)
|
|
if len(childIssues) != 0 {
|
|
issues = append(issues, childIssues...)
|
|
continue
|
|
}
|
|
result.Index(i).Set(decoded)
|
|
}
|
|
return result, issues
|
|
}
|
|
|
|
func decodeArray(node *document.Node, descriptor *typeDescriptor, path Path, schema *schemaDescriptor) (reflect.Value, []decodeIssue) {
|
|
if node.Kind != document.Sequence {
|
|
return typeMismatch(path, node, "sequence")
|
|
}
|
|
if len(node.Sequence) != descriptor.length {
|
|
return reflect.Value{}, []decodeIssue{newDecodeIssue(CodeDecodeArrayLength, path, node, "configuration array has the wrong length")}
|
|
}
|
|
result := reflect.New(descriptor.typeOf).Elem()
|
|
var issues []decodeIssue
|
|
for i, child := range node.Sequence {
|
|
decoded, childIssues := decodeValue(child, reflect.Zero(descriptor.typeOf.Elem()), descriptor.element, path.Index(i), schema)
|
|
if len(childIssues) != 0 {
|
|
issues = append(issues, childIssues...)
|
|
continue
|
|
}
|
|
result.Index(i).Set(decoded)
|
|
}
|
|
return result, issues
|
|
}
|
|
|
|
func decodeDynamic(node *document.Node, base reflect.Value, path Path, schema *schemaDescriptor) (reflect.Value, []decodeIssue) {
|
|
if node.Kind == document.Mapping && base.IsValid() && !base.IsNil() {
|
|
concrete := base.Elem()
|
|
operation := valueOperation{schema: schema}
|
|
descriptor, issue := operation.dynamicDescriptor(concrete.Type(), path)
|
|
if issue == nil && acceptsMapping(descriptor) {
|
|
decoded, issues := decodeValue(node, concrete, descriptor, path, schema)
|
|
if len(issues) != 0 {
|
|
return reflect.Value{}, issues
|
|
}
|
|
result := reflect.New(base.Type()).Elem()
|
|
result.Set(decoded)
|
|
return result, nil
|
|
}
|
|
}
|
|
var value any
|
|
switch node.Kind {
|
|
case document.Null:
|
|
return reflect.Zero(base.Type()), nil
|
|
case document.String:
|
|
value = node.Text
|
|
case document.Boolean:
|
|
value = node.Text == "true"
|
|
case document.Number:
|
|
if integerPattern.MatchString(node.Text) {
|
|
parsed, err := strconv.ParseInt(node.Text, 10, 64)
|
|
if err != nil {
|
|
return rangeFailure(path, node, "dynamic integer")
|
|
}
|
|
value = parsed
|
|
} else {
|
|
parsed, err := strconv.ParseFloat(node.Text, 64)
|
|
if err != nil || math.IsNaN(parsed) || math.IsInf(parsed, 0) {
|
|
return rangeFailure(path, node, "dynamic number")
|
|
}
|
|
value = parsed
|
|
}
|
|
case document.Sequence:
|
|
sequence := make([]any, len(node.Sequence))
|
|
var issues []decodeIssue
|
|
for i, child := range node.Sequence {
|
|
decoded, childIssues := decodeDynamic(child, reflect.Zero(reflect.TypeFor[any]()), path.Index(i), schema)
|
|
if len(childIssues) != 0 {
|
|
issues = append(issues, childIssues...)
|
|
continue
|
|
}
|
|
sequence[i] = decoded.Interface()
|
|
}
|
|
if len(issues) != 0 {
|
|
return reflect.Value{}, issues
|
|
}
|
|
value = sequence
|
|
case document.Mapping:
|
|
mapping := make(map[string]any, len(node.Mapping))
|
|
var issues []decodeIssue
|
|
for _, entry := range node.Mapping {
|
|
decoded, childIssues := decodeDynamic(entry.Value, reflect.Zero(reflect.TypeFor[any]()), path.Key(entry.Key), schema)
|
|
if len(childIssues) != 0 {
|
|
issues = append(issues, childIssues...)
|
|
continue
|
|
}
|
|
mapping[entry.Key] = decoded.Interface()
|
|
}
|
|
if len(issues) != 0 {
|
|
return reflect.Value{}, issues
|
|
}
|
|
value = mapping
|
|
default:
|
|
return reflect.Value{}, []decodeIssue{newDecodeIssue(CodeDecodeType, path, node, "dynamic configuration value has an unsupported kind")}
|
|
}
|
|
result := reflect.New(base.Type()).Elem()
|
|
if value != nil {
|
|
result.Set(reflect.ValueOf(value))
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func acceptsMapping(descriptor *typeDescriptor) bool {
|
|
for descriptor != nil && descriptor.kind == descriptorPointer {
|
|
descriptor = descriptor.element
|
|
}
|
|
return descriptor != nil && (descriptor.kind == descriptorStruct || descriptor.kind == descriptorMap || descriptor.kind == descriptorDynamic)
|
|
}
|
|
|
|
func fieldForDecode(root reflect.Value, index []int, allocate bool) (reflect.Value, bool) {
|
|
current := root
|
|
for position, fieldIndex := range index {
|
|
if current.Kind() != reflect.Struct {
|
|
return reflect.Value{}, false
|
|
}
|
|
current = current.Field(fieldIndex)
|
|
if position == len(index)-1 {
|
|
return current, current.CanSet()
|
|
}
|
|
for current.Kind() == reflect.Pointer {
|
|
if current.IsNil() {
|
|
if !allocate || !current.CanSet() {
|
|
return reflect.Value{}, false
|
|
}
|
|
current.Set(reflect.New(current.Type().Elem()))
|
|
}
|
|
current = current.Elem()
|
|
}
|
|
}
|
|
return reflect.Value{}, false
|
|
}
|
|
|
|
func collectPresence(node *document.Node, path Path, presence map[string]inputPresence) {
|
|
presence[path.String()] = inputPresence{null: node.Kind == document.Null, location: node.Location}
|
|
switch node.Kind {
|
|
case document.Mapping:
|
|
for _, entry := range node.Mapping {
|
|
collectPresence(entry.Value, path.Key(entry.Key), presence)
|
|
}
|
|
case document.Sequence:
|
|
for i, child := range node.Sequence {
|
|
collectPresence(child, path.Index(i), presence)
|
|
}
|
|
}
|
|
}
|
|
|
|
func collectOrigins(node *document.Node, path Path, presence map[string]inputPresence, origins map[string]Origin) {
|
|
if input, ok := presence[path.String()]; ok {
|
|
origins[path.String()] = Origin{Layer: LayerBase, Locations: []Location{{SourceID: input.location.SourceID, Line: input.location.Line, Column: input.location.Column}}}
|
|
} else {
|
|
origins[path.String()] = Origin{Layer: LayerDefault}
|
|
}
|
|
switch node.Kind {
|
|
case document.Mapping:
|
|
for _, entry := range node.Mapping {
|
|
collectOrigins(entry.Value, path.Key(entry.Key), presence, origins)
|
|
}
|
|
case document.Sequence:
|
|
for i, child := range node.Sequence {
|
|
collectOrigins(child, path.Index(i), presence, origins)
|
|
}
|
|
}
|
|
}
|
|
|
|
func decodeIssueFromProjection(issue *projectionIssue) decodeIssue {
|
|
return decodeIssue{code: issue.code, path: issue.path.clone(), message: issue.message}
|
|
}
|
|
|
|
func newDecodeIssue(code Code, path Path, node *document.Node, message string) decodeIssue {
|
|
return decodeIssue{code: code, path: path.clone(), message: message, location: node.Location}
|
|
}
|
|
|
|
func typeMismatch(path Path, node *document.Node, expected string) (reflect.Value, []decodeIssue) {
|
|
return reflect.Value{}, []decodeIssue{newDecodeIssue(CodeDecodeType, path, node, "configuration value must be a "+expected)}
|
|
}
|
|
|
|
func rangeFailure(path Path, node *document.Node, expected string) (reflect.Value, []decodeIssue) {
|
|
return reflect.Value{}, []decodeIssue{newDecodeIssue(CodeDecodeRange, path, node, expected+" is outside the supported range")}
|
|
}
|