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 := compileSequenceIdentityPolicy(request.definition.sequenceIdentityRegistrations) request.sequencePolicy = policy diagnostics = append(diagnostics, policyDiagnostics...) request.definition.sequenceIdentityRegistrations = 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) { var assembly *assemblyAccumulator 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 } var lower collectionPolicyScope if assembly != nil && assembly.current != nil { lower = assembly.current.collectionPolicies } composition, diagnostics := composeUnderlayGroup(acquired, request.protectionVersion(), request.sequencePolicy, lower) input.sources = append(input.sources, acquired.sources...) if len(diagnostics) != 0 { return input, diagnostics } if composition != nil { if assembly == nil { assembly = newAssemblyAccumulator(composition) } else { var declarations []sequenceDeclarationSpec for _, scope := range composition.acquisition { declarations = append(declarations, acquiredSequenceDeclarations(scope)...) } if diagnostics := assembly.applyOverlayWithCollectionPolicy( composition.root, composition.origins, composition.collectionPolicies, composition.applications, LayerUnderlay, request.sequencePolicy, declarations, ); 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, request.sequencePolicy) if len(diagnostics) != 0 { return input, diagnostics } } if request.sourceVersionMode == sourceVersionModeDefault { var mainRoot *document.Node if mainComposition != nil { mainRoot = mainComposition.root } else if input.document != nil { mainRoot = input.document } if diagnostics := checkVersion(request.definition.Version, mainRoot); hasErrors(diagnostics) { return input, diagnostics } } if mainComposition != nil { if assembly == nil { assembly = newAssemblyAccumulator(mainComposition) } else { var declarations []sequenceDeclarationSpec for _, scope := range mainComposition.acquisition { declarations = append(declarations, acquiredSequenceDeclarations(scope)...) } if diagnostics := assembly.applyOverlayWithCollectionPolicy( mainComposition.root, mainComposition.origins, mainComposition.collectionPolicies, mainComposition.applications, LayerBase, request.sequencePolicy, declarations, ); 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 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 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, directiveContext: rootDirectiveContextLayered, 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 }