This repository has been archived by the owner on Jan 16, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce PathFinder for better discovery of paths
PathFinder exists to convert paths with extended syntax into their canonical paths, so something like the following: `/foo/baz=quux/waldo=corge` Might get expanded into something like: `/foo/0/fred/2/grault/1` Where both `baz=quux` and `waldo=corge` were used to find the indexes for their associated objects. The prior incarnation of this code only supported using the extended syntax as the last key in the pointer. PathFinder is also a bit nicer, being split out and tested independently of Patch, which shouldn't care at all about finding canonical paths. This also, I believe, brings it up to parity with `go-patch`.
- Loading branch information
Kris Hicks
committed
May 3, 2017
1 parent
9c57216
commit 1755f47
Showing
4 changed files
with
211 additions
and
87 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
package yamlpatch | ||
|
||
import ( | ||
"fmt" | ||
"strconv" | ||
"strings" | ||
) | ||
|
||
// PathFinder can be used to find RFC6902-standard paths given non-standard | ||
// (key=value) pointer syntax | ||
type PathFinder struct { | ||
root interface{} | ||
} | ||
|
||
// NewPathFinder takes an interface that represents a YAML document and returns | ||
// a new PathFinder | ||
func NewPathFinder(iface interface{}) *PathFinder { | ||
return &PathFinder{ | ||
root: iface, | ||
} | ||
} | ||
|
||
// Find expands the given path into all matching paths, returning the canonical | ||
// versions of those matching paths | ||
func (p *PathFinder) Find(path string) []string { | ||
parts := strings.Split(path, "/") | ||
|
||
if parts[1] == "" { | ||
return []string{"/"} | ||
} | ||
|
||
routes := map[string]interface{}{ | ||
"": p.root, | ||
} | ||
|
||
for _, part := range parts[1:] { | ||
routes = find(part, routes) | ||
} | ||
|
||
var paths []string | ||
for k := range routes { | ||
paths = append(paths, k) | ||
} | ||
|
||
return paths | ||
} | ||
|
||
func find(part string, routes map[string]interface{}) map[string]interface{} { | ||
matches := map[string]interface{}{} | ||
|
||
for prefix, iface := range routes { | ||
if strings.Contains(part, "=") { | ||
kv := strings.Split(part, "=") | ||
if newMatches := findAll(prefix, kv[0], kv[1], iface); len(newMatches) > 0 { | ||
matches = newMatches | ||
} | ||
continue | ||
} | ||
|
||
switch it := iface.(type) { | ||
case map[interface{}]interface{}: | ||
for k, v := range it { | ||
if ks, ok := k.(string); ok && ks == part { | ||
path := fmt.Sprintf("%s/%s", prefix, ks) | ||
matches[path] = v | ||
} | ||
} | ||
case []interface{}: | ||
if idx, err := strconv.Atoi(part); err == nil && idx >= 0 && idx <= len(it)-1 { | ||
path := fmt.Sprintf("%s/%d", prefix, idx) | ||
matches[path] = it[idx] | ||
} | ||
default: | ||
panic(fmt.Sprintf("don't know how to handle %T: %s", iface, iface)) | ||
} | ||
} | ||
|
||
return matches | ||
} | ||
|
||
func findAll(prefix, findKey, findValue string, iface interface{}) map[string]interface{} { | ||
matches := map[string]interface{}{} | ||
|
||
switch it := iface.(type) { | ||
case map[interface{}]interface{}: | ||
for k, v := range it { | ||
if ks, ok := k.(string); ok { | ||
switch vs := v.(type) { | ||
case string: | ||
if ks == findKey && vs == findValue { | ||
return map[string]interface{}{ | ||
prefix: it, | ||
} | ||
} | ||
default: | ||
for route, match := range findAll(fmt.Sprintf("%s/%s", prefix, ks), findKey, findValue, v) { | ||
matches[route] = match | ||
} | ||
} | ||
} | ||
} | ||
case []interface{}: | ||
for i, v := range it { | ||
for route, match := range findAll(fmt.Sprintf("%s/%d", prefix, i), findKey, findValue, v) { | ||
matches[route] = match | ||
} | ||
} | ||
default: | ||
panic(fmt.Sprintf("don't know how to handle %T: %s", iface, iface)) | ||
} | ||
|
||
return matches | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package yamlpatch_test | ||
|
||
import ( | ||
yamlpatch "github.com/krishicks/yaml-patch" | ||
yaml "gopkg.in/yaml.v2" | ||
|
||
. "github.com/onsi/ginkgo" | ||
. "github.com/onsi/ginkgo/extensions/table" | ||
. "github.com/onsi/gomega" | ||
) | ||
|
||
var _ = Describe("Pathfinder", func() { | ||
var pathfinder *yamlpatch.PathFinder | ||
|
||
BeforeEach(func() { | ||
var iface interface{} | ||
|
||
bs := []byte(` | ||
jobs: | ||
- name: job1 | ||
plan: | ||
- get: A | ||
args: | ||
- arg: arg1 | ||
- arg: arg2 | ||
- get: B | ||
- name: job2 | ||
plan: | ||
- aggregate: | ||
- get: C | ||
- get: A | ||
`) | ||
|
||
err := yaml.Unmarshal(bs, &iface) | ||
Expect(err).NotTo(HaveOccurred()) | ||
pathfinder = yamlpatch.NewPathFinder(iface) | ||
}) | ||
|
||
Describe("Find", func() { | ||
DescribeTable( | ||
"should", | ||
func(path string, expected []string) { | ||
actual := pathfinder.Find(path) | ||
Expect(actual).To(HaveLen(len(expected))) | ||
for _, el := range expected { | ||
Expect(actual).To(ContainElement(el)) | ||
} | ||
}, | ||
Entry("return a route for the root object", "/", []string{"/"}), | ||
Entry("return a route for an object under the root", "/jobs", []string{"/jobs"}), | ||
Entry("return a route for an element within an object under the root", "/jobs/0", []string{"/jobs/0"}), | ||
Entry("return a route for an object within an element within an object under the root", "/jobs/0/plan", []string{"/jobs/0/plan"}), | ||
Entry("return a route for an object within an element within an object under the root", "/jobs/0/plan/1", []string{"/jobs/0/plan/1"}), | ||
Entry("return routes for multiple matches", "/jobs/get=A", []string{"/jobs/0/plan/0", "/jobs/1/plan/0/aggregate/1"}), | ||
Entry("return a route for a single submatch with help", "/jobs/get=A/args/arg=arg2", []string{"/jobs/0/plan/0/args/1"}), | ||
Entry("return a route for a single submatch with no help", "/jobs/get=A/arg=arg2", []string{"/jobs/0/plan/0/args/1"}), | ||
) | ||
DescribeTable( | ||
"should not", | ||
func(path string) { | ||
Expect(pathfinder.Find(path)).To(BeNil()) | ||
}, | ||
Entry("return any routes when given a bad index", "/jobs/2"), | ||
Entry("return any routes when given a bad index", "/jobs/-1"), | ||
Entry("return any routes when given a bad pointer", "/plan"), | ||
) | ||
}) | ||
}) |