├── .gitignore ├── doc.go ├── testdata ├── code.json.gz ├── 357.json ├── pools.json └── serieslysample.json ├── README.markdown ├── map.go ├── ptrtool └── ptrtool.go ├── LICENSE ├── map_test.go ├── reflect.go ├── reflect_test.go ├── bytes.go └── bytes_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | #* 2 | *~ 3 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package jsonpointer implements RFC6901 JSON Pointers 2 | package jsonpointer 3 | -------------------------------------------------------------------------------- /testdata/code.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustin/go-jsonpointer/HEAD/testdata/code.json.gz -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # JSON Pointer for go 2 | 3 | This is an implementation of [JSON Pointer](http://tools.ietf.org/html/rfc6901). 4 | 5 | [![Coverage Status](https://coveralls.io/repos/dustin/go-jsonpointer/badge.png?branch=master)](https://coveralls.io/r/dustin/go-jsonpointer?branch=master) 6 | -------------------------------------------------------------------------------- /testdata/357.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "357", 3 | "city": "", 4 | "state": "", 5 | "code": "", 6 | "country": "", 7 | "phone": "", 8 | "website": "", 9 | "type": "brewery", 10 | "updated": "2010-07-22 20:00:20", 11 | "description": "", 12 | "address": [], 13 | "address2": ["x"], 14 | "address3": [ ] 15 | } 16 | -------------------------------------------------------------------------------- /map.go: -------------------------------------------------------------------------------- 1 | package jsonpointer 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | // Get the value at the specified path. 9 | func Get(m map[string]interface{}, path string) interface{} { 10 | if path == "" { 11 | return m 12 | } 13 | 14 | parts := strings.Split(path[1:], "/") 15 | var rv interface{} = m 16 | 17 | for _, p := range parts { 18 | switch v := rv.(type) { 19 | case map[string]interface{}: 20 | if strings.Contains(p, "~") { 21 | p = strings.Replace(p, "~1", "/", -1) 22 | p = strings.Replace(p, "~0", "~", -1) 23 | } 24 | rv = v[p] 25 | case []interface{}: 26 | i, err := strconv.Atoi(p) 27 | if err == nil && i < len(v) { 28 | rv = v[i] 29 | } else { 30 | return nil 31 | } 32 | default: 33 | return nil 34 | } 35 | } 36 | 37 | return rv 38 | } 39 | -------------------------------------------------------------------------------- /ptrtool/ptrtool.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "os" 10 | 11 | "github.com/dustin/go-jsonpointer" 12 | ) 13 | 14 | func listPointers(d []byte) { 15 | l, err := jsonpointer.ListPointers(d) 16 | if err != nil { 17 | log.Fatalf("Error listing pointers: %v", err) 18 | } 19 | for _, p := range l { 20 | fmt.Println(p) 21 | } 22 | 23 | } 24 | 25 | func selectItems(d []byte, pointers []string) { 26 | m, err := jsonpointer.FindMany(d, pointers) 27 | if err != nil { 28 | log.Fatalf("Error finding pointers: %v", err) 29 | } 30 | for k, v := range m { 31 | b := &bytes.Buffer{} 32 | json.Indent(b, v, "", " ") 33 | fmt.Printf("%v\n%s\n\n", k, b) 34 | } 35 | } 36 | 37 | func main() { 38 | d, err := ioutil.ReadAll(os.Stdin) 39 | if err != nil { 40 | log.Fatalf("Error reading json from stdin: %v", err) 41 | } 42 | if len(os.Args) == 1 { 43 | listPointers(d) 44 | } else { 45 | selectItems(d, os.Args[1:]) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Dustin Sallings 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /testdata/pools.json: -------------------------------------------------------------------------------- 1 | { 2 | "componentsVersion": { 3 | "ale": "8cffe61", 4 | "couch": "1.2.0a-be4fa61-git", 5 | "couch_index_merger": "1.2.0a-be4fa61-git", 6 | "couch_set_view": "1.2.0a-be4fa61-git", 7 | "couch_view_parser": "1.0.0", 8 | "crypto": "2.0.4", 9 | "inets": "5.7.1", 10 | "kernel": "2.14.5", 11 | "lhttpc": "1.3.0", 12 | "mapreduce": "1.0.0", 13 | "mnesia": "4.5", 14 | "mochiweb": "1.4.1", 15 | "ns_server": "2.0.0-1976-rel-enterprise", 16 | "oauth": "7d85d3ef", 17 | "os_mon": "2.2.7", 18 | "public_key": "0.13", 19 | "sasl": "2.1.10", 20 | "ssl": "4.1.6", 21 | "stdlib": "1.17.5" 22 | }, 23 | "implementationVersion": "2.0.0-1976-rel-enterprise", 24 | "isAdminCreds": false, 25 | "pools": [ 26 | { 27 | "name": "default", 28 | "streamingUri": "/poolsStreaming/default?uuid=8820305b03a5d328c54bafde4c3f469e", 29 | "uri": "/pools/default?uuid=8820305b03a5d328c54bafde4c3f469e" 30 | } 31 | ], 32 | "settings": { 33 | "maxParallelIndexers": "/settings/maxParallelIndexers?uuid=8820305b03a5d328c54bafde4c3f469e", 34 | "viewUpdateDaemon": "/settings/viewUpdateDaemon?uuid=8820305b03a5d328c54bafde4c3f469e" 35 | }, 36 | "uuid": "8820305b03a5d328c54bafde4c3f469e" 37 | } 38 | -------------------------------------------------------------------------------- /map_test.go: -------------------------------------------------------------------------------- 1 | package jsonpointer 2 | 3 | import ( 4 | "io/ioutil" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/dustin/gojson" 9 | ) 10 | 11 | const objSrc = `{ 12 | "foo": ["bar", "baz"], 13 | "": 0, 14 | "a/b": 1, 15 | "c%d": 2, 16 | "e^f": 3, 17 | "g|h": 4, 18 | "i\\j": 5, 19 | "k\"l": 6, 20 | " ": 7, 21 | "m~n": 8, 22 | "g/n/r": "has slash, will travel", 23 | "g": { "n": {"r": "where's tito?"}} 24 | }` 25 | 26 | var obj = map[string]interface{}{} 27 | 28 | var tests = []struct { 29 | path string 30 | exp interface{} 31 | }{ 32 | {"", obj}, 33 | {"/foo", []interface{}{"bar", "baz"}}, 34 | {"/foo/0", "bar"}, 35 | {"/foo/99", nil}, 36 | {"/foo/0/3", nil}, 37 | {"/", 0.0}, 38 | {"/a~1b", 1.0}, 39 | {"/c%d", 2.0}, 40 | {"/e^f", 3.0}, 41 | {"/g|h", 4.0}, 42 | {"/i\\j", 5.0}, 43 | {"/k\"l", 6.0}, 44 | {"/ ", 7.0}, 45 | {"/m~0n", 8.0}, 46 | {"/g~1n~1r", "has slash, will travel"}, 47 | {"/g/n/r", "where's tito?"}, 48 | } 49 | 50 | func init() { 51 | err := json.Unmarshal([]byte(objSrc), &obj) 52 | if err != nil { 53 | panic(err) 54 | } 55 | } 56 | 57 | func TestPaths(t *testing.T) { 58 | for _, test := range tests { 59 | got := Get(obj, test.path) 60 | if !reflect.DeepEqual(got, test.exp) { 61 | t.Errorf("On %v, expected %+v (%T), got %+v (%T)", 62 | test.path, test.exp, test.exp, got, got) 63 | } else { 64 | t.Logf("Success - got %v for %v", got, test.path) 65 | } 66 | } 67 | } 68 | 69 | func BenchmarkPaths(b *testing.B) { 70 | for i := 0; i < b.N; i++ { 71 | for _, test := range tests { 72 | Get(obj, test.path) 73 | } 74 | } 75 | } 76 | 77 | func BenchmarkParseAndPath(b *testing.B) { 78 | for i := 0; i < b.N; i++ { 79 | for _, test := range tests { 80 | o := map[string]interface{}{} 81 | err := json.Unmarshal([]byte(objSrc), &o) 82 | if err != nil { 83 | b.Fatalf("Error parsing: %v", err) 84 | } 85 | Get(o, test.path) 86 | } 87 | } 88 | } 89 | 90 | var bug3Data = []byte(`{"foo" : "bar"}`) 91 | 92 | func TestFindSpaceBeforeColon(t *testing.T) { 93 | val, err := Find(bug3Data, "/foo") 94 | if err != nil { 95 | t.Fatalf("Failed to find /foo: %v", err) 96 | } 97 | x, ok := json.UnquoteBytes(val) 98 | if !ok { 99 | t.Fatalf("Failed to unquote json bytes from %q", val) 100 | } 101 | if string(x) != "bar" { 102 | t.Fatalf("Expected %q, got %q", "bar", val) 103 | } 104 | } 105 | 106 | func TestListSpaceBeforeColon(t *testing.T) { 107 | ptrs, err := ListPointers(bug3Data) 108 | if err != nil { 109 | t.Fatalf("Error listing pointers: %v", err) 110 | } 111 | if len(ptrs) != 2 || ptrs[0] != "" || ptrs[1] != "/foo" { 112 | t.Fatalf(`Expected ["", "/foo"], got %#v`, ptrs) 113 | } 114 | } 115 | 116 | func TestIndexNotFoundSameAsPropertyNotFound(t *testing.T) { 117 | data, err := ioutil.ReadFile("testdata/357.json") 118 | if err != nil { 119 | t.Fatalf("Error beer-sample brewery 357 data: %v", err) 120 | } 121 | 122 | expectedResult, expectedError := Find(data, "/doesNotExist") 123 | 124 | missingVals := []string{ 125 | "/address/0", 126 | "/address/1", 127 | "/address2/1", 128 | "/address2/2", 129 | "/address3/0", 130 | "/address3/1", 131 | } 132 | 133 | for _, a := range missingVals { 134 | found, err := Find(data, a) 135 | 136 | if !reflect.DeepEqual(err, expectedError) { 137 | t.Errorf("Expected %v at %v, got %v", expectedError, a, err) 138 | } 139 | if !reflect.DeepEqual(expectedResult, found) { 140 | t.Errorf("Expected %v at %v, got %v", expectedResult, a, found) 141 | } 142 | } 143 | } 144 | 145 | const bug822src = `{ 146 | "foo": ["bar", "baz"], 147 | "": 0, 148 | "a/b": 1, 149 | "c%d": 2, 150 | "e^f": 3, 151 | "g|h": 4, 152 | "i\\j": 5, 153 | "k\"l": 6, 154 | "k2": {}, 155 | " ": 7, 156 | "m~n": 8, 157 | "g/n/r": "has slash, will travel", 158 | "g": { "n": {"r": "where's tito?"}}, 159 | "h": {} 160 | }` 161 | 162 | func TestListEmptyObjectPanic822(t *testing.T) { 163 | ptrs, err := ListPointers([]byte(bug822src)) 164 | if err != nil { 165 | t.Fatalf("Error parsing: %v", err) 166 | } 167 | t.Logf("Got pointers: %v", ptrs) 168 | } 169 | 170 | func TestFindEmptyObjectPanic823(t *testing.T) { 171 | for _, test := range tests { 172 | _, err := Find([]byte(bug822src), test.path) 173 | if err != nil { 174 | t.Errorf("Error looking for %v: %v", test.path, err) 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /reflect.go: -------------------------------------------------------------------------------- 1 | package jsonpointer 2 | 3 | import ( 4 | "reflect" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | // Reflect gets the value at the specified path from a struct. 10 | func Reflect(o interface{}, path string) interface{} { 11 | if path == "" { 12 | return o 13 | } 14 | 15 | parts := parsePointer(path) 16 | var rv interface{} = o 17 | 18 | OUTER: 19 | for _, p := range parts { 20 | val := reflect.ValueOf(rv) 21 | if val.Kind() == reflect.Ptr { 22 | val = val.Elem() 23 | } 24 | 25 | if val.Kind() == reflect.Struct { 26 | typ := val.Type() 27 | for i := 0; i < typ.NumField(); i++ { 28 | sf := typ.Field(i) 29 | tag := sf.Tag.Get("json") 30 | name := parseJSONTagName(tag) 31 | if (name != "" && name == p) || sf.Name == p { 32 | rv = val.Field(i).Interface() 33 | continue OUTER 34 | } 35 | } 36 | // Found no matching field. 37 | return nil 38 | } else if val.Kind() == reflect.Map { 39 | // our pointer always gives us a string key 40 | // here we try to convert it into the correct type 41 | mapKey, canConvert := makeMapKeyFromString(val.Type().Key(), p) 42 | if canConvert { 43 | field := val.MapIndex(mapKey) 44 | if field.IsValid() { 45 | rv = field.Interface() 46 | } else { 47 | return nil 48 | } 49 | } else { 50 | return nil 51 | } 52 | } else if val.Kind() == reflect.Slice || val.Kind() == reflect.Array { 53 | i, err := strconv.Atoi(p) 54 | if err == nil && i < val.Len() { 55 | rv = val.Index(i).Interface() 56 | } else { 57 | return nil 58 | } 59 | } else { 60 | return nil 61 | } 62 | } 63 | 64 | return rv 65 | } 66 | 67 | // ReflectListPointers lists all possible pointers from the given struct. 68 | func ReflectListPointers(o interface{}) ([]string, error) { 69 | return reflectListPointersRecursive(o, ""), nil 70 | } 71 | 72 | func reflectListPointersRecursive(o interface{}, prefix string) []string { 73 | rv := []string{prefix + ""} 74 | 75 | val := reflect.ValueOf(o) 76 | if val.Kind() == reflect.Ptr { 77 | val = val.Elem() 78 | } 79 | 80 | if val.Kind() == reflect.Struct { 81 | 82 | typ := val.Type() 83 | for i := 0; i < typ.NumField(); i++ { 84 | child := val.Field(i).Interface() 85 | sf := typ.Field(i) 86 | tag := sf.Tag.Get("json") 87 | name := parseJSONTagName(tag) 88 | if name != "" { 89 | // use the tag name 90 | childReults := reflectListPointersRecursive(child, prefix+encodePointer([]string{name})) 91 | rv = append(rv, childReults...) 92 | } else { 93 | // use the original field name 94 | childResults := reflectListPointersRecursive(child, prefix+encodePointer([]string{sf.Name})) 95 | rv = append(rv, childResults...) 96 | } 97 | } 98 | 99 | } else if val.Kind() == reflect.Map { 100 | for _, k := range val.MapKeys() { 101 | child := val.MapIndex(k).Interface() 102 | mapKeyName := makeMapKeyName(k) 103 | childReults := reflectListPointersRecursive(child, prefix+encodePointer([]string{mapKeyName})) 104 | rv = append(rv, childReults...) 105 | } 106 | } else if val.Kind() == reflect.Slice || val.Kind() == reflect.Array { 107 | for i := 0; i < val.Len(); i++ { 108 | child := val.Index(i).Interface() 109 | childResults := reflectListPointersRecursive(child, prefix+encodePointer([]string{strconv.Itoa(i)})) 110 | rv = append(rv, childResults...) 111 | } 112 | } 113 | return rv 114 | } 115 | 116 | // makeMapKeyName takes a map key value and creates a string representation 117 | func makeMapKeyName(v reflect.Value) string { 118 | switch v.Kind() { 119 | case reflect.Float32, reflect.Float64: 120 | fv := v.Float() 121 | return strconv.FormatFloat(fv, 'f', -1, v.Type().Bits()) 122 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 123 | iv := v.Int() 124 | return strconv.FormatInt(iv, 10) 125 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 126 | iv := v.Uint() 127 | return strconv.FormatUint(iv, 10) 128 | default: 129 | return v.String() 130 | } 131 | } 132 | 133 | // makeMapKeyFromString takes the key type for a map, and a string 134 | // representing the key, it then tries to convert the string 135 | // representation into a value of the correct type. 136 | func makeMapKeyFromString(mapKeyType reflect.Type, pointer string) (reflect.Value, bool) { 137 | valp := reflect.New(mapKeyType) 138 | val := reflect.Indirect(valp) 139 | switch mapKeyType.Kind() { 140 | case reflect.String: 141 | return reflect.ValueOf(pointer), true 142 | case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: 143 | iv, err := strconv.ParseInt(pointer, 10, mapKeyType.Bits()) 144 | if err == nil { 145 | val.SetInt(iv) 146 | return val, true 147 | } 148 | case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: 149 | iv, err := strconv.ParseUint(pointer, 10, mapKeyType.Bits()) 150 | if err == nil { 151 | val.SetUint(iv) 152 | return val, true 153 | } 154 | case reflect.Float32, reflect.Float64: 155 | fv, err := strconv.ParseFloat(pointer, mapKeyType.Bits()) 156 | if err == nil { 157 | val.SetFloat(fv) 158 | return val, true 159 | } 160 | } 161 | 162 | return reflect.ValueOf(nil), false 163 | } 164 | 165 | // parseJSONTagName extracts the JSON field name from a struct tag 166 | func parseJSONTagName(tag string) string { 167 | if idx := strings.Index(tag, ","); idx != -1 { 168 | return tag[:idx] 169 | } 170 | return tag 171 | } 172 | -------------------------------------------------------------------------------- /reflect_test.go: -------------------------------------------------------------------------------- 1 | package jsonpointer 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | type address struct { 9 | Street string `json:"street"` 10 | Zip string 11 | } 12 | 13 | type person struct { 14 | Name string `json:"name,omitempty"` 15 | Twitter string 16 | Aliases []string `json:"aliases"` 17 | Addresses []*address `json:"addresses"` 18 | NameTildeContained string `json:"name~contained"` 19 | NameSlashContained string `json:"name/contained"` 20 | AnActualArray [4]int 21 | NestedMap map[string]float64 `json:"nestedmap"` 22 | MapIntKey map[int]string 23 | MapUintKey map[uint]string 24 | MapFloatKey map[float64]string 25 | } 26 | 27 | var input = &person{ 28 | Name: "marty", 29 | Twitter: "mschoch", 30 | Aliases: []string{ 31 | "jabroni", 32 | "beer", 33 | }, 34 | Addresses: []*address{ 35 | &address{ 36 | Street: "123 Sesame St.", 37 | Zip: "99099", 38 | }, 39 | }, 40 | NameTildeContained: "yessir", 41 | NameSlashContained: "nosir", 42 | AnActualArray: [4]int{0, 1, 2, 3}, 43 | NestedMap: map[string]float64{ 44 | "pi": 3.14, 45 | "back/saidhe": 2.71, 46 | "till~duh": 1.41, 47 | }, 48 | MapIntKey: map[int]string{ 49 | 1: "one", 50 | 2: "two", 51 | }, 52 | MapUintKey: map[uint]string{ 53 | 3: "three", 54 | 4: "four", 55 | }, 56 | MapFloatKey: map[float64]string{ 57 | 3.14: "pi", 58 | 4.15: "notpi", 59 | }, 60 | } 61 | 62 | func benchReflect(b *testing.B, path string) { 63 | for i := 0; i < b.N; i++ { 64 | if Reflect(input, path) == nil { 65 | b.FailNow() 66 | } 67 | } 68 | } 69 | 70 | func BenchmarkReflectRoot(b *testing.B) { 71 | benchReflect(b, "") 72 | } 73 | 74 | func BenchmarkReflectToplevelExact(b *testing.B) { 75 | benchReflect(b, "/Twitter") 76 | } 77 | 78 | func BenchmarkReflectToplevelTagged(b *testing.B) { 79 | benchReflect(b, "/Name") 80 | } 81 | 82 | func BenchmarkReflectToplevelTaggedLower(b *testing.B) { 83 | benchReflect(b, "/name") 84 | } 85 | 86 | func BenchmarkReflectDeep(b *testing.B) { 87 | benchReflect(b, "/addresses/0/Zip") 88 | } 89 | 90 | func BenchmarkReflectSlash(b *testing.B) { 91 | benchReflect(b, "/name~1contained") 92 | } 93 | 94 | func BenchmarkReflectTilde(b *testing.B) { 95 | benchReflect(b, "/name~0contained") 96 | } 97 | 98 | func compareStringArrayIgnoringOrder(a, b []string) bool { 99 | if len(a) != len(b) { 100 | return false 101 | } 102 | tmp := make(map[string]bool, len(a)) 103 | for _, av := range a { 104 | tmp[av] = true 105 | } 106 | for _, bv := range b { 107 | if tmp[bv] != true { 108 | return false 109 | } 110 | } 111 | return true 112 | } 113 | 114 | func TestReflectListPointers(t *testing.T) { 115 | pointers, err := ReflectListPointers(input) 116 | if err != nil { 117 | t.Fatal(err) 118 | } 119 | expect := []string{"", "/name", "/Twitter", "/aliases", 120 | "/aliases/0", "/aliases/1", "/addresses", "/addresses/0", 121 | "/addresses/0/street", "/addresses/0/Zip", 122 | "/name~0contained", "/name~1contained", "/AnActualArray", 123 | "/AnActualArray/0", "/AnActualArray/1", "/AnActualArray/2", 124 | "/AnActualArray/3", "/nestedmap", "/nestedmap/pi", 125 | "/nestedmap/back~1saidhe", "/nestedmap/till~0duh", 126 | "/MapIntKey", "/MapIntKey/1", "/MapIntKey/2", "/MapUintKey", 127 | "/MapUintKey/3", "/MapUintKey/4", "/MapFloatKey", 128 | "/MapFloatKey/3.14", "/MapFloatKey/4.15"} 129 | if !compareStringArrayIgnoringOrder(expect, pointers) { 130 | t.Fatalf("expected %#v, got %#v", expect, pointers) 131 | } 132 | } 133 | 134 | func TestReflectNonObjectOrSlice(t *testing.T) { 135 | got := Reflect(36, "/test") 136 | if got != nil { 137 | t.Errorf("expected nil, got %#v", got) 138 | } 139 | } 140 | 141 | type structThatCanBeUsedAsKey struct { 142 | name string 143 | domain string 144 | } 145 | 146 | func TestReflectMapThatWontWork(t *testing.T) { 147 | 148 | amapthatwontwork := map[structThatCanBeUsedAsKey]string{} 149 | akey := structThatCanBeUsedAsKey{name: "marty", domain: "couchbase"} 150 | amapthatwontwork[akey] = "verycontrived" 151 | 152 | got := Reflect(amapthatwontwork, "/anykey") 153 | if got != nil { 154 | t.Errorf("expected nil, got %#v", got) 155 | } 156 | } 157 | 158 | func TestReflect(t *testing.T) { 159 | 160 | tests := []struct { 161 | path string 162 | exp interface{} 163 | }{ 164 | { 165 | path: "", 166 | exp: input, 167 | }, 168 | { 169 | path: "/", exp: nil, 170 | }, 171 | { 172 | path: "/name", 173 | exp: "marty", 174 | }, 175 | { 176 | path: "/Name", 177 | exp: "marty", 178 | }, 179 | { 180 | path: "/Twitter", 181 | exp: "mschoch", 182 | }, 183 | { 184 | path: "/aliases/0", 185 | exp: "jabroni", 186 | }, 187 | { 188 | path: "/Aliases/0", 189 | exp: "jabroni", 190 | }, 191 | { 192 | path: "/addresses/0/street", 193 | exp: "123 Sesame St.", 194 | }, 195 | { 196 | path: "/addresses/4/street", 197 | exp: nil, 198 | }, 199 | { 200 | path: "/doesntexist", 201 | exp: nil, 202 | }, 203 | { 204 | path: "/does/not/exit", 205 | exp: nil, 206 | }, 207 | { 208 | path: "/doesntexist/7", 209 | exp: nil, 210 | }, 211 | { 212 | path: "/name~0contained", 213 | exp: "yessir", 214 | }, 215 | { 216 | path: "/name~1contained", 217 | exp: "nosir", 218 | }, 219 | { 220 | path: "/AnActualArray/2", 221 | exp: 2, 222 | }, 223 | { 224 | path: "/AnActualArray/5", 225 | exp: nil, 226 | }, 227 | { 228 | path: "/nestedmap/pi", 229 | exp: 3.14, 230 | }, 231 | { 232 | path: "/nestedmap/back~1saidhe", 233 | exp: 2.71, 234 | }, 235 | { 236 | path: "/nestedmap/till~0duh", 237 | exp: 1.41, 238 | }, 239 | { 240 | path: "/MapIntKey/1", 241 | exp: "one", 242 | }, 243 | { 244 | path: "/MapUintKey/3", 245 | exp: "three", 246 | }, 247 | { 248 | path: "/MapFloatKey/3.14", 249 | exp: "pi", 250 | }, 251 | { 252 | path: "/MapFloatKey/4.0", 253 | exp: nil, 254 | }, 255 | } 256 | 257 | for _, test := range tests { 258 | output := Reflect(input, test.path) 259 | if !reflect.DeepEqual(output, test.exp) { 260 | t.Errorf("Expected %#v for %q, got %#v", test.exp, test.path, output) 261 | } 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /bytes.go: -------------------------------------------------------------------------------- 1 | package jsonpointer 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/dustin/gojson" 10 | ) 11 | 12 | func arreq(a, b []string) bool { 13 | if len(a) == len(b) { 14 | for i := range a { 15 | if a[i] != b[i] { 16 | return false 17 | } 18 | } 19 | return true 20 | } 21 | 22 | return false 23 | } 24 | 25 | func unescape(s string) string { 26 | n := strings.Count(s, "~") 27 | if n == 0 { 28 | return s 29 | } 30 | 31 | t := make([]byte, len(s)-n+1) // remove one char per ~ 32 | w := 0 33 | start := 0 34 | for i := 0; i < n; i++ { 35 | j := start + strings.Index(s[start:], "~") 36 | w += copy(t[w:], s[start:j]) 37 | if len(s) < j+2 { 38 | t[w] = '~' 39 | w++ 40 | break 41 | } 42 | c := s[j+1] 43 | switch c { 44 | case '0': 45 | t[w] = '~' 46 | w++ 47 | case '1': 48 | t[w] = '/' 49 | w++ 50 | default: 51 | t[w] = '~' 52 | w++ 53 | t[w] = c 54 | w++ 55 | } 56 | start = j + 2 57 | } 58 | w += copy(t[w:], s[start:]) 59 | return string(t[0:w]) 60 | } 61 | 62 | func parsePointer(s string) []string { 63 | a := strings.Split(s[1:], "/") 64 | if !strings.Contains(s, "~") { 65 | return a 66 | } 67 | 68 | for i := range a { 69 | if strings.Contains(a[i], "~") { 70 | a[i] = unescape(a[i]) 71 | } 72 | } 73 | return a 74 | } 75 | 76 | func escape(s string, out []rune) []rune { 77 | for _, c := range s { 78 | switch c { 79 | case '/': 80 | out = append(out, '~', '1') 81 | case '~': 82 | out = append(out, '~', '0') 83 | default: 84 | out = append(out, c) 85 | } 86 | } 87 | return out 88 | } 89 | 90 | func encodePointer(p []string) string { 91 | out := make([]rune, 0, 64) 92 | 93 | for _, s := range p { 94 | out = append(out, '/') 95 | out = escape(s, out) 96 | } 97 | return string(out) 98 | } 99 | 100 | func grokLiteral(b []byte) string { 101 | s, ok := json.UnquoteBytes(b) 102 | if !ok { 103 | panic("could not grok literal " + string(b)) 104 | } 105 | return string(s) 106 | } 107 | 108 | func isSpace(c rune) bool { 109 | return c == ' ' || c == '\t' || c == '\r' || c == '\n' 110 | } 111 | 112 | // FindDecode finds an object by JSONPointer path and then decode the 113 | // result into a user-specified object. Errors if a properly 114 | // formatted JSON document can't be found at the given path. 115 | func FindDecode(data []byte, path string, into interface{}) error { 116 | d, err := Find(data, path) 117 | if err != nil { 118 | return err 119 | } 120 | return json.Unmarshal(d, into) 121 | } 122 | 123 | // Find a section of raw JSON by specifying a JSONPointer. 124 | func Find(data []byte, path string) ([]byte, error) { 125 | if path == "" { 126 | return data, nil 127 | } 128 | 129 | needle := parsePointer(path) 130 | 131 | scan := &json.Scanner{} 132 | scan.Reset() 133 | 134 | offset := 0 135 | beganLiteral := 0 136 | current := make([]string, 0, 32) 137 | for { 138 | if offset >= len(data) { 139 | break 140 | } 141 | newOp := scan.Step(scan, int(data[offset])) 142 | offset++ 143 | 144 | switch newOp { 145 | case json.ScanBeginArray: 146 | current = append(current, "0") 147 | case json.ScanObjectKey: 148 | current[len(current)-1] = grokLiteral(data[beganLiteral-1 : offset-1]) 149 | case json.ScanBeginLiteral: 150 | beganLiteral = offset 151 | case json.ScanArrayValue: 152 | n := mustParseInt(current[len(current)-1]) 153 | current[len(current)-1] = strconv.Itoa(n + 1) 154 | case json.ScanEndArray, json.ScanEndObject: 155 | current = sliceToEnd(current) 156 | case json.ScanBeginObject: 157 | current = append(current, "") 158 | case json.ScanContinue, json.ScanSkipSpace, json.ScanObjectValue, json.ScanEnd: 159 | default: 160 | return nil, fmt.Errorf("found unhandled json op: %v", newOp) 161 | } 162 | 163 | if (newOp == json.ScanBeginArray || newOp == json.ScanArrayValue || 164 | newOp == json.ScanObjectKey) && arreq(needle, current) { 165 | otmp := offset 166 | for isSpace(rune(data[otmp])) { 167 | otmp++ 168 | } 169 | if data[otmp] == ']' { 170 | // special case an array offset miss 171 | offset = otmp 172 | return nil, nil 173 | } 174 | val, _, err := json.NextValue(data[offset:], scan) 175 | return val, err 176 | } 177 | } 178 | 179 | return nil, nil 180 | } 181 | 182 | func sliceToEnd(s []string) []string { 183 | end := len(s) - 1 184 | if end >= 0 { 185 | s = s[:end] 186 | } 187 | return s 188 | 189 | } 190 | 191 | func mustParseInt(s string) int { 192 | n, err := strconv.Atoi(s) 193 | if err == nil { 194 | return n 195 | } 196 | panic(err) 197 | } 198 | 199 | // ListPointers lists all possible pointers from the given input. 200 | func ListPointers(data []byte) ([]string, error) { 201 | if len(data) == 0 { 202 | return nil, fmt.Errorf("Invalid JSON") 203 | } 204 | rv := []string{""} 205 | 206 | scan := &json.Scanner{} 207 | scan.Reset() 208 | 209 | offset := 0 210 | beganLiteral := 0 211 | var current []string 212 | for { 213 | if offset >= len(data) { 214 | return rv, nil 215 | } 216 | newOp := scan.Step(scan, int(data[offset])) 217 | offset++ 218 | 219 | switch newOp { 220 | case json.ScanBeginArray: 221 | current = append(current, "0") 222 | case json.ScanObjectKey: 223 | current[len(current)-1] = grokLiteral(data[beganLiteral-1 : offset-1]) 224 | case json.ScanBeginLiteral: 225 | beganLiteral = offset 226 | case json.ScanArrayValue: 227 | n := mustParseInt(current[len(current)-1]) 228 | current[len(current)-1] = strconv.Itoa(n + 1) 229 | case json.ScanEndArray, json.ScanEndObject: 230 | current = sliceToEnd(current) 231 | case json.ScanBeginObject: 232 | current = append(current, "") 233 | case json.ScanError: 234 | return nil, fmt.Errorf("Error reading JSON object at offset %v", offset) 235 | } 236 | 237 | if newOp == json.ScanBeginArray || newOp == json.ScanArrayValue || 238 | newOp == json.ScanObjectKey { 239 | rv = append(rv, encodePointer(current)) 240 | } 241 | } 242 | } 243 | 244 | // FindMany finds several jsonpointers in one pass through the input. 245 | func FindMany(data []byte, paths []string) (map[string][]byte, error) { 246 | tpaths := make([]string, 0, len(paths)) 247 | m := map[string][]byte{} 248 | for _, p := range paths { 249 | if p == "" { 250 | m[p] = data 251 | } else { 252 | tpaths = append(tpaths, p) 253 | } 254 | } 255 | sort.Strings(tpaths) 256 | 257 | scan := &json.Scanner{} 258 | scan.Reset() 259 | 260 | offset := 0 261 | todo := len(tpaths) 262 | beganLiteral := 0 263 | matchedAt := 0 264 | var current []string 265 | for todo > 0 { 266 | if offset >= len(data) { 267 | break 268 | } 269 | newOp := scan.Step(scan, int(data[offset])) 270 | offset++ 271 | 272 | switch newOp { 273 | case json.ScanBeginArray: 274 | current = append(current, "0") 275 | case json.ScanObjectKey: 276 | current[len(current)-1] = grokLiteral(data[beganLiteral-1 : offset-1]) 277 | case json.ScanBeginLiteral: 278 | beganLiteral = offset 279 | case json.ScanArrayValue: 280 | n := mustParseInt(current[len(current)-1]) 281 | current[len(current)-1] = strconv.Itoa(n + 1) 282 | case json.ScanEndArray, json.ScanEndObject: 283 | current = sliceToEnd(current) 284 | case json.ScanBeginObject: 285 | current = append(current, "") 286 | } 287 | 288 | if newOp == json.ScanBeginArray || newOp == json.ScanArrayValue || 289 | newOp == json.ScanObjectKey { 290 | 291 | if matchedAt < len(current)-1 { 292 | continue 293 | } 294 | if matchedAt > len(current) { 295 | matchedAt = len(current) 296 | } 297 | 298 | currentStr := encodePointer(current) 299 | off := sort.SearchStrings(tpaths, currentStr) 300 | if off < len(tpaths) { 301 | // Check to see if the path we're 302 | // going down could even lead to a 303 | // possible match. 304 | if strings.HasPrefix(tpaths[off], currentStr) { 305 | matchedAt++ 306 | } 307 | // And if it's not an exact match, keep parsing. 308 | if tpaths[off] != currentStr { 309 | continue 310 | } 311 | } else { 312 | // Fell of the end of the list, no possible match 313 | continue 314 | } 315 | 316 | // At this point, we have an exact match, so grab it. 317 | stmp := &json.Scanner{} 318 | val, _, err := json.NextValue(data[offset:], stmp) 319 | if err != nil { 320 | return m, err 321 | } 322 | m[currentStr] = val 323 | todo-- 324 | } 325 | } 326 | 327 | return m, nil 328 | } 329 | -------------------------------------------------------------------------------- /bytes_test.go: -------------------------------------------------------------------------------- 1 | package jsonpointer 2 | 3 | import ( 4 | "compress/gzip" 5 | "io/ioutil" 6 | "math/rand" 7 | "os" 8 | "reflect" 9 | "strings" 10 | "testing" 11 | "testing/quick" 12 | 13 | "github.com/dustin/gojson" 14 | ) 15 | 16 | var ptests = []struct { 17 | path string 18 | exp interface{} 19 | }{ 20 | {"/foo", []interface{}{"bar", "baz"}}, 21 | {"/foo/0", "bar"}, 22 | {"/", 0.0}, 23 | {"/a~1b", 1.0}, 24 | {"/c%d", 2.0}, 25 | {"/e^f", 3.0}, 26 | {"/g|h", 4.0}, 27 | {"/i\\j", 5.0}, 28 | {"/k\"l", 6.0}, 29 | {"/ ", 7.0}, 30 | {"/m~0n", 8.0}, 31 | {"/g~1n~1r", "has slash, will travel"}, 32 | {"/g/n/r", "where's tito?"}, 33 | } 34 | 35 | func TestFindDecode(t *testing.T) { 36 | in := []byte(objSrc) 37 | 38 | var fl float64 39 | if err := FindDecode(in, "/g|h", &fl); err != nil { 40 | t.Errorf("Failed to decode /g|h: %v", err) 41 | } 42 | if fl != 4.0 { 43 | t.Errorf("Expected 4.0 at /g|h, got %v", fl) 44 | } 45 | 46 | fl = 0 47 | if err := FindDecode(in, "/z", &fl); err == nil { 48 | t.Errorf("Expected failure to decode /z: got %v", fl) 49 | } 50 | 51 | if err := FindDecode([]byte(`{"a": 1.x35}`), "/a", &fl); err == nil { 52 | t.Errorf("Expected failure, got %v", fl) 53 | } 54 | } 55 | 56 | func TestListPointers(t *testing.T) { 57 | got, err := ListPointers(nil) 58 | if err == nil { 59 | t.Errorf("Expected error on nil input, got %v", got) 60 | } 61 | got, err = ListPointers([]byte(`{"x": {"y"}}`)) 62 | if err == nil { 63 | t.Errorf("Expected error on broken input, got %v", got) 64 | } 65 | got, err = ListPointers([]byte(objSrc)) 66 | if err != nil { 67 | t.Fatalf("Error getting list of pointers: %v", err) 68 | } 69 | 70 | exp := []string{"", "/foo", "/foo/0", "/foo/1", "/", "/a~1b", 71 | "/c%d", "/e^f", "/g|h", "/i\\j", "/k\"l", "/ ", "/m~0n", 72 | "/g~1n~1r", "/g", "/g/n", "/g/n/r", 73 | } 74 | 75 | if !reflect.DeepEqual(exp, got) { 76 | t.Fatalf("Expected\n%#v\ngot\n%#v", exp, got) 77 | } 78 | } 79 | 80 | func TestPointerRoot(t *testing.T) { 81 | got, err := Find([]byte(objSrc), "") 82 | if err != nil { 83 | t.Fatalf("Error finding root: %v", err) 84 | } 85 | if !reflect.DeepEqual([]byte(objSrc), got) { 86 | t.Fatalf("Error finding root, found\n%s\n, wanted\n%s", 87 | got, objSrc) 88 | } 89 | } 90 | 91 | func TestPointerManyRoot(t *testing.T) { 92 | got, err := FindMany([]byte(objSrc), []string{""}) 93 | if err != nil { 94 | t.Fatalf("Error finding root: %v", err) 95 | } 96 | if !reflect.DeepEqual([]byte(objSrc), got[""]) { 97 | t.Fatalf("Error finding root, found\n%s\n, wanted\n%s", 98 | got, objSrc) 99 | } 100 | } 101 | 102 | func TestPointerManyBroken(t *testing.T) { 103 | got, err := FindMany([]byte(`{"a": {"b": "something}}`), []string{"/a/b"}) 104 | if err == nil { 105 | t.Errorf("Expected error parsing broken JSON, got %v", got) 106 | } 107 | } 108 | 109 | func TestPointerMissing(t *testing.T) { 110 | got, err := Find([]byte(objSrc), "/missing") 111 | if err != nil { 112 | t.Fatalf("Error finding missing item: %v", err) 113 | } 114 | if got != nil { 115 | t.Fatalf("Expected nil looking for /missing, got %v", 116 | got) 117 | } 118 | } 119 | 120 | func TestManyPointers(t *testing.T) { 121 | pointers := []string{} 122 | exp := map[string]interface{}{} 123 | for _, test := range ptests { 124 | pointers = append(pointers, test.path) 125 | exp[test.path] = test.exp 126 | } 127 | 128 | rv, err := FindMany([]byte(objSrc), pointers) 129 | if err != nil { 130 | t.Fatalf("Error finding many: %v", err) 131 | } 132 | 133 | got := map[string]interface{}{} 134 | for k, v := range rv { 135 | var val interface{} 136 | err = json.Unmarshal(v, &val) 137 | if err != nil { 138 | t.Fatalf("Error unmarshaling %s: %v", v, err) 139 | } 140 | got[k] = val 141 | } 142 | 143 | if !reflect.DeepEqual(got, exp) { 144 | for k, v := range exp { 145 | if !reflect.DeepEqual(got[k], v) { 146 | t.Errorf("At %v, expected %#v, got %#v", k, v, got[k]) 147 | } 148 | } 149 | t.Fail() 150 | } 151 | } 152 | 153 | func TestManyPointersMissing(t *testing.T) { 154 | got, err := FindMany([]byte(objSrc), []string{"/missing"}) 155 | if err != nil { 156 | t.Fatalf("Error finding missing item: %v", err) 157 | } 158 | if len(got) != 0 { 159 | t.Fatalf("Expected empty looking for many /missing, got %v", 160 | got) 161 | } 162 | } 163 | 164 | var badDocs = [][]byte{ 165 | []byte{}, []byte(" "), nil, 166 | []byte{'{'}, []byte{'['}, 167 | []byte{'}'}, []byte{']'}, 168 | } 169 | 170 | func TestManyPointersBadDoc(t *testing.T) { 171 | for _, b := range badDocs { 172 | got, _ := FindMany(b, []string{"/broken"}) 173 | if len(got) > 0 { 174 | t.Errorf("Expected failure on %v, got %v", b, got) 175 | } 176 | } 177 | } 178 | 179 | func TestPointersBadDoc(t *testing.T) { 180 | for _, b := range badDocs { 181 | got, _ := Find(b, "/broken") 182 | if len(got) > 0 { 183 | t.Errorf("Expected failure on %s, got %v", b, got) 184 | } 185 | } 186 | } 187 | 188 | func TestPointer(t *testing.T) { 189 | 190 | for _, test := range ptests { 191 | got, err := Find([]byte(objSrc), test.path) 192 | var val interface{} 193 | if err == nil { 194 | err = json.Unmarshal([]byte(got), &val) 195 | } 196 | if err != nil { 197 | t.Errorf("Got an error on key %v: %v", test.path, err) 198 | } else if !reflect.DeepEqual(val, test.exp) { 199 | t.Errorf("On %#v, expected %+v (%T), got %+v (%T)", 200 | test.path, test.exp, test.exp, val, val) 201 | } else { 202 | t.Logf("Success - got %s for %#v", got, test.path) 203 | } 204 | } 205 | } 206 | 207 | func TestPointerCoder(t *testing.T) { 208 | tests := map[string][]string{ 209 | "/": []string{""}, 210 | "/a": []string{"a"}, 211 | "/a~1b": []string{"a/b"}, 212 | "/m~0n": []string{"m~n"}, 213 | "/ ": []string{" "}, 214 | "/g~1n~1r": []string{"g/n/r"}, 215 | "/g/n/r": []string{"g", "n", "r"}, 216 | } 217 | 218 | for k, v := range tests { 219 | parsed := parsePointer(k) 220 | encoded := encodePointer(v) 221 | 222 | if k != encoded { 223 | t.Errorf("Expected to encode %#v as %#v, got %#v", 224 | v, k, encoded) 225 | } 226 | if !arreq(v, parsed) { 227 | t.Errorf("Expected to decode %#v as %#v, got %#v", 228 | k, v, parsed) 229 | } 230 | } 231 | } 232 | 233 | func TestCBugg406(t *testing.T) { 234 | data, err := ioutil.ReadFile("testdata/pools.json") 235 | if err != nil { 236 | t.Fatalf("Error reading pools data: %v", err) 237 | } 238 | 239 | found, err := Find(data, "/implementationVersion") 240 | if err != nil { 241 | t.Fatalf("Failed to find thing: %v", err) 242 | } 243 | exp := ` "2.0.0-1976-rel-enterprise"` 244 | if string(found) != exp { 245 | t.Fatalf("Expected %q, got %q", exp, found) 246 | } 247 | } 248 | 249 | func BenchmarkEncodePointer(b *testing.B) { 250 | aPath := []string{"a", "ab", "a~0b", "a~1b", "a~0~1~0~1b"} 251 | for i := 0; i < b.N; i++ { 252 | encodePointer(aPath) 253 | } 254 | } 255 | 256 | func BenchmarkAll(b *testing.B) { 257 | obj := []byte(objSrc) 258 | for i := 0; i < b.N; i++ { 259 | for _, test := range tests { 260 | Find(obj, test.path) 261 | } 262 | } 263 | } 264 | 265 | func BenchmarkManyPointer(b *testing.B) { 266 | pointers := []string{} 267 | for _, test := range ptests { 268 | pointers = append(pointers, test.path) 269 | } 270 | obj := []byte(objSrc) 271 | 272 | b.ResetTimer() 273 | for i := 0; i < b.N; i++ { 274 | FindMany(obj, pointers) 275 | } 276 | } 277 | 278 | func TestMustParseInt(t *testing.T) { 279 | tests := map[string]bool{ 280 | "": true, 281 | "0": false, 282 | "13": false, 283 | } 284 | 285 | for in, out := range tests { 286 | var panicked bool 287 | func() { 288 | defer func() { 289 | panicked = recover() != nil 290 | }() 291 | mustParseInt(in) 292 | if panicked != out { 293 | t.Logf("Expected panicked=%v", panicked) 294 | } 295 | }() 296 | } 297 | } 298 | 299 | func TestFindBrokenJSON(t *testing.T) { 300 | x, err := Find([]byte(`{]`), "/foo/x") 301 | if err == nil { 302 | t.Errorf("Expected error, got %q", x) 303 | } 304 | } 305 | 306 | func TestGrokLiteral(t *testing.T) { 307 | brokenStr := "---broken---" 308 | tests := []struct { 309 | in []byte 310 | exp string 311 | }{ 312 | {[]byte(`"simple"`), "simple"}, 313 | {[]byte(`"has\nnewline"`), "has\nnewline"}, 314 | {[]byte(`"broken`), brokenStr}, 315 | } 316 | 317 | for _, test := range tests { 318 | var got string 319 | func() { 320 | defer func() { 321 | if e := recover(); e != nil { 322 | got = brokenStr 323 | } 324 | }() 325 | got = grokLiteral(test.in) 326 | }() 327 | if test.exp != got { 328 | t.Errorf("Expected %q for %s, got %q", 329 | test.exp, test.in, got) 330 | } 331 | } 332 | } 333 | 334 | func TestSerieslySample(t *testing.T) { 335 | data, err := ioutil.ReadFile("testdata/serieslysample.json") 336 | if err != nil { 337 | t.Fatalf("Error opening sample file: %v", err) 338 | } 339 | 340 | tests := []struct { 341 | pointer string 342 | exp string 343 | }{ 344 | {"/kind", "Listing"}, 345 | {"/data/children/0/data/id", "w568e"}, 346 | {"/data/children/0/data/name", "t3_w568e"}, 347 | } 348 | 349 | for _, test := range tests { 350 | var found string 351 | err := FindDecode(data, test.pointer, &found) 352 | if err != nil { 353 | t.Errorf("Error on %v: %v", test.pointer, err) 354 | } 355 | if found != test.exp { 356 | t.Errorf("Expected %q, got %q", test.exp, found) 357 | } 358 | } 359 | } 360 | 361 | func TestSerieslySampleMany(t *testing.T) { 362 | data, err := ioutil.ReadFile("testdata/serieslysample.json") 363 | if err != nil { 364 | t.Fatalf("Error opening sample file: %v", err) 365 | } 366 | 367 | keys := []string{"/kind", "/data/children/0/data/id", "/data/children/0/data/name"} 368 | exp := []string{` "Listing"`, ` "w568e"`, ` "t3_w568e"`} 369 | 370 | found, err := FindMany(data, keys) 371 | if err != nil { 372 | t.Fatalf("Error in FindMany: %v", err) 373 | } 374 | 375 | for i, k := range keys { 376 | if string(found[k]) != exp[i] { 377 | t.Errorf("Expected %q on %q, got %q", exp[i], k, found[k]) 378 | } 379 | } 380 | } 381 | 382 | func TestSerieslySampleList(t *testing.T) { 383 | data, err := ioutil.ReadFile("testdata/serieslysample.json") 384 | if err != nil { 385 | t.Fatalf("Error opening sample file: %v", err) 386 | } 387 | 388 | pointers, err := ListPointers(data) 389 | if err != nil { 390 | t.Fatalf("Error listing pointers: %v", err) 391 | } 392 | exp := 932 393 | if len(pointers) != exp { 394 | t.Fatalf("Expected %v pointers, got %v", exp, len(pointers)) 395 | } 396 | } 397 | 398 | func Test357ListPointers(t *testing.T) { 399 | data, err := ioutil.ReadFile("testdata/357.json") 400 | if err != nil { 401 | t.Fatalf("Error beer-sample brewery 357 data: %v", err) 402 | } 403 | 404 | exp := []string{"", "/name", "/city", "/state", "/code", 405 | "/country", "/phone", "/website", "/type", "/updated", 406 | "/description", 407 | "/address", "/address/0", "/address2", 408 | "/address2/0", "/address3", "/address3/0"} 409 | 410 | got, err := ListPointers(data) 411 | if err != nil { 412 | t.Fatalf("error listing pointers: %v", err) 413 | } 414 | if !reflect.DeepEqual(exp, got) { 415 | t.Fatalf("Expected\n%#v\ngot\n%#v", exp, got) 416 | } 417 | } 418 | 419 | func TestEscape(t *testing.T) { 420 | tests := []string{ 421 | "/", "~1", "~0", "/~1", "/~1/", 422 | } 423 | 424 | for _, test := range tests { 425 | esc := string(escape(test, nil)) 426 | got := unescape(esc) 427 | if got != test { 428 | t.Errorf("unescape(escape(%q) [%q]) = %q", test, esc, got) 429 | } 430 | } 431 | 432 | tf := func(s chars) bool { 433 | uns := unescape(string(s)) 434 | got := string(escape(uns, nil)) 435 | return got == string(s) 436 | } 437 | quick.Check(tf, nil) 438 | } 439 | 440 | func TestUnescape(t *testing.T) { 441 | tests := []struct { 442 | in, exp string 443 | }{ 444 | {"", ""}, 445 | {"/", "/"}, 446 | {"/thing", "/thing"}, 447 | {"~0", "~"}, 448 | {"~1", "/"}, 449 | {"~2", "~2"}, 450 | {"~", "~"}, 451 | {"thing~", "thing~"}, 452 | } 453 | for _, test := range tests { 454 | got := string(unescape(test.in)) 455 | if got != test.exp { 456 | t.Errorf("on %q, got %q, wanted %q", test.in, got, test.exp) 457 | } 458 | } 459 | } 460 | 461 | var codeJSON []byte 462 | 463 | func init() { 464 | f, err := os.Open("testdata/code.json.gz") 465 | if err != nil { 466 | panic(err) 467 | } 468 | defer f.Close() 469 | gz, err := gzip.NewReader(f) 470 | if err != nil { 471 | panic(err) 472 | } 473 | data, err := ioutil.ReadAll(gz) 474 | if err != nil { 475 | panic(err) 476 | } 477 | 478 | codeJSON = data 479 | } 480 | 481 | func BenchmarkLarge3Key(b *testing.B) { 482 | keys := []string{ 483 | "/tree/kids/0/kids/0/name", 484 | "/tree/kids/0/name", 485 | "/tree/kids/0/kids/0/kids/0/kids/0/kids/0/name", 486 | } 487 | b.SetBytes(int64(len(codeJSON))) 488 | 489 | for i := 0; i < b.N; i++ { 490 | found, err := FindMany(codeJSON, keys) 491 | if err != nil || len(found) != 3 { 492 | b.Fatalf("Didn't find all the things from %v/%v", 493 | found, err) 494 | } 495 | } 496 | } 497 | 498 | func BenchmarkLargeShallow(b *testing.B) { 499 | keys := []string{ 500 | "/tree/kids/0/kids/0/kids/1/kids/1/kids/3/name", 501 | } 502 | b.SetBytes(int64(len(codeJSON))) 503 | 504 | for i := 0; i < b.N; i++ { 505 | found, err := FindMany(codeJSON, keys) 506 | if err != nil || len(found) != 1 { 507 | b.Fatalf("Didn't find all the things: %v/%v", 508 | found, err) 509 | } 510 | } 511 | } 512 | 513 | func BenchmarkLargeMissing(b *testing.B) { 514 | keys := []string{ 515 | "/this/does/not/exist", 516 | } 517 | b.SetBytes(int64(len(codeJSON))) 518 | 519 | for i := 0; i < b.N; i++ { 520 | found, err := FindMany(codeJSON, keys) 521 | if err != nil || len(found) != 0 { 522 | b.Fatalf("Didn't find all the things: %v/%v", 523 | found, err) 524 | } 525 | } 526 | } 527 | 528 | func BenchmarkLargeIdentity(b *testing.B) { 529 | keys := []string{ 530 | "", 531 | } 532 | b.SetBytes(int64(len(codeJSON))) 533 | 534 | for i := 0; i < b.N; i++ { 535 | found, err := FindMany(codeJSON, keys) 536 | if err != nil || len(found) != 1 { 537 | b.Fatalf("Didn't find all the things: %v/%v", 538 | found, err) 539 | } 540 | } 541 | } 542 | 543 | func BenchmarkLargeBest(b *testing.B) { 544 | keys := []string{ 545 | "/tree/name", 546 | } 547 | b.SetBytes(int64(len(codeJSON))) 548 | 549 | for i := 0; i < b.N; i++ { 550 | found, err := FindMany(codeJSON, keys) 551 | if err != nil || len(found) != 1 { 552 | b.Fatalf("Didn't find all the things: %v/%v", 553 | found, err) 554 | } 555 | } 556 | } 557 | 558 | const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01233456789/~." 559 | 560 | type chars string 561 | 562 | func (c chars) Generate(rand *rand.Rand, _ int) reflect.Value { 563 | size := rand.Intn(128) 564 | var o []byte 565 | for i := 0; i < size; i++ { 566 | o = append(o, alphabet[rand.Intn(len(alphabet))]) 567 | } 568 | s := chars(escape(string(o), nil)) 569 | return reflect.ValueOf(s) 570 | } 571 | 572 | // unescape unescapes a tilde escaped string. 573 | // 574 | // It's dumb looking, but it benches faster than strings.NewReplacer 575 | func oldunescape(s string) string { 576 | return strings.Replace(strings.Replace(s, "~1", "/", -1), "~0", "~", -1) 577 | } 578 | 579 | func TestNewEscaper(t *testing.T) { 580 | of := func(in chars) string { 581 | return oldunescape(string(in)) 582 | } 583 | nf := func(in chars) string { 584 | return unescape(string(in)) 585 | } 586 | if err := quick.CheckEqual(of, nf, nil); err != nil { 587 | t.Errorf("quickcheck failure: %v", err) 588 | } 589 | } 590 | 591 | func BenchmarkLargeMap(b *testing.B) { 592 | keys := []string{ 593 | "/tree/kids/0/kids/0/kids/0/kids/0/kids/0/name", 594 | } 595 | b.SetBytes(int64(len(codeJSON))) 596 | 597 | for i := 0; i < b.N; i++ { 598 | m := map[string]interface{}{} 599 | err := json.Unmarshal(codeJSON, &m) 600 | if err != nil { 601 | b.Fatalf("Error parsing JSON: %v", err) 602 | } 603 | Get(m, keys[0]) 604 | } 605 | } 606 | 607 | const ( 608 | tildeTestKey = "/name~0contained" 609 | slashTestKey = "/name~1contained" 610 | twoTestKey = "/name~1cont~0ned" 611 | ) 612 | 613 | func testDoubleReplacer(s string) string { 614 | return unescape(s) 615 | } 616 | 617 | func BenchmarkReplacerSlash(b *testing.B) { 618 | r := strings.NewReplacer("~1", "/", "~0", "~") 619 | for i := 0; i < b.N; i++ { 620 | r.Replace(slashTestKey) 621 | } 622 | } 623 | 624 | func BenchmarkReplacerTilde(b *testing.B) { 625 | r := strings.NewReplacer("~1", "/", "~0", "~") 626 | for i := 0; i < b.N; i++ { 627 | r.Replace(tildeTestKey) 628 | } 629 | } 630 | 631 | func BenchmarkDblReplacerSlash(b *testing.B) { 632 | for i := 0; i < b.N; i++ { 633 | testDoubleReplacer(slashTestKey) 634 | } 635 | } 636 | 637 | func BenchmarkDblReplacerTilde(b *testing.B) { 638 | for i := 0; i < b.N; i++ { 639 | testDoubleReplacer(tildeTestKey) 640 | } 641 | } 642 | 643 | func BenchmarkDblReplacerTwo(b *testing.B) { 644 | for i := 0; i < b.N; i++ { 645 | testDoubleReplacer(twoTestKey) 646 | } 647 | } 648 | -------------------------------------------------------------------------------- /testdata/serieslysample.json: -------------------------------------------------------------------------------- 1 | {"kind": "Listing", "data": {"modhash": "", "children": [{"kind": "t3", "data": {"domain": "imgur.com", "banned_by": null, "media_embed": {}, "subreddit": "pics", "selftext_html": null, "selftext": "", "likes": null, "link_flair_text": null, "id": "w568e", "clicked": false, "title": "It's 103+ out, so my daughter and niece waited for our garbage men to come by so they could run out and give them some ice cold Gatorades ", "num_comments": 1232, "score": 2675, "approved_by": null, "over_18": false, "hidden": false, "thumbnail": "http://a.thumbs.redditmedia.com/OoulK88mGUsZMp-B.jpg", "subreddit_id": "t5_2qh0u", "edited": false, "link_flair_css_class": null, "author_flair_css_class": null, "downs": 22897, "saved": false, "is_self": false, "permalink": "/r/pics/comments/w568e/its_103_out_so_my_daughter_and_niece_waited_for/", "name": "t3_w568e", "created": 1341628133.0, "url": "http://imgur.com/bW1mm", "author_flair_text": null, "author": "shellykidd", "created_utc": 1341602933.0, "media": null, "num_reports": null, "ups": 25572}}, {"kind": "t3", "data": {"domain": "i.imgur.com", "banned_by": null, "media_embed": {}, "subreddit": "pics", "selftext_html": null, "selftext": "", "likes": null, "link_flair_text": null, "id": "w58hj", "clicked": false, "title": "My roommate after winning over $18million in the biggest buy-in poker tournament in history a few days ago.", "num_comments": 598, "score": 1827, "approved_by": null, "over_18": false, "hidden": false, "thumbnail": "http://f.thumbs.redditmedia.com/vZK4yn6NhliqFIrG.jpg", "subreddit_id": "t5_2qh0u", "edited": false, "link_flair_css_class": null, "author_flair_css_class": null, "downs": 3533, "saved": false, "is_self": false, "permalink": "/r/pics/comments/w58hj/my_roommate_after_winning_over_18million_in_the/", "name": "t3_w58hj", "created": 1341630384.0, "url": "http://i.imgur.com/qBlai.jpg", "author_flair_text": null, "author": "executiveproducer", "created_utc": 1341605184.0, "media": null, "num_reports": null, "ups": 5360}}, {"kind": "t3", "data": {"domain": "imgur.com", "banned_by": null, "media_embed": {}, "subreddit": "pics", "selftext_html": null, "selftext": "", "likes": null, "link_flair_text": null, "id": "w5aor", "clicked": false, "title": "I'm a garbage man, and this made my day!", "num_comments": 156, "score": 1555, "approved_by": null, "over_18": false, "hidden": false, "thumbnail": "http://e.thumbs.redditmedia.com/KC0hWtJEM6ziiCG4.jpg", "subreddit_id": "t5_2qh0u", "edited": false, "link_flair_css_class": null, "author_flair_css_class": null, "downs": 1556, "saved": false, "is_self": false, "permalink": "/r/pics/comments/w5aor/im_a_garbage_man_and_this_made_my_day/", "name": "t3_w5aor", "created": 1341632510.0, "url": "http://imgur.com/a/RqGYh", "author_flair_text": null, "author": "birdcanfly", "created_utc": 1341607310.0, "media": null, "num_reports": null, "ups": 3111}}, {"kind": "t3", "data": {"domain": "i.imgur.com", "banned_by": null, "media_embed": {}, "subreddit": "pics", "selftext_html": null, "selftext": "", "likes": null, "link_flair_text": null, "id": "w53xc", "clicked": false, "title": "The never ending story", "num_comments": 93, "score": 1728, "approved_by": null, "over_18": false, "hidden": false, "thumbnail": "http://a.thumbs.redditmedia.com/EO-SkS2UTxuINEgZ.jpg", "subreddit_id": "t5_2qh0u", "edited": false, "link_flair_css_class": null, "author_flair_css_class": null, "downs": 3033, "saved": false, "is_self": false, "permalink": "/r/pics/comments/w53xc/the_never_ending_story/", "name": "t3_w53xc", "created": 1341626000.0, "url": "http://i.imgur.com/We17d.jpg", "author_flair_text": null, "author": "BabblingCrap", "created_utc": 1341600800.0, "media": null, "num_reports": null, "ups": 4761}}, {"kind": "t3", "data": {"domain": "i.imgur.com", "banned_by": null, "media_embed": {}, "subreddit": "pics", "selftext_html": null, "selftext": "", "likes": null, "link_flair_text": null, "id": "w59nw", "clicked": false, "title": "So nervous I could vomit...", "num_comments": 290, "score": 972, "approved_by": null, "over_18": false, "hidden": false, "thumbnail": "http://c.thumbs.redditmedia.com/TyWXYiL_u6bXJVp8.jpg", "subreddit_id": "t5_2qh0u", "edited": false, "link_flair_css_class": null, "author_flair_css_class": null, "downs": 1212, "saved": false, "is_self": false, "permalink": "/r/pics/comments/w59nw/so_nervous_i_could_vomit/", "name": "t3_w59nw", "created": 1341631540.0, "url": "http://i.imgur.com/i5k1g.jpg", "author_flair_text": null, "author": "fknbwdwn", "created_utc": 1341606340.0, "media": null, "num_reports": null, "ups": 2184}}, {"kind": "t3", "data": {"domain": "i.imgur.com", "banned_by": null, "media_embed": {}, "subreddit": "pics", "selftext_html": null, "selftext": "", "likes": null, "link_flair_text": null, "id": "w52ue", "clicked": false, "title": "Grandparents' TV stand", "num_comments": 80, "score": 1304, "approved_by": null, "over_18": false, "hidden": false, "thumbnail": "http://c.thumbs.redditmedia.com/Gdu6Y4IjEpjxIF4J.jpg", "subreddit_id": "t5_2qh0u", "edited": false, "link_flair_css_class": null, "author_flair_css_class": null, "downs": 766, "saved": false, "is_self": false, "permalink": "/r/pics/comments/w52ue/grandparents_tv_stand/", "name": "t3_w52ue", "created": 1341625007.0, "url": "http://i.imgur.com/PVwS7.jpg", "author_flair_text": null, "author": "CelestialAvatar", "created_utc": 1341599807.0, "media": null, "num_reports": null, "ups": 2070}}, {"kind": "t3", "data": {"domain": "i.imgur.com", "banned_by": null, "media_embed": {}, "subreddit": "pics", "selftext_html": null, "selftext": "", "likes": null, "link_flair_text": null, "id": "w4oq1", "clicked": false, "title": "Shot of a lifetime on the 4th of July. Perfect timing!! ", "num_comments": 899, "score": 2760, "approved_by": null, "over_18": false, "hidden": false, "thumbnail": "http://b.thumbs.redditmedia.com/brb289xCEnh41MuC.jpg", "subreddit_id": "t5_2qh0u", "edited": false, "link_flair_css_class": null, "author_flair_css_class": null, "downs": 31678, "saved": false, "is_self": false, "permalink": "/r/pics/comments/w4oq1/shot_of_a_lifetime_on_the_4th_of_july_perfect/", "name": "t3_w4oq1", "created": 1341610291.0, "url": "http://i.imgur.com/9txJd.jpg", "author_flair_text": null, "author": "littlefish90", "created_utc": 1341585091.0, "media": null, "num_reports": null, "ups": 34438}}, {"kind": "t3", "data": {"domain": "imgur.com", "banned_by": null, "media_embed": {}, "subreddit": "pics", "selftext_html": null, "selftext": "", "likes": null, "link_flair_text": null, "id": "w4yfo", "clicked": false, "title": "My 11 year old cousins face when he came 2nd in the National Pokemon Championships in Canada, he won himself and his dad a trip to Hawaii!", "num_comments": 308, "score": 1562, "approved_by": null, "over_18": false, "hidden": false, "thumbnail": "http://f.thumbs.redditmedia.com/EEegOt7uSxmwf0Kw.jpg", "subreddit_id": "t5_2qh0u", "edited": false, "link_flair_css_class": null, "author_flair_css_class": null, "downs": 4559, "saved": false, "is_self": false, "permalink": "/r/pics/comments/w4yfo/my_11_year_old_cousins_face_when_he_came_2nd_in/", "name": "t3_w4yfo", "created": 1341620761.0, "url": "http://imgur.com/Ub094", "author_flair_text": null, "author": "sweetchilli", "created_utc": 1341595561.0, "media": null, "num_reports": null, "ups": 6121}}, {"kind": "t3", "data": {"domain": "i.imgur.com", "banned_by": null, "media_embed": {}, "subreddit": "pics", "selftext_html": null, "selftext": "", "likes": null, "link_flair_text": null, "id": "w5e3b", "clicked": false, "title": "Ron Perlman giving a terminally ill child his dream. It's so nice to see him go out of his way to get into character for a child's request.", "num_comments": 26, "score": 657, "approved_by": null, "over_18": false, "hidden": false, "thumbnail": "http://b.thumbs.redditmedia.com/09rxnolBvOFceknC.jpg", "subreddit_id": "t5_2qh0u", "edited": false, "link_flair_css_class": null, "author_flair_css_class": null, "downs": 171, "saved": false, "is_self": false, "permalink": "/r/pics/comments/w5e3b/ron_perlman_giving_a_terminally_ill_child_his/", "name": "t3_w5e3b", "created": 1341636072.0, "url": "http://i.imgur.com/yXUuP.jpg", "author_flair_text": null, "author": "Shiftyze", "created_utc": 1341610872.0, "media": null, "num_reports": null, "ups": 828}}, {"kind": "t3", "data": {"domain": "imgur.com", "banned_by": null, "media_embed": {}, "subreddit": "pics", "selftext_html": null, "selftext": "", "likes": null, "link_flair_text": null, "id": "w4vuy", "clicked": false, "title": "You go little guy...", "num_comments": 93, "score": 1615, "approved_by": null, "over_18": false, "hidden": false, "thumbnail": "http://b.thumbs.redditmedia.com/T6GOYcmATXhpv2Sy.jpg", "subreddit_id": "t5_2qh0u", "edited": false, "link_flair_css_class": null, "author_flair_css_class": null, "downs": 4098, "saved": false, "is_self": false, "permalink": "/r/pics/comments/w4vuy/you_go_little_guy/", "name": "t3_w4vuy", "created": 1341618076.0, "url": "http://www.imgur.com/xT4mp.jpeg", "author_flair_text": null, "author": "ksmith22", "created_utc": 1341592876.0, "media": null, "num_reports": null, "ups": 5713}}, {"kind": "t3", "data": {"domain": "i.imgur.com", "banned_by": null, "media_embed": {}, "subreddit": "pics", "selftext_html": null, "selftext": "", "likes": null, "link_flair_text": null, "id": "w4qqn", "clicked": false, "title": "A clash of two worlds in London; skinheads and hippies. Circa 1969.", "num_comments": 690, "score": 1803, "approved_by": null, "over_18": false, "hidden": false, "thumbnail": "http://b.thumbs.redditmedia.com/Lg5SeruPj0a3dnZy.jpg", "subreddit_id": "t5_2qh0u", "edited": false, "link_flair_css_class": null, "author_flair_css_class": null, "downs": 5903, "saved": false, "is_self": false, "permalink": "/r/pics/comments/w4qqn/a_clash_of_two_worlds_in_london_skinheads_and/", "name": "t3_w4qqn", "created": 1341612670.0, "url": "http://i.imgur.com/TcuZs.jpg", "author_flair_text": null, "author": "RarneyBubble", "created_utc": 1341587470.0, "media": null, "num_reports": null, "ups": 7706}}, {"kind": "t3", "data": {"domain": "imgur.com", "banned_by": null, "media_embed": {}, "subreddit": "pics", "selftext_html": null, "selftext": "", "likes": null, "link_flair_text": null, "id": "w4qq2", "clicked": false, "title": "No Flash vs. Flash", "num_comments": 233, "score": 1742, "approved_by": null, "over_18": false, "hidden": false, "thumbnail": "http://c.thumbs.redditmedia.com/ooWLthXrnYWvaF-V.jpg", "subreddit_id": "t5_2qh0u", "edited": false, "link_flair_css_class": null, "author_flair_css_class": null, "downs": 9154, "saved": false, "is_self": false, "permalink": "/r/pics/comments/w4qq2/no_flash_vs_flash/", "name": "t3_w4qq2", "created": 1341612655.0, "url": "http://imgur.com/a/JhN3l", "author_flair_text": null, "author": "nessaleigh", "created_utc": 1341587455.0, "media": null, "num_reports": null, "ups": 10896}}, {"kind": "t3", "data": {"domain": "imgur.com", "banned_by": null, "media_embed": {}, "subreddit": "pics", "selftext_html": null, "selftext": "", "likes": null, "link_flair_text": null, "id": "w4q71", "clicked": false, "title": "Meanwhile in Lithuania", "num_comments": 304, "score": 1710, "approved_by": null, "over_18": false, "hidden": false, "thumbnail": "http://c.thumbs.redditmedia.com/Q3YogQ70Rp1CxIEb.jpg", "subreddit_id": "t5_2qh0u", "edited": false, "link_flair_css_class": null, "author_flair_css_class": null, "downs": 2601, "saved": false, "is_self": false, "permalink": "/r/pics/comments/w4q71/meanwhile_in_lithuania/", "name": "t3_w4q71", "created": 1341612021.0, "url": "http://imgur.com/a/10wfb", "author_flair_text": null, "author": "c00lwhip", "created_utc": 1341586821.0, "media": null, "num_reports": null, "ups": 4311}}, {"kind": "t3", "data": {"domain": "i.imgur.com", "banned_by": null, "media_embed": {}, "subreddit": "pics", "selftext_html": null, "selftext": "", "likes": null, "link_flair_text": null, "id": "w4pxv", "clicked": false, "title": "My stepson handed me playdough and said, \"Make a puppy.\" He now thinks I'm a god. [X-Post from r/Parenting]", "num_comments": 178, "score": 1671, "approved_by": null, "over_18": false, "hidden": false, "thumbnail": "http://d.thumbs.redditmedia.com/d53B2HCbt33qDfe9.jpg", "subreddit_id": "t5_2qh0u", "edited": false, "link_flair_css_class": null, "author_flair_css_class": null, "downs": 6652, "saved": false, "is_self": false, "permalink": "/r/pics/comments/w4pxv/my_stepson_handed_me_playdough_and_said_make_a/", "name": "t3_w4pxv", "created": 1341611712.0, "url": "http://i.imgur.com/IJWwq.jpg", "author_flair_text": null, "author": "scruffy01", "created_utc": 1341586512.0, "media": null, "num_reports": null, "ups": 8323}}, {"kind": "t3", "data": {"domain": "i.imgur.com", "banned_by": null, "media_embed": {}, "subreddit": "pics", "selftext_html": null, "selftext": "", "likes": null, "link_flair_text": null, "id": "w4qa5", "clicked": false, "title": "My friend Doug is running across america to raise awareness of Diabetes. He will be the first diabetic person to do this. It'd be great if we can support him! (x-post from r/diabetes)", "num_comments": 453, "score": 1567, "approved_by": null, "over_18": false, "hidden": false, "thumbnail": "http://d.thumbs.redditmedia.com/ye4arY4fzaACkGMW.jpg", "subreddit_id": "t5_2qh0u", "edited": false, "link_flair_css_class": null, "author_flair_css_class": null, "downs": 6360, "saved": false, "is_self": false, "permalink": "/r/pics/comments/w4qa5/my_friend_doug_is_running_across_america_to_raise/", "name": "t3_w4qa5", "created": 1341612130.0, "url": "http://i.imgur.com/MQeJB.jpg", "author_flair_text": null, "author": "recoverelapse", "created_utc": 1341586930.0, "media": null, "num_reports": null, "ups": 7927}}, {"kind": "t3", "data": {"domain": "i.imgur.com", "banned_by": null, "media_embed": {}, "subreddit": "pics", "selftext_html": null, "selftext": "", "likes": null, "link_flair_text": null, "id": "w5288", "clicked": false, "title": "Ingenious ", "num_comments": 35, "score": 724, "approved_by": null, "over_18": false, "hidden": false, "thumbnail": "http://d.thumbs.redditmedia.com/hleJ97Ozk9E5ZPJW.jpg", "subreddit_id": "t5_2qh0u", "edited": false, "link_flair_css_class": null, "author_flair_css_class": null, "downs": 327, "saved": false, "is_self": false, "permalink": "/r/pics/comments/w5288/ingenious/", "name": "t3_w5288", "created": 1341624424.0, "url": "http://i.imgur.com/Rja4x.jpg", "author_flair_text": null, "author": "influenza", "created_utc": 1341599224.0, "media": null, "num_reports": null, "ups": 1051}}, {"kind": "t3", "data": {"domain": "i.imgur.com", "banned_by": null, "media_embed": {}, "subreddit": "pics", "selftext_html": null, "selftext": "", "likes": null, "link_flair_text": null, "id": "w55cj", "clicked": false, "title": "Reddit, it has happened. Adult sized capri suns!", "num_comments": 47, "score": 579, "approved_by": null, "over_18": false, "hidden": false, "thumbnail": "http://d.thumbs.redditmedia.com/M8alPpk32nx2Ulc3.jpg", "subreddit_id": "t5_2qh0u", "edited": false, "link_flair_css_class": null, "author_flair_css_class": null, "downs": 181, "saved": false, "is_self": false, "permalink": "/r/pics/comments/w55cj/reddit_it_has_happened_adult_sized_capri_suns/", "name": "t3_w55cj", "created": 1341627317.0, "url": "http://i.imgur.com/2goMA.jpg", "author_flair_text": null, "author": "talljewishkid", "created_utc": 1341602117.0, "media": null, "num_reports": null, "ups": 760}}, {"kind": "t3", "data": {"domain": "i.imgur.com", "banned_by": null, "media_embed": {}, "subreddit": "pics", "selftext_html": null, "selftext": "", "likes": null, "link_flair_text": null, "id": "w4qdk", "clicked": false, "title": "Strickland moved to my town.", "num_comments": 61, "score": 1254, "approved_by": null, "over_18": false, "hidden": false, "thumbnail": "http://b.thumbs.redditmedia.com/gKAzS0WMZ9Y145DU.jpg", "subreddit_id": "t5_2qh0u", "edited": false, "link_flair_css_class": null, "author_flair_css_class": null, "downs": 1146, "saved": false, "is_self": false, "permalink": "/r/pics/comments/w4qdk/strickland_moved_to_my_town/", "name": "t3_w4qdk", "created": 1341612230.0, "url": "http://i.imgur.com/hTYDB.jpg", "author_flair_text": null, "author": "RedBeardedOwl", "created_utc": 1341587030.0, "media": null, "num_reports": null, "ups": 2400}}, {"kind": "t3", "data": {"domain": "imgur.com", "banned_by": null, "media_embed": {}, "subreddit": "pics", "selftext_html": null, "selftext": "", "likes": null, "link_flair_text": null, "id": "w4nyz", "clicked": false, "title": "This is a 150 million year old fossil of Sciurumimus albersdoerferi, recently discovered in a Bavarian limestone quarry.", "num_comments": 133, "score": 1346, "approved_by": null, "over_18": false, "hidden": false, "thumbnail": "http://f.thumbs.redditmedia.com/zqK6gMUkOGyV2GfY.jpg", "subreddit_id": "t5_2qh0u", "edited": false, "link_flair_css_class": null, "author_flair_css_class": null, "downs": 1004, "saved": false, "is_self": false, "permalink": "/r/pics/comments/w4nyz/this_is_a_150_million_year_old_fossil_of/", "name": "t3_w4nyz", "created": 1341609405.0, "url": "http://imgur.com/7t9Nu", "author_flair_text": null, "author": "gloon", "created_utc": 1341584205.0, "media": null, "num_reports": null, "ups": 2350}}, {"kind": "t3", "data": {"domain": "i.imgur.com", "banned_by": null, "media_embed": {}, "subreddit": "pics", "selftext_html": null, "selftext": "", "likes": null, "link_flair_text": null, "id": "w4jxa", "clicked": false, "title": "Raw Titanium", "num_comments": 237, "score": 1799, "approved_by": null, "over_18": false, "hidden": false, "thumbnail": "http://f.thumbs.redditmedia.com/IFqSt2IC9FEhUYAY.jpg", "subreddit_id": "t5_2qh0u", "edited": false, "link_flair_css_class": null, "author_flair_css_class": null, "downs": 4435, "saved": false, "is_self": false, "permalink": "/r/pics/comments/w4jxa/raw_titanium/", "name": "t3_w4jxa", "created": 1341603294.0, "url": "http://i.imgur.com/EgZtw.jpg", "author_flair_text": null, "author": "Scopolamina", "created_utc": 1341578094.0, "media": null, "num_reports": null, "ups": 6234}}, {"kind": "t3", "data": {"domain": "i.imgur.com", "banned_by": null, "media_embed": {}, "subreddit": "pics", "selftext_html": null, "selftext": "", "likes": null, "link_flair_text": null, "id": "w4lok", "clicked": false, "title": "Grafitti becomes art when the artist becomes famous enough. Our city council framed this piece behind plexiglass.", "num_comments": 166, "score": 1444, "approved_by": null, "over_18": false, "hidden": false, "thumbnail": "http://c.thumbs.redditmedia.com/VevdHZYhEqNBwg1h.jpg", "subreddit_id": "t5_2qh0u", "edited": false, "link_flair_css_class": null, "author_flair_css_class": null, "downs": 1556, "saved": false, "is_self": false, "permalink": "/r/pics/comments/w4lok/grafitti_becomes_art_when_the_artist_becomes/", "name": "t3_w4lok", "created": 1341606211.0, "url": "http://i.imgur.com/XMZKa.jpg", "author_flair_text": null, "author": "Malicious78", "created_utc": 1341581011.0, "media": null, "num_reports": null, "ups": 3000}}, {"kind": "t3", "data": {"domain": "imgur.com", "banned_by": null, "media_embed": {}, "subreddit": "pics", "selftext_html": null, "selftext": "", "likes": null, "link_flair_text": null, "id": "w4jnl", "clicked": false, "title": "The Dragon Gate of Harlech House, Dublin.", "num_comments": 102, "score": 1677, "approved_by": null, "over_18": false, "hidden": false, "thumbnail": "http://f.thumbs.redditmedia.com/HdTgBfok-QuAhMUG.jpg", "subreddit_id": "t5_2qh0u", "edited": false, "link_flair_css_class": null, "author_flair_css_class": null, "downs": 3875, "saved": false, "is_self": false, "permalink": "/r/pics/comments/w4jnl/the_dragon_gate_of_harlech_house_dublin/", "name": "t3_w4jnl", "created": 1341602845.0, "url": "http://imgur.com/7v9WP", "author_flair_text": null, "author": "boomboomhead", "created_utc": 1341577645.0, "media": null, "num_reports": null, "ups": 5552}}, {"kind": "t3", "data": {"domain": "i.imgur.com", "banned_by": null, "media_embed": {}, "subreddit": "pics", "selftext_html": null, "selftext": "", "likes": null, "link_flair_text": null, "id": "w4uta", "clicked": false, "title": "Found an alligator in my watch strap!", "num_comments": 23, "score": 773, "approved_by": null, "over_18": false, "hidden": false, "thumbnail": "http://b.thumbs.redditmedia.com/1PIgC5hSvTb0-Gsa.jpg", "subreddit_id": "t5_2qh0u", "edited": false, "link_flair_css_class": null, "author_flair_css_class": null, "downs": 534, "saved": false, "is_self": false, "permalink": "/r/pics/comments/w4uta/found_an_alligator_in_my_watch_strap/", "name": "t3_w4uta", "created": 1341617017.0, "url": "http://i.imgur.com/ziRoX.jpg", "author_flair_text": null, "author": "remarkedvial", "created_utc": 1341591817.0, "media": null, "num_reports": null, "ups": 1307}}, {"kind": "t3", "data": {"domain": "imgur.com", "banned_by": null, "media_embed": {}, "subreddit": "pics", "selftext_html": null, "selftext": "", "likes": null, "link_flair_text": null, "id": "w4s2t", "clicked": false, "title": "\"Life, uh, finds a way.\"", "num_comments": 22, "score": 757, "approved_by": null, "over_18": false, "hidden": false, "thumbnail": "http://b.thumbs.redditmedia.com/5behq6xV50b_n6y7.jpg", "subreddit_id": "t5_2qh0u", "edited": false, "link_flair_css_class": null, "author_flair_css_class": null, "downs": 263, "saved": false, "is_self": false, "permalink": "/r/pics/comments/w4s2t/life_uh_finds_a_way/", "name": "t3_w4s2t", "created": 1341614129.0, "url": "http://imgur.com/UYA2B", "author_flair_text": null, "author": "bessiemucho", "created_utc": 1341588929.0, "media": null, "num_reports": null, "ups": 1020}}, {"kind": "t3", "data": {"domain": "i.imgur.com", "banned_by": null, "media_embed": {}, "subreddit": "pics", "selftext_html": null, "selftext": "", "likes": null, "link_flair_text": null, "id": "w4nqz", "clicked": false, "title": "How I put parmesan on my food when I'm at home away from the judgmental eyes of the wait staff.", "num_comments": 72, "score": 957, "approved_by": null, "over_18": false, "hidden": false, "thumbnail": "http://a.thumbs.redditmedia.com/5ZhWE3fXdqjqaY-l.jpg", "subreddit_id": "t5_2qh0u", "edited": false, "link_flair_css_class": null, "author_flair_css_class": null, "downs": 621, "saved": false, "is_self": false, "permalink": "/r/pics/comments/w4nqz/how_i_put_parmesan_on_my_food_when_im_at_home/", "name": "t3_w4nqz", "created": 1341609102.0, "url": "http://i.imgur.com/D8CT4.gif", "author_flair_text": null, "author": "MartialFur", "created_utc": 1341583902.0, "media": null, "num_reports": null, "ups": 1578}}], "after": "t3_w4nqz", "before": null}} 2 | --------------------------------------------------------------------------------