270 lines
11 KiB
Go
270 lines
11 KiB
Go
package engine
|
|
|
|
import (
|
|
"strconv"
|
|
|
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
|
)
|
|
|
|
// assembledInput is the source/document handoff across schema and version checks.
|
|
type assembledInput struct {
|
|
parsed parsedSource
|
|
document *document.Node
|
|
composition *composedDocument
|
|
acquisition []acquisitionScope
|
|
inheritance *inheritanceScope
|
|
sources []SourceInfo
|
|
contexts map[string]ResourceContext
|
|
versionState *sourceVersionState
|
|
}
|
|
|
|
// acquiredContribution keeps a source-local payload beside its placed document.
|
|
// The latter is used for composition; the former preserves the acquisition
|
|
// boundary for later scoped assembly and evidence.
|
|
type acquiredContribution struct {
|
|
source SourceInfo
|
|
identity importNodeKey
|
|
role importContributionRole
|
|
scopeOwner importNodeKey
|
|
payload *document.Node
|
|
document *document.Node
|
|
destination Path
|
|
relativeDestination Path
|
|
processing *sourceVersionProcessingResult
|
|
nested *composedDocument
|
|
pendingItem *acquisitionScope
|
|
}
|
|
|
|
// acquisitionScope is the ordered strict mapping scope produced by one input
|
|
// and its ordinary imports.
|
|
type acquisitionScope struct {
|
|
owner importNodeKey
|
|
destination Path
|
|
contributions []acquiredContribution
|
|
}
|
|
|
|
func cloneAcquisitionScope(scope acquisitionScope) acquisitionScope {
|
|
cloned := acquisitionScope{owner: scope.owner, destination: scope.destination.clone()}
|
|
if scope.contributions != nil {
|
|
cloned.contributions = make([]acquiredContribution, len(scope.contributions))
|
|
for index, contribution := range scope.contributions {
|
|
cloned.contributions[index] = acquiredContribution{
|
|
source: cloneSourceInfo(contribution.source), identity: contribution.identity,
|
|
role: contribution.role, scopeOwner: contribution.scopeOwner, payload: contribution.payload,
|
|
document: contribution.document, destination: contribution.destination.clone(),
|
|
relativeDestination: contribution.relativeDestination.clone(), processing: contribution.processing,
|
|
nested: cloneComposedDocument(contribution.nested), pendingItem: cloneAcquisitionScopePointer(contribution.pendingItem),
|
|
}
|
|
}
|
|
}
|
|
return cloned
|
|
}
|
|
|
|
func cloneAcquisitionScopePointer(scope *acquisitionScope) *acquisitionScope {
|
|
if scope == nil {
|
|
return nil
|
|
}
|
|
cloned := cloneAcquisitionScope(*scope)
|
|
return &cloned
|
|
}
|
|
|
|
func cloneAcquisitionScopes(scopes []acquisitionScope) []acquisitionScope {
|
|
if scopes == nil {
|
|
return nil
|
|
}
|
|
cloned := make([]acquisitionScope, len(scopes))
|
|
for index, scope := range scopes {
|
|
cloned[index] = cloneAcquisitionScope(scope)
|
|
}
|
|
return cloned
|
|
}
|
|
|
|
func assembleBaseInput[T any](source Source, options Options[T]) (assembledInput, []Diagnostic) {
|
|
return assembleBaseInputTracked(source, options, Path{}, &importAccounting{}, nil, nil)
|
|
}
|
|
|
|
func assembleBaseInputTracked[T any](source Source, options Options[T], destination Path, accounting *importAccounting, visited map[resourceIdentity]importRoute, hostToken *namespaceToken) (assembledInput, []Diagnostic) {
|
|
return assembleBaseInputTrackedWithVersion(source, options, destination, accounting, visited, hostToken, nil)
|
|
}
|
|
|
|
func assembleBaseInputTrackedWithVersion[T any](source Source, options Options[T], destination Path, accounting *importAccounting, visited map[resourceIdentity]importRoute, hostToken *namespaceToken, versionState *sourceVersionState) (assembledInput, []Diagnostic) {
|
|
return assembleBaseInputTrackedForRole(source, options, destination, accounting, visited, hostToken, versionState, sourceVersionMain)
|
|
}
|
|
|
|
func assembleBaseInputTrackedForRole[T any](source Source, options Options[T], destination Path, accounting *importAccounting, visited map[resourceIdentity]importRoute, hostToken *namespaceToken, versionState *sourceVersionState, role sourceVersionRole) (assembledInput, []Diagnostic) {
|
|
limits := defaultImportLimits()
|
|
parseLimits := defaultParseLimits()
|
|
parseLimits.source.remaining = limits.bytes - accounting.bytes
|
|
parsed, diagnostics := parseSource(source, "source-"+strconv.Itoa(accounting.sources+1), parseLimits)
|
|
if parsed.source != nil {
|
|
parsed.source.Destination = destination.clone()
|
|
parsed.source.placementMode = options.placementMode
|
|
}
|
|
input := assembledInput{parsed: parsed, document: parsed.document, sources: sourceInfos(parsed.source), versionState: versionState}
|
|
if len(diagnostics) != 0 {
|
|
input.sources = sourceInfos(parsed.source)
|
|
// Mapping value issues already identify application content. Other early
|
|
// failures describe source syntax or access, not a placed value.
|
|
if source.kind == sourceMapping && source.mapIssue != nil && len(destination.segments) != 0 {
|
|
for index := range diagnostics {
|
|
relative := diagnostics[index].Path
|
|
segments := clonePathSegments(destination.segments, len(relative.segments))
|
|
diagnostics[index].Path = Path{segments: append(segments, relative.segments...)}
|
|
}
|
|
}
|
|
return input, diagnostics
|
|
}
|
|
if !options.Imports && parsed.document != nil {
|
|
if diagnostics := disabledDirectiveDiagnostics(parsed.document, Path{}); len(diagnostics) != 0 {
|
|
return input, diagnostics
|
|
}
|
|
}
|
|
var versionProcessing *sourceVersionProcessingResult
|
|
if versionState != nil && versionState.mode != sourceVersionModeDefault {
|
|
var sourceID string
|
|
if parsed.source != nil {
|
|
sourceID = parsed.source.ID
|
|
}
|
|
processed, failures := versionState.process(parsed.document, role, destination, sourceID, options.placementMode)
|
|
if len(failures) != 0 {
|
|
return input, failures
|
|
}
|
|
versionProcessing = &processed
|
|
if role == sourceVersionMain {
|
|
versionState.mainProcessing = &processed
|
|
}
|
|
}
|
|
if options.Imports {
|
|
input.composition, input.sources, diagnostics = loadImportedDocumentsTrackedWithProcessing(source, options.Filesystem, parsed, destination, options.Definition.Version, limits, accounting, visited, hostToken, versionState, versionProcessing, options.sequencePolicy)
|
|
if len(diagnostics) != 0 {
|
|
return input, diagnostics
|
|
}
|
|
input.acquisition = cloneAcquisitionScopes(input.composition.acquisition)
|
|
input.document = input.composition.root
|
|
} else {
|
|
input.sources = sourceInfos(parsed.source)
|
|
if versionProcessing != nil {
|
|
input.document = versionProcessing.payload
|
|
}
|
|
if options.Filesystem != nil && !activeOverlay(options.Overlay) {
|
|
if diagnostic := validateFilesystemContext(source, options.Filesystem, parsed); diagnostic != nil {
|
|
return input, []Diagnostic{*diagnostic}
|
|
}
|
|
}
|
|
}
|
|
if !options.Imports && input.document != nil {
|
|
_, placed, err := placeDocumentWithMode(input.document, destination, options.placementMode, input.document.Location, parseLimits.document.Depth, parseLimits.document.Nodes)
|
|
if err != nil {
|
|
return input, []Diagnostic{sourceDiagnostic(CodeSourceLimitExceeded, parsed.source.ID, "configuration exceeds the source depth or node limit after destination placement")}
|
|
}
|
|
localPayload := input.document
|
|
input.document = placed
|
|
identity := importKeyForMainSource(parsed.source)
|
|
input.acquisition = []acquisitionScope{{
|
|
owner: identity,
|
|
destination: destination.clone(),
|
|
contributions: []acquiredContribution{{
|
|
source: cloneSourceInfo(*parsed.source), payload: localPayload,
|
|
identity: identity, role: importRoleMain, scopeOwner: identity,
|
|
document: placed, destination: destination.clone(), processing: versionProcessing,
|
|
}},
|
|
}}
|
|
rawNodes := countInputNodes(parsed.document)
|
|
placementNodes := destinationWrapperNodeCountForMode(destination, options.placementMode)
|
|
if accounting.sources+1 > limits.sources || accounting.bytes+parsed.bytes > limits.bytes || accounting.nodes+rawNodes+placementNodes > limits.nodes {
|
|
return input, []Diagnostic{sourceDiagnostic(CodeSourceLimitExceeded, parsed.source.ID, "configuration exceeds the aggregate input limit")}
|
|
}
|
|
if diagnostic := registerAcquiredSource(source, options.Filesystem, parsed.source, visited, hostToken); diagnostic != nil {
|
|
return input, []Diagnostic{*diagnostic}
|
|
}
|
|
accounting.sources++
|
|
accounting.bytes += parsed.bytes
|
|
accounting.nodes += rawNodes + placementNodes
|
|
}
|
|
if !activeOverlay(options.Overlay) || options.Imports {
|
|
contexts, diagnostic := contextsForAcquiredSources(source, options.Filesystem, parsed.source, input.sources)
|
|
if diagnostic != nil {
|
|
return input, []Diagnostic{*diagnostic}
|
|
}
|
|
input.contexts = contexts
|
|
}
|
|
return input, nil
|
|
}
|
|
|
|
func registerAcquiredSource(source Source, context *FilesystemContext, info *SourceInfo, visited map[resourceIdentity]importRoute, hostToken *namespaceToken) *Diagnostic {
|
|
if visited == nil || info == nil || info.Path == "" {
|
|
return nil
|
|
}
|
|
identity := resourceIdentity{namespace: hostToken, path: info.Path}
|
|
if source.kind == sourceFilesystem {
|
|
identity.namespace = source.namespace.token
|
|
} else if context != nil && context.FS != nil {
|
|
identity.namespace = context.FS.namespace.token
|
|
}
|
|
if previous, repeated := visited[identity]; repeated {
|
|
value := importRepeatedDiagnostic(Location{SourceID: info.ID}, previous, importRoute{})
|
|
return &value
|
|
}
|
|
visited[identity] = importRoute{canonical: info.Path}
|
|
return nil
|
|
}
|
|
|
|
func applyInputLayers[T any](source Source, options Options[T], input assembledInput) (assembledInput, []Diagnostic) {
|
|
if !activeOverlay(options.Overlay) && len(options.Overrides) == 0 {
|
|
return input, nil
|
|
}
|
|
composition, diagnostics := compositionForInputWithPolicy(input, options.sequencePolicy)
|
|
if len(diagnostics) != 0 {
|
|
return input, diagnostics
|
|
}
|
|
assembly := newAssemblyAccumulator(composition)
|
|
if activeOverlay(options.Overlay) {
|
|
input.sources, diagnostics = loadSelectedOverlayIntoWithVersion(
|
|
source, options.Filesystem, input.parsed, assembly, input.sources,
|
|
options.Overlay, options.Definition.Version, defaultImportLimits(), input.versionState,
|
|
)
|
|
if len(diagnostics) != 0 {
|
|
return input, diagnostics
|
|
}
|
|
contexts, diagnostic := contextsForAcquiredSources(source, options.Filesystem, input.parsed.source, input.sources[len(input.sources)-1:])
|
|
if diagnostic != nil {
|
|
return input, []Diagnostic{*diagnostic}
|
|
}
|
|
mergeResourceContexts(&input.contexts, contexts)
|
|
}
|
|
if len(options.Overrides) != 0 {
|
|
input.sources, diagnostics = assembly.applyOverrides(
|
|
input.sources, options.Overrides, options.Definition.Version, defaultImportLimits(),
|
|
)
|
|
if len(diagnostics) != 0 {
|
|
return input, diagnostics
|
|
}
|
|
}
|
|
input.composition = assembly.current
|
|
input.document = assembly.current.root
|
|
return input, nil
|
|
}
|
|
|
|
// compositionForInput initializes one handoff for overlays and CLI, including
|
|
// defaults-only CLI input. Raw source bytes and mapping keys retain their budgets.
|
|
func compositionForInput(input assembledInput) (*composedDocument, []Diagnostic) {
|
|
return compositionForInputWithPolicy(input, sequencePolicy{})
|
|
}
|
|
|
|
func compositionForInputWithPolicy(input assembledInput, policy sequencePolicy) (*composedDocument, []Diagnostic) {
|
|
if input.composition != nil {
|
|
return input.composition, nil
|
|
}
|
|
if input.document == nil {
|
|
return &composedDocument{root: &document.Node{Kind: document.Mapping}, origins: make(map[string]Origin)}, nil
|
|
}
|
|
composition, diagnostics := composeDocumentsWithPolicy(policy, input.document)
|
|
if len(diagnostics) != 0 {
|
|
return nil, diagnostics
|
|
}
|
|
composition.inputBytes = input.parsed.bytes
|
|
composition.inputNodes = countInputNodes(input.document)
|
|
composition.acquisition = cloneAcquisitionScopes(input.acquisition)
|
|
return composition, nil
|
|
}
|