Files
ConfigKit/internal/engine/load_request.go

327 lines
14 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]
importDefaults importDefaults
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
placementMode placementMode
}
type loadGroup struct{ inputs []loadInput }
type acquiredUnderlayGroup struct {
index int
scopes []acquisitionScope
importGraph importInclusionGraph
sources []SourceInfo
contexts map[string]ResourceContext
inputBytes int64
inputNodes int
}
func loadGroupedWithSecretLookup[T any](request loadRequest[T], secrets SecretBinding, lookup secretLookup) (*Loaded[T], error) {
request.ordinary = false
var diagnostics []Diagnostic
request.importDefaults = snapshotImportDefaults(request.importDefaults)
diagnostics = append(diagnostics, validateImportDefaults(request.importDefaults)...)
policy, policyDiagnostics := compileSequencePolicy(request.definition.sequenceRegistrations)
request.sequencePolicy = policy
diagnostics = append(diagnostics, policyDiagnostics...)
request.definition.sequenceRegistrations = nil
mainOptions, mainDiagnostics := selectOptions(request.main.source, []Options[T]{{
Definition: request.definition, Imports: request.main.imports, Filesystem: request.main.filesystem, destination: request.main.destination.clone(),
SourceVersionMode: request.sourceVersionMode, placementMode: request.main.placementMode,
importDefaults: request.importDefaults, importDefaultsValidated: true,
}})
request.main.destination = mainOptions.destination
diagnostics = append(diagnostics, mainDiagnostics...)
request.effectiveVersion = effectiveSourceVersionRuleForPlacement(request.definition.Version, request.main.destination, request.sourceVersionMode, request.main.placementMode)
for groupIndex := range request.underlays {
for inputIndex := range request.underlays[groupIndex].inputs {
input := &request.underlays[groupIndex].inputs[inputIndex]
inputOptions, inputDiagnostics := selectOptions(input.source, []Options[T]{{
Imports: input.imports, Filesystem: input.filesystem, destination: input.destination.clone(), placementMode: input.placementMode,
importDefaults: request.importDefaults, importDefaultsValidated: true,
}})
input.destination = inputOptions.destination
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 {
acquired, diagnostics := acquireUnderlayGroupScopes(group, index+1, request.protectionVersion(), accounting, input.versionState, request.sequencePolicy)
mergeResourceContexts(&input.contexts, acquired.contexts)
if len(diagnostics) != 0 {
input.sources = append(input.sources, acquired.sources...)
return input, diagnostics
}
composition, diagnostics := composeUnderlayGroup(acquired, request.protectionVersion(), request.sequencePolicy, collectionPolicyScope{})
input.sources = append(input.sources, acquired.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 := compositionForInputWithPolicy(input, request.sequencePolicy)
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
var overlayPolicy []sequencePolicy
if request.ordinary {
overlayPolicy = []sequencePolicy{request.sequencePolicy}
}
input.sources, diagnostics = loadSelectedOverlayIntoWithVersion(selection, request.overlayFilesystem, selected, assembly, input.sources, request.overlay, request.protectionVersion(), defaultImportLimits(), input.versionState, overlayPolicy...)
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) {
return acquireUnderlayGroupWithPolicyVersion(group, index, rule, accounting, versionState, sequencePolicy{})
}
func acquireUnderlayGroupWithPolicyVersion(group loadGroup, index int, rule *VersionRule, accounting *importAccounting, versionState *sourceVersionState, policy sequencePolicy) (*composedDocument, []SourceInfo, map[string]ResourceContext, []Diagnostic) {
acquired, diagnostics := acquireUnderlayGroupScopes(group, index, rule, accounting, versionState, policy)
if len(diagnostics) != 0 {
return nil, acquired.sources, acquired.contexts, diagnostics
}
composition, diagnostics := composeUnderlayGroup(acquired, rule, policy, collectionPolicyScope{})
return composition, acquired.sources, acquired.contexts, diagnostics
}
func acquireUnderlayGroupScopes(group loadGroup, index int, rule *VersionRule, accounting *importAccounting, versionState *sourceVersionState, policy sequencePolicy) (*acquiredUnderlayGroup, []Diagnostic) {
initialBytes, initialNodes := accounting.bytes, accounting.nodes
visited := make(map[resourceIdentity]importRoute)
hostToken := &namespaceToken{}
nativeAnchors := make(map[*namespaceToken]string)
for inputIndex, contribution := range group.inputs {
if token := nativeTokenForLoadInput(contribution); token != nil {
if _, exists := nativeAnchors[token]; !exists {
nativeAnchors[token] = explicitNamespaceAnchor(index, inputIndex+1)
}
}
}
acquiredGroup := &acquiredUnderlayGroup{index: index}
inclusionGraph := newImportInclusionGraph()
for inputIndex, contribution := range group.inputs {
options := Options[struct{}]{
Imports: contribution.imports, Filesystem: contribution.filesystem,
Definition: Definition[struct{}]{Version: rule}, destination: contribution.destination.clone(),
placementMode: contribution.placementMode, sequencePolicy: policy, deferAssembly: true,
importDefaults: func() importDefaults {
if versionState == nil {
return importDefaults{}
}
return snapshotImportDefaults(versionState.importDefaults)
}(),
}
acquired, diagnostics := assembleBaseInputTrackedForRole(contribution.source, options, contribution.destination, accounting, visited, hostToken, versionState, sourceVersionContribution)
for _, source := range acquired.sources {
source.Layer = LayerUnderlay
source.UnderlayIndex = index
acquiredGroup.sources = append(acquiredGroup.sources, source)
}
mergeResourceContexts(&acquiredGroup.contexts, acquired.contexts)
if len(diagnostics) != 0 {
return acquiredGroup, projectPriorInputDiagnosticPaths(diagnostics, acquiredGroup.scopes)
}
if acquired.document == nil && len(acquired.acquisition) == 0 {
continue
}
anchors := importNamespaceAnchors{
kind: "host", host: explicitNamespaceAnchor(index, 0),
input: explicitNamespaceAnchor(index, inputIndex+1),
}
if token := nativeTokenForLoadInput(contribution); token != nil {
anchors.kind = "native"
anchors.native = nativeAnchors[token]
}
inputScopes := cloneAcquisitionScopes(acquired.acquisition)
inputGraph := newImportInclusionGraph()
if acquired.composition != nil {
inputGraph = acquired.composition.importGraph
}
inputGraph, keyMap := reanchorImportGraph(inputGraph, anchors)
for scopeIndex := range inputScopes {
reanchorAcquisitionScope(&inputScopes[scopeIndex], keyMap, anchors)
}
acquiredGroup.scopes = append(acquiredGroup.scopes, inputScopes...)
mergeImportInclusionGraph(&inclusionGraph, inputGraph)
}
acquiredGroup.importGraph = inclusionGraph.clone()
acquiredGroup.inputBytes = accounting.bytes - initialBytes
acquiredGroup.inputNodes = accounting.nodes - initialNodes
return acquiredGroup, nil
}
func composeUnderlayGroup(acquired *acquiredUnderlayGroup, rule *VersionRule, policy sequencePolicy, lower collectionPolicyScope) (*composedDocument, []Diagnostic) {
if acquired == nil {
return nil, nil
}
groupScope := scheduleAcquisitionGroup(acquired.scopes, acquired.importGraph)
if len(groupScope.contributions) == 0 {
return nil, nil
}
groupScope.owner = importNodeKey{namespaceKind: "group", namespaceAnchor: explicitNamespaceAnchor(acquired.index, 0), resourceKind: "scope"}
for contributionIndex := range groupScope.contributions {
groupScope.contributions[contributionIndex].scopeOwner = groupScope.owner
}
accounting := &importAccounting{
sources: len(acquired.sources), bytes: acquired.inputBytes, nodes: acquired.inputNodes,
}
composition, diagnostics := composeAcquiredScopeWithPolicy(
&groupScope, lower, policy, acquired.importGraph, acquired.sources, nil, nil, accounting,
)
if len(diagnostics) != 0 {
return nil, diagnostics
}
var documents []*document.Node
for _, contribution := range groupScope.contributions {
documents = append(documents, contribution.document)
}
if composition != nil && rule != nil {
ranges := indexSequenceRanges(composition.sequenceRanges)
for sourceIndex, input := range documents {
if groupScope.contributions[sourceIndex].role == importRoleMain {
continue
}
if node := fragmentVersionClaimWithIndex(input, rule, sourceIndex, ranges); node != nil {
location := publicLocation(node.Location)
return nil, []Diagnostic{{
Code: CodeImportVersion, Severity: SeverityError, Path: rule.Path.clone(),
Message: "imported fragments cannot contribute to the schema-version path", Primary: &location,
}}
}
}
for sourceIndex, input := range documents {
if node := fragmentVersionClaimWithIndex(input, rule, sourceIndex, ranges); node != nil {
location := publicLocation(node.Location)
return nil, []Diagnostic{{
Code: CodeUnderlayVersion, Severity: SeverityError, Path: rule.Path.clone(),
Message: "underlay inputs cannot contribute to the schema-version path", Primary: &location,
}}
}
}
}
composition.inputBytes = acquired.inputBytes
composition.inputNodes = acquired.inputNodes
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, nil
}
func nativeTokenForLoadInput(input loadInput) *namespaceToken {
if input.source.kind == sourceFilesystem && input.source.namespace != nil {
return input.source.namespace.token
}
if input.filesystem != nil && input.filesystem.FS != nil && input.filesystem.FS.namespace != nil {
return input.filesystem.FS.namespace.token
}
return nil
}