From 5bff9a5774ad0346175b4c2b25031a63d504a1e0 Mon Sep 17 00:00:00 2001 From: Mildred Ki'Lya Date: Thu, 31 Dec 2015 16:09:55 +0100 Subject: [PATCH] Add NodeReader interface See issue ipfs/go-ipld#17 --- ipld.go | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++ reader.go | 60 +++++++++++++++++++++++++++++++++++++++++ reader_test.go | 57 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 189 insertions(+) create mode 100644 reader.go create mode 100644 reader_test.go diff --git a/ipld.go b/ipld.go index 16f3556..a8cea58 100644 --- a/ipld.go +++ b/ipld.go @@ -3,6 +3,7 @@ package ipld import ( "errors" "reflect" + "sort" mh "github.com/jbenet/go-multihash" ) @@ -187,3 +188,74 @@ func LinkCast(v interface{}) (l Link, ok bool) { } return l, true } + +func (n Node) Read(fun ReadFun) error { + return read(n, fun, []interface{}{}) +} + +func read(curr interface{}, fun ReadFun, path []interface{}) error { + if nc, ok := curr.(Node); ok { // it's a node! + err := fun(path, TokenNode, nil) + if err == SkipNode { + return nil + } else if err != nil { + return err + } + + // Iterate in fixed order (by default, go randomize iteration order) + // Simulate reading from a file where the order is fixed + keys := make([]string, 0, len(nc)) + for k := range nc { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, k := range keys { + err := fun(path, TokenKey, k) + if err == SkipNode { + return nil + } else if err != nil { + return err + } + + subpath := append(path, k) + err = read(nc[k], fun, subpath) + if err != nil { + return err + } + } + + } else if sc, ok := curr.([]interface{}); ok { // it's a slice! + err := fun(path, TokenArray, nil) + if err == SkipNode { + return nil + } else if err != nil { + return err + } + + for i, v := range sc { + err := fun(path, TokenIndex, i) + if err == SkipNode { + return nil + } else if err != nil { + return err + } + + subpath := append(path, i) + err = read(v, fun, subpath) + if err != nil { + return err + } + } + + } else { + err := fun(path, TokenValue, curr) + if err == SkipNode { + return nil + } else if err != nil { + return err + } + } + return nil +} + diff --git a/reader.go b/reader.go new file mode 100644 index 0000000..953d8ae --- /dev/null +++ b/reader.go @@ -0,0 +1,60 @@ +package ipld + +type ReaderToken int +const ( + TokenNode ReaderToken = iota + TokenKey + TokenArray + TokenIndex + TokenValue +) + +// Callback function to be called when reading a Node. The path represents the +// location in the hierarchy, tokenType is the type of token being read, and +// value is the corresponding value. The function can return SkipNode to skip +// recursing, or an error to stop the reading at any time. +type ReadFun func(path []interface{}, tokenType ReaderToken, value interface{}) error + +// Represents a Node that can be read using the special Read function +type NodeReader interface { + + // Read the node from the beginning. Return any errors found during the + // process. The function f is called for every value found in the node stream, + // on the following events: + // + // - Once for each node (TokenNode) with a nil value + // - Once for each key of each node (TokenKey) with the key name as value + // - Once for each array / slice (TokenArray) with a nil value + // - Once for each slice item (TokenIndex) with the index as value + // - Once for each other value (TokenValue) with the actual value given + // + // For example, the node that can be represented using the following JSON + // construct: + // + // { + // "key": "value", + // "items": ["a", "b", "c"], + // "count": 3 + // } + // + // Will result in the function called in this way: + // + // f({}, TokenNode, nil) + // f({}, TokenKey, "key") + // f({"key"}, TokenValue, "value") + // f({}, TokenKey, "items") + // f({"items"}, TokenArray, nil) + // f({"items"}, TokenIndex, 0) + // f({"items", 0}, TokenValue, "a") + // f({"items"}, TokenIndex, 1) + // f({"items", 1}, TokenValue, "b") + // f({"items"}, TokenIndex, 2) + // f({"items", 2}, TokenValue, "c") + // f({}, TokenKey, "count") + // f({"count"}, TokenValue, 3) + // + // Iteration order for maps is fixed by the order of the pairs in memory. + // + Read(f ReadFun) error; +} + diff --git a/reader_test.go b/reader_test.go new file mode 100644 index 0000000..f330e46 --- /dev/null +++ b/reader_test.go @@ -0,0 +1,57 @@ +package ipld + +import ( + "github.com/mildred/assrt" + "testing" +) + +type callback struct { + path []interface{}; + tokenType ReaderToken; + value interface{}; +} + +func TestReader(t *testing.T) { + assert := assrt.NewAssert(t) + + var node *Node + var i int = 0 + + node = &Node{ + "key": "value", + "items": []interface{}{"a", "b", "c"}, + "count": 3, + } + + callbacks := []callback{ + callback{[]interface{}{}, TokenNode, nil}, + callback{[]interface{}{}, TokenKey, "count"}, + callback{[]interface{}{"count"}, TokenValue, 3}, + callback{[]interface{}{}, TokenKey, "items"}, + callback{[]interface{}{"items"}, TokenArray, nil}, + callback{[]interface{}{"items"}, TokenIndex, 0}, + callback{[]interface{}{"items", 0}, TokenValue, "a"}, + callback{[]interface{}{"items"}, TokenIndex, 1}, + callback{[]interface{}{"items", 1}, TokenValue, "b"}, + callback{[]interface{}{"items"}, TokenIndex, 2}, + callback{[]interface{}{"items", 2}, TokenValue, "c"}, + callback{[]interface{}{}, TokenKey, "key"}, + callback{[]interface{}{"key"}, TokenValue, "value"}, + } + + err := node.Read(func(path []interface{}, tokenType ReaderToken, value interface{}) error { + assert.Logf("At callback %d", i) + if i >= len(callbacks) { + assert.Fail() + } else { + cb := callbacks[i] + assert.Equal(cb.path, path) + assert.Equal(cb.tokenType, tokenType) + assert.Equal(cb.value, value) + } + i++ + return nil + }) + assert.Nil(err) + +}