Preserve matched record metadata after resolution
This commit is contained in:
@@ -141,6 +141,15 @@ func cloneMetadata(metadata Metadata) Metadata {
|
||||
func applyResolutionMetadata(metadata *Metadata, resolved resolutionResult, entry resolutionInput) {
|
||||
finalPaths := make(map[string]*document.Node)
|
||||
indexDocument(resolved.final, Path{}, finalPaths)
|
||||
fields := make(map[string]FieldInfo, len(finalPaths))
|
||||
walkDocument(resolved.final, Path{}, func(path Path, _ *document.Node) {
|
||||
if prior, matched := resolved.correspondence.entryPath(path); matched {
|
||||
if info, found := entry.lookup(prior); found {
|
||||
fields[path.String()] = cloneFieldInfo(info)
|
||||
}
|
||||
}
|
||||
})
|
||||
metadata.fields = fields
|
||||
snapshotCoverage, snapshotPointers := newResolutionCoverageIndexFromSnapshots(resolved.snapshots)
|
||||
var deltaCoverage pathPrefixIndex
|
||||
snapshotIDs := make([]int, len(resolved.deltas))
|
||||
@@ -163,7 +172,7 @@ func applyResolutionMetadata(metadata *Metadata, resolved resolutionResult, entr
|
||||
}
|
||||
|
||||
applyResolutionFieldOrigins(metadata.fields, resolved.snapshots, &deltaCoverage, snapshotIDs)
|
||||
refreshResolutionFields(metadata.fields, finalPaths, resolved.final, entry, resolved.snapshots, snapshotCoverage)
|
||||
refreshResolutionFields(metadata.fields, finalPaths, resolved.final, entry, resolved.snapshots, snapshotCoverage, resolved.correspondence)
|
||||
}
|
||||
|
||||
func applyResolutionFieldOrigins(fields map[string]FieldInfo, snapshots []resolutionSnapshot, deltaIndex *pathPrefixIndex, snapshotIDs []int) {
|
||||
@@ -192,7 +201,7 @@ func applyResolutionFieldOrigins(fields map[string]FieldInfo, snapshots []resolu
|
||||
}
|
||||
}
|
||||
|
||||
func refreshResolutionFields(fields map[string]FieldInfo, finalPaths map[string]*document.Node, final *document.Node, entry resolutionInput, snapshots []resolutionSnapshot, snapshotCoverage *resolutionCoverageIndex) {
|
||||
func refreshResolutionFields(fields map[string]FieldInfo, finalPaths map[string]*document.Node, final *document.Node, entry resolutionInput, snapshots []resolutionSnapshot, snapshotCoverage *resolutionCoverageIndex, correspondence resolutionCorrespondence) {
|
||||
for pointer := range fields {
|
||||
if _, ok := finalPaths[pointer]; !ok {
|
||||
delete(fields, pointer)
|
||||
@@ -200,7 +209,9 @@ func refreshResolutionFields(fields map[string]FieldInfo, finalPaths map[string]
|
||||
}
|
||||
walkDocument(final, Path{}, func(path Path, node *document.Node) {
|
||||
pointer := path.String()
|
||||
entryInfo, entryFound := entry.lookup(path)
|
||||
prior, matched := correspondence.entryPath(path)
|
||||
entryInfo, entryFound := entry.lookup(prior)
|
||||
entryFound = entryFound && matched
|
||||
if info, ok := fields[pointer]; ok {
|
||||
info.Explicit = entryFound && entryInfo.Explicit
|
||||
info.Null = node.Kind == document.Null
|
||||
|
||||
@@ -162,6 +162,13 @@ func documentWithMetadataLocations(root *document.Node, metadata Metadata, fallb
|
||||
}
|
||||
var lookup *documentLookup
|
||||
fallbackLocation := func(path Path) (document.Location, bool) {
|
||||
if model := metadata.historyEvidence(); model != nil && model.resolutionPayload != nil {
|
||||
var matched bool
|
||||
path, matched = model.resolutionPayload.correspondence.entryPath(path)
|
||||
if !matched {
|
||||
return document.Location{}, false
|
||||
}
|
||||
}
|
||||
if lookup == nil {
|
||||
lookup = buildDocumentLookup(fallbackRoot)
|
||||
}
|
||||
@@ -255,6 +262,13 @@ func metadataFieldLocation(metadata Metadata, path Path, origin Origin) (documen
|
||||
return location, true
|
||||
}
|
||||
if model := metadata.historyEvidence(); model != nil {
|
||||
if model.resolutionPayload != nil {
|
||||
var matched bool
|
||||
path, matched = model.resolutionPayload.correspondence.entryPath(path)
|
||||
if !matched {
|
||||
return document.Location{}, false
|
||||
}
|
||||
}
|
||||
location, ok := model.latestHistoricalLocation(path)
|
||||
if ok {
|
||||
return document.Location{SourceID: location.SourceID, Line: location.Line, Column: location.Column}, true
|
||||
|
||||
@@ -203,3 +203,65 @@ func TestRegisteredResolutionFallbackAndHelperTarget(t *testing.T) {
|
||||
t.Fatalf("diagnostics = %#v", failure.Diagnostics())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisteredResolutionMetadataFollowsEntryIdentity(t *testing.T) {
|
||||
type item struct {
|
||||
ID string `yaml:"id"`
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Count int `yaml:"count"`
|
||||
}
|
||||
type config struct {
|
||||
Items []item `yaml:"items"`
|
||||
}
|
||||
entryFixed := false
|
||||
definition := DefinitionWithSequenceIdentityRegistrations(Definition[config]{Resolve: func(value *config, r *Resolution) error {
|
||||
entryFixed = r.IsExplicit(Keys("items").Index(0).Key("enabled")) && !r.IsExplicit(Keys("items").Index(1).Key("enabled"))
|
||||
value.Items[0], value.Items[1] = value.Items[1], value.Items[0]
|
||||
value.Items = append(value.Items, item{ID: "new"})
|
||||
r.RecordNormalization(Keys("items"), "reorder and add")
|
||||
return nil
|
||||
}}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}})
|
||||
result, err := testLoad(Bytes("records", []byte("items: [{id: a, enabled: false}, {id: b, count: 0}]\n")), Options[config]{Definition: definition})
|
||||
if err != nil || !entryFixed {
|
||||
t.Fatalf("load = %#v, fixed entry = %t, error = %v", result, entryFixed, err)
|
||||
}
|
||||
for _, check := range []struct {
|
||||
index int
|
||||
key string
|
||||
explicit bool
|
||||
}{{0, "enabled", false}, {0, "count", true}, {1, "enabled", true}, {1, "count", false}, {2, "id", false}} {
|
||||
info, ok := result.Metadata().Lookup(Keys("items").Index(check.index).Key(check.key))
|
||||
if !ok || info.Explicit != check.explicit || info.Origin.Layer != LayerResolution {
|
||||
t.Errorf("item %d %s = %#v, %t", check.index, check.key, info, ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisteredResolutionValidationLocationFollowsMovedRecord(t *testing.T) {
|
||||
type item struct {
|
||||
ID string `yaml:"id"`
|
||||
Value string `yaml:"value"`
|
||||
}
|
||||
type config struct {
|
||||
Items []item `yaml:"items"`
|
||||
}
|
||||
definition := DefinitionWithSequenceIdentityRegistrations(Definition[config]{
|
||||
Resolve: func(value *config, r *Resolution) error {
|
||||
value.Items[0], value.Items[1] = value.Items[1], value.Items[0]
|
||||
r.RecordNormalization(Keys("items"), "reorder")
|
||||
return nil
|
||||
},
|
||||
Validate: func(_ *config, v *Validation) { v.Error(Keys("items").Index(0).Key("value"), "invalid") },
|
||||
}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}})
|
||||
_, err := testLoad(Bytes("records", []byte("items:\n - id: a\n value: first\n - id: b\n value: second\n")), Options[config]{Definition: definition})
|
||||
var failure *LoadError
|
||||
if !errors.As(err, &failure) {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
for _, diagnostic := range failure.Diagnostics() {
|
||||
if diagnostic.Path.String() == "/items/0/value" && diagnostic.Primary != nil && diagnostic.Primary.Line == 5 {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("diagnostics = %#v", failure.Diagnostics())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user