package engine import "gitea.maximumdirect.net/eric/ConfigKit/internal/document" type sequenceCombinationContext struct { destination Path } type sequenceItemProjection struct { inputIndex int outputIndex int } type sequenceItemReplacement struct { lowerIndex int higherIndex int outputIndex int } type sequenceCombinationResult struct { value *document.Node lowerItems []sequenceItemProjection higherItems []sequenceItemProjection replacements []sequenceItemReplacement appendedHigher []int } func combineSequence( lower, higher *document.Node, policy collectionPolicyResolution, context sequenceCombinationContext, ) (sequenceCombinationResult, []Diagnostic) { if higher == nil { return unchangedSequenceValue(lower), nil } switch policy.operation { case sequenceModeReplace: return replaceSequence(higher, policy.effectiveIdentityFields, context) case sequenceModeAppend, sequenceModeUpsert: return combineIdentifiedSequences(lower, higher, policy.operation, policy.effectiveIdentityFields, context) default: return sequenceCombinationResult{}, []Diagnostic{sequenceCombinationDiagnostic( context.destination, "sequence operation is invalid", higher, )} } } func unchangedSequenceValue(lower *document.Node) sequenceCombinationResult { result := sequenceCombinationResult{value: document.Clone(lower)} if lower != nil && lower.Kind == document.Sequence { result.lowerItems = sequenceItemProjections(len(lower.Sequence), 0) } return result } func replaceSequence( higher *document.Node, identityFields []string, context sequenceCombinationContext, ) (sequenceCombinationResult, []Diagnostic) { if higher.Kind == document.Sequence && len(identityFields) != 0 { if _, diagnostics := validateSequenceIdentity(higher, context.destination, identityFields, sequenceCombinationIdentityCodes); len(diagnostics) != 0 { return sequenceCombinationResult{}, diagnostics } } result := sequenceCombinationResult{value: document.Clone(higher)} if higher.Kind == document.Sequence { result.higherItems = sequenceItemProjections(len(higher.Sequence), 0) } return result, nil } func combineIdentifiedSequences( lower, higher *document.Node, operation sequenceMode, identityFields []string, context sequenceCombinationContext, ) (sequenceCombinationResult, []Diagnostic) { if higher.Kind != document.Sequence { return sequenceCombinationResult{}, []Diagnostic{sequenceCombinationDiagnostic( context.destination, "append and upsert require a sequence contribution", higher, )} } if lower != nil && lower.Kind != document.Sequence { return sequenceCombinationResult{}, []Diagnostic{sequenceCombinationDiagnostic( context.destination, "append and upsert require a lower sequence or an absent lower value", lower, )} } if len(identityFields) == 0 { return sequenceCombinationResult{}, []Diagnostic{sequenceCombinationDiagnostic( context.destination, "append and upsert require an identity definition", higher, )} } higherIndex, diagnostics := validateSequenceIdentity(higher, context.destination, identityFields, sequenceCombinationIdentityCodes) if len(diagnostics) != 0 { return sequenceCombinationResult{}, diagnostics } var lowerIndex map[string]sequenceIdentityRecord if lower != nil { lowerIndex, diagnostics = validateSequenceIdentity(lower, context.destination, identityFields, sequenceCombinationIdentityCodes) if len(diagnostics) != 0 { return sequenceCombinationResult{}, diagnostics } } if operation == sequenceModeAppend { if duplicate := duplicateAcrossSequenceContributions(lowerIndex, higherIndex, context.destination); duplicate != nil { return sequenceCombinationResult{}, []Diagnostic{*duplicate} } } if operation == sequenceModeAppend { return appendSequenceItems(lower, higher), nil } if operation == sequenceModeUpsert { return upsertSequenceItems(lower, higher, lowerIndex, higherIndex), nil } return sequenceCombinationResult{}, []Diagnostic{sequenceCombinationDiagnostic( context.destination, "sequence operation is invalid", higher, )} } func appendSequenceItems(lower, higher *document.Node) sequenceCombinationResult { lowerCount := 0 if lower != nil { lowerCount = len(lower.Sequence) } items := make([]*document.Node, 0, lowerCount+len(higher.Sequence)) if lower != nil { items = append(items, lower.Sequence...) } items = append(items, higher.Sequence...) return sequenceCombinationResult{ value: cloneSequenceWithItems(higher, items), lowerItems: sequenceItemProjections(lowerCount, 0), higherItems: sequenceItemProjections(len(higher.Sequence), lowerCount), appendedHigher: sequenceIndexes(len(higher.Sequence)), } } func upsertSequenceItems( lower, higher *document.Node, lowerIndex, higherIndex map[string]sequenceIdentityRecord, ) sequenceCombinationResult { lowerCount := 0 var lowerItems []*document.Node if lower != nil { lowerCount = len(lower.Sequence) lowerItems = lower.Sequence } matchedLower := make(map[int]sequenceIdentityRecord, len(higherIndex)) matchedLowerByHigher := make([]int, len(higher.Sequence)) for index := range matchedLowerByHigher { matchedLowerByHigher[index] = -1 } for tuple, incoming := range higherIndex { if retained, exists := lowerIndex[tuple]; exists { matchedLower[retained.index] = incoming matchedLowerByHigher[incoming.index] = retained.index } } items := make([]*document.Node, lowerCount, lowerCount+len(higher.Sequence)) result := sequenceCombinationResult{ lowerItems: make([]sequenceItemProjection, 0, lowerCount-len(matchedLower)), higherItems: sequenceItemProjections(len(higher.Sequence), 0), } for lowerPosition, item := range lowerItems { if incoming, replaced := matchedLower[lowerPosition]; replaced { items[lowerPosition] = higher.Sequence[incoming.index] result.higherItems[incoming.index].outputIndex = lowerPosition result.replacements = append(result.replacements, sequenceItemReplacement{ lowerIndex: lowerPosition, higherIndex: incoming.index, outputIndex: lowerPosition, }) continue } items[lowerPosition] = item result.lowerItems = append(result.lowerItems, sequenceItemProjection{inputIndex: lowerPosition, outputIndex: lowerPosition}) } for higherPosition := range higher.Sequence { if matchedLowerByHigher[higherPosition] >= 0 { continue } outputIndex := len(items) items = append(items, higher.Sequence[higherPosition]) result.appendedHigher = append(result.appendedHigher, higherPosition) result.higherItems[higherPosition].outputIndex = outputIndex } result.value = cloneSequenceWithItems(higher, items) return result } func duplicateAcrossSequenceContributions( lowerIndex, higherIndex map[string]sequenceIdentityRecord, path Path, ) *Diagnostic { higherTuples := make([]string, len(higherIndex)) for tuple, record := range higherIndex { higherTuples[record.index] = tuple } for _, tuple := range higherTuples { if lower, exists := lowerIndex[tuple]; exists { higher := higherIndex[tuple] primary := publicLocation(lower.location) related := publicLocation(higher.location) return &Diagnostic{ Code: CodeSequenceConflict, Severity: SeverityError, Path: path.clone(), Message: "sequence contributions contain a duplicate identity", Primary: &primary, Related: []Location{related}, } } } return nil } func cloneSequenceWithItems(template *document.Node, items []*document.Node) *document.Node { cloned := *template cloned.Mapping = nil cloned.Sequence = make([]*document.Node, len(items)) for index, item := range items { cloned.Sequence[index] = document.Clone(item) } return &cloned } func sequenceItemProjections(length, outputOffset int) []sequenceItemProjection { projections := make([]sequenceItemProjection, length) for index := range projections { projections[index] = sequenceItemProjection{inputIndex: index, outputIndex: index + outputOffset} } return projections } func sequenceIndexes(length int) []int { indexes := make([]int, length) for index := range indexes { indexes[index] = index } return indexes } func sequenceCombinationDiagnostic(path Path, message string, node *document.Node) Diagnostic { var primary *Location if node != nil { location := publicLocation(node.Location) primary = &location } return Diagnostic{Code: CodeSequenceInvalid, Severity: SeverityError, Path: path.clone(), Message: message, Primary: primary} } var sequenceCombinationIdentityCodes = sequenceIdentityDiagnosticCodes{ invalid: CodeSequenceInvalid, conflict: CodeSequenceConflict, }