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

View File

@@ -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):