600 lines
21 KiB
Go
600 lines
21 KiB
Go
package engine
|
|
|
|
import (
|
|
"maps"
|
|
|
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
|
)
|
|
|
|
// composedDocument is the private handoff between source assembly and later
|
|
// precedence layers. Claims remain ordered by source and declaration traversal;
|
|
// origins retain every source that contributes to a composite mapping.
|
|
type composedDocument struct {
|
|
root *document.Node
|
|
acquisition []acquisitionScope
|
|
importGraph importInclusionGraph
|
|
collectionPolicies collectionPolicyScope
|
|
claims []compositionClaim
|
|
sequenceRanges []sequenceRange
|
|
origins map[string]Origin
|
|
overlays []overlayRecord
|
|
applications []historyApplication
|
|
inputBytes int64
|
|
inputNodes int
|
|
}
|
|
|
|
type sequenceHistoryMode uint8
|
|
|
|
const (
|
|
sequenceHistoryReplace sequenceHistoryMode = iota + 1
|
|
sequenceHistoryAppend
|
|
sequenceHistoryMapped
|
|
)
|
|
|
|
type sequenceHistoryContribution struct {
|
|
path Path
|
|
mode sequenceHistoryMode
|
|
offset int
|
|
length int
|
|
items []sequenceItemProjection
|
|
replacements []sequenceItemReplacement
|
|
}
|
|
|
|
// assemblyAccumulator owns one mutable working document. Acquired trees held by
|
|
// history applications are separate from its root and remain unchanged.
|
|
type assemblyAccumulator struct {
|
|
current *composedDocument
|
|
index mappingIndex
|
|
}
|
|
|
|
func newAssemblyAccumulator(owned *composedDocument) *assemblyAccumulator {
|
|
return &assemblyAccumulator{current: owned}
|
|
}
|
|
|
|
// cloneComposedDocument detaches mutable composition and provenance state;
|
|
// history application trees are immutable and retain their existing sharing.
|
|
func cloneComposedDocument(base *composedDocument) *composedDocument {
|
|
if base == nil {
|
|
return nil
|
|
}
|
|
return &composedDocument{
|
|
root: document.Clone(base.root), claims: cloneCompositionClaims(base.claims),
|
|
acquisition: cloneAcquisitionScopes(base.acquisition),
|
|
importGraph: base.importGraph.clone(),
|
|
collectionPolicies: cloneCollectionPolicyScope(base.collectionPolicies),
|
|
sequenceRanges: cloneSequenceRanges(base.sequenceRanges),
|
|
origins: cloneOriginIndex(base.origins), overlays: cloneOverlayRecords(base.overlays),
|
|
applications: cloneHistoryApplications(base.applications),
|
|
inputBytes: base.inputBytes, inputNodes: base.inputNodes,
|
|
}
|
|
}
|
|
|
|
type compositionClaim struct {
|
|
node *document.Node // borrowed immutable contribution, not the assembly tree
|
|
path Path
|
|
kind document.Kind
|
|
location Location
|
|
sourceIndex int
|
|
}
|
|
|
|
type compositionClaims struct {
|
|
ordered []compositionClaim
|
|
byPath map[string][]compositionClaim
|
|
}
|
|
|
|
// sequenceRange records where one source's sequence items were assigned in the
|
|
// composed sequence at path. End is exclusive.
|
|
type sequenceRange struct {
|
|
sourceIndex int
|
|
path Path
|
|
start int
|
|
end int
|
|
items []sequenceItemProjection
|
|
projection sequenceItemProjectionIndex
|
|
}
|
|
|
|
func (assigned sequenceRange) outputForInput(inputIndex int) (int, bool) {
|
|
if assigned.projection.outputByInput != nil {
|
|
return assigned.projection.outputForInput(inputIndex)
|
|
}
|
|
if assigned.items != nil {
|
|
output, found := 0, false
|
|
for _, item := range assigned.items {
|
|
if item.inputIndex == inputIndex && inputIndex >= 0 && item.outputIndex >= 0 {
|
|
output, found = item.outputIndex, true
|
|
}
|
|
}
|
|
return output, found
|
|
}
|
|
if inputIndex < 0 || inputIndex >= assigned.end-assigned.start {
|
|
return 0, false
|
|
}
|
|
return assigned.start + inputIndex, true
|
|
}
|
|
|
|
func (assigned sequenceRange) inputForOutput(outputIndex int) (int, bool) {
|
|
if assigned.projection.inputByOutput != nil {
|
|
return assigned.projection.inputForOutput(outputIndex)
|
|
}
|
|
if assigned.items != nil {
|
|
input, found := 0, false
|
|
for _, item := range assigned.items {
|
|
if item.outputIndex == outputIndex && outputIndex >= 0 && item.inputIndex >= 0 {
|
|
input, found = item.inputIndex, true
|
|
}
|
|
}
|
|
return input, found
|
|
}
|
|
if outputIndex < assigned.start || outputIndex >= assigned.end {
|
|
return 0, false
|
|
}
|
|
return outputIndex - assigned.start, true
|
|
}
|
|
|
|
func indexSequenceRange(assigned sequenceRange) sequenceRange {
|
|
if assigned.items != nil && assigned.projection.outputByInput == nil && assigned.projection.inputByOutput == nil {
|
|
assigned.projection = newSequenceItemProjectionIndex(assigned.items)
|
|
}
|
|
return assigned
|
|
}
|
|
|
|
// composeDocuments recursively joins disjoint mapping entries. Source order
|
|
// controls only output traversal and diagnostics: every non-mapping overlap is
|
|
// rejected before a result is assembled.
|
|
func composeDocuments(inputs ...*document.Node) (*composedDocument, []Diagnostic) {
|
|
return composeDocumentsWithPolicy(sequencePolicy{}, inputs...)
|
|
}
|
|
|
|
// composeDocumentsWithPolicy composes one strict tier using the registered
|
|
// sequence rules when policy contains registrations. The ordinary adapter above
|
|
// deliberately preserves the existing unregistered behavior for current callers.
|
|
func composeDocumentsWithPolicy(policy sequencePolicy, inputs ...*document.Node) (*composedDocument, []Diagnostic) {
|
|
return composeDocumentsWithCollectionPolicy(policy, collectionPolicyScope{}, nil, inputs...)
|
|
}
|
|
|
|
// composeDocumentsWithCollectionPolicy resolves collection declarations and
|
|
// eligible lower policy before strictly composing completed peer documents.
|
|
func composeDocumentsWithCollectionPolicy(
|
|
policy sequencePolicy,
|
|
lower collectionPolicyScope,
|
|
declarations []sequenceDeclarationSpec,
|
|
inputs ...*document.Node,
|
|
) (*composedDocument, []Diagnostic) {
|
|
if len(inputs) == 0 {
|
|
return nil, []Diagnostic{compositionInvalidDiagnostic("strict composition requires at least one document", nil)}
|
|
}
|
|
|
|
collected := compositionClaims{byPath: make(map[string][]compositionClaim)}
|
|
for sourceIndex, input := range inputs {
|
|
if input == nil || input.Kind != document.Mapping {
|
|
var location *Location
|
|
if input != nil {
|
|
converted := publicLocation(input.Location)
|
|
location = &converted
|
|
}
|
|
return nil, []Diagnostic{compositionInvalidDiagnostic("strict composition requires mapping document roots", location)}
|
|
}
|
|
collected.append(input, Path{}, sourceIndex)
|
|
}
|
|
|
|
identityFieldsByPath, collectionPolicies, diagnostics := resolveStrictCollectionPolicies(
|
|
collected.ordered, policy, lower, declarations,
|
|
)
|
|
if len(diagnostics) != 0 {
|
|
return nil, diagnostics
|
|
}
|
|
|
|
checked := make(map[string]struct{}, len(collected.byPath))
|
|
for _, claim := range collected.ordered {
|
|
pointer := claim.path.String()
|
|
if _, seen := checked[pointer]; seen {
|
|
continue
|
|
}
|
|
checked[pointer] = struct{}{}
|
|
claims := collected.byPath[pointer]
|
|
if len(claims) < 2 || allMappingClaims(claims) || (allSequenceClaims(claims) && len(identityFieldsByPath[pointer]) != 0) {
|
|
continue
|
|
}
|
|
return nil, []Diagnostic{compositionConflictDiagnostic(claim.path, claims)}
|
|
}
|
|
|
|
validated, ranges, diagnostics := validateRegisteredSequences(collected.ordered, identityFieldsByPath)
|
|
if len(diagnostics) != 0 {
|
|
return nil, diagnostics
|
|
}
|
|
|
|
root := document.Clone(inputs[0])
|
|
index := new(mappingIndex)
|
|
for sourceIndex, input := range inputs[1:] {
|
|
mergeComposedMappings(root, input, Path{}, index, sourceIndex+1, identityFieldsByPath, validated)
|
|
}
|
|
applications := make([]historyApplication, 0, len(inputs))
|
|
seenSequences := make(map[string]bool)
|
|
rangesBySource := make([][]sequenceRange, len(inputs))
|
|
for _, assigned := range ranges {
|
|
rangesBySource[assigned.sourceIndex] = append(rangesBySource[assigned.sourceIndex], assigned)
|
|
}
|
|
for sourceIndex, input := range inputs {
|
|
application := historyApplication{layer: LayerBase, value: input}
|
|
for _, assigned := range rangesBySource[sourceIndex] {
|
|
pointer := assigned.path.String()
|
|
mode := sequenceHistoryAppend
|
|
if !seenSequences[pointer] {
|
|
mode = sequenceHistoryReplace
|
|
}
|
|
seenSequences[pointer] = true
|
|
application.sequences = append(application.sequences, sequenceHistoryContribution{
|
|
path: assigned.path.clone(), mode: mode, offset: assigned.start, length: assigned.end - assigned.start,
|
|
})
|
|
}
|
|
application.sequencesByPath = indexSequenceHistoryContributions(application.sequences)
|
|
applications = append(applications, application)
|
|
}
|
|
origins := projectedCompositionOrigins(inputs, ranges)
|
|
completeCompositionOrigins(root, Path{}, origins)
|
|
return &composedDocument{
|
|
root: root,
|
|
collectionPolicies: collectionPolicies,
|
|
claims: cloneCompositionClaims(collected.ordered),
|
|
sequenceRanges: cloneSequenceRanges(ranges),
|
|
origins: origins,
|
|
applications: applications,
|
|
}, nil
|
|
}
|
|
|
|
// Conflict claims treat sequences atomically, but inspection needs origins for
|
|
// every descendant. Preserve composite mapping claims while filling those gaps.
|
|
func completeCompositionOrigins(node *document.Node, path Path, origins map[string]Origin) {
|
|
pointer := path.String()
|
|
if _, present := origins[pointer]; !present {
|
|
origins[pointer] = Origin{Layer: LayerBase, Locations: []Location{publicLocation(node.Location)}}
|
|
}
|
|
switch node.Kind {
|
|
case document.Mapping:
|
|
for _, entry := range node.Mapping {
|
|
completeCompositionOrigins(entry.Value, path.Key(entry.Key), origins)
|
|
}
|
|
case document.Sequence:
|
|
for index, child := range node.Sequence {
|
|
completeCompositionOrigins(child, path.Index(index), origins)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *compositionClaims) append(node *document.Node, path Path, sourceIndex int) {
|
|
claim := compositionClaim{
|
|
node: node,
|
|
path: path.clone(),
|
|
kind: node.Kind,
|
|
location: publicLocation(node.Location),
|
|
sourceIndex: sourceIndex,
|
|
}
|
|
c.ordered = append(c.ordered, claim)
|
|
pointer := path.String()
|
|
c.byPath[pointer] = append(c.byPath[pointer], claim)
|
|
|
|
if node.Kind != document.Mapping {
|
|
return
|
|
}
|
|
for _, entry := range node.Mapping {
|
|
c.append(entry.Value, path.Key(entry.Key), sourceIndex)
|
|
}
|
|
}
|
|
|
|
func allSequenceClaims(claims []compositionClaim) bool {
|
|
for _, claim := range claims {
|
|
if claim.kind != document.Sequence {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
type validatedSequence struct {
|
|
items []*document.Node
|
|
}
|
|
|
|
func resolveStrictCollectionPolicies(
|
|
claims []compositionClaim,
|
|
applicationPolicy sequencePolicy,
|
|
lower collectionPolicyScope,
|
|
declarations []sequenceDeclarationSpec,
|
|
) (map[string][]string, collectionPolicyScope, []Diagnostic) {
|
|
paths := make([]Path, 0, len(claims)+len(declarations))
|
|
seen := make(map[string]bool, len(claims)+len(declarations))
|
|
valuePresent := make(map[string]bool)
|
|
declarationsByPath := make(map[string][]sequenceDeclarationSpec, len(declarations))
|
|
for _, claim := range claims {
|
|
if len(lower.collections) != 0 {
|
|
valuePresent[claim.path.String()] = true
|
|
}
|
|
if claim.kind != document.Sequence {
|
|
continue
|
|
}
|
|
key := claim.path.String()
|
|
if !seen[key] {
|
|
seen[key] = true
|
|
paths = append(paths, claim.path.clone())
|
|
}
|
|
}
|
|
for _, declaration := range declarations {
|
|
key := declaration.destination.String()
|
|
declarationsByPath[key] = append(declarationsByPath[key], declaration)
|
|
if !seen[key] {
|
|
seen[key] = true
|
|
paths = append(paths, declaration.destination.clone())
|
|
}
|
|
}
|
|
|
|
identityFieldsByPath := make(map[string][]string)
|
|
effectiveLower := lower
|
|
if len(lower.collections) != 0 {
|
|
effectiveLower, _ = lowerCollectionPoliciesForClaims(lower, claims, declarationsByPath, applicationPolicy)
|
|
}
|
|
collectionPolicies := cloneCollectionPolicyScope(effectiveLower)
|
|
var diagnostics []Diagnostic
|
|
for _, path := range paths {
|
|
key := path.String()
|
|
currentDeclarations := declarationsByPath[key]
|
|
|
|
lowerEntry, hasLower := effectiveLower.collections[key]
|
|
var lowerPolicy *collectionPolicy
|
|
if hasLower {
|
|
copy := cloneCollectionPolicy(lowerEntry.policy)
|
|
lowerPolicy = ©
|
|
}
|
|
applicationIdentity, _ := applicationPolicy.identityFieldsAt(path)
|
|
|
|
resolution, found := resolveCollectionPolicy(path, currentDeclarations, applicationIdentity, lowerPolicy)
|
|
if len(found) != 0 {
|
|
diagnostics = append(diagnostics, found...)
|
|
continue
|
|
}
|
|
if len(resolution.effectiveIdentityFields) != 0 {
|
|
identityFieldsByPath[key] = cloneStrings(resolution.effectiveIdentityFields)
|
|
}
|
|
|
|
policyForPath := resolution.policy
|
|
if hasLower && !valuePresent[key] {
|
|
// A declaration without a new value changes policy metadata but
|
|
// leaves retained record ownership untouched.
|
|
policyForPath = cloneCollectionPolicy(lowerEntry.policy)
|
|
policyForPath.identityFields = cloneStrings(resolution.policy.identityFields)
|
|
policyForPath.identityPresent = resolution.policy.identityPresent
|
|
policyForPath.identityLocation = cloneLocation(resolution.policy.identityLocation)
|
|
policyForPath.explicitMode = resolution.policy.explicitMode
|
|
policyForPath.explicitModePresent = resolution.policy.explicitModePresent
|
|
policyForPath.modeLocation = cloneLocation(resolution.policy.modeLocation)
|
|
}
|
|
if collectionPolicies.collections == nil {
|
|
collectionPolicies.collections = make(map[string]collectionPolicyEntry)
|
|
}
|
|
collectionPolicies.collections[key] = collectionPolicyEntry{destination: path.clone(), policy: policyForPath}
|
|
}
|
|
return identityFieldsByPath, collectionPolicies, canonicalDiagnostics(diagnostics)
|
|
}
|
|
|
|
func validateRegisteredSequences(claims []compositionClaim, identityFieldsByPath map[string][]string) (map[string]map[int]validatedSequence, []sequenceRange, []Diagnostic) {
|
|
itemsByPath := make(map[string]map[int]validatedSequence)
|
|
identitiesByPath := make(map[string]map[string]document.Location)
|
|
lengthsByPath := make(map[string]int)
|
|
var ranges []sequenceRange
|
|
for _, claim := range claims {
|
|
if claim.kind != document.Sequence {
|
|
continue
|
|
}
|
|
identityFields := identityFieldsByPath[claim.path.String()]
|
|
if len(identityFields) == 0 {
|
|
continue
|
|
}
|
|
node := claim.node
|
|
pathKey := claim.path.String()
|
|
identities := identitiesByPath[pathKey]
|
|
if identities == nil {
|
|
identities = make(map[string]document.Location, len(node.Sequence))
|
|
identitiesByPath[pathKey] = identities
|
|
}
|
|
start := lengthsByPath[pathKey]
|
|
contribution := validatedSequence{items: node.Sequence}
|
|
records, diagnostics := validateNormalizedSequenceIdentityFrom(node, claim.path, identityFields, sequenceIdentityDiagnosticCodes{
|
|
invalid: CodeComposeInvalid, conflict: CodeComposeConflict,
|
|
}, start, sequenceStructuralRecord)
|
|
if len(diagnostics) != 0 {
|
|
return nil, nil, diagnostics
|
|
}
|
|
byIndex := make([]sequenceIdentityRecord, len(node.Sequence))
|
|
for _, record := range records {
|
|
byIndex[record.index] = record
|
|
}
|
|
for _, record := range byIndex {
|
|
if first, duplicate := identities[record.tuple]; duplicate {
|
|
current := publicLocation(record.location)
|
|
return nil, nil, []Diagnostic{{
|
|
Code: CodeComposeConflict, Severity: SeverityError,
|
|
Path: claim.path.clone(), Message: "registered sequence contains duplicate identities",
|
|
Primary: ptrLocation(publicLocation(first)), Related: []Location{current},
|
|
}}
|
|
}
|
|
identities[record.tuple] = record.location
|
|
}
|
|
if itemsByPath[pathKey] == nil {
|
|
itemsByPath[pathKey] = make(map[int]validatedSequence)
|
|
}
|
|
itemsByPath[pathKey][claim.sourceIndex] = contribution
|
|
lengthsByPath[pathKey] = start + len(node.Sequence)
|
|
ranges = append(ranges, sequenceRange{
|
|
sourceIndex: claim.sourceIndex, path: claim.path.clone(), start: start, end: start + len(node.Sequence),
|
|
})
|
|
}
|
|
return itemsByPath, ranges, nil
|
|
}
|
|
|
|
func mergeComposedMappings(target, incoming *document.Node, path Path, index *mappingIndex, sourceIndex int, identityFieldsByPath map[string][]string, validated map[string]map[int]validatedSequence) {
|
|
for _, incomingEntry := range incoming.Mapping {
|
|
entryPath := path.Key(incomingEntry.Key)
|
|
position := index.find(target, incomingEntry.Key)
|
|
if position < 0 {
|
|
cloned := incomingEntry
|
|
cloned.Value = document.Clone(incomingEntry.Value)
|
|
index.append(target, cloned)
|
|
continue
|
|
}
|
|
current := target.Mapping[position].Value
|
|
if current.Kind == document.Sequence && incomingEntry.Value.Kind == document.Sequence && len(identityFieldsByPath[entryPath.String()]) != 0 {
|
|
contribution := validated[entryPath.String()][sourceIndex]
|
|
if contribution.items != nil {
|
|
for _, item := range contribution.items {
|
|
current.Sequence = append(current.Sequence, document.Clone(item))
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
mergeDisjointMappingsAtPath(current, incomingEntry.Value, entryPath, index, sourceIndex, identityFieldsByPath, validated)
|
|
}
|
|
}
|
|
|
|
func mergeDisjointMappingsAtPath(target, incoming *document.Node, path Path, index *mappingIndex, sourceIndex int, identityFieldsByPath map[string][]string, validated map[string]map[int]validatedSequence) {
|
|
if target.Kind != document.Mapping || incoming.Kind != document.Mapping {
|
|
return
|
|
}
|
|
mergeComposedMappings(target, incoming, path, index, sourceIndex, identityFieldsByPath, validated)
|
|
}
|
|
|
|
func cloneSequenceRanges(ranges []sequenceRange) []sequenceRange {
|
|
cloned := make([]sequenceRange, len(ranges))
|
|
for i, assigned := range ranges {
|
|
cloned[i] = assigned
|
|
cloned[i].path = assigned.path.clone()
|
|
if assigned.items != nil {
|
|
cloned[i].items = append([]sequenceItemProjection{}, assigned.items...)
|
|
}
|
|
cloned[i].projection.outputByInput = maps.Clone(assigned.projection.outputByInput)
|
|
cloned[i].projection.inputByOutput = maps.Clone(assigned.projection.inputByOutput)
|
|
}
|
|
return cloned
|
|
}
|
|
|
|
func allMappingClaims(claims []compositionClaim) bool {
|
|
for _, claim := range claims {
|
|
if claim.kind != document.Mapping {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func mergeDisjointMappings(target, incoming *document.Node, index *mappingIndex) {
|
|
for _, incomingEntry := range incoming.Mapping {
|
|
position := index.find(target, incomingEntry.Key)
|
|
if position < 0 {
|
|
cloned := incomingEntry
|
|
cloned.Value = document.Clone(incomingEntry.Value)
|
|
index.append(target, cloned)
|
|
continue
|
|
}
|
|
// Claim validation guarantees that the only remaining overlap is a
|
|
// mapping pair. Empty mappings naturally contribute no child entries.
|
|
mergeDisjointMappings(target.Mapping[position].Value, incomingEntry.Value, index)
|
|
}
|
|
}
|
|
|
|
func documentEntryIndex(entries []document.Entry, key string) int {
|
|
for index := range entries {
|
|
if entries[index].Key == key {
|
|
return index
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func projectedCompositionOrigins(inputs []*document.Node, ranges []sequenceRange) map[string]Origin {
|
|
origins := make(map[string]Origin)
|
|
sequencesBySource := make([][]sequenceHistoryContribution, len(inputs))
|
|
for _, assigned := range ranges {
|
|
contribution := sequenceHistoryContribution{
|
|
path: assigned.path.clone(), mode: sequenceHistoryAppend,
|
|
offset: assigned.start, length: assigned.end - assigned.start,
|
|
}
|
|
if assigned.items != nil {
|
|
contribution.mode = sequenceHistoryMapped
|
|
contribution.items = assigned.items
|
|
}
|
|
sequencesBySource[assigned.sourceIndex] = append(sequencesBySource[assigned.sourceIndex], contribution)
|
|
}
|
|
for sourceIndex, input := range inputs {
|
|
application := historyApplication{value: input, sequences: sequencesBySource[sourceIndex]}
|
|
application.sequencesByPath = indexSequenceHistoryContributions(application.sequences)
|
|
walkHistoryApplication(application, func(_, effective Path, node *document.Node) {
|
|
pointer := effective.String()
|
|
origin := origins[pointer]
|
|
origin.Layer = LayerBase
|
|
origin.Locations = append(origin.Locations, publicLocation(node.Location))
|
|
origins[pointer] = origin
|
|
})
|
|
}
|
|
return origins
|
|
}
|
|
|
|
type sequenceRangeKey struct {
|
|
sourceIndex int
|
|
path string
|
|
}
|
|
|
|
type sequenceRangeIndex map[sequenceRangeKey]sequenceRange
|
|
|
|
func indexSequenceRanges(ranges []sequenceRange) sequenceRangeIndex {
|
|
if len(ranges) == 0 {
|
|
return nil
|
|
}
|
|
index := make(sequenceRangeIndex, len(ranges))
|
|
for _, assigned := range ranges {
|
|
assigned = indexSequenceRange(assigned)
|
|
index[sequenceRangeKey{sourceIndex: assigned.sourceIndex, path: assigned.path.String()}] = assigned
|
|
}
|
|
return index
|
|
}
|
|
|
|
func applyCompositionOrigins(decoded *decodedConfiguration, origins map[string]Origin) {
|
|
for pointer, origin := range origins {
|
|
if _, present := decoded.presence[pointer]; present {
|
|
decoded.origins[pointer] = cloneOrigin(origin)
|
|
}
|
|
}
|
|
}
|
|
|
|
func cloneCompositionClaims(claims []compositionClaim) []compositionClaim {
|
|
cloned := make([]compositionClaim, len(claims))
|
|
for index, claim := range claims {
|
|
cloned[index] = claim
|
|
cloned[index].path = claim.path.clone()
|
|
}
|
|
return cloned
|
|
}
|
|
|
|
func compositionInvalidDiagnostic(message string, location *Location) Diagnostic {
|
|
return Diagnostic{
|
|
Code: CodeComposeInvalid, Severity: SeverityError, Message: message,
|
|
Primary: cloneLocation(location),
|
|
}
|
|
}
|
|
|
|
func compositionConflictDiagnostic(path Path, claims []compositionClaim) Diagnostic {
|
|
primary := claims[0].location
|
|
related := make([]Location, 0, len(claims)-1)
|
|
for _, claim := range claims[1:] {
|
|
related = append(related, claim.location)
|
|
}
|
|
return Diagnostic{
|
|
Code: CodeComposeConflict,
|
|
Severity: SeverityError,
|
|
Path: path.clone(),
|
|
Message: "configuration path is defined by multiple sources",
|
|
Primary: &primary,
|
|
Related: related,
|
|
}
|
|
}
|
|
|
|
func publicLocation(location document.Location) Location {
|
|
return Location{SourceID: location.SourceID, Line: location.Line, Column: location.Column}
|
|
}
|