├── .circleci ├── config.yml └── cover.test.sh ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── codecov.yml ├── go.mod ├── pointer.go ├── pointer_test.go ├── traversal.go └── traversal_test.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | jobs: 3 | build: 4 | working_directory: /go/src/github.com/qri-io/jsonschema 5 | docker: 6 | - image: circleci/golang:1.9 7 | environment: 8 | GOLANG_ENV: test 9 | PORT: 3000 10 | environment: 11 | TEST_RESULTS: /tmp/test-results 12 | steps: 13 | - checkout 14 | - run: mkdir -p $TEST_RESULTS 15 | - run: go get github.com/jstemmer/go-junit-report github.com/golang/lint/golint 16 | - run: 17 | name: Install deps 18 | command: > 19 | go get -v -d -u 20 | github.com/jstemmer/go-junit-report 21 | - run: 22 | name: Run Lint Tests 23 | command: golint -set_exit_status ./... 24 | - run: 25 | name: Run Tests 26 | command: go test -v -race -coverprofile=coverage.txt -covermode=atomic 27 | - run: 28 | name: Publish coverage info to codecov.io 29 | command: bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /.circleci/cover.test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | echo "" > coverage.txt 5 | go test -v -race -coverprofile=profile.out -covermode=atomic github.com/qri-io/varName 6 | if [ -f profile.out ]; then 7 | cat profile.out >> coverage.txt 8 | rm profile.out 9 | fi -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | coverage.txt -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # (2020-05-06) 2 | 3 | This is an update to jsonpointer. It adds usability functions and options for perfomance optimized use. 4 | 5 | ### Bug Fixes 6 | 7 | * **Test:** fix failing tests when using go 1.14. [c51da06](https://github.com/qri-io/jsonpointer/commit/c51da06b3a9796e12c0a8309b728b015c01387c0) 8 | 9 | ### Features 10 | 11 | * **Head,Tail,IsEmpty:** added methods to get the first token, all tokens after the head and to check if a given pointer is empty [c51da06](https://github.com/qri-io/jsonpointer/commit/c51da06b3a9796e12c0a8309b728b015c01387c0) 12 | * **RawDescendant,NewPointer:** methods that allow to directly append to the current pointer without safety checks and a way to create a pointer with pre-allocated memory for performance intensive use cases [c51da06](https://github.com/qri-io/jsonpointer/commit/c51da06b3a9796e12c0a8309b728b015c01387c0) 13 | 14 | # (2019-05-23) 15 | 16 | This is the first proper release of jsonpointer. In preparation for go 1.13, in which `go.mod` files and go modules are the primary way to handle go dependencies, we are going to do an official release of all our modules. This will be version v0.1.0 of jsonpointer. 17 | 18 | ### Bug Fixes 19 | 20 | * **Parse:** fix incorrect handling of empty url fragment strings ([5919095](https://github.com/qri-io/jsonpointer/commit/5919095)) 21 | 22 | 23 | ### Features 24 | 25 | * **Descendant,WalkJSON:** added pointer descendant method, experimental WalkJSON func ([707e879](https://github.com/qri-io/jsonpointer/commit/707e879)) 26 | * initial commit ([448ab45](https://github.com/qri-io/jsonpointer/commit/448ab45)) 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Brendan O'Brien 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Let's keep all our changelog commands the same across all our packages: 2 | update-changelog: 3 | conventional-changelog -p angular -i CHANGELOG.md -s -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Qri](https://img.shields.io/badge/made%20by-qri-magenta.svg?style=flat-square)](https://qri.io) 2 | [![GoDoc](https://godoc.org/github.com/qri-io/jsonpointer?status.svg)](http://godoc.org/github.com/qri-io/jsonpointer) 3 | [![License](https://img.shields.io/github/license/qri-io/jsonpointer.svg?style=flat-square)](./LICENSE) 4 | [![Codecov](https://img.shields.io/codecov/c/github/qri-io/jsonpointer.svg?style=flat-square)](https://codecov.io/gh/qri-io/jsonpointer) 5 | [![CI](https://img.shields.io/circleci/project/github/qri-io/jsonpointer.svg?style=flat-square)](https://circleci.com/gh/qri-io/jsonpointer) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/qri-io/jsonpointer)](https://goreportcard.com/report/github.com/qri-io/jsonpointer) 7 | 8 | 9 | # jsonpointer 10 | golang implementation of [IETF RFC6901](https://tools.ietf.org/html/rfc6901): 11 | _JSON Pointer defines a string syntax for identifying a specific value within a JavaScript Object Notation (JSON) document._ 12 | 13 | ### Installation 14 | install with: 15 | `go get -u github.com/qri-io/jsonpointer` 16 | 17 | 18 | ### Usage 19 | Here's a quick example pulled from the [godoc](https://godoc.org/github.com/qri-io/jsonpointer): 20 | 21 | ```go 22 | import ( 23 | "encoding/json" 24 | "fmt" 25 | "github.com/qri-io/jsonpointer" 26 | ) 27 | 28 | var document = []byte(`{ 29 | "foo": { 30 | "bar": { 31 | "baz": [0,"hello!"] 32 | } 33 | } 34 | }`) 35 | 36 | func main() { 37 | parsed := map[string]interface{}{} 38 | // be sure to handle errors in real-world code! 39 | json.Unmarshal(document, &parsed) 40 | 41 | // parse a json pointer. Pointers can also be url fragments 42 | // the following are equivelent pointers: 43 | // "/foo/bar/baz/1" 44 | // "#/foo/bar/baz/1" 45 | // "http://example.com/document.json#/foo/bar/baz/1" 46 | ptr, _ := jsonpointer.Parse("/foo/bar/baz/1") 47 | 48 | // evaluate the pointer against the document 49 | // evaluation always starts at the root of the document 50 | got, _ := ptr.Eval(parsed) 51 | 52 | fmt.Println(got) 53 | // Output: hello! 54 | } 55 | 56 | ``` 57 | 58 | ### License 59 | MIT 60 | 61 | ### Issues & Contributions 62 | Contributions & Issues are more than welcome! Everything happens over on this repo's [github page](https://github.com/qri-io/jsonpointer) -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | ci: 3 | - "ci/circle-ci" 4 | notify: 5 | require_ci_to_pass: no 6 | after_n_builds: 2 7 | coverage: 8 | range: "80...100" 9 | comment: off -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/qri-io/jsonpointer 2 | -------------------------------------------------------------------------------- /pointer.go: -------------------------------------------------------------------------------- 1 | // Package jsonpointer implements IETF rfc6901 2 | // JSON Pointers are a string syntax for 3 | // identifying a specific value within a JavaScript Object Notation 4 | // (JSON) document [RFC4627]. JSON Pointer is intended to be easily 5 | // expressed in JSON string values as well as Uniform Resource 6 | // Identifier (URI) [RFC3986] fragment identifiers. 7 | // 8 | // this package is intended to work like net/url from the go 9 | // standard library 10 | package jsonpointer 11 | 12 | import ( 13 | "fmt" 14 | "net/url" 15 | "strconv" 16 | "strings" 17 | ) 18 | 19 | const defaultPointerAllocationSize = 32 20 | 21 | // Parse parses str into a Pointer structure. 22 | // str may be a pointer or a url string. 23 | // If a url string, Parse will use the URL's fragment component 24 | // (the bit after the '#' symbol) 25 | func Parse(str string) (Pointer, error) { 26 | // fast paths that skip url parse step 27 | if len(str) == 0 || str == "#" { 28 | return Pointer{}, nil 29 | } else if str[0] == '/' { 30 | return parse(str) 31 | } 32 | 33 | u, err := url.Parse(str) 34 | if err != nil { 35 | return nil, err 36 | } 37 | return parse(u.Fragment) 38 | } 39 | 40 | // IsEmpty is a utility function to check if the Pointer 41 | // is empty / nil equivalent 42 | func (p Pointer) IsEmpty() bool { 43 | return len(p) == 0 44 | } 45 | 46 | // Head returns the root of the Pointer 47 | func (p Pointer) Head() *string { 48 | if len(p) == 0 { 49 | return nil 50 | } 51 | return &p[0] 52 | } 53 | 54 | // Tail returns everything after the Pointer head 55 | func (p Pointer) Tail() Pointer { 56 | return Pointer(p[1:]) 57 | } 58 | 59 | // The ABNF syntax of a JSON Pointer is: 60 | // json-pointer = *( "/" reference-token ) 61 | // reference-token = *( unescaped / escaped ) 62 | // unescaped = %x00-2E / %x30-7D / %x7F-10FFFF 63 | // ; %x2F ('/') and %x7E ('~') are excluded from 'unescaped' 64 | // escaped = "~" ( "0" / "1" ) 65 | // ; representing '~' and '/', respectively 66 | func parse(str string) (Pointer, error) { 67 | if len(str) == 0 { 68 | return Pointer{}, nil 69 | } 70 | 71 | if str[0] != '/' { 72 | return nil, fmt.Errorf("non-empty references must begin with a '/' character") 73 | } 74 | str = str[1:] 75 | 76 | toks := strings.Split(str, separator) 77 | for i, t := range toks { 78 | toks[i] = unescapeToken(t) 79 | } 80 | return Pointer(toks), nil 81 | } 82 | 83 | // Pointer represents a parsed JSON pointer 84 | type Pointer []string 85 | 86 | // NewPointer creates a Pointer with a pre-allocated block of memory 87 | // to avoid repeated slice expansions 88 | func NewPointer() Pointer { 89 | return make([]string, 0, defaultPointerAllocationSize) 90 | } 91 | 92 | // String implements the stringer interface for Pointer, 93 | // giving the escaped string 94 | func (p Pointer) String() (str string) { 95 | for _, tok := range p { 96 | str += "/" + escapeToken(tok) 97 | } 98 | return 99 | } 100 | 101 | // Eval evaluates a json pointer against a given root JSON document 102 | // Evaluation of a JSON Pointer begins with a reference to the root 103 | // value of a JSON document and completes with a reference to some value 104 | // within the document. Each reference token in the JSON Pointer is 105 | // evaluated sequentially. 106 | func (p Pointer) Eval(data interface{}) (result interface{}, err error) { 107 | result = data 108 | for _, tok := range p { 109 | if result, err = p.evalToken(tok, result); err != nil { 110 | return nil, err 111 | } 112 | } 113 | return 114 | } 115 | 116 | // Descendant returns a new pointer to a descendant of the current pointer 117 | // parsing the input path into components 118 | func (p Pointer) Descendant(path string) (Pointer, error) { 119 | if !strings.HasPrefix(path, "/") { 120 | path = "/" + path 121 | } 122 | dpath, err := parse(path) 123 | if err != nil { 124 | return p, err 125 | } 126 | 127 | if p.String() == "/" { 128 | return dpath, nil 129 | } 130 | 131 | return append(p, dpath...), nil 132 | } 133 | 134 | // RawDescendant extends the pointer with 1 or more path tokens 135 | // The function itself is unsafe as it doesnt fully parse the input 136 | // and assumes the user is directly managing the pointer 137 | // This allows for much faster pointer management 138 | func (p Pointer) RawDescendant(path ...string) Pointer { 139 | return append(p, path...) 140 | } 141 | 142 | // Evaluation of each reference token begins by decoding any escaped 143 | // character sequence. This is performed by first transforming any 144 | // occurrence of the sequence '~1' to '/', and then transforming any 145 | // occurrence of the sequence '~0' to '~'. By performing the 146 | // substitutions in this order, an implementation avoids the error of 147 | // turning '~01' first into '~1' and then into '/', which would be 148 | // incorrect (the string '~01' correctly becomes '~1' after 149 | // transformation). 150 | // The reference token then modifies which value is referenced according 151 | // to the following scheme: 152 | func (p Pointer) evalToken(tok string, data interface{}) (interface{}, error) { 153 | switch ch := data.(type) { 154 | case map[string]interface{}: 155 | return ch[tok], nil 156 | case []interface{}: 157 | i, err := strconv.Atoi(tok) 158 | if err != nil { 159 | return nil, fmt.Errorf("invalid array index: %s", tok) 160 | } 161 | if i >= len(ch) { 162 | return nil, fmt.Errorf("index %d exceeds array length of %d", i, len(ch)) 163 | } 164 | return ch[i], nil 165 | default: 166 | return nil, fmt.Errorf("invalid JSON pointer: %s", p.String()) 167 | } 168 | } 169 | 170 | const ( 171 | separator = "/" 172 | escapedSeparator = "~1" 173 | tilde = "~" 174 | escapedTilde = "~0" 175 | ) 176 | 177 | func unescapeToken(tok string) string { 178 | tok = strings.Replace(tok, escapedSeparator, separator, -1) 179 | return strings.Replace(tok, escapedTilde, tilde, -1) 180 | } 181 | 182 | func escapeToken(tok string) string { 183 | tok = strings.Replace(tok, tilde, escapedTilde, -1) 184 | return strings.Replace(tok, separator, escapedSeparator, -1) 185 | } 186 | -------------------------------------------------------------------------------- /pointer_test.go: -------------------------------------------------------------------------------- 1 | package jsonpointer 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func Example() { 12 | var document = []byte(`{ 13 | "foo": { 14 | "bar": { 15 | "baz": [0,"hello!"] 16 | } 17 | } 18 | }`) 19 | 20 | // unmarshal our document into generic go structs 21 | parsed := map[string]interface{}{} 22 | // be sure to handle errors in real-world code! 23 | json.Unmarshal(document, &parsed) 24 | 25 | // parse a json pointer. Pointers can also be url fragments 26 | // the following are equivelent pointers: 27 | // "/foo/bar/baz/1" 28 | // "#/foo/bar/baz/1" 29 | // "http://example.com/document.json#/foo/bar/baz/1" 30 | ptr, _ := Parse("/foo/bar/baz/1") 31 | 32 | // evaluate the pointer against the document 33 | // evaluation always starts at the root of the document 34 | got, _ := ptr.Eval(parsed) 35 | 36 | fmt.Println(got) 37 | // Output: hello! 38 | } 39 | 40 | // doc pulled from spec: 41 | var docBytes = []byte(`{ 42 | "foo": ["bar", "baz"], 43 | "": 0, 44 | "a/b": 1, 45 | "c%d": 2, 46 | "e^f": 3, 47 | "g|h": 4, 48 | "i\\j": 5, 49 | "k\"l": 6, 50 | " ": 7, 51 | "m~n": 8 52 | }`) 53 | 54 | func TestParse(t *testing.T) { 55 | cases := []struct { 56 | raw string 57 | parsed string 58 | err string 59 | }{ 60 | {"#/", "/", ""}, 61 | {"#/foo", "/foo", ""}, 62 | {"#/foo/", "/foo/", ""}, 63 | 64 | {"://", "", "missing protocol scheme"}, 65 | {"#7", "", "non-empty references must begin with a '/' character"}, 66 | {"", "", ""}, 67 | {"https://example.com#", "", ""}, 68 | } 69 | 70 | for i, c := range cases { 71 | got, err := Parse(c.raw) 72 | if !(err == nil && c.err == "" || err != nil && strings.Contains(err.Error(), c.err)) { 73 | t.Errorf("case %d error mismatch. expected: '%s', got: '%s'", i, c.err, err) 74 | continue 75 | } 76 | 77 | if c.err == "" && got.String() != c.parsed { 78 | t.Errorf("case %d string output mismatch: expected: '%s', got: '%s'", i, c.parsed, got.String()) 79 | continue 80 | } 81 | } 82 | } 83 | 84 | func TestEval(t *testing.T) { 85 | doc := map[string]interface{}{} 86 | if err := json.Unmarshal(docBytes, &doc); err != nil { 87 | t.Errorf("error unmarshaling document json: %s", err.Error()) 88 | return 89 | } 90 | 91 | cases := []struct { 92 | ptrstring string 93 | expect interface{} 94 | err string 95 | }{ 96 | // "raw" references 97 | {"", doc, ""}, 98 | {"/foo", doc["foo"], ""}, 99 | {"/foo/0", "bar", ""}, 100 | {"/", float64(0), ""}, 101 | {"/a~1b", float64(1), ""}, 102 | {"/c%d", float64(2), ""}, 103 | {"/e^f", float64(3), ""}, 104 | {"/g|h", float64(4), ""}, 105 | {"/i\\j", float64(5), ""}, 106 | {"/k\"l", float64(6), ""}, 107 | {"/ ", float64(7), ""}, 108 | {"/m~0n", float64(8), ""}, 109 | // 110 | {"/undefined", nil, ""}, 111 | 112 | // url fragment references 113 | {"#", doc, ""}, 114 | {"#/foo", doc["foo"], ""}, 115 | {"#/foo/0", "bar", ""}, 116 | {"#/", float64(0), ""}, 117 | {"#/a~1b", float64(1), ""}, 118 | {"#/c%25d", float64(2), ""}, 119 | {"#/e%5Ef", float64(3), ""}, 120 | {"#/g%7Ch", float64(4), ""}, 121 | {"#/i%5Cj", float64(5), ""}, 122 | {"#/k%22l", float64(6), ""}, 123 | {"#/%20", float64(7), ""}, 124 | {"#/m~0n", float64(8), ""}, 125 | 126 | {"https://example.com#/m~0n", float64(8), ""}, 127 | 128 | // bad references 129 | {"/foo/bar", nil, "invalid array index: bar"}, 130 | {"/foo/3", nil, "index 3 exceeds array length of 2"}, 131 | {"/bar/baz", nil, "invalid JSON pointer: /bar/baz"}, 132 | } 133 | 134 | for i, c := range cases { 135 | ptr, err := Parse(c.ptrstring) 136 | if err != nil { 137 | t.Errorf("case %d unexpected parse error: %s", i, err.Error()) 138 | continue 139 | } 140 | 141 | got, err := ptr.Eval(doc) 142 | if !(err == nil && c.err == "" || err != nil && err.Error() == c.err) { 143 | t.Errorf("case %d error mismatch. expected: '%s', got: '%s'", i, c.err, err) 144 | continue 145 | } 146 | 147 | if !reflect.DeepEqual(c.expect, got) { 148 | t.Errorf("case %d result mismatch. expected: %v, got: %v", i, c.expect, got) 149 | continue 150 | } 151 | } 152 | } 153 | 154 | func TestDescendent(t *testing.T) { 155 | cases := []struct { 156 | parent string 157 | path string 158 | parsed string 159 | err string 160 | }{ 161 | {"#/", "0", "/0", ""}, 162 | {"/0", "0", "/0/0", ""}, 163 | {"/foo", "0", "/foo/0", ""}, 164 | {"/foo", "0", "/foo/0", ""}, 165 | {"/foo/0", "0", "/foo/0/0", ""}, 166 | } 167 | 168 | for i, c := range cases { 169 | p, err := Parse(c.parent) 170 | if err != nil { 171 | t.Errorf("case %d error parsing parent: %s", i, err.Error()) 172 | continue 173 | } 174 | 175 | desc, err := p.Descendant(c.path) 176 | if !(err == nil && c.err == "" || err != nil && err.Error() == c.err) { 177 | t.Errorf("case %d error mismatch. expected: %s, got: %s", i, c.err, err) 178 | continue 179 | } 180 | 181 | if desc.String() != c.parsed { 182 | t.Errorf("case %d: expected: %s, got: %s", i, c.parsed, desc.String()) 183 | continue 184 | } 185 | } 186 | } 187 | 188 | func BenchmarkEval(b *testing.B) { 189 | document := []byte(`{ 190 | "foo": { 191 | "bar": { 192 | "baz": [0,"hello!"] 193 | } 194 | } 195 | }`) 196 | 197 | parsed := map[string]interface{}{} 198 | json.Unmarshal(document, &parsed) 199 | ptr, _ := Parse("/foo/bar/baz/1") 200 | 201 | b.ResetTimer() 202 | for i := 0; i < b.N; i++ { 203 | if _, err := ptr.Eval(parsed); err != nil { 204 | b.Errorf("error evaluating: %s", err.Error()) 205 | continue 206 | } 207 | 208 | } 209 | } 210 | 211 | func TestEscapeToken(t *testing.T) { 212 | cases := []struct { 213 | input string 214 | output string 215 | }{ 216 | {"/abc~1/~/0/~0/", "/abc~1/~/0/~0/"}, 217 | } 218 | for i, c := range cases { 219 | got := unescapeToken(escapeToken(c.input)) 220 | if got != c.output { 221 | t.Errorf("case %d result mismatch. expected: '%s', got: '%s'", i, c.output, got) 222 | } 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /traversal.go: -------------------------------------------------------------------------------- 1 | package jsonpointer 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | // JSONContainer returns any existing child value for a given JSON property string 8 | type JSONContainer interface { 9 | // JSONProp takes a string reference for a given JSON property. 10 | // implementations must return any matching property of that name, 11 | // nil if no such subproperty exists. 12 | // Note that implementations on slice-types are expected to convert 13 | // prop to an integer value 14 | JSONProp(prop string) interface{} 15 | } 16 | 17 | // JSONParent is an interface that enables tree traversal by listing 18 | // all immediate children of an object 19 | type JSONParent interface { 20 | // JSONChildren should return all immidiate children of this element 21 | // with json property names as keys, go types as values 22 | // Note that implementations on slice-types are expected to convert 23 | // integers to string keys 24 | JSONProps() map[string]interface{} 25 | } 26 | 27 | // WalkJSON calls visit on all elements in a tree of decoded json 28 | func WalkJSON(tree interface{}, visit func(elem interface{}) error) error { 29 | if tree == nil { 30 | return nil 31 | } 32 | 33 | if err := visit(tree); err != nil { 34 | return err 35 | } 36 | 37 | if con, ok := tree.(JSONParent); ok { 38 | for _, ch := range con.JSONProps() { 39 | if err := WalkJSON(ch, visit); err != nil { 40 | return err 41 | } 42 | } 43 | return nil 44 | } 45 | 46 | // fast-path for common json types 47 | switch t := tree.(type) { 48 | case map[string]interface{}: 49 | for _, val := range t { 50 | if err := WalkJSON(val, visit); err != nil { 51 | return err 52 | } 53 | } 54 | return nil 55 | case []interface{}: 56 | for _, val := range t { 57 | if err := WalkJSON(val, visit); err != nil { 58 | return err 59 | } 60 | } 61 | return nil 62 | } 63 | 64 | return walkValue(reflect.ValueOf(tree), visit) 65 | } 66 | 67 | func walkValue(v reflect.Value, visit func(elem interface{}) error) error { 68 | switch v.Kind() { 69 | case reflect.Invalid: 70 | return nil 71 | case reflect.Ptr: 72 | if !v.IsNil() { 73 | walkValue(v.Elem(), visit) 74 | } 75 | case reflect.Map: 76 | for _, key := range v.MapKeys() { 77 | mi := v.MapIndex(key) 78 | if mi.CanInterface() { 79 | WalkJSON(mi.Interface(), visit) 80 | } 81 | } 82 | case reflect.Struct: 83 | // t := v.Type() 84 | // TypeOf returns the reflection Type that represents the dynamic type of variable. 85 | // If variable is a nil interface value, TypeOf returns nil. 86 | for i := 0; i < v.NumField(); i++ { 87 | f := v.Field(i) 88 | // fmt.Printf("%d: %s %s %s = %v\n", i, t.Field(i).Name, f.Type(), t.Field(i).Tag.Get("json"), f.CanInterface()) 89 | if f.CanInterface() { 90 | WalkJSON(f.Interface(), visit) 91 | } 92 | } 93 | case reflect.Slice, reflect.Array: 94 | for i := 0; i < v.Len(); i++ { 95 | WalkJSON(v.Index(i).Interface(), visit) 96 | } 97 | } 98 | return nil 99 | } 100 | -------------------------------------------------------------------------------- /traversal_test.go: -------------------------------------------------------------------------------- 1 | package jsonpointer 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | ) 7 | 8 | type A struct { 9 | Foo string `json:"$foo,omitempty"` 10 | Foo2 bool `json:"$foo2"` 11 | Bar B 12 | } 13 | 14 | type B struct { 15 | nope string 16 | Baz int 17 | C C 18 | D D 19 | } 20 | 21 | type C struct { 22 | Nope string 23 | } 24 | 25 | func (c C) JSONProps() map[string]interface{} { 26 | return map[string]interface{}{ 27 | "bat": "book", 28 | "stuff": false, 29 | "other": nil, 30 | } 31 | } 32 | 33 | type D []string 34 | 35 | var data = []byte(`{ 36 | "$foo" : "fooval", 37 | "$foo2" : true, 38 | "bar" : { 39 | "baz" : 1, 40 | "C" : { 41 | "won't" : "register" 42 | } 43 | } 44 | }`) 45 | 46 | func TestWalkJSON(t *testing.T) { 47 | a := &A{} 48 | if err := json.Unmarshal(data, a); err != nil { 49 | t.Errorf("unexpected unmarshal error: %s", err.Error()) 50 | return 51 | } 52 | 53 | elements := 0 54 | expectElements := 9 55 | WalkJSON(a, func(elem interface{}) error { 56 | t.Logf("%#v", elem) 57 | elements++ 58 | return nil 59 | }) 60 | 61 | if elements != expectElements { 62 | t.Errorf("expected %d elements, got: %d", expectElements, elements) 63 | } 64 | 65 | } 66 | --------------------------------------------------------------------------------