253 lines
8.4 KiB
Go
253 lines
8.4 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
|
|
}
|
|
|
|
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,
|
|
}
|