Files
ConfigKit/internal/engine/sequence_combine.go

257 lines
8.6 KiB
Go

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
discardedLower []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(lower, 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(
lower, 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)
}
if lower != nil && lower.Kind == document.Sequence {
result.discardedLower = sequenceIndexes(len(lower.Sequence))
}
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,
}