Files
ConfigKit/internal/engine/load_input.go

380 lines
16 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 {
source Source
parsed parsedSource
document *document.Node
composition *composedDocument
acquisition []acquisitionScope
inheritance *inheritanceScope
sources []SourceInfo
contexts map[string]ResourceContext
versionState *sourceVersionState
accounting *importAccounting
}
// 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
sequenceDeclarations []sequenceDeclarationSpec
nested *composedDocument
pendingItem *acquisitionScope
placementCharged bool
}
// acquisitionScope is the ordered strict mapping scope produced by one input
// and its ordinary imports.
type acquisitionScope struct {
owner importNodeKey
destination Path
contributions []acquiredContribution
collectionPolicies collectionPolicyScope
}
func cloneAcquisitionScope(scope acquisitionScope) acquisitionScope {
cloned := acquisitionScope{
owner: scope.owner, destination: scope.destination.clone(),
collectionPolicies: cloneCollectionPolicyScope(scope.collectionPolicies),
}
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,
sequenceDeclarations: cloneSequenceDeclarations(contribution.sequenceDeclarations),
nested: cloneComposedDocument(contribution.nested), pendingItem: cloneAcquisitionScopePointer(contribution.pendingItem),
placementCharged: contribution.placementCharged,
}
}
}
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) {
sourceID := "source-" + strconv.Itoa(accounting.sources+1)
selectedSource := source
if source.kind == sourceSearch {
resolved, failedSource, diagnostics := resolveSearchInput(source, sourceID, defaultDiscoveryDirectoryHelpers(), defaultDiscoveryHostProbe())
if len(diagnostics) != 0 {
if failedSource != nil {
failedSource.Destination = destination.clone()
failedSource.placementMode = options.placementMode
}
return assembledInput{source: resolved, sources: sourceInfos(failedSource), versionState: versionState, accounting: accounting}, diagnostics
}
selectedSource = resolved
if options.Filesystem != nil {
if diagnostics := filesystemOptionDiagnostics(selectedSource, options.Filesystem); len(diagnostics) != 0 {
return assembledInput{source: selectedSource, versionState: versionState, accounting: accounting}, diagnostics
}
}
if selectedSource.kind == sourceNone && activeOverlay(options.Overlay) && options.Filesystem == nil {
return assembledInput{source: selectedSource, versionState: versionState, accounting: accounting}, []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "NoSource overlays require explicit filesystem context")}
}
}
limits := defaultImportLimits()
parseLimits := defaultParseLimits()
parseLimits.source.remaining = limits.bytes - accounting.bytes
parsed, diagnostics := parseSource(selectedSource, sourceID, parseLimits)
if parsed.source != nil {
parsed.source.Destination = destination.clone()
parsed.source.placementMode = options.placementMode
}
input := assembledInput{source: selectedSource, parsed: parsed, document: parsed.document, sources: sourceInfos(parsed.source), versionState: versionState, accounting: accounting}
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 selectedSource.kind == sourceMapping && selectedSource.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
}
directiveContext := options.directiveContext
if directiveContext == 0 {
directiveContext = rootDirectiveContextImportsOnly
}
if !options.Imports && parsed.document != nil {
var directiveDiagnostics []Diagnostic
if directiveContext == rootDirectiveContextSource || directiveContext == rootDirectiveContextLayered {
directiveDiagnostics = disabledRootImportDiagnostics(parsed.document)
} else {
directiveDiagnostics = disabledDirectiveDiagnostics(parsed.document, Path{})
}
if len(directiveDiagnostics) != 0 {
return input, directiveDiagnostics
}
}
var versionProcessing *sourceVersionProcessingResult
if directiveContext == rootDirectiveContextSource {
var sourceID string
if parsed.source != nil {
sourceID = parsed.source.ID
}
var processed sourceVersionProcessingResult
var failures []Diagnostic
if versionState != nil {
processed, failures = versionState.processWithDirectiveContext(parsed.document, role, destination, sourceID, options.placementMode, directiveContext)
} else {
processed = processSourceVersionWithDirectiveContext(parsed.document, options.Definition.Version, role, destination, sourceVersionModeDefault, directiveContext)
failures = processed.diagnostics
}
if len(failures) != 0 {
return input, failures
}
versionProcessing = &processed
if role == sourceVersionMain && versionState != nil {
versionState.mainProcessing = &processed
}
} else if directiveContext == rootDirectiveContextLayered {
extracted, failures := extractRootDirectives(parsed.document, rootDirectiveContextLayered)
if len(failures) != 0 {
return input, failures
}
processed := sourceVersionProcessingResult{
payload: extracted.body, imports: extracted.imports, sequences: extracted.sequences,
directiveContext: rootDirectiveContextLayered,
}
if versionState != nil && versionState.mode != sourceVersionModeDefault {
var sourceID string
if parsed.source != nil {
sourceID = parsed.source.ID
}
versionProcessed, versionFailures := versionState.process(extracted.body, role, destination, sourceID, options.placementMode)
if len(versionFailures) != 0 {
return input, versionFailures
}
versionProcessed.imports = extracted.imports
versionProcessed.sequences = append(cloneSequenceDeclarations(extracted.sequences), versionProcessed.sequences...)
versionProcessed.directiveContext = rootDirectiveContextLayered
processed = versionProcessed
if role == sourceVersionMain {
versionState.mainProcessing = &processed
}
}
versionProcessing = &processed
} else 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 && selectedSource.kind != sourceNone {
if options.deferAssembly {
input.composition, input.sources, diagnostics = loadImportedDocumentsTrackedWithProcessingDeferred(selectedSource, options.Filesystem, parsed, destination, options.Definition.Version, limits, accounting, visited, hostToken, versionState, versionProcessing, options.sequencePolicy, true)
} else {
input.composition, input.sources, diagnostics = loadImportedDocumentsTrackedWithProcessing(selectedSource, options.Filesystem, parsed, destination, options.Definition.Version, limits, accounting, visited, hostToken, versionState, versionProcessing, options.sequencePolicy)
}
if len(diagnostics) != 0 {
return input, diagnostics
}
if input.composition != nil {
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 && (selectedSource.kind == sourceNone || !activeOverlay(options.Overlay)) {
if diagnostic := validateFilesystemContext(selectedSource, 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)
var sequenceDeclarations []sequenceDeclarationSpec
if versionProcessing != nil {
sequenceDeclarations = cloneSequenceDeclarations(versionProcessing.sequences)
}
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,
sequenceDeclarations: sequenceDeclarations,
}},
}}
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(selectedSource, 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(selectedSource, 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) {
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, options.sequencePolicy,
)
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)
}
input.composition = assembly.current
input.document = assembly.current.root
return input, nil
}
// compositionForInput initializes the structural handoff used by inheritance
// expansion. 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) {
return compositionForInputWithPolicies(input, collectionPolicyScope{}, policy)
}
func compositionForInputWithPolicies(input assembledInput, lower collectionPolicyScope, policy sequencePolicy) (*composedDocument, []Diagnostic) {
if input.composition != nil {
if input.composition.root == nil && len(input.composition.acquisition) != 0 {
return composeAcquiredInputWithPolicies(input, lower, policy)
}
return input.composition, nil
}
if len(input.acquisition) != 0 {
acquired := &composedDocument{acquisition: cloneAcquisitionScopes(input.acquisition)}
if input.accounting != nil {
acquired.inputBytes = input.accounting.bytes
acquired.inputNodes = input.accounting.nodes
} else {
acquired.inputBytes = input.parsed.bytes
acquired.inputNodes = countInputNodes(input.parsed.document)
}
input.composition = acquired
return composeAcquiredInputWithPolicies(input, lower, policy)
}
if input.document == nil {
return &composedDocument{root: &document.Node{Kind: document.Mapping}, origins: make(map[string]Origin)}, nil
}
composition, diagnostics := composeDocumentsWithCollectionPolicy(policy, lower, nil, 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
}