215 lines
8.7 KiB
Go
215 lines
8.7 KiB
Go
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]
|
|
sequencePolicy sequencePolicy
|
|
sourceVersionMode sourceVersionMode
|
|
effectiveVersion *VersionRule
|
|
overlay string
|
|
overlayFilesystem *FilesystemContext
|
|
overrides []Override
|
|
ordinary bool
|
|
}
|
|
|
|
func (request loadRequest[T]) protectionVersion() *VersionRule {
|
|
if request.effectiveVersion != nil {
|
|
return request.effectiveVersion
|
|
}
|
|
return request.definition.Version
|
|
}
|
|
|
|
type loadInput struct {
|
|
source Source
|
|
imports bool
|
|
filesystem *FilesystemContext
|
|
destination Path
|
|
}
|
|
|
|
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
|
|
policy, policyDiagnostics := compileSequencePolicy(request.definition.sequenceRegistrations)
|
|
request.sequencePolicy = policy
|
|
diagnostics = append(diagnostics, policyDiagnostics...)
|
|
request.definition.sequenceRegistrations = nil
|
|
if normalized, err := normalizeDestination(request.main.destination); err != nil {
|
|
diagnostics = append(diagnostics, invalidDestinationDiagnostic(request.main.destination))
|
|
} else {
|
|
request.main.destination = normalized
|
|
}
|
|
request.effectiveVersion = effectiveSourceVersionRule(request.definition.Version, request.main.destination, request.sourceVersionMode)
|
|
_, mainDiagnostics := selectOptions(request.main.source, []Options[T]{{
|
|
Definition: request.definition, Imports: request.main.imports, Filesystem: request.main.filesystem,
|
|
SourceVersionMode: request.sourceVersionMode,
|
|
}})
|
|
diagnostics = append(diagnostics, mainDiagnostics...)
|
|
for groupIndex := range request.underlays {
|
|
for inputIndex := range request.underlays[groupIndex].inputs {
|
|
input := &request.underlays[groupIndex].inputs[inputIndex]
|
|
if normalized, err := normalizeDestination(input.destination); err != nil {
|
|
diagnostics = append(diagnostics, invalidDestinationDiagnostic(input.destination))
|
|
} else {
|
|
input.destination = normalized
|
|
}
|
|
_, 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, contexts, diagnostics := acquireUnderlayGroupWithVersion(group, index+1, request.protectionVersion(), accounting, input.versionState)
|
|
input.sources = append(input.sources, sources...)
|
|
mergeResourceContexts(&input.contexts, contexts)
|
|
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 = loadSelectedOverlayIntoWithVersion(selection, request.overlayFilesystem, selected, assembly, input.sources, request.overlay, request.protectionVersion(), defaultImportLimits(), input.versionState)
|
|
if len(diagnostics) != 0 {
|
|
return input, diagnostics
|
|
}
|
|
contexts, diagnostic := contextsForAcquiredSources(selection, request.overlayFilesystem, selected.source, input.sources[len(input.sources)-1:])
|
|
if diagnostic != nil {
|
|
return input, []Diagnostic{*diagnostic}
|
|
}
|
|
mergeResourceContexts(&input.contexts, contexts)
|
|
}
|
|
if len(request.overrides) != 0 {
|
|
var diagnostics []Diagnostic
|
|
input.sources, diagnostics = assembly.applyOverrides(input.sources, request.overrides, request.protectionVersion(), 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, map[string]ResourceContext, []Diagnostic) {
|
|
return acquireUnderlayGroupWithVersion(group, index, rule, accounting, nil)
|
|
}
|
|
|
|
func acquireUnderlayGroupWithVersion(group loadGroup, index int, rule *VersionRule, accounting *importAccounting, versionState *sourceVersionState) (*composedDocument, []SourceInfo, map[string]ResourceContext, []Diagnostic) {
|
|
initialBytes, initialNodes := accounting.bytes, accounting.nodes
|
|
visited := make(map[resourceIdentity]importRoute)
|
|
hostToken := &namespaceToken{}
|
|
var documents []*document.Node
|
|
var sources []SourceInfo
|
|
var contexts map[string]ResourceContext
|
|
for _, contribution := range group.inputs {
|
|
options := Options[struct{}]{Imports: contribution.imports, Filesystem: contribution.filesystem, Definition: Definition[struct{}]{Version: rule}}
|
|
acquired, diagnostics := assembleBaseInputTrackedForRole(contribution.source, options, contribution.destination, accounting, visited, hostToken, versionState, sourceVersionContribution)
|
|
for _, source := range acquired.sources {
|
|
source.Layer = LayerUnderlay
|
|
source.UnderlayIndex = index
|
|
sources = append(sources, source)
|
|
}
|
|
mergeResourceContexts(&contexts, acquired.contexts)
|
|
if len(diagnostics) != 0 {
|
|
return nil, sources, contexts, diagnostics
|
|
}
|
|
if acquired.document == nil {
|
|
continue
|
|
}
|
|
if node := fragmentVersionClaim(acquired.document, rule); node != nil {
|
|
location := publicLocation(node.Location)
|
|
return nil, sources, contexts, []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, contexts, nil
|
|
}
|
|
composition, diagnostics := composeDocuments(documents...)
|
|
if len(diagnostics) != 0 {
|
|
return nil, sources, contexts, diagnostics
|
|
}
|
|
composition.inputBytes = accounting.bytes - initialBytes
|
|
composition.inputNodes = accounting.nodes - initialNodes
|
|
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, contexts, nil
|
|
}
|