219 lines
7.8 KiB
Go
219 lines
7.8 KiB
Go
package engine
|
|
|
|
import (
|
|
"reflect"
|
|
|
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
|
)
|
|
|
|
// decodedOverrideResult keeps the typed pre-CLI projection and raw CLI
|
|
// contributions alongside the effective typed value and its inspection data.
|
|
type decodedOverrideResult struct {
|
|
value reflect.Value
|
|
final *document.Node
|
|
presence map[string]inputPresence
|
|
origins map[string]Origin
|
|
overrides []preparedOverride
|
|
}
|
|
|
|
// applyDecodedOverrides assigns prepared CLI values to a detached, decoded
|
|
// configuration. Each sparse mapping is decoded against the current typed
|
|
// value, preserving omitted mapping fields while replacing supplied sequences.
|
|
func applyDecodedOverrides(
|
|
schema *schemaDescriptor,
|
|
decoded decodedConfiguration,
|
|
batch preparedOverrideBatch,
|
|
context decodeContext,
|
|
) (decodedOverrideResult, []decodeIssue) {
|
|
if schema == nil || schema.root == nil || !decoded.value.IsValid() || decoded.value.Type() != schema.root.typeOf || decoded.final == nil {
|
|
return decodedOverrideResult{}, []decodeIssue{{code: CodeSchemaUnsupportedType, message: "decoded configuration does not match the schema"}}
|
|
}
|
|
|
|
working, cloneIssue := cloneConfiguration(schema, decoded.value, defaultProjectionLimits())
|
|
if cloneIssue != nil {
|
|
return decodedOverrideResult{}, []decodeIssue{decodeIssueFromProjection(cloneIssue)}
|
|
}
|
|
presence := cloneInputPresence(decoded.presence)
|
|
origins := cloneOrigins(decoded.origins)
|
|
var lookup *documentLookup
|
|
if len(batch.overrides) != 0 {
|
|
lookup = buildDocumentLookup(decoded.final)
|
|
}
|
|
|
|
for _, override := range batch.overrides {
|
|
if len(override.path.segments) == 0 {
|
|
return decodedOverrideResult{}, []decodeIssue{{code: CodeOverrideInvalidPath, message: "configuration override path is invalid", location: document.Location{SourceID: override.source.ID}}}
|
|
}
|
|
input, issue := decodedOverrideInput(override)
|
|
if issue != nil {
|
|
return decodedOverrideResult{}, []decodeIssue{*issue}
|
|
}
|
|
updated, issues := decodeValue(input, working, schema.root, Path{}, schema, context)
|
|
if len(issues) != 0 {
|
|
return decodedOverrideResult{}, issues
|
|
}
|
|
working = updated
|
|
|
|
location := document.Location{SourceID: override.source.ID}
|
|
recordOverrideAncestors(presence, origins, override.path, location)
|
|
// Prepared paths cannot overlap, so other assignments cannot change
|
|
// this target's pre-CLI subtree.
|
|
existing, _ := documentAtPathWithLookup(decoded.final, override.path, lookup)
|
|
updateOverridePresence(presence, existing, override.value, override.path, location)
|
|
updateOverrideOrigins(origins, existing, override.value, override.path, location)
|
|
}
|
|
final, projectionIssue := projectConfiguration(schema, working, defaultProjectionLimits())
|
|
if projectionIssue != nil {
|
|
return decodedOverrideResult{}, []decodeIssue{decodeIssueFromProjection(projectionIssue)}
|
|
}
|
|
|
|
// New fields created by decoding a partial mapping into a previously nil
|
|
// pointer have no input origin. Fill those gaps as defaults while retaining
|
|
// all surviving source and CLI contributions already in the origin map.
|
|
fillDefaultOrigins(final, Path{}, origins)
|
|
|
|
return decodedOverrideResult{
|
|
value: working,
|
|
final: final,
|
|
presence: presence,
|
|
origins: origins,
|
|
overrides: clonePreparedOverrides(batch.overrides),
|
|
}, nil
|
|
}
|
|
|
|
func recordOverrideAncestors(presence map[string]inputPresence, origins map[string]Origin, path Path, location document.Location) {
|
|
mergeOverlayOrigin(origins, Path{}, location, LayerCLI)
|
|
prefix := Path{}
|
|
for index, segment := range path.segments {
|
|
prefix = prefix.Key(segment.key)
|
|
if index == len(path.segments)-1 {
|
|
break
|
|
}
|
|
pointer := prefix.String()
|
|
if _, exists := presence[pointer]; !exists {
|
|
presence[pointer] = inputPresence{location: location}
|
|
}
|
|
mergeOverlayOrigin(origins, prefix, location, LayerCLI)
|
|
}
|
|
}
|
|
|
|
func decodedOverrideInput(override preparedOverride) (*document.Node, *decodeIssue) {
|
|
location := document.Location{SourceID: override.source.ID}
|
|
value := document.Clone(override.value)
|
|
if value == nil {
|
|
return nil, &decodeIssue{code: CodeOverrideInvalidPath, path: override.path.clone(), message: "configuration override value is invalid", location: location}
|
|
}
|
|
for index := len(override.path.segments) - 1; index >= 0; index-- {
|
|
segment := override.path.segments[index]
|
|
if segment.kind == pathSegmentIndex {
|
|
return nil, &decodeIssue{code: CodeOverrideInvalidPath, path: override.path.clone(), message: "configuration override path is invalid", location: location}
|
|
}
|
|
value = &document.Node{
|
|
Kind: document.Mapping,
|
|
Location: location,
|
|
Mapping: []document.Entry{{
|
|
Key: segment.key, KeyLocation: location, ValueLocation: location, Value: value,
|
|
}},
|
|
}
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func cloneInputPresence(input map[string]inputPresence) map[string]inputPresence {
|
|
cloned := make(map[string]inputPresence, len(input))
|
|
for path, value := range input {
|
|
cloned[path] = value
|
|
}
|
|
return cloned
|
|
}
|
|
|
|
func cloneOrigins(input map[string]Origin) map[string]Origin {
|
|
cloned := make(map[string]Origin, len(input))
|
|
for path, value := range input {
|
|
cloned[path] = cloneOrigin(value)
|
|
}
|
|
return cloned
|
|
}
|
|
|
|
func clonePreparedOverrides(input []preparedOverride) []preparedOverride {
|
|
cloned := make([]preparedOverride, len(input))
|
|
for index, value := range input {
|
|
cloned[index] = preparedOverride{
|
|
path: value.path.clone(),
|
|
value: document.Clone(value.value),
|
|
source: value.source,
|
|
bytes: value.bytes,
|
|
nodes: value.nodes,
|
|
}
|
|
}
|
|
return cloned
|
|
}
|
|
|
|
func updateOverridePresence(presence map[string]inputPresence, existing, incoming *document.Node, path Path, location document.Location) {
|
|
if existing != nil && existing.Kind == document.Mapping && incoming.Kind == document.Mapping {
|
|
if _, exists := presence[path.String()]; !exists {
|
|
presence[path.String()] = inputPresence{location: location}
|
|
}
|
|
var index mappingIndex
|
|
for _, entry := range incoming.Mapping {
|
|
childPath := path.Key(entry.Key)
|
|
position := index.find(existing, entry.Key)
|
|
var previous *document.Node
|
|
if position >= 0 {
|
|
previous = existing.Mapping[position].Value
|
|
}
|
|
updateOverridePresence(presence, previous, entry.Value, childPath, location)
|
|
}
|
|
return
|
|
}
|
|
|
|
deletePresencePathSubtree(presence, existing, path)
|
|
collectPresence(incoming, path, presence)
|
|
}
|
|
|
|
func deletePresencePathSubtree(presence map[string]inputPresence, existing *document.Node, path Path) {
|
|
// The typed pre-CLI tree contains every effective descendant. Visit only
|
|
// the replaced subtree rather than searching unrelated metadata entries.
|
|
walkDocument(existing, path, func(path Path, _ *document.Node) {
|
|
delete(presence, path.String())
|
|
})
|
|
}
|
|
|
|
func updateOverrideOrigins(origins map[string]Origin, existing, incoming *document.Node, path Path, location document.Location) {
|
|
if existing != nil && existing.Kind == document.Mapping && incoming.Kind == document.Mapping {
|
|
mergeOverlayOrigin(origins, path, location, LayerCLI)
|
|
var index mappingIndex
|
|
for _, entry := range incoming.Mapping {
|
|
childPath := path.Key(entry.Key)
|
|
position := index.find(existing, entry.Key)
|
|
var previous *document.Node
|
|
if position >= 0 {
|
|
previous = existing.Mapping[position].Value
|
|
}
|
|
updateOverrideOrigins(origins, previous, entry.Value, childPath, location)
|
|
}
|
|
return
|
|
}
|
|
|
|
walkDocument(existing, path, func(path Path, _ *document.Node) {
|
|
delete(origins, path.String())
|
|
})
|
|
collectOverlayOrigins(origins, incoming, path, LayerCLI)
|
|
}
|
|
|
|
func fillDefaultOrigins(node *document.Node, path Path, origins map[string]Origin) {
|
|
if _, exists := origins[path.String()]; !exists {
|
|
origins[path.String()] = Origin{Layer: LayerDefault}
|
|
}
|
|
switch node.Kind {
|
|
case document.Mapping:
|
|
for _, entry := range node.Mapping {
|
|
fillDefaultOrigins(entry.Value, path.Key(entry.Key), origins)
|
|
}
|
|
case document.Sequence:
|
|
for index, child := range node.Sequence {
|
|
fillDefaultOrigins(child, path.Index(index), origins)
|
|
}
|
|
}
|
|
}
|