Activate sibling inheritance in configuration loading

This commit is contained in:
2026-09-29 23:04:13 +00:00
parent 501d59f21b
commit df9a02539c
12 changed files with 435 additions and 16 deletions

View File

@@ -0,0 +1,53 @@
package engine
import "gitea.maximumdirect.net/eric/ConfigKit/internal/document"
func containsInheritanceDirective(root *document.Node) bool {
found := false
walkDocument(root, Path{}, func(path Path, _ *document.Node) {
if len(path.segments) != 0 && path.segments[len(path.segments)-1].kind == pathSegmentKey &&
path.segments[len(path.segments)-1].key == inheritanceKey {
found = true
}
})
return found
}
func inheritanceVersionDiagnostic(rule *VersionRule, expanded *inheritanceScope) *Diagnostic {
if rule == nil || expanded == nil {
return nil
}
for _, operation := range expanded.operations {
var location Location
var affected bool
if operation.control != nil {
affected = pathsIntersect(operation.control.path, rule.Path)
if len(operation.control.locations) != 0 {
location = operation.control.locations[0]
}
} else if operation.transfer != nil {
if relative, within := relativeHistoryPath(operation.transfer.to, rule.Path); within {
_, affected = documentAtPath(operation.parent, relative)
} else if pathContains(rule.Path, operation.transfer.to) {
affected = true
}
if len(operation.transfer.locations) != 0 {
location = operation.transfer.locations[0]
}
}
if affected {
return &Diagnostic{
Code: CodeInheritanceVersion, Severity: SeverityError, Path: rule.Path.clone(),
Message: "inheritance cannot change the schema-version path", Primary: &location,
}
}
}
return nil
}
func inheritanceOperations(expanded *inheritanceScope) []inheritanceOperation {
if expanded == nil {
return nil
}
return expanded.operations
}

View File

@@ -195,6 +195,25 @@ func executeLoadRequest[T any](request loadRequest[T], secrets SecretBinding, lo
mergeResourceContexts(&input.contexts, contexts)
}
}
if input.document != nil && containsInheritanceDirective(input.document) {
composition, problems := compositionForInput(input)
if len(problems) != 0 {
accumulator.add(problems...)
return nil, accumulator.loadError()
}
expanded, problems := expandInheritanceWithOrigins(composition.root, composition.origins, defaultProjectionLimits())
if len(problems) != 0 {
accumulator.add(problems...)
return nil, accumulator.loadError()
}
if problem := inheritanceVersionDiagnostic(request.definition.Version, expanded); problem != nil {
accumulator.add(*problem)
return nil, accumulator.loadError()
}
input.composition = composition
input.inheritance = expanded
input.document = expanded.root
}
decoded, diagnostics := decodeLoadInput(schema, request.definition, input)
accumulator.add(diagnostics...)
@@ -206,7 +225,7 @@ func executeLoadRequest[T any](request loadRequest[T], secrets SecretBinding, lo
if hasErrors(diagnostics) {
return nil, accumulator.loadError()
}
if !historyNodesWithinLimit(decoded.baseline, decoded.final, resolved.final, resolved.applications, resolved.snapshots) {
if !historyNodesWithinLimit(decoded.baseline, decoded.final, resolved.final, resolved.applications, resolved.snapshots, input.inheritance) {
accumulator.add(Diagnostic{Code: CodeExplainLimitExceeded, Severity: SeverityError, Message: "retained configuration history exceeds the node limit"})
return nil, accumulator.loadError()
}

View File

@@ -11,6 +11,7 @@ type assembledInput struct {
parsed parsedSource
document *document.Node
composition *composedDocument
inheritance *inheritanceScope
sources []SourceInfo
contexts map[string]ResourceContext
}

View File

@@ -15,6 +15,7 @@ type resolvedLoad[T any] struct {
final *document.Node
snapshots []resolutionSnapshot
applications []historyApplication
inheritance *inheritanceScope
contexts map[string]ResourceContext
}
@@ -35,7 +36,9 @@ func decodeLoadInput[T any](schema *schemaDescriptor, definition Definition[T],
}
return decodedConfiguration{}, diagnostics
}
if input.composition != nil {
if input.inheritance != nil {
applyCompositionOrigins(&decoded, input.inheritance.origins)
} else if input.composition != nil {
applyCompositionOrigins(&decoded, input.composition.origins)
}
@@ -46,7 +49,9 @@ func resolveLoadInput[T any](schema *schemaDescriptor, definition Definition[T],
var diagnostics []Diagnostic
configuration := decoded.value.Interface().(T)
metadata := metadataFromDecoded(decoded)
if input.composition != nil {
if input.inheritance != nil {
applyInheritanceMetadata(&metadata, input.composition.root, input.composition.origins, input.composition.overlays, input.inheritance)
} else if input.composition != nil {
applyOverlayMetadata(&metadata, input.composition.overlays)
}
final := decoded.final
@@ -68,7 +73,7 @@ func resolveLoadInput[T any](schema *schemaDescriptor, definition Definition[T],
} else if input.document != nil {
applications = []historyApplication{{layer: LayerBase, value: input.document}}
}
return resolvedLoad[T]{configuration: configuration, decoded: decoded, metadata: metadata, final: final, snapshots: resolutionSnapshots, applications: applications, contexts: input.contexts}, diagnostics
return resolvedLoad[T]{configuration: configuration, decoded: decoded, metadata: metadata, final: final, snapshots: resolutionSnapshots, applications: applications, inheritance: input.inheritance, contexts: input.contexts}, diagnostics
}
func detachLoadConfiguration[T any](schema *schemaDescriptor, configuration T, mode secretCloneMode) (T, *Diagnostic) {
@@ -104,6 +109,7 @@ func publishLoadResult[T any](schema *schemaDescriptor, definition Definition[T]
final: final,
resolution: resolutionSnapshots,
applications: applications,
inheritance: inheritanceOperations(resolved.inheritance),
secrets: secrets,
secretReferences: cloneSecretReferences(references),
}

View File

@@ -31,8 +31,8 @@ func (m Metadata) Lookup(path Path) (FieldInfo, bool) {
return detached, true
}
// IsExplicit reports whether a surviving input contribution explicitly defined
// path.
// IsExplicit reports whether a surviving input contribution, including one
// inherited from a sibling, explicitly defined path.
func (m Metadata) IsExplicit(path Path) bool {
info, ok := m.Lookup(path)
return ok && info.Explicit

View File

@@ -41,7 +41,7 @@ type resolutionResult struct {
diagnostics []Diagnostic
}
// IsExplicit reports whether surviving input explicitly defined path.
// IsExplicit reports whether surviving direct or inherited input defined path.
func (r *Resolution) IsExplicit(path Path) bool {
state := r.activeState()
if !state.validPath(path) {