151 lines
5.5 KiB
Go
151 lines
5.5 KiB
Go
package configkit
|
|
|
|
import (
|
|
"math"
|
|
"reflect"
|
|
"slices"
|
|
"strconv"
|
|
|
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
|
)
|
|
|
|
type overrideValueBuilder struct {
|
|
sourceID string
|
|
active map[valueVisit]bool
|
|
bytes int64
|
|
nodes int
|
|
}
|
|
|
|
func (b *overrideValueBuilder) build(value any, path Path, depth int) (*document.Node, *Diagnostic) {
|
|
if depth > maxDocumentDepth {
|
|
return nil, overrideValueDiagnostic(CodeOverrideLimitExceeded, path, "configuration override exceeds the depth limit")
|
|
}
|
|
b.nodes++
|
|
if b.nodes > maxDocumentNodes {
|
|
return nil, overrideValueDiagnostic(CodeOverrideLimitExceeded, path, "configuration override exceeds the node limit")
|
|
}
|
|
location := document.Location{SourceID: b.sourceID}
|
|
switch value := value.(type) {
|
|
case nil:
|
|
return &document.Node{Kind: document.Null, Location: location}, nil
|
|
case string:
|
|
if !b.addBytes(len(value)) {
|
|
return nil, overrideValueDiagnostic(CodeOverrideLimitExceeded, path, "configuration override exceeds the record string-byte limit")
|
|
}
|
|
return &document.Node{Kind: document.String, Text: value, Location: location}, nil
|
|
case bool:
|
|
return &document.Node{Kind: document.Boolean, Text: strconv.FormatBool(value), Location: location}, nil
|
|
case int:
|
|
return overrideNumber(strconv.FormatInt(int64(value), 10), location), nil
|
|
case int8:
|
|
return overrideNumber(strconv.FormatInt(int64(value), 10), location), nil
|
|
case int16:
|
|
return overrideNumber(strconv.FormatInt(int64(value), 10), location), nil
|
|
case int32:
|
|
return overrideNumber(strconv.FormatInt(int64(value), 10), location), nil
|
|
case int64:
|
|
return overrideNumber(strconv.FormatInt(value, 10), location), nil
|
|
case uint:
|
|
return overrideNumber(strconv.FormatUint(uint64(value), 10), location), nil
|
|
case uint8:
|
|
return overrideNumber(strconv.FormatUint(uint64(value), 10), location), nil
|
|
case uint16:
|
|
return overrideNumber(strconv.FormatUint(uint64(value), 10), location), nil
|
|
case uint32:
|
|
return overrideNumber(strconv.FormatUint(uint64(value), 10), location), nil
|
|
case uint64:
|
|
return overrideNumber(strconv.FormatUint(value, 10), location), nil
|
|
case float32:
|
|
if math.IsNaN(float64(value)) || math.IsInf(float64(value), 0) {
|
|
return nil, overrideValueDiagnostic(CodeOverrideInvalidValue, path, "configuration override float must be finite")
|
|
}
|
|
return overrideNumber(strconv.FormatFloat(float64(value), 'g', -1, 32), location), nil
|
|
case float64:
|
|
if math.IsNaN(value) || math.IsInf(value, 0) {
|
|
return nil, overrideValueDiagnostic(CodeOverrideInvalidValue, path, "configuration override float must be finite")
|
|
}
|
|
return overrideNumber(strconv.FormatFloat(value, 'g', -1, 64), location), nil
|
|
case []any:
|
|
if value == nil {
|
|
return &document.Node{Kind: document.Null, Location: location}, nil
|
|
}
|
|
leave, diagnostic := b.enter(reflect.ValueOf(value), path)
|
|
if diagnostic != nil {
|
|
return nil, diagnostic
|
|
}
|
|
defer leave()
|
|
if len(value) > maxDocumentNodes-b.nodes {
|
|
return nil, overrideValueDiagnostic(CodeOverrideLimitExceeded, path, "configuration override exceeds the node limit")
|
|
}
|
|
node := &document.Node{Kind: document.Sequence, Location: location, Sequence: make([]*document.Node, len(value))}
|
|
for index, child := range value {
|
|
built, diagnostic := b.build(child, path.Index(index), depth+1)
|
|
if diagnostic != nil {
|
|
return nil, diagnostic
|
|
}
|
|
node.Sequence[index] = built
|
|
}
|
|
return node, nil
|
|
case map[string]any:
|
|
if value == nil {
|
|
return &document.Node{Kind: document.Null, Location: location}, nil
|
|
}
|
|
leave, diagnostic := b.enter(reflect.ValueOf(value), path)
|
|
if diagnostic != nil {
|
|
return nil, diagnostic
|
|
}
|
|
defer leave()
|
|
if len(value) > (maxDocumentNodes-b.nodes)/2 {
|
|
return nil, overrideValueDiagnostic(CodeOverrideLimitExceeded, path, "configuration override exceeds the node limit")
|
|
}
|
|
keys := make([]string, 0, len(value))
|
|
for key := range value {
|
|
keys = append(keys, key)
|
|
}
|
|
slices.Sort(keys)
|
|
node := &document.Node{Kind: document.Mapping, Location: location, Mapping: make([]document.Entry, 0, len(keys))}
|
|
for _, key := range keys {
|
|
b.nodes++ // mapping key
|
|
if !b.addBytes(len(key)) {
|
|
return nil, overrideValueDiagnostic(CodeOverrideLimitExceeded, path.Key(key), "configuration override exceeds the record string-byte limit")
|
|
}
|
|
built, diagnostic := b.build(value[key], path.Key(key), depth+1)
|
|
if diagnostic != nil {
|
|
return nil, diagnostic
|
|
}
|
|
node.Mapping = append(node.Mapping, document.Entry{Key: key, KeyLocation: location, ValueLocation: location, Value: built})
|
|
}
|
|
return node, nil
|
|
default:
|
|
return nil, overrideValueDiagnostic(CodeOverrideInvalidValue, path, "configuration override value has an unsupported type")
|
|
}
|
|
}
|
|
|
|
func (b *overrideValueBuilder) addBytes(count int) bool {
|
|
if count < 0 || int64(count) > maxSourceBytes-b.bytes {
|
|
return false
|
|
}
|
|
b.bytes += int64(count)
|
|
return true
|
|
}
|
|
|
|
func (b *overrideValueBuilder) enter(value reflect.Value, path Path) (func(), *Diagnostic) {
|
|
visit, ok := visitFor(value)
|
|
if !ok {
|
|
return func() {}, nil
|
|
}
|
|
if b.active[visit] {
|
|
return nil, overrideValueDiagnostic(CodeOverrideInvalidValue, path, "configuration override values must not contain cycles")
|
|
}
|
|
b.active[visit] = true
|
|
return func() { delete(b.active, visit) }, nil
|
|
}
|
|
|
|
func overrideValueDiagnostic(code Code, path Path, message string) *Diagnostic {
|
|
return &Diagnostic{Code: code, Severity: SeverityError, Path: path.clone(), Message: message}
|
|
}
|
|
|
|
func overrideNumber(text string, location document.Location) *document.Node {
|
|
return &document.Node{Kind: document.Number, Text: text, Location: location}
|
|
}
|