Implement shared sequence operations
This commit is contained in:
256
internal/engine/sequence_combine.go
Normal file
256
internal/engine/sequence_combine.go
Normal file
@@ -0,0 +1,256 @@
|
||||
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,
|
||||
}
|
||||
Reference in New Issue
Block a user