377 lines
15 KiB
Go
377 lines
15 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) {
|
|
return loadGroupedWithSecretLookupLimit(request, secrets, lookup, historyNodeLimit)
|
|
}
|
|
|
|
func loadGroupedWithSecretLookupLimit[T any](request loadRequest[T], secrets SecretBinding, lookup secretLookup, historyLimit int) (*Loaded[T], error) {
|
|
request.ordinary = false
|
|
defaults, policy, diagnostics := loadSettings(request.importDefaults, request.definition, request.sourceVersionMode)
|
|
request.importDefaults = defaults
|
|
request.sequencePolicy = policy
|
|
request.definition.sequenceIdentityRegistrations = nil
|
|
main, mainDiagnostics := normalizeInput(request.main, "")
|
|
request.main = main
|
|
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]
|
|
normalized, inputDiagnostics := normalizeInput(*input, "")
|
|
*input = normalized
|
|
diagnostics = append(diagnostics, inputDiagnostics...)
|
|
}
|
|
}
|
|
diagnostics = append(diagnostics, overlayOptionDiagnostics(request.overlay)...)
|
|
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, historyLimit)
|
|
}
|
|
|
|
func assembleLayeredInput[T any](request loadRequest[T], input assembledInput, policy acquisitionPolicy, state *acquisitionState) (assembledInput, []Diagnostic) {
|
|
var assembly *assemblyAccumulator
|
|
mainSourceCount := len(input.sources)
|
|
mainEvidence := input.versionState.evidenceSince(0)
|
|
projectVersion := sequenceVersionPath(request.protectionVersion())
|
|
for index, group := range request.underlays {
|
|
evidenceStart := len(input.versionState.evidenceSince(0))
|
|
acquired, diagnostics := acquireUnderlayGroupScopes(group, index+1, request.protectionVersion(), state, policy)
|
|
mergeResourceContexts(&input.contexts, acquired.contexts)
|
|
if len(diagnostics) != 0 {
|
|
input.sources = append(input.sources, acquired.sources...)
|
|
return input, diagnostics
|
|
}
|
|
var lower collectionPolicyScope
|
|
if assembly != nil && assembly.current != nil {
|
|
lower = assembly.current.collectionPolicies
|
|
}
|
|
protectionRule := request.protectionVersion()
|
|
if projectVersion {
|
|
// The main item's final position is known only after tier assembly.
|
|
// Source-local checks still run during acquisition above.
|
|
protectionRule = nil
|
|
}
|
|
composition, diagnostics := composeUnderlayGroup(acquired, protectionRule, policy.sequencePolicy, lower)
|
|
sourceStart := len(input.sources)
|
|
input.sources = append(input.sources, acquired.sources...)
|
|
if len(diagnostics) != 0 {
|
|
return input, diagnostics
|
|
}
|
|
if composition != nil {
|
|
_, diagnostics = applyCompletedTier(
|
|
&assembly, composition, composition.collectionPolicies, LayerUnderlay, policy.sequencePolicy,
|
|
input.sources[sourceStart:], input.versionState.evidenceSince(evidenceStart),
|
|
)
|
|
if len(diagnostics) != 0 {
|
|
return input, diagnostics
|
|
}
|
|
}
|
|
}
|
|
|
|
mainPresent := input.document != nil || input.composition != nil || len(input.acquisition) != 0
|
|
var mainComposition *composedDocument
|
|
if mainPresent {
|
|
var lower collectionPolicyScope
|
|
if assembly != nil && assembly.current != nil {
|
|
lower = assembly.current.collectionPolicies
|
|
}
|
|
var diagnostics []Diagnostic
|
|
mainComposition, diagnostics = compositionForInputWithPolicies(input, lower, policy.sequencePolicy)
|
|
if len(diagnostics) != 0 {
|
|
return input, diagnostics
|
|
}
|
|
}
|
|
var versionDiagnostics []Diagnostic
|
|
if request.sourceVersionMode == sourceVersionModeDefault {
|
|
var mainRoot *document.Node
|
|
if mainComposition != nil {
|
|
mainRoot = mainComposition.root
|
|
} else if input.document != nil {
|
|
mainRoot = input.document
|
|
}
|
|
versionDiagnostics = checkVersion(request.definition.Version, mainRoot)
|
|
if hasErrors(versionDiagnostics) {
|
|
return input, versionDiagnostics
|
|
}
|
|
}
|
|
if mainComposition != nil {
|
|
input.versionState.locateMainVersion(mainComposition.root)
|
|
operations, diagnostics := applyCompletedTier(
|
|
&assembly, mainComposition, mainComposition.collectionPolicies, LayerBase, policy.sequencePolicy,
|
|
input.sources[:mainSourceCount], mainEvidence,
|
|
)
|
|
if len(diagnostics) != 0 {
|
|
return input, append(versionDiagnostics, diagnostics...)
|
|
}
|
|
input.versionState.projectMainVersion(operations)
|
|
}
|
|
if input.versionState != nil {
|
|
request.effectiveVersion = input.versionState.effectiveRule
|
|
}
|
|
if projectVersion && assembly != nil {
|
|
if diagnostic := projectedUnderlayVersionDiagnostic(assembly.current.applications, input.sources, request.protectionVersion()); diagnostic != nil {
|
|
return input, append(versionDiagnostics, *diagnostic)
|
|
}
|
|
}
|
|
if assembly == nil && activeOverlay(request.overlay) {
|
|
composition, _ := compositionForInput(assembledInput{})
|
|
assembly = newAssemblyAccumulator(composition)
|
|
}
|
|
if assembly == nil {
|
|
return input, versionDiagnostics
|
|
}
|
|
assembly.current.inputBytes = state.accounting.bytes
|
|
assembly.current.inputNodes = state.accounting.nodes
|
|
if activeOverlay(request.overlay) {
|
|
if request.overlayFilesystem == nil {
|
|
return input, append(versionDiagnostics, optionDiagnostic(CodeOptionsInvalid, "selected overlay requires explicit filesystem context"))
|
|
}
|
|
resolved, diagnostic := resolveAbsentFilesystemContext(request.overlayFilesystem)
|
|
if diagnostic != nil {
|
|
return input, append(versionDiagnostics, *diagnostic)
|
|
}
|
|
if diagnostic := validateResolvedFilesystemDirectories(resolved); diagnostic != nil {
|
|
return input, append(versionDiagnostics, *diagnostic)
|
|
}
|
|
overlayRequest := selectedOverlayRequest{
|
|
name: request.overlay, protectionRule: request.protectionVersion(),
|
|
sequencePolicy: policy.sequencePolicy, limits: defaultImportLimits(), versionState: input.versionState,
|
|
}
|
|
var diagnostics []Diagnostic
|
|
access, diagnostic := openSelectedOverlayAccess(resolved)
|
|
if diagnostic != nil {
|
|
return input, append(versionDiagnostics, *diagnostic)
|
|
}
|
|
input.sources, diagnostics = loadSelectedOverlay(overlayRequest, access, assembly, input.sources)
|
|
if len(diagnostics) != 0 {
|
|
return input, append(versionDiagnostics, diagnostics...)
|
|
}
|
|
contexts, diagnostic := contextsForResolvedSources(&resolved, input.sources[len(input.sources)-1:])
|
|
if diagnostic != nil {
|
|
return input, append(versionDiagnostics, *diagnostic)
|
|
}
|
|
mergeResourceContexts(&input.contexts, contexts)
|
|
}
|
|
input.composition = assembly.current
|
|
input.document = assembly.current.root
|
|
return input, versionDiagnostics
|
|
}
|
|
|
|
// applyCompletedTier joins one already-composed precedence tier to the load's
|
|
// assembly and projects only that tier's source/version evidence through the
|
|
// resulting sequence operations. Callers select the tier-local evidence ranges
|
|
// so lower-tier and main-tier coordinates remain distinct.
|
|
func applyCompletedTier(
|
|
assembly **assemblyAccumulator,
|
|
composition *composedDocument,
|
|
incomingPolicies collectionPolicyScope,
|
|
layer Layer,
|
|
sequencePolicy sequencePolicy,
|
|
incomingSources []SourceInfo,
|
|
incomingVersions []*sourceVersionEvidence,
|
|
) ([]sequenceHistoryContribution, []Diagnostic) {
|
|
if composition == nil {
|
|
return nil, nil
|
|
}
|
|
if *assembly == nil {
|
|
*assembly = newAssemblyAccumulator(composition)
|
|
return nil, nil
|
|
}
|
|
|
|
var declarations []sequenceDeclarationSpec
|
|
for _, scope := range composition.acquisition {
|
|
declarations = append(declarations, acquiredSequenceDeclarations(scope)...)
|
|
}
|
|
operations, diagnostics := (*assembly).applyOverlayWithCollectionProjection(
|
|
composition.root, composition.origins, incomingPolicies, composition.applications, layer,
|
|
sequencePolicy, declarations,
|
|
)
|
|
if len(diagnostics) != 0 {
|
|
return nil, diagnostics
|
|
}
|
|
projectTierSourceDestinations(incomingSources, incomingVersions, operations)
|
|
return operations, nil
|
|
}
|
|
|
|
func acquireUnderlayGroupScopes(group loadGroup, index int, rule *VersionRule, state *acquisitionState, policy acquisitionPolicy) (*acquiredUnderlayGroup, []Diagnostic) {
|
|
initialBytes, initialNodes := state.accounting.bytes, state.accounting.nodes
|
|
tier := &tierAcquisitionState{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 {
|
|
acquired, diagnostics := acquireInput(inputAcquisitionRequest{
|
|
input: contribution, role: sourceVersionContribution,
|
|
directiveContext: rootDirectiveContextLayered, protectionRule: rule,
|
|
}, policy, state, tier)
|
|
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 = state.accounting.bytes - initialBytes
|
|
acquiredGroup.inputNodes = state.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
|
|
}
|