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 }