Add snapshotted mapping configuration sources
This commit is contained in:
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user