Project sequence evidence through item mappings
This commit is contained in:
@@ -83,6 +83,48 @@ type sequenceRange struct {
|
||||
path Path
|
||||
start int
|
||||
end int
|
||||
items []sequenceItemProjection
|
||||
projection sequenceItemProjectionIndex
|
||||
}
|
||||
|
||||
func (assigned sequenceRange) projections() []sequenceItemProjection {
|
||||
if assigned.items != nil {
|
||||
return append([]sequenceItemProjection{}, assigned.items...)
|
||||
}
|
||||
return sequenceItemProjections(assigned.end-assigned.start, assigned.start)
|
||||
}
|
||||
|
||||
func (assigned sequenceRange) outputForInput(inputIndex int) (int, bool) {
|
||||
if assigned.projection.outputByInput != nil {
|
||||
return assigned.projection.outputForInput(inputIndex)
|
||||
}
|
||||
if assigned.items != nil {
|
||||
return newSequenceItemProjectionIndex(assigned.items).outputForInput(inputIndex)
|
||||
}
|
||||
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 {
|
||||
return newSequenceItemProjectionIndex(assigned.items).inputForOutput(outputIndex)
|
||||
}
|
||||
if outputIndex < assigned.start || outputIndex >= assigned.end {
|
||||
return 0, false
|
||||
}
|
||||
return outputIndex - assigned.start, true
|
||||
}
|
||||
|
||||
func indexSequenceRange(assigned sequenceRange) sequenceRange {
|
||||
if assigned.projection.outputByInput == nil && assigned.projection.inputByOutput == nil {
|
||||
assigned.projection = newSequenceItemProjectionIndex(assigned.projections())
|
||||
}
|
||||
return assigned
|
||||
}
|
||||
|
||||
// composeDocuments recursively joins disjoint mapping entries. Source order
|
||||
@@ -371,6 +413,11 @@ func cloneSequenceRanges(ranges []sequenceRange) []sequenceRange {
|
||||
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 = append([]int(nil), assigned.projection.outputByInput...)
|
||||
cloned[i].projection.inputByOutput = append([]int(nil), assigned.projection.inputByOutput...)
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
@@ -410,12 +457,22 @@ func documentEntryIndex(entries []document.Entry, key string) int {
|
||||
|
||||
func projectedCompositionOrigins(inputs []*document.Node, ranges []sequenceRange) map[string]Origin {
|
||||
origins := make(map[string]Origin)
|
||||
rangeBySourcePath := make(map[sequenceRangeKey]int, len(ranges))
|
||||
for index, assigned := range ranges {
|
||||
rangeBySourcePath[sequenceRangeKey{sourceIndex: assigned.sourceIndex, path: assigned.path.String()}] = index
|
||||
sequencesBySource := make([][]sequenceHistoryContribution, len(inputs))
|
||||
for _, assigned := range ranges {
|
||||
sequencesBySource[assigned.sourceIndex] = append(sequencesBySource[assigned.sourceIndex], sequenceHistoryContribution{
|
||||
path: assigned.path.clone(), mode: sequenceHistoryMapped, items: assigned.projections(),
|
||||
})
|
||||
}
|
||||
for sourceIndex, input := range inputs {
|
||||
collectProjectedCompositionOrigins(input, Path{}, Path{}, sourceIndex, ranges, rangeBySourcePath, origins)
|
||||
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
|
||||
}
|
||||
@@ -433,39 +490,12 @@ func indexSequenceRanges(ranges []sequenceRange) sequenceRangeIndex {
|
||||
}
|
||||
index := make(sequenceRangeIndex, len(ranges))
|
||||
for _, assigned := range ranges {
|
||||
assigned = indexSequenceRange(assigned)
|
||||
index[sequenceRangeKey{sourceIndex: assigned.sourceIndex, path: assigned.path.String()}] = assigned
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
// Keep local paths for matching Stage 2 ranges and effective paths for emitted
|
||||
// evidence. The paths are transformed segment by segment; pointer strings are
|
||||
// only used as map keys after projection is complete.
|
||||
func collectProjectedCompositionOrigins(node *document.Node, localPath, effectivePath Path, sourceIndex int, ranges []sequenceRange, rangeBySourcePath map[sequenceRangeKey]int, origins map[string]Origin) {
|
||||
pointer := effectivePath.String()
|
||||
origin := origins[pointer]
|
||||
origin.Layer = LayerBase
|
||||
origin.Locations = append(origin.Locations, publicLocation(node.Location))
|
||||
origins[pointer] = origin
|
||||
|
||||
switch node.Kind {
|
||||
case document.Mapping:
|
||||
for _, entry := range node.Mapping {
|
||||
collectProjectedCompositionOrigins(entry.Value, localPath.Key(entry.Key), effectivePath.Key(entry.Key), sourceIndex, ranges, rangeBySourcePath, origins)
|
||||
}
|
||||
case document.Sequence:
|
||||
start := 0
|
||||
if rangeIndex, ok := rangeBySourcePath[sequenceRangeKey{sourceIndex: sourceIndex, path: localPath.String()}]; ok {
|
||||
start = ranges[rangeIndex].start
|
||||
}
|
||||
for index, child := range node.Sequence {
|
||||
localChild := localPath.Index(index)
|
||||
effectiveChild := effectivePath.Index(start + index)
|
||||
collectProjectedCompositionOrigins(child, localChild, effectiveChild, sourceIndex, ranges, rangeBySourcePath, origins)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func applyCompositionOrigins(decoded *decodedConfiguration, origins map[string]Origin) {
|
||||
for pointer, origin := range origins {
|
||||
if _, present := decoded.presence[pointer]; present {
|
||||
|
||||
@@ -357,6 +357,78 @@ escaped/~:
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectedCompositionOriginsUseNoncontiguousItemMappings(t *testing.T) {
|
||||
lowerRoot := parseCompositionDocument(t, "lower", `records:
|
||||
- id: first
|
||||
label: equal
|
||||
- id: middle
|
||||
- id: last
|
||||
nested: {old: lower}
|
||||
`)
|
||||
higherRoot := parseCompositionDocument(t, "higher", `records:
|
||||
- id: last
|
||||
nested: {new: higher}
|
||||
- id: first
|
||||
label: equal
|
||||
- id: added
|
||||
`)
|
||||
lower, _ := documentAtPath(lowerRoot, Keys("records"))
|
||||
higher, _ := documentAtPath(higherRoot, Keys("records"))
|
||||
combined, diagnostics := combineSequence(
|
||||
lower, higher, sequenceCombinationPolicy(sequenceModeUpsert, "id"),
|
||||
sequenceCombinationContext{destination: Keys("records")},
|
||||
)
|
||||
if len(diagnostics) != 0 {
|
||||
t.Fatalf("combine diagnostics = %#v", diagnostics)
|
||||
}
|
||||
inputs := []*document.Node{lowerRoot, higherRoot}
|
||||
ranges := []sequenceRange{
|
||||
{
|
||||
sourceIndex: 0, path: Keys("records"), start: 0, end: 3,
|
||||
items: combined.lowerItems,
|
||||
}, {
|
||||
sourceIndex: 1, path: Keys("records"), start: 0, end: 3,
|
||||
items: combined.higherItems,
|
||||
},
|
||||
}
|
||||
origins := projectedCompositionOrigins(inputs, ranges)
|
||||
if got, want := sequenceIdentityValues(combined.value, "id"), []string{"first", "middle", "last", "added"}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("combined IDs = %#v, want %#v", got, want)
|
||||
}
|
||||
for path, want := range map[string][]string{
|
||||
"/records/0/label": {"higher"},
|
||||
"/records/1/id": {"lower"},
|
||||
"/records/2/nested/new": {"higher"},
|
||||
"/records/3/id": {"higher"},
|
||||
"/records/0/nested/new": nil,
|
||||
"/records/2/nested/old": nil,
|
||||
} {
|
||||
origin := origins[path]
|
||||
var got []string
|
||||
for _, location := range origin.Locations {
|
||||
got = append(got, location.SourceID)
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("origin sources at %s = %#v, want %#v", path, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloneSequenceRangesPreservesPreparedProjection(t *testing.T) {
|
||||
ranges := []sequenceRange{{
|
||||
start: 0, end: 1,
|
||||
projection: newSequenceItemProjectionIndex([]sequenceItemProjection{{inputIndex: 0, outputIndex: 4}}),
|
||||
}}
|
||||
cloned := cloneSequenceRanges(ranges)
|
||||
if output, ok := cloned[0].outputForInput(0); !ok || output != 4 {
|
||||
t.Fatalf("cloned projection = %d, %v; want output index 4", output, ok)
|
||||
}
|
||||
cloned[0].projection.outputByInput[0] = 9
|
||||
if output, ok := ranges[0].outputForInput(0); !ok || output != 4 {
|
||||
t.Fatalf("cloned projection aliases original: %d, %v", output, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestComposeDocumentsValidatesRegisteredSequenceSingletons(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -165,10 +165,11 @@ func fragmentVersionClaimWithIndex(root *document.Node, rule *VersionRule, sourc
|
||||
return current
|
||||
}
|
||||
if assigned, found := ranges[sequenceRangeKey{sourceIndex: sourceIndex, path: currentPath.String()}]; found {
|
||||
if index < assigned.start || index >= assigned.end {
|
||||
projected, ok := assigned.inputForOutput(index)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
index -= assigned.start
|
||||
index = projected
|
||||
}
|
||||
if index < 0 || index >= len(current.Sequence) {
|
||||
return nil
|
||||
|
||||
@@ -273,8 +273,7 @@ func sequenceHistoryAt(contributions map[string]sequenceHistoryContributionIndex
|
||||
|
||||
type sequenceHistoryContributionIndex struct {
|
||||
sequenceHistoryContribution
|
||||
outputIndexes []int
|
||||
inputIndexes []int
|
||||
projection sequenceItemProjectionIndex
|
||||
replacedOutputs map[int]struct{}
|
||||
}
|
||||
|
||||
@@ -286,26 +285,7 @@ func indexSequenceHistoryContributions(values []sequenceHistoryContribution) map
|
||||
for _, value := range values {
|
||||
contribution := sequenceHistoryContributionIndex{sequenceHistoryContribution: value}
|
||||
if value.mode == sequenceHistoryMapped {
|
||||
maxOutput := -1
|
||||
for _, projection := range value.items {
|
||||
if projection.outputIndex > maxOutput {
|
||||
maxOutput = projection.outputIndex
|
||||
}
|
||||
}
|
||||
contribution.outputIndexes = make([]int, len(value.items))
|
||||
for inputIndex := range contribution.outputIndexes {
|
||||
contribution.outputIndexes[inputIndex] = -1
|
||||
}
|
||||
contribution.inputIndexes = make([]int, maxOutput+1)
|
||||
for outputIndex := range contribution.inputIndexes {
|
||||
contribution.inputIndexes[outputIndex] = -1
|
||||
}
|
||||
for _, projection := range value.items {
|
||||
if projection.inputIndex >= 0 && projection.inputIndex < len(contribution.outputIndexes) && projection.outputIndex >= 0 && projection.outputIndex < len(contribution.inputIndexes) {
|
||||
contribution.outputIndexes[projection.inputIndex] = projection.outputIndex
|
||||
contribution.inputIndexes[projection.outputIndex] = projection.inputIndex
|
||||
}
|
||||
}
|
||||
contribution.projection = newSequenceItemProjectionIndex(value.items)
|
||||
contribution.replacedOutputs = make(map[int]struct{}, len(value.replacements))
|
||||
for _, replacement := range value.replacements {
|
||||
contribution.replacedOutputs[replacement.outputIndex] = struct{}{}
|
||||
@@ -317,17 +297,19 @@ func indexSequenceHistoryContributions(values []sequenceHistoryContribution) map
|
||||
}
|
||||
|
||||
func (contribution sequenceHistoryContributionIndex) outputForInput(inputIndex int) int {
|
||||
if inputIndex < 0 || inputIndex >= len(contribution.outputIndexes) {
|
||||
outputIndex, ok := contribution.projection.outputForInput(inputIndex)
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
return contribution.outputIndexes[inputIndex]
|
||||
return outputIndex
|
||||
}
|
||||
|
||||
func (contribution sequenceHistoryContributionIndex) inputForOutput(outputIndex int) int {
|
||||
if outputIndex < 0 || outputIndex >= len(contribution.inputIndexes) {
|
||||
inputIndex, ok := contribution.projection.inputForOutput(outputIndex)
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
return contribution.inputIndexes[outputIndex]
|
||||
return inputIndex
|
||||
}
|
||||
|
||||
func applySequenceHistory(target **document.Node, incoming *document.Node, contribution sequenceHistoryContributionIndex) {
|
||||
|
||||
@@ -230,6 +230,77 @@ func TestReplacedProjectedSensitiveRecordRemainsRedacted(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMappedUpsertProjectsSensitiveReplacementEvidence(t *testing.T) {
|
||||
lowerRoot := parseCompositionDocument(t, "lower", `records:
|
||||
- id: first
|
||||
- id: middle
|
||||
- id: last
|
||||
token: MAPPED-SECRET-CANARY
|
||||
`)
|
||||
higherRoot := parseCompositionDocument(t, "higher", `records:
|
||||
- id: last
|
||||
token: visible
|
||||
- id: first
|
||||
- id: added
|
||||
`)
|
||||
lower, _ := documentAtPath(lowerRoot, Keys("records"))
|
||||
higher, _ := documentAtPath(higherRoot, Keys("records"))
|
||||
combined, diagnostics := combineSequence(
|
||||
lower, higher,
|
||||
sequenceCombinationPolicy(sequenceModeUpsert, "id"),
|
||||
sequenceCombinationContext{destination: Keys("records")},
|
||||
)
|
||||
if len(diagnostics) != 0 {
|
||||
t.Fatalf("combine diagnostics = %#v", diagnostics)
|
||||
}
|
||||
secretPath := Keys("records").Index(2).Key("token")
|
||||
secretNode, present := documentAtPath(lowerRoot, Keys("records").Index(2).Key("token"))
|
||||
if !present {
|
||||
t.Fatal("sensitive source token is missing")
|
||||
}
|
||||
secretNode.Sensitive = true
|
||||
application := historyApplication{
|
||||
layer: LayerOverlay, value: higherRoot,
|
||||
sequences: []sequenceHistoryContribution{{
|
||||
path: Keys("records"), mode: sequenceHistoryMapped,
|
||||
items: combined.higherItems, replacements: combined.replacements,
|
||||
}},
|
||||
}
|
||||
application.sequencesByPath = indexSequenceHistoryContributions(application.sequences)
|
||||
applications := []historyApplication{{value: lowerRoot}, application}
|
||||
replayed := &document.Node{Kind: document.Mapping}
|
||||
for _, current := range applications {
|
||||
applyHistoryApplication(&replayed, current, new(mappingIndex))
|
||||
}
|
||||
replayedRecords, _ := documentAtPath(replayed, Keys("records"))
|
||||
if !reflect.DeepEqual(replayedRecords.Sequence, combined.value.Sequence) {
|
||||
t.Fatalf("replayed value = %#v, want combination = %#v", replayedRecords.Sequence, combined.value.Sequence)
|
||||
}
|
||||
finalRoot := document.Clone(lowerRoot)
|
||||
finalRecords, _ := documentAtPath(finalRoot, Keys("records"))
|
||||
finalRecords.Sequence = document.Clone(combined.value).Sequence
|
||||
|
||||
var metadata Metadata
|
||||
finalizeMetadataPaths(&metadata, finalRoot, applications)
|
||||
inspection := &Inspection{
|
||||
metadata: metadata, baseline: &document.Node{Kind: document.Mapping},
|
||||
input: finalRoot, final: finalRoot, applications: applications,
|
||||
}
|
||||
secretRule := func(candidate Path) bool { return reflect.DeepEqual(candidate, secretPath) }
|
||||
inspection.secrets = classifySecretPaths(nil, secretRule, metadata, finalRoot, applications)
|
||||
explanation, ok := inspection.Explain(secretPath)
|
||||
if !ok || !explanation.Effective.Redacted {
|
||||
t.Fatalf("mapped replacement explanation = %#v, %v", explanation, ok)
|
||||
}
|
||||
var rendered bytes.Buffer
|
||||
if err := inspection.WriteExplanation(&rendered, secretPath); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(rendered.String(), "MAPPED-SECRET-CANARY") || !strings.Contains(rendered.String(), "[REDACTED]") {
|
||||
t.Fatalf("mapped replacement history leaked or was not redacted: %s", rendered.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSequenceHistoryReplaysCombinationEvidence(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -817,8 +817,13 @@ func projectNestedEvidence(composed *composedDocument, scope acquisitionScope) {
|
||||
projectedApplications = append(projectedApplications, applications[sourceIndex])
|
||||
continue
|
||||
}
|
||||
start := ranges[sequenceRangeKey{sourceIndex: sourceIndex, path: contribution.destination.String()}].start
|
||||
prefix := contribution.destination.Index(start)
|
||||
itemIndex := 0
|
||||
if assigned, found := ranges[sequenceRangeKey{sourceIndex: sourceIndex, path: contribution.destination.String()}]; found {
|
||||
if projected, ok := assigned.outputForInput(0); ok {
|
||||
itemIndex = projected
|
||||
}
|
||||
}
|
||||
prefix := contribution.destination.Index(itemIndex)
|
||||
if sourceIndex < len(applications) {
|
||||
outer := applications[sourceIndex]
|
||||
structural := document.Clone(outer.value)
|
||||
@@ -875,7 +880,12 @@ func projectAcquiredDestinations(composed *composedDocument, scope acquisitionSc
|
||||
}
|
||||
contributionPrefix := prefix
|
||||
contributionInsideItem := insideItem
|
||||
start := ranges[sequenceRangeKey{sourceIndex: sourceIndex, path: contribution.destination.String()}].start
|
||||
itemIndex := 0
|
||||
if assigned, found := ranges[sequenceRangeKey{sourceIndex: sourceIndex, path: contribution.destination.String()}]; found {
|
||||
if projected, ok := assigned.outputForInput(0); ok {
|
||||
itemIndex = projected
|
||||
}
|
||||
}
|
||||
if contribution.source.placementMode == placementModeSequenceItem {
|
||||
container := appendDestination(contributionPrefix, contribution.destination)
|
||||
contribution.source.Destination = container.clone()
|
||||
@@ -885,7 +895,7 @@ func projectAcquiredDestinations(composed *composedDocument, scope acquisitionSc
|
||||
sources[position].Destination = container
|
||||
sources[position].destinationUnresolved = false
|
||||
}
|
||||
contributionPrefix = appendDestination(container, Path{}.Index(start))
|
||||
contributionPrefix = appendDestination(container, Path{}.Index(itemIndex))
|
||||
contributionInsideItem = true
|
||||
projectSourceVersionEvidence(contribution.processing, contributionPrefix, nil, sourceIndex)
|
||||
} else if contributionInsideItem {
|
||||
@@ -928,7 +938,9 @@ func projectSourceVersionEvidence(processing *sourceVersionProcessingResult, des
|
||||
}
|
||||
item, ok := schemaIndex(path.segments[position])
|
||||
if ok && path.segments[position].kind != pathSegmentKey {
|
||||
path.segments[position] = pathSegment{kind: pathSegmentIndex, index: assigned.start + item}
|
||||
if outputIndex, projected := assigned.outputForInput(item); projected {
|
||||
path.segments[position] = pathSegment{kind: pathSegmentIndex, index: outputIndex}
|
||||
}
|
||||
}
|
||||
}
|
||||
paths[index] = path
|
||||
|
||||
@@ -525,6 +525,82 @@ func TestLoadMainItemPlacementKeepsItsHelpersInTheItem(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectAcquiredDestinationsUsesMappedSequenceItemIndex(t *testing.T) {
|
||||
child := &composedDocument{}
|
||||
childScope := acquisitionScope{contributions: []acquiredContribution{{
|
||||
source: SourceInfo{ID: "nested-source"}, destination: Keys("settings"),
|
||||
}}}
|
||||
child.acquisition = []acquisitionScope{childScope}
|
||||
composed := &composedDocument{sequenceRanges: []sequenceRange{{
|
||||
sourceIndex: 0, path: Keys("records"), start: 0, end: 1,
|
||||
items: []sequenceItemProjection{{inputIndex: 0, outputIndex: 2}},
|
||||
}}}
|
||||
scope := acquisitionScope{contributions: []acquiredContribution{{
|
||||
source: SourceInfo{ID: "outer-source", placementMode: placementModeSequenceItem},
|
||||
destination: Keys("records"), nested: child,
|
||||
}}}
|
||||
sources := []SourceInfo{{ID: "outer-source"}, {ID: "nested-source"}}
|
||||
|
||||
projectAcquiredDestinations(composed, scope, sources)
|
||||
if got, want := sources[0].Destination.String(), "/records"; got != want {
|
||||
t.Errorf("outer source destination = %q, want %q", got, want)
|
||||
}
|
||||
if got, want := sources[1].Destination.String(), "/records/2/settings"; got != want {
|
||||
t.Errorf("nested source destination = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectNestedEvidenceUsesMappedSequenceItemIndex(t *testing.T) {
|
||||
outer := historyApplication{
|
||||
value: parseCompositionDocument(t, "outer-source", "records: [{id: outer}]\n"),
|
||||
sequences: []sequenceHistoryContribution{{
|
||||
path: Keys("records"), mode: sequenceHistoryMapped,
|
||||
items: []sequenceItemProjection{{inputIndex: 0, outputIndex: 2}},
|
||||
}},
|
||||
}
|
||||
outer.sequencesByPath = indexSequenceHistoryContributions(outer.sequences)
|
||||
nestedRoot := parseCompositionDocument(t, "nested-source", "settings: {value: nested}\n")
|
||||
nestedOrigin := Origin{Layer: LayerBase, Locations: []Location{{SourceID: "nested-source"}}}
|
||||
nested := &composedDocument{
|
||||
root: nestedRoot, applications: []historyApplication{{value: nestedRoot}},
|
||||
origins: map[string]Origin{"/settings/value": nestedOrigin},
|
||||
}
|
||||
scope := acquisitionScope{contributions: []acquiredContribution{{
|
||||
source: SourceInfo{ID: "outer-source"}, destination: Keys("records"), nested: nested,
|
||||
}}}
|
||||
composed := &composedDocument{
|
||||
applications: []historyApplication{outer},
|
||||
sequenceRanges: []sequenceRange{{
|
||||
sourceIndex: 0, path: Keys("records"), start: 0, end: 1,
|
||||
items: []sequenceItemProjection{{inputIndex: 0, outputIndex: 2}},
|
||||
}},
|
||||
origins: make(map[string]Origin),
|
||||
}
|
||||
|
||||
projectNestedEvidence(composed, scope)
|
||||
if got, want := composed.applications[1].path.String(), "/records/2"; got != want {
|
||||
t.Fatalf("nested history application path = %q, want %q", got, want)
|
||||
}
|
||||
if origin := composed.origins["/records/2/settings/value"]; len(origin.Locations) != 1 || origin.Locations[0].SourceID != "nested-source" {
|
||||
t.Fatalf("nested origin = %#v", origin)
|
||||
}
|
||||
query := Keys("records").Index(2).Key("settings").Key("value")
|
||||
if operation, relevant := applicationOperationPath(composed.applications[1], query, nil); !relevant || operation.String() != query.String() {
|
||||
t.Fatalf("nested historical query = %s, %v", operation.String(), relevant)
|
||||
}
|
||||
replayed := parseCompositionDocument(t, "lower", "records: [{id: first}, {id: middle}, {id: last}]\n")
|
||||
for _, application := range composed.applications {
|
||||
applyHistoryApplication(&replayed, application, new(mappingIndex))
|
||||
}
|
||||
if value, present := documentAtPath(replayed, query); !present || value.Text != "nested" {
|
||||
t.Fatalf("replayed nested value = %#v, %v", value, present)
|
||||
}
|
||||
composed.applications[1].path.segments[0].key = "detached"
|
||||
if got := nested.applications[0].path.String(); got != "" {
|
||||
t.Fatalf("projected nested history path aliases its source: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestItemImportCanJoinAnAuthoredListUnderRegisteredComposition(t *testing.T) {
|
||||
t.Parallel()
|
||||
type config struct {
|
||||
|
||||
50
internal/engine/sequence_projection.go
Normal file
50
internal/engine/sequence_projection.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package engine
|
||||
|
||||
type sequenceItemProjectionIndex struct {
|
||||
outputByInput []int
|
||||
inputByOutput []int
|
||||
}
|
||||
|
||||
func newSequenceItemProjectionIndex(projections []sequenceItemProjection) sequenceItemProjectionIndex {
|
||||
maxInput := -1
|
||||
maxOutput := -1
|
||||
for _, projection := range projections {
|
||||
if projection.inputIndex > maxInput {
|
||||
maxInput = projection.inputIndex
|
||||
}
|
||||
if projection.outputIndex > maxOutput {
|
||||
maxOutput = projection.outputIndex
|
||||
}
|
||||
}
|
||||
index := sequenceItemProjectionIndex{
|
||||
outputByInput: make([]int, maxInput+1),
|
||||
inputByOutput: make([]int, maxOutput+1),
|
||||
}
|
||||
for input := range index.outputByInput {
|
||||
index.outputByInput[input] = -1
|
||||
}
|
||||
for output := range index.inputByOutput {
|
||||
index.inputByOutput[output] = -1
|
||||
}
|
||||
for _, projection := range projections {
|
||||
if projection.inputIndex >= 0 && projection.inputIndex < len(index.outputByInput) && projection.outputIndex >= 0 && projection.outputIndex < len(index.inputByOutput) {
|
||||
index.outputByInput[projection.inputIndex] = projection.outputIndex
|
||||
index.inputByOutput[projection.outputIndex] = projection.inputIndex
|
||||
}
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
func (index sequenceItemProjectionIndex) outputForInput(inputIndex int) (int, bool) {
|
||||
if inputIndex < 0 || inputIndex >= len(index.outputByInput) || index.outputByInput[inputIndex] < 0 {
|
||||
return 0, false
|
||||
}
|
||||
return index.outputByInput[inputIndex], true
|
||||
}
|
||||
|
||||
func (index sequenceItemProjectionIndex) inputForOutput(outputIndex int) (int, bool) {
|
||||
if outputIndex < 0 || outputIndex >= len(index.inputByOutput) || index.inputByOutput[outputIndex] < 0 {
|
||||
return 0, false
|
||||
}
|
||||
return index.inputByOutput[outputIndex], true
|
||||
}
|
||||
@@ -48,6 +48,16 @@ func TestFragmentVersionClaimUsesAssignedSequenceProjection(t *testing.T) {
|
||||
if node := fragmentVersionClaimProjected(fragment, &VersionRule{Path: Keys("items").Index(1).Key("version")}, 1, rangeForPeer); node == nil || node.Text != "v1" {
|
||||
t.Fatalf("assigned peer index 1 claim = %#v, want its version node", node)
|
||||
}
|
||||
mappedRange := []sequenceRange{{
|
||||
sourceIndex: 1, path: Keys("items"), start: 0, end: 3,
|
||||
items: []sequenceItemProjection{{inputIndex: 0, outputIndex: 2}},
|
||||
}}
|
||||
if node := fragmentVersionClaimProjected(fragment, &VersionRule{Path: Keys("items").Index(0).Key("version")}, 1, mappedRange); node != nil {
|
||||
t.Fatalf("unassigned mapped peer index claimed a version: %#v", node)
|
||||
}
|
||||
if node := fragmentVersionClaimProjected(fragment, &VersionRule{Path: Keys("items").Index(2).Key("version")}, 1, mappedRange); node == nil || node.Text != "v1" {
|
||||
t.Fatalf("mapped peer index 2 claim = %#v, want its version node", node)
|
||||
}
|
||||
nested := sourceVersionTestDocument(t, "items: [{nested: [{version: nested-v}]}]\n")
|
||||
nestedRanges := []sequenceRange{
|
||||
{sourceIndex: 1, path: Keys("items"), start: 1, end: 2},
|
||||
|
||||
Reference in New Issue
Block a user