Add snapshotted mapping configuration sources
This commit is contained in:
@@ -54,7 +54,7 @@ func filesystemOptionDiagnostics(source Source, context *FilesystemContext) []Di
|
||||
if context.Root != "" && !fs.ValidPath(context.Root) {
|
||||
return []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "filesystem root must be a slash-relative valid path")}
|
||||
}
|
||||
case sourceBytes, sourceReader:
|
||||
case sourceBytes, sourceReader, sourceMapping:
|
||||
if context.BaseDir == "" || context.Root == "" {
|
||||
return []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "byte and reader filesystem context requires base and root directories")}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ func selectOptions[T any](source Source, supplied []Options[T]) (Options[T], []D
|
||||
if source.kind == sourceNone && options.Overlay != "" && options.Overlay != "default" {
|
||||
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "NoSource cannot select an active overlay"))
|
||||
}
|
||||
if options.Filesystem == nil && (source.kind == sourceBytes || source.kind == sourceReader) && (options.Imports || activeOverlay(options.Overlay)) {
|
||||
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "byte and reader imports or overlays require filesystem context"))
|
||||
if options.Filesystem == nil && (source.kind == sourceBytes || source.kind == sourceReader || source.kind == sourceMapping) && (options.Imports || activeOverlay(options.Overlay)) {
|
||||
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "synthetic-source imports or overlays require filesystem context"))
|
||||
}
|
||||
if definition.Version != nil {
|
||||
if !definition.Version.Path.valid() {
|
||||
|
||||
@@ -94,11 +94,12 @@ func prepareOverrides(base *composedDocument, sources []SourceInfo, overrides []
|
||||
}
|
||||
pathAccepted[index] = true
|
||||
|
||||
builder := overrideValueBuilder{sourceID: prepared[index].source.ID, active: make(map[valueVisit]bool)}
|
||||
builder := valueBuilder{sourceID: prepared[index].source.ID, active: make(map[valueVisit]bool)}
|
||||
value, issue := builder.build(override.Value, override.Path, len(override.Path.segments))
|
||||
if issue != nil {
|
||||
issue.Primary = location
|
||||
diagnostics = append(diagnostics, *issue)
|
||||
diagnostic := issue.overrideDiagnostic()
|
||||
diagnostic.Primary = location
|
||||
diagnostics = append(diagnostics, diagnostic)
|
||||
continue
|
||||
}
|
||||
if relative, ok := overrideDirectivePath(value, Path{}); ok {
|
||||
|
||||
@@ -1,150 +1,21 @@
|
||||
package engine
|
||||
|
||||
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
|
||||
func (issue *valueIssue) overrideDiagnostic() Diagnostic {
|
||||
code := CodeOverrideInvalidValue
|
||||
var message string
|
||||
switch issue.kind {
|
||||
case valueDepthLimit:
|
||||
code, message = CodeOverrideLimitExceeded, "configuration override exceeds the depth limit"
|
||||
case valueNodeLimit:
|
||||
code, message = CodeOverrideLimitExceeded, "configuration override exceeds the node limit"
|
||||
case valueByteLimit:
|
||||
code, message = CodeOverrideLimitExceeded, "configuration override exceeds the record string-byte limit"
|
||||
case valueNonFinite:
|
||||
message = "configuration override float must be finite"
|
||||
case valueCycle:
|
||||
message = "configuration override values must not contain cycles"
|
||||
default:
|
||||
return nil, overrideValueDiagnostic(CodeOverrideInvalidValue, path, "configuration override value has an unsupported type")
|
||||
message = "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}
|
||||
return Diagnostic{Code: code, Severity: SeverityError, Path: issue.path.clone(), Message: message}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,9 @@ func parseSource(source Source, sourceID string, limits parseLimits) (parsedSour
|
||||
if len(diagnostics) != 0 || read.info == nil {
|
||||
return parsedSource{source: read.info}, diagnostics
|
||||
}
|
||||
if read.document != nil {
|
||||
return parsedSource{document: read.document, source: read.info, bytes: read.bytes}, nil
|
||||
}
|
||||
|
||||
root, issues := yamladapter.Parse(bytes.NewReader(read.data), sourceID, limits.document)
|
||||
if len(issues) != 0 {
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sync/atomic"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -25,6 +27,10 @@ type Source struct {
|
||||
namespace *nativeNamespace
|
||||
data []byte
|
||||
reader *readerSource
|
||||
mapping *document.Node
|
||||
mapIssue *valueIssue
|
||||
mapBytes int64
|
||||
mapNodes int
|
||||
createErr sourceCreationError
|
||||
}
|
||||
|
||||
@@ -36,6 +42,7 @@ const (
|
||||
sourceFilesystem
|
||||
sourceBytes
|
||||
sourceReader
|
||||
sourceMapping
|
||||
sourceNone
|
||||
)
|
||||
|
||||
@@ -50,6 +57,7 @@ const (
|
||||
creationInvalidFilesystem
|
||||
creationInvalidFilesystemLabel
|
||||
creationInvalidFilesystemPath
|
||||
creationInvalidMappingRoot
|
||||
)
|
||||
|
||||
// Filesystem is a reusable opaque namespace for caller-owned fs.FS inputs.
|
||||
@@ -148,6 +156,24 @@ func Reader(label string, input io.Reader) Source {
|
||||
return source
|
||||
}
|
||||
|
||||
// Mapping snapshots supported built-in values into an owned document at
|
||||
// construction. Invalid values are retained as safe deferred diagnostics.
|
||||
func Mapping(label string, values map[string]any) Source {
|
||||
source := Source{kind: sourceMapping, label: label}
|
||||
if !validOverrideLabel(label) {
|
||||
source.createErr = creationInvalidLabel
|
||||
return source
|
||||
}
|
||||
if values == nil {
|
||||
source.createErr = creationInvalidMappingRoot
|
||||
return source
|
||||
}
|
||||
builder := valueBuilder{active: make(map[valueVisit]bool)}
|
||||
source.mapping, source.mapIssue = builder.build(values, Path{}, 0)
|
||||
source.mapBytes, source.mapNodes = builder.bytes, builder.nodes
|
||||
return source
|
||||
}
|
||||
|
||||
// NoSource explicitly selects a defaults-only input. It creates no source record.
|
||||
func NoSource() Source {
|
||||
return Source{kind: sourceNone}
|
||||
@@ -163,14 +189,24 @@ func defaultSourceLimits() sourceLimits {
|
||||
}
|
||||
|
||||
type sourceRead struct {
|
||||
data []byte
|
||||
info *SourceInfo
|
||||
data []byte
|
||||
document *document.Node
|
||||
bytes int64
|
||||
info *SourceInfo
|
||||
}
|
||||
|
||||
func readSource(source Source, sourceID string, limits sourceLimits) (sourceRead, []Diagnostic) {
|
||||
if diagnostic := sourceConstructorDiagnostic(source); diagnostic != nil {
|
||||
if source.kind == sourceMapping {
|
||||
diagnostic.Primary = &Location{SourceID: sourceID}
|
||||
return sourceRead{info: mappingSourceInfo(source, sourceID)}, []Diagnostic{*diagnostic}
|
||||
}
|
||||
return sourceRead{}, []Diagnostic{*diagnostic}
|
||||
}
|
||||
if source.kind == sourceMapping && source.mapIssue != nil {
|
||||
diagnostic := source.mapIssue.sourceDiagnostic(sourceID)
|
||||
return sourceRead{info: mappingSourceInfo(source, sourceID)}, []Diagnostic{diagnostic}
|
||||
}
|
||||
if source.kind == sourceNone {
|
||||
return sourceRead{}, nil
|
||||
}
|
||||
@@ -219,6 +255,13 @@ func readSource(source Source, sourceID string, limits sourceLimits) (sourceRead
|
||||
return sourceRead{info: info}, []Diagnostic{sourceDiagnostic(CodeSourceReaderUsed, sourceID, "reader source has already been consumed")}
|
||||
}
|
||||
input = source.reader.input
|
||||
case sourceMapping:
|
||||
if source.mapBytes > limit || source.mapNodes > maxDocumentNodes {
|
||||
return sourceRead{info: info}, []Diagnostic{sourceDiagnostic(CodeSourceLimitExceeded, sourceID, "mapping source exceeds the configured input limit")}
|
||||
}
|
||||
owned := document.Clone(source.mapping)
|
||||
setDocumentSourceID(owned, sourceID)
|
||||
return sourceRead{document: owned, bytes: source.mapBytes, info: info}, nil
|
||||
default:
|
||||
return sourceRead{info: info}, []Diagnostic{sourceDiagnostic(CodeSourceInvalid, sourceID, "source is invalid")}
|
||||
}
|
||||
@@ -229,6 +272,14 @@ func readSource(source Source, sourceID string, limits sourceLimits) (sourceRead
|
||||
return sourceRead{data: bytes.Clone(data), info: info}, nil
|
||||
}
|
||||
|
||||
func mappingSourceInfo(source Source, sourceID string) *SourceInfo {
|
||||
label := source.label
|
||||
if !validOverrideLabel(label) {
|
||||
label = "mapping"
|
||||
}
|
||||
return &SourceInfo{ID: sourceID, Label: label, Layer: LayerBase, Kind: SourceKindMapping}
|
||||
}
|
||||
|
||||
func sourceConstructorDiagnostic(source Source) *Diagnostic {
|
||||
var message string
|
||||
code := CodeSourceInvalid
|
||||
@@ -242,6 +293,9 @@ func sourceConstructorDiagnostic(source Source) *Diagnostic {
|
||||
message = "source path must not be empty"
|
||||
case creationInvalidLabel:
|
||||
message = "source label must not be empty"
|
||||
if source.kind == sourceMapping {
|
||||
message = "mapping source label must be nonempty and safe to display"
|
||||
}
|
||||
case creationInvalidReader:
|
||||
message = "source reader must not be nil"
|
||||
case creationTooLarge:
|
||||
@@ -253,6 +307,8 @@ func sourceConstructorDiagnostic(source Source) *Diagnostic {
|
||||
message = "filesystem label must be nonempty and safe to display"
|
||||
case creationInvalidFilesystemPath:
|
||||
message = "filesystem source path must be a non-root slash-relative valid path"
|
||||
case creationInvalidMappingRoot:
|
||||
message = "mapping source root must be a non-nil mapping"
|
||||
}
|
||||
diagnostic := sourceDiagnostic(code, "", message)
|
||||
return &diagnostic
|
||||
@@ -268,11 +324,29 @@ func sourceSourceKind(kind sourceKind) SourceKind {
|
||||
return SourceKindBytes
|
||||
case sourceReader:
|
||||
return SourceKindReader
|
||||
case sourceMapping:
|
||||
return SourceKindMapping
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func setDocumentSourceID(node *document.Node, sourceID string) {
|
||||
if node == nil {
|
||||
return
|
||||
}
|
||||
node.Location.SourceID = sourceID
|
||||
for index := range node.Mapping {
|
||||
entry := &node.Mapping[index]
|
||||
entry.KeyLocation.SourceID = sourceID
|
||||
entry.ValueLocation.SourceID = sourceID
|
||||
setDocumentSourceID(entry.Value, sourceID)
|
||||
}
|
||||
for _, child := range node.Sequence {
|
||||
setDocumentSourceID(child, sourceID)
|
||||
}
|
||||
}
|
||||
|
||||
func classifySourceError(err error, sourceID string) Diagnostic {
|
||||
switch {
|
||||
case errors.Is(err, fs.ErrNotExist):
|
||||
|
||||
19
internal/engine/source_value.go
Normal file
19
internal/engine/source_value.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package engine
|
||||
|
||||
func (issue *valueIssue) sourceDiagnostic(sourceID string) Diagnostic {
|
||||
code := CodeSourceInvalid
|
||||
message := "mapping source has an unsupported value"
|
||||
switch issue.kind {
|
||||
case valueDepthLimit:
|
||||
code, message = CodeSourceLimitExceeded, "mapping source exceeds the depth limit"
|
||||
case valueNodeLimit:
|
||||
code, message = CodeSourceLimitExceeded, "mapping source exceeds the node limit"
|
||||
case valueByteLimit:
|
||||
code, message = CodeSourceLimitExceeded, "mapping source exceeds the string-byte limit"
|
||||
case valueNonFinite:
|
||||
message = "mapping source float must be finite"
|
||||
case valueCycle:
|
||||
message = "mapping source values must not contain cycles"
|
||||
}
|
||||
return Diagnostic{Code: code, Severity: SeverityError, Path: issue.path.clone(), Message: message, Primary: &Location{SourceID: sourceID}}
|
||||
}
|
||||
166
internal/engine/value.go
Normal file
166
internal/engine/value.go
Normal 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}
|
||||
}
|
||||
Reference in New Issue
Block a user