Decode typed configuration over defaults
This commit is contained in:
455
decode.go
Normal file
455
decode.go
Normal file
@@ -0,0 +1,455 @@
|
||||
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")}
|
||||
}
|
||||
Reference in New Issue
Block a user