Add snapshotted mapping configuration sources

This commit is contained in:
2026-09-29 17:43:19 +00:00
parent 1ff3c812a7
commit 90c6e8ed7e
12 changed files with 454 additions and 159 deletions

166
internal/engine/value.go Normal file
View File

@@ -0,0 +1,166 @@
package engine
import (
"math"
"reflect"
"slices"
"strconv"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
)
type valueBuilder struct {
sourceID string
active map[valueVisit]bool
bytes int64
nodes int
}
type valueIssueKind uint8
const (
valueDepthLimit valueIssueKind = iota
valueNodeLimit
valueByteLimit
valueNonFinite
valueUnsupported
valueCycle
)
type valueIssue struct {
kind valueIssueKind
path Path
}
func newValueIssue(kind valueIssueKind, path Path) *valueIssue {
return &valueIssue{kind: kind, path: path.clone()}
}
func (b *valueBuilder) build(value any, path Path, depth int) (*document.Node, *valueIssue) {
if depth > maxDocumentDepth {
return nil, newValueIssue(valueDepthLimit, path)
}
b.nodes++
if b.nodes > maxDocumentNodes {
return nil, newValueIssue(valueNodeLimit, path)
}
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, newValueIssue(valueByteLimit, path)
}
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 valueNumber(strconv.FormatInt(int64(value), 10), location), nil
case int8:
return valueNumber(strconv.FormatInt(int64(value), 10), location), nil
case int16:
return valueNumber(strconv.FormatInt(int64(value), 10), location), nil
case int32:
return valueNumber(strconv.FormatInt(int64(value), 10), location), nil
case int64:
return valueNumber(strconv.FormatInt(value, 10), location), nil
case uint:
return valueNumber(strconv.FormatUint(uint64(value), 10), location), nil
case uint8:
return valueNumber(strconv.FormatUint(uint64(value), 10), location), nil
case uint16:
return valueNumber(strconv.FormatUint(uint64(value), 10), location), nil
case uint32:
return valueNumber(strconv.FormatUint(uint64(value), 10), location), nil
case uint64:
return valueNumber(strconv.FormatUint(value, 10), location), nil
case float32:
if math.IsNaN(float64(value)) || math.IsInf(float64(value), 0) {
return nil, newValueIssue(valueNonFinite, path)
}
return valueNumber(strconv.FormatFloat(float64(value), 'g', -1, 32), location), nil
case float64:
if math.IsNaN(value) || math.IsInf(value, 0) {
return nil, newValueIssue(valueNonFinite, path)
}
return valueNumber(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, newValueIssue(valueNodeLimit, path)
}
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, newValueIssue(valueNodeLimit, path)
}
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, newValueIssue(valueByteLimit, path.Key(key))
}
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, newValueIssue(valueUnsupported, path)
}
}
func (b *valueBuilder) addBytes(count int) bool {
if count < 0 || int64(count) > maxSourceBytes-b.bytes {
return false
}
b.bytes += int64(count)
return true
}
func (b *valueBuilder) enter(value reflect.Value, path Path) (func(), *valueIssue) {
visit, ok := visitFor(value)
if !ok {
return func() {}, nil
}
if b.active[visit] {
return nil, newValueIssue(valueCycle, path)
}
b.active[visit] = true
return func() { delete(b.active, visit) }, nil
}
func valueNumber(text string, location document.Location) *document.Node {
return &document.Node{Kind: document.Number, Text: text, Location: location}
}