From 6bf826c466d5822550361c39a101a199fc6041a5 Mon Sep 17 00:00:00 2001 From: Tobie Morgan Hitchcock Date: Mon, 24 Oct 2016 14:16:53 +0100 Subject: [PATCH] Enable path walking with a callback function --- util/data/data.go | 145 +++++++++++++++++++++++++++++++++++++++++ util/data/data_test.go | 135 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 280 insertions(+) diff --git a/util/data/data.go b/util/data/data.go index 3cd2aea3..af988d16 100644 --- a/util/data/data.go +++ b/util/data/data.go @@ -723,3 +723,148 @@ func (d *Doc) Dec(value interface{}, path ...string) (*Doc, error) { return &Doc{nil}, fmt.Errorf("Not possible to decrement.") } + +// -------------------------------------------------------------------------------- + +type walker func(key string, val interface{}) error + +// Walk walks the value or values at a specified path. +func (d *Doc) Walk(exec walker, path ...string) error { + + return d.walk(exec, nil, path...) + +} + +func (d *Doc) join(parts ...[]string) string { + var path []string + for _, part := range parts { + path = append(path, part...) + } + return strings.Join(path, ".") +} + +func (d *Doc) walk(exec walker, prev []string, path ...string) error { + + path = d.path(path...) + + if len(path) == 0 { + d.data = nil + return nil + } + + // If the value found at the current + // path part is undefined, then ensure + // that it is an object + + if d.data == nil { + d.data = map[string]interface{}{} + } + + // Define the temporary object so + // that we can loop over and traverse + // down the path parts of the data + + object := d.data + + // Loop over each part of the path + // whilst detecting if the data at + // the current path is an {} or [] + + for k, p := range path { + + // If the value found at the current + // path part is an object, then move + // to the next part of the path + + if m, ok := object.(map[string]interface{}); ok { + if object, ok = m[p]; !ok { + return exec(d.join(prev, path), nil) + } + continue + } + + // If the value found at the current + // path part is an array, then perform + // the query on the specified items + + if a, ok := object.([]interface{}); ok { + + var i int + var e error + + if p == "*" { + e = errors.New("") + } else if p == "first" { + i = 0 + } else if p == "last" { + i = len(a) - 1 + } else { + i, e = strconv.Atoi(p) + } + + // If the path part is a numeric index + // then run the query on the specified + // index of the current data array + + if e == nil { + + if 0 == len(a) || i >= len(a) { + return fmt.Errorf("No item with index %d in array, using path %s", i, path) + } + + if k == len(path)-1 { + return exec(d.join(prev, path), a[i]) + } else { + var keep []string + keep = append(keep, prev...) + keep = append(keep, path[:k]...) + keep = append(keep, fmt.Sprintf("%d", i)) + return Consume(a[i]).walk(exec, keep, path[k+1:]...) + } + + } + + // If the path part is an asterisk + // then run the query on all of the + // items in the current data array + + if p == "*" { + + for i := len(a) - 1; i >= 0; i-- { + + if k == len(path)-1 { + var keep []string + keep = append(keep, prev...) + keep = append(keep, path[:k]...) + keep = append(keep, fmt.Sprintf("%d", i)) + if err := exec(d.join(keep), a[i]); err != nil { + return err + } + } else { + var keep []string + keep = append(keep, prev...) + keep = append(keep, path[:k]...) + keep = append(keep, fmt.Sprintf("%d", i)) + if err := Consume(a[i]).walk(exec, keep, path[k+1:]...); err != nil { + return err + } + } + + } + + return nil + + } + + } + + // The current path item is not an object or an array + // but there are still other items in the search path. + + return fmt.Errorf("Can not get path %s from %v", path, object) + + } + + return exec(d.join(prev, path), object) + +} diff --git a/util/data/data_test.go b/util/data/data_test.go index 6937b3f0..b55c399c 100644 --- a/util/data/data_test.go +++ b/util/data/data_test.go @@ -15,6 +15,7 @@ package data import ( + "errors" "testing" "time" @@ -783,6 +784,140 @@ func TestOperations(t *testing.T) { // ---------------------------------------------------------------------------------------------------- + tmp := []interface{}{ + map[string]interface{}{ + "test": "one", + }, + map[string]interface{}{ + "test": "two", + }, + map[string]interface{}{ + "test": "tre", + }, + } + + Convey("Can del array", t, func() { + err := doc.Del("the.item.arrays") + So(err, ShouldBeNil) + }) + + Convey("Can walk nil", t, func() { + doc.Walk(func(key string, val interface{}) error { + doc.Set(tmp, "none") + return nil + }) + So(doc.Exists("none"), ShouldBeFalse) + }) + + Convey("Can walk array", t, func() { + doc.Walk(func(key string, val interface{}) error { + So(key, ShouldResemble, "the.item.arrays") + doc.Set(tmp, key) + return nil + }, "the.item.arrays") + So(doc.Get("the.item.arrays").Data(), ShouldResemble, tmp) + }) + + Convey("Can walk array → *", t, func() { + doc.Walk(func(key string, val interface{}) error { + So(key, ShouldBeIn, "the.item.arrays.0", "the.item.arrays.1", "the.item.arrays.2") + So(val, ShouldBeIn, tmp[0], tmp[1], tmp[2]) + return nil + }, "the.item.arrays.*") + }) + + Convey("Can walk array → * → object", t, func() { + doc.Walk(func(key string, val interface{}) error { + So(key, ShouldBeIn, "the.item.arrays.0.test", "the.item.arrays.1.test", "the.item.arrays.2.test") + So(val, ShouldBeIn, "one", "two", "tre") + return nil + }, "the.item.arrays.*.test") + }) + + Convey("Can walk array → first → object", t, func() { + doc.Walk(func(key string, val interface{}) error { + So(key, ShouldResemble, "the.item.arrays.0.test") + So(val, ShouldResemble, "one") + return nil + }, "the.item.arrays.first.test") + }) + + Convey("Can walk array → last → object", t, func() { + doc.Walk(func(key string, val interface{}) error { + So(key, ShouldResemble, "the.item.arrays.2.test") + So(val, ShouldResemble, "tre") + return nil + }, "the.item.arrays.last.test") + }) + + Convey("Can walk array → 0 → value", t, func() { + doc.Walk(func(key string, val interface{}) error { + So(key, ShouldResemble, "the.item.arrays.0") + So(val, ShouldResemble, map[string]interface{}{"test": "one"}) + return nil + }, "the.item.arrays.0") + }) + + Convey("Can walk array → 1 → value", t, func() { + doc.Walk(func(key string, val interface{}) error { + So(key, ShouldResemble, "the.item.arrays.1") + So(val, ShouldResemble, map[string]interface{}{"test": "two"}) + return nil + }, "the.item.arrays.1") + }) + + Convey("Can walk array → 2 → value", t, func() { + doc.Walk(func(key string, val interface{}) error { + So(key, ShouldResemble, "the.item.arrays.2") + So(val, ShouldResemble, map[string]interface{}{"test": "tre"}) + return nil + }, "the.item.arrays.2") + }) + + Convey("Can walk array → 3 → value", t, func() { + err := doc.Walk(func(key string, val interface{}) error { + return nil + }, "the.item.arrays.3") + So(err, ShouldNotBeNil) + }) + + Convey("Can walk array → 0 → value → value", t, func() { + err := doc.Walk(func(key string, val interface{}) error { + return nil + }, "the.item.arrays.0.test.value") + So(err, ShouldNotBeNil) + }) + + Convey("Can walk array → 0 → value → value → value", t, func() { + err := doc.Walk(func(key string, val interface{}) error { + return nil + }, "the.item.arrays.0.test.value.value") + So(err, ShouldNotBeNil) + }) + + Convey("Can force error from walk", t, func() { + err := doc.Walk(func(key string, val interface{}) error { + return errors.New("Testing") + }, "the.item.something") + So(err, ShouldNotBeNil) + }) + + Convey("Can force error from walk array → *", t, func() { + err := doc.Walk(func(key string, val interface{}) error { + return errors.New("Testing") + }, "the.item.arrays.*") + So(err, ShouldNotBeNil) + }) + + Convey("Can force error from walk array → * → value", t, func() { + err := doc.Walk(func(key string, val interface{}) error { + return errors.New("Testing") + }, "the.item.arrays.*.test") + So(err, ShouldNotBeNil) + }) + + // ---------------------------------------------------------------------------------------------------- + Convey("Can copy object", t, func() { So(doc.Copy(), ShouldResemble, doc.Data()) })