Compose grouped underlays in shared load lifecycle

This commit is contained in:
2026-09-29 17:52:20 +00:00
parent 90c6e8ed7e
commit 1cdf90d831
7 changed files with 494 additions and 25 deletions

View File

@@ -0,0 +1,171 @@
package engine
import (
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
)
// loadRequest keeps source acquisition separate from precedence assembly.
// The ordinary entry point and grouped entry point share the final lifecycle.
type loadRequest[T any] struct {
main loadInput
underlays []loadGroup
definition Definition[T]
overlay string
overlayFilesystem *FilesystemContext
overrides []Override
ordinary bool
}
type loadInput struct {
source Source
imports bool
filesystem *FilesystemContext
}
type loadGroup struct{ inputs []loadInput }
func loadGroupedWithSecretLookup[T any](request loadRequest[T], secrets SecretBinding, lookup secretLookup) (*Loaded[T], error) {
request.ordinary = false
var diagnostics []Diagnostic
_, mainDiagnostics := selectOptions(request.main.source, []Options[T]{{
Definition: request.definition, Imports: request.main.imports, Filesystem: request.main.filesystem,
}})
diagnostics = append(diagnostics, mainDiagnostics...)
for _, group := range request.underlays {
for _, input := range group.inputs {
_, inputDiagnostics := selectOptions(input.source, []Options[T]{{Imports: input.imports, Filesystem: input.filesystem}})
diagnostics = append(diagnostics, inputDiagnostics...)
}
}
if !validOverlayName(request.overlay) {
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "overlay name must contain only ASCII letters, digits, underscore, or hyphen"))
}
if activeOverlay(request.overlay) {
if request.overlayFilesystem == nil {
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "selected overlay requires explicit filesystem context"))
} else {
diagnostics = append(diagnostics, filesystemOptionDiagnostics(Source{kind: sourceBytes}, request.overlayFilesystem)...)
}
}
if len(diagnostics) != 0 {
return nil, (&diagnosticAccumulator{diagnostics: canonicalDiagnostics(diagnostics)}).loadError()
}
return executeLoadRequest(request, secrets, lookup)
}
func assembleLayeredInput[T any](request loadRequest[T], input assembledInput, accounting *importAccounting) (assembledInput, []Diagnostic) {
groups := make([]*composedDocument, 0, len(request.underlays))
for index, group := range request.underlays {
composition, sources, diagnostics := acquireUnderlayGroup(group, index+1, request.definition.Version, accounting)
input.sources = append(input.sources, sources...)
if len(diagnostics) != 0 {
return input, diagnostics
}
if composition != nil {
groups = append(groups, composition)
}
}
var assembly *assemblyAccumulator
for _, group := range groups {
if assembly == nil {
assembly = newAssemblyAccumulator(group)
continue
}
if diagnostics := assembly.applyOverlay(group.root, group.origins, group.applications, LayerUnderlay); len(diagnostics) != 0 {
return input, diagnostics
}
}
if input.document != nil {
mainComposition, diagnostics := compositionForInput(input)
if len(diagnostics) != 0 {
return input, diagnostics
}
if assembly == nil {
assembly = newAssemblyAccumulator(mainComposition)
} else if diagnostics := assembly.applyOverlay(mainComposition.root, mainComposition.origins, mainComposition.applications, LayerBase); len(diagnostics) != 0 {
return input, diagnostics
}
}
if assembly == nil && (activeOverlay(request.overlay) || len(request.overrides) != 0) {
composition, _ := compositionForInput(assembledInput{})
assembly = newAssemblyAccumulator(composition)
}
if assembly == nil {
return input, nil
}
assembly.current.inputBytes = accounting.bytes
assembly.current.inputNodes = accounting.nodes
if activeOverlay(request.overlay) {
// The selected overlay context is independent of the main input. This
// synthetic descriptor creates no source record or input contribution.
selection := Source{kind: sourceBytes}
selected := parsedSource{document: &document.Node{Kind: document.Mapping}, source: &SourceInfo{}}
var diagnostics []Diagnostic
input.sources, diagnostics = loadSelectedOverlayInto(selection, request.overlayFilesystem, selected, assembly, input.sources, request.overlay, request.definition.Version, defaultImportLimits())
if len(diagnostics) != 0 {
return input, diagnostics
}
}
if len(request.overrides) != 0 {
var diagnostics []Diagnostic
input.sources, diagnostics = assembly.applyOverrides(input.sources, request.overrides, request.definition.Version, defaultImportLimits())
if len(diagnostics) != 0 {
return input, diagnostics
}
}
input.composition = assembly.current
input.document = assembly.current.root
return input, nil
}
func acquireUnderlayGroup(group loadGroup, index int, rule *VersionRule, accounting *importAccounting) (*composedDocument, []SourceInfo, []Diagnostic) {
visited := make(map[resourceIdentity]importRoute)
hostToken := &namespaceToken{}
var documents []*document.Node
var sources []SourceInfo
for _, contribution := range group.inputs {
options := Options[struct{}]{Imports: contribution.imports, Filesystem: contribution.filesystem, Definition: Definition[struct{}]{Version: rule}}
acquired, diagnostics := assembleBaseInputTracked(contribution.source, options, accounting, visited, hostToken)
for _, source := range acquired.sources {
source.Layer = LayerUnderlay
source.UnderlayIndex = index
sources = append(sources, source)
}
if len(diagnostics) != 0 {
return nil, sources, diagnostics
}
if acquired.document == nil {
continue
}
if node := fragmentVersionClaim(acquired.document, rule); node != nil {
location := publicLocation(node.Location)
return nil, sources, []Diagnostic{{
Code: CodeUnderlayVersion, Severity: SeverityError, Path: rule.Path.clone(),
Message: "underlay inputs cannot contribute to the schema-version path", Primary: &location,
}}
}
if acquired.composition != nil {
for _, application := range acquired.composition.applications {
documents = append(documents, application.value)
}
} else {
documents = append(documents, acquired.document)
}
}
if len(documents) == 0 {
return nil, sources, nil
}
composition, diagnostics := composeDocuments(documents...)
if len(diagnostics) != 0 {
return nil, sources, diagnostics
}
for pointer, origin := range composition.origins {
origin.Layer = LayerUnderlay
composition.origins[pointer] = origin
}
for index := range composition.applications {
composition.applications[index].layer = LayerUnderlay
}
return composition, sources, nil
}