Compare canonical paths by token spelling
This commit is contained in:
@@ -41,7 +41,7 @@ func (m Metadata) effectiveField(path Path) (FieldInfo, bool) {
|
||||
}
|
||||
pointer := path.String()
|
||||
candidate, ok := m.effectivePaths[pointer]
|
||||
if !ok || !matchesPathShapeWithPointer(path, candidate, pointer) {
|
||||
if !ok || !matchesPathShape(path, candidate) {
|
||||
return FieldInfo{}, false
|
||||
}
|
||||
info, ok := m.fields[pointer]
|
||||
@@ -89,18 +89,14 @@ func (m Metadata) HistoricalPaths() []Path {
|
||||
// Textual pointers are lookup keys, but explicit key/index kinds remain part of
|
||||
// the query. Candidate paths are constructed from actual document containers.
|
||||
func matchesPathShape(query, candidate Path) bool {
|
||||
if !query.valid() {
|
||||
return false
|
||||
}
|
||||
return matchesPathShapeWithPointer(query, candidate, query.String())
|
||||
}
|
||||
|
||||
func matchesPathShapeWithPointer(query, candidate Path, pointer string) bool {
|
||||
if pointer != candidate.String() || len(query.segments) != len(candidate.segments) {
|
||||
if !query.valid() || len(query.segments) != len(candidate.segments) {
|
||||
return false
|
||||
}
|
||||
for i, segment := range query.segments {
|
||||
actual := candidate.segments[i]
|
||||
if !pathTokenSpellingEqual(segment, actual) {
|
||||
return false
|
||||
}
|
||||
if actual.kind == pathSegmentIndex {
|
||||
index, ok := schemaIndex(segment)
|
||||
if !ok || index != actual.index {
|
||||
|
||||
@@ -128,6 +128,45 @@ func clonePathSegments(segments []pathSegment, extra int) []pathSegment {
|
||||
return cloned
|
||||
}
|
||||
|
||||
func pathTokenSpellingEqual(left, right pathSegment) bool {
|
||||
switch {
|
||||
case left.kind == pathSegmentIndex && right.kind == pathSegmentIndex:
|
||||
return left.index == right.index
|
||||
case left.kind == pathSegmentIndex:
|
||||
return pathIndexSpellingEqual(left.index, right.key)
|
||||
case right.kind == pathSegmentIndex:
|
||||
return pathIndexSpellingEqual(right.index, left.key)
|
||||
default:
|
||||
return left.key == right.key
|
||||
}
|
||||
}
|
||||
|
||||
func pathIndexSpellingEqual(index int, token string) bool {
|
||||
var scratch [20]byte
|
||||
spelling := strconv.AppendInt(scratch[:0], int64(index), 10)
|
||||
if len(spelling) != len(token) {
|
||||
return false
|
||||
}
|
||||
for i, character := range spelling {
|
||||
if character != token[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func pathSpellingEqual(left, right Path) bool {
|
||||
if len(left.segments) != len(right.segments) {
|
||||
return false
|
||||
}
|
||||
for i, segment := range left.segments {
|
||||
if !pathTokenSpellingEqual(segment, right.segments[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func escapePathToken(token string) string {
|
||||
token = strings.ReplaceAll(token, "~", "~0")
|
||||
return strings.ReplaceAll(token, "/", "~1")
|
||||
|
||||
@@ -48,6 +48,106 @@ func TestPathRoundTripsSemanticKeys(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathContainmentMatchesPointerSpelling(t *testing.T) {
|
||||
parse := func(pointer string) Path {
|
||||
path, err := ParsePath(pointer)
|
||||
if err != nil {
|
||||
t.Fatalf("ParsePath(%q) error = %v", pointer, err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
maximum := int(^uint(0) >> 1)
|
||||
minimum := -maximum - 1
|
||||
paths := []Path{
|
||||
{},
|
||||
Keys(""),
|
||||
Keys("a"),
|
||||
Keys("ab"),
|
||||
Keys("a", "b"),
|
||||
Keys("a/b"),
|
||||
Keys("a~1b"),
|
||||
Keys("~"),
|
||||
Keys("0"),
|
||||
Keys("01"),
|
||||
Keys("+1"),
|
||||
Keys("items").Index(0),
|
||||
Keys("items").Index(maximum),
|
||||
Keys("items").Index(minimum),
|
||||
parse("/"),
|
||||
parse("/a~1b"),
|
||||
parse("/a~01b"),
|
||||
parse("/~0"),
|
||||
parse("/0"),
|
||||
parse("/01"),
|
||||
parse("/+1"),
|
||||
parse("/items/0"),
|
||||
parse("/items/" + strconv.Itoa(maximum)),
|
||||
parse("/items/" + strconv.Itoa(minimum)),
|
||||
Path{}.Index(-1),
|
||||
}
|
||||
for _, container := range paths {
|
||||
for _, descendant := range paths {
|
||||
got := pathContains(container, descendant)
|
||||
want := pathContainsByPointer(container, descendant)
|
||||
if got != want {
|
||||
t.Errorf("pathContains(%q, %q) = %v, want %v", container.String(), descendant.String(), got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendUniquePathMatchesPointerSpelling(t *testing.T) {
|
||||
parse := func(pointer string) Path {
|
||||
path, err := ParsePath(pointer)
|
||||
if err != nil {
|
||||
t.Fatalf("ParsePath(%q) error = %v", pointer, err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
paths := []Path{
|
||||
Keys("a/b"), parse("/a~1b"),
|
||||
Keys("a~1b"), parse("/a~01b"),
|
||||
Keys("items").Index(0), parse("/items/0"), Keys("items", "0"),
|
||||
Keys("items", "01"), parse("/items/01"),
|
||||
Path{}.Index(-1), Keys("-1"),
|
||||
Keys("a", "b"), Keys("a/b"),
|
||||
}
|
||||
var got, want []Path
|
||||
for _, path := range paths {
|
||||
got = appendUniquePath(got, path)
|
||||
want = appendUniquePathByPointer(want, path)
|
||||
}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("unique path counts = %d, want %d", len(got), len(want))
|
||||
}
|
||||
for i := range got {
|
||||
if got[i].String() != want[i].String() {
|
||||
t.Fatalf("unique path %d = %q, want %q", i, got[i].String(), want[i].String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func pathContainsByPointer(container, descendant Path) bool {
|
||||
if !container.valid() || !descendant.valid() || len(container.segments) > len(descendant.segments) {
|
||||
return false
|
||||
}
|
||||
prefix := container.String()
|
||||
if prefix == "" {
|
||||
return true
|
||||
}
|
||||
value := descendant.String()
|
||||
return value == prefix || len(value) > len(prefix) && value[:len(prefix)] == prefix && value[len(prefix)] == '/'
|
||||
}
|
||||
|
||||
func appendUniquePathByPointer(paths []Path, path Path) []Path {
|
||||
for _, existing := range paths {
|
||||
if existing.String() == path.String() {
|
||||
return paths
|
||||
}
|
||||
}
|
||||
return append(paths, path.clone())
|
||||
}
|
||||
|
||||
func TestPathPreservesSegmentIntentAndImmutability(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -337,12 +337,12 @@ func pathContains(container, descendant Path) bool {
|
||||
if !container.valid() || !descendant.valid() || len(container.segments) > len(descendant.segments) {
|
||||
return false
|
||||
}
|
||||
if container.String() == "" {
|
||||
return true
|
||||
for i, segment := range container.segments {
|
||||
if !pathTokenSpellingEqual(segment, descendant.segments[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
prefix := container.String()
|
||||
value := descendant.String()
|
||||
return value == prefix || strings.HasPrefix(value, prefix+"/")
|
||||
return true
|
||||
}
|
||||
|
||||
func appendUniqueString(values []string, value string) []string {
|
||||
@@ -354,7 +354,7 @@ func appendUniqueString(values []string, value string) []string {
|
||||
|
||||
func appendUniquePath(paths []Path, path Path) []Path {
|
||||
for _, existing := range paths {
|
||||
if existing.String() == path.String() {
|
||||
if pathSpellingEqual(existing, path) {
|
||||
return paths
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user