Added ReplaceInPlace Option

Allows for replacing the input byte slice instead of allocating new
memory.
This commit is contained in:
Josh Baker 2016-10-25 12:42:02 -07:00
parent e6094ca119
commit a47c1e938e
2 changed files with 147 additions and 34 deletions

119
sjson.go
View File

@ -23,6 +23,13 @@ type Options struct {
// Optimistic is a hint that the value likely exists which // Optimistic is a hint that the value likely exists which
// allows for the sjson to perform a fast-track search and replace. // allows for the sjson to perform a fast-track search and replace.
Optimistic bool Optimistic bool
// ReplaceInPlace is a hint to replace the input json rather than
// allocate a new json byte slice. When this field is specified
// the input json will not longer be valid and it should not be used
// There is no guarentees that the memory will be replaced in-place.
// The Optimistic flag must be set to true and the input must be a
// byte slice in order to use this field.
ReplaceInPlace bool
} }
type pathResult struct { type pathResult struct {
@ -71,9 +78,11 @@ func parsePath(path string) (pathResult, error) {
r.more = true r.more = true
return r, nil return r, nil
} else if path[i] == '*' || path[i] == '?' { } else if path[i] == '*' || path[i] == '?' {
return r, &errorType{"wildcard characters not allowed in path"} return r, &errorType{
"wildcard characters not allowed in path"}
} else if path[i] == '#' { } else if path[i] == '#' {
return r, &errorType{"array access character not allowed in path"} return r, &errorType{
"array access character not allowed in path"}
} }
epart = append(epart, path[i]) epart = append(epart, path[i])
} }
@ -87,14 +96,21 @@ func parsePath(path string) (pathResult, error) {
return r, nil return r, nil
} }
// appendStringify makes a json string and appends to buf. func mustMarshalString(s string) bool {
func appendStringify(buf []byte, s string) []byte {
for i := 0; i < len(s); i++ { for i := 0; i < len(s); i++ {
if s[i] < ' ' || s[i] > 0x7f || s[i] == '"' { if s[i] < ' ' || s[i] > 0x7f || s[i] == '"' {
b, _ := jsongo.Marshal(s) return true
return append(buf, b...)
} }
} }
return false
}
// appendStringify makes a json string and appends to buf.
func appendStringify(buf []byte, s string) []byte {
if mustMarshalString(s) {
b, _ := jsongo.Marshal(s)
return append(buf, b...)
}
buf = append(buf, '"') buf = append(buf, '"')
buf = append(buf, s...) buf = append(buf, s...)
buf = append(buf, '"') buf = append(buf, '"')
@ -102,7 +118,8 @@ func appendStringify(buf []byte, s string) []byte {
} }
// appendBuild builds a json block from a json path. // appendBuild builds a json block from a json path.
func appendBuild(buf []byte, array bool, paths []pathResult, raw string, stringify bool) []byte { func appendBuild(buf []byte, array bool, paths []pathResult, raw string,
stringify bool) []byte {
if !array { if !array {
buf = appendStringify(buf, paths[0].part) buf = appendStringify(buf, paths[0].part)
buf = append(buf, ':') buf = append(buf, ':')
@ -215,7 +232,8 @@ loop:
var errNoChange = &errorType{"no change"} var errNoChange = &errorType{"no change"}
func appendRawPaths(buf []byte, jstr string, paths []pathResult, raw string, stringify, del bool) ([]byte, error) { func appendRawPaths(buf []byte, jstr string, paths []pathResult, raw string,
stringify, del bool) ([]byte, error) {
var err error var err error
var res gjson.Result var res gjson.Result
var found bool var found bool
@ -234,7 +252,8 @@ func appendRawPaths(buf []byte, jstr string, paths []pathResult, raw string, str
if res.Index > 0 { if res.Index > 0 {
if len(paths) > 1 { if len(paths) > 1 {
buf = append(buf, jstr[:res.Index]...) buf = append(buf, jstr[:res.Index]...)
buf, err = appendRawPaths(buf, res.Raw, paths[1:], raw, stringify, del) buf, err = appendRawPaths(buf, res.Raw, paths[1:], raw,
stringify, del)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -247,7 +266,8 @@ func appendRawPaths(buf []byte, jstr string, paths []pathResult, raw string, str
var delNextComma bool var delNextComma bool
buf, delNextComma = deleteTailItem(buf) buf, delNextComma = deleteTailItem(buf)
if delNextComma { if delNextComma {
for i, j := res.Index+len(res.Raw), 0; i < len(jstr); i, j = i+1, j+1 { i, j := res.Index+len(res.Raw), 0
for ; i < len(jstr); i, j = i+1, j+1 {
if jstr[i] <= ' ' { if jstr[i] <= ' ' {
continue continue
} }
@ -322,7 +342,9 @@ func appendRawPaths(buf []byte, jstr string, paths []pathResult, raw string, str
if paths[0].part == "-1" && !paths[0].force { if paths[0].part == "-1" && !paths[0].force {
appendit = true appendit = true
} else { } else {
return nil, &errorType{"cannot set array element for non-numeric key '" + paths[0].part + "'"} return nil, &errorType{
"cannot set array element for non-numeric key '" +
paths[0].part + "'"}
} }
} }
if appendit { if appendit {
@ -376,7 +398,8 @@ func isOptimisticPath(path string) bool {
return true return true
} }
func set(jstr, path, raw string, stringify, del, optimistic bool) ([]byte, error) { func set(jstr, path, raw string,
stringify, del, optimistic, inplace bool) ([]byte, error) {
if path == "" { if path == "" {
return nil, &errorType{"path cannot be empty"} return nil, &errorType{"path cannot be empty"}
} }
@ -387,6 +410,27 @@ func set(jstr, path, raw string, stringify, del, optimistic bool) ([]byte, error
if stringify { if stringify {
sz += 2 sz += 2
} }
if inplace && sz <= len(jstr) {
if !stringify || !mustMarshalString(raw) {
jsonh := *(*reflect.StringHeader)(unsafe.Pointer(&jstr))
jsonbh := reflect.SliceHeader{
Data: jsonh.Data, Len: jsonh.Len, Cap: jsonh.Len}
jbytes := *(*[]byte)(unsafe.Pointer(&jsonbh))
if stringify {
jbytes[res.Index] = '"'
copy(jbytes[res.Index+1:], []byte(raw))
jbytes[res.Index+1+len(raw)] = '"'
copy(jbytes[res.Index+1+len(raw)+1:],
jbytes[res.Index+len(res.Raw):])
} else {
copy(jbytes[res.Index:], []byte(raw))
copy(jbytes[res.Index+len(raw):],
jbytes[res.Index+len(res.Raw):])
}
return jbytes[:sz], nil
}
return nil, nil
}
buf := make([]byte, 0, sz) buf := make([]byte, 0, sz)
buf = append(buf, jstr[:res.Index]...) buf = append(buf, jstr[:res.Index]...)
if stringify { if stringify {
@ -452,6 +496,15 @@ func Set(json, path string, value interface{}) (string, error) {
// An error is returned if the path is not valid. // An error is returned if the path is not valid.
func SetOptions(json, path string, value interface{}, func SetOptions(json, path string, value interface{},
opts *Options) (string, error) { opts *Options) (string, error) {
if opts != nil {
if opts.ReplaceInPlace {
// it's not safe to replace bytes in-place for strings
// copy the Options and set options.ReplaceInPlace to false.
nopts := *opts
opts = &nopts
opts.ReplaceInPlace = false
}
}
jsonh := *(*reflect.StringHeader)(unsafe.Pointer(&json)) jsonh := *(*reflect.StringHeader)(unsafe.Pointer(&json))
jsonbh := reflect.SliceHeader{Data: jsonh.Data, Len: jsonh.Len} jsonbh := reflect.SliceHeader{Data: jsonh.Data, Len: jsonh.Len}
jsonb := *(*[]byte)(unsafe.Pointer(&jsonbh)) jsonb := *(*[]byte)(unsafe.Pointer(&jsonbh))
@ -471,9 +524,10 @@ func SetBytes(json []byte, path string, value interface{}) ([]byte, error) {
// SetOptions(string(data), path, value) // SetOptions(string(data), path, value)
func SetBytesOptions(json []byte, path string, value interface{}, func SetBytesOptions(json []byte, path string, value interface{},
opts *Options) ([]byte, error) { opts *Options) ([]byte, error) {
var optimistic bool var optimistic, inplace bool
if opts != nil { if opts != nil {
optimistic = opts.Optimistic optimistic = opts.Optimistic
inplace = opts.ReplaceInPlace
} }
jstr := *(*string)(unsafe.Pointer(&json)) jstr := *(*string)(unsafe.Pointer(&json))
var res []byte var res []byte
@ -485,50 +539,50 @@ func SetBytesOptions(json []byte, path string, value interface{},
return nil, err return nil, err
} }
raw := *(*string)(unsafe.Pointer(&b)) raw := *(*string)(unsafe.Pointer(&b))
res, err = set(jstr, path, raw, false, false, optimistic) res, err = set(jstr, path, raw, false, false, optimistic, inplace)
case dtype: case dtype:
res, err = set(jstr, path, "", false, true, optimistic) res, err = set(jstr, path, "", false, true, optimistic, inplace)
case string: case string:
res, err = set(jstr, path, v, true, false, optimistic) res, err = set(jstr, path, v, true, false, optimistic, inplace)
case []byte: case []byte:
raw := *(*string)(unsafe.Pointer(&v)) raw := *(*string)(unsafe.Pointer(&v))
res, err = set(jstr, path, raw, true, false, optimistic) res, err = set(jstr, path, raw, true, false, optimistic, inplace)
case bool: case bool:
if v { if v {
res, err = set(jstr, path, "true", false, false, optimistic) res, err = set(jstr, path, "true", false, false, optimistic, inplace)
} else { } else {
res, err = set(jstr, path, "false", false, false, optimistic) res, err = set(jstr, path, "false", false, false, optimistic, inplace)
} }
case int8: case int8:
res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), res, err = set(jstr, path, strconv.FormatInt(int64(v), 10),
false, false, optimistic) false, false, optimistic, inplace)
case int16: case int16:
res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), res, err = set(jstr, path, strconv.FormatInt(int64(v), 10),
false, false, optimistic) false, false, optimistic, inplace)
case int32: case int32:
res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), res, err = set(jstr, path, strconv.FormatInt(int64(v), 10),
false, false, optimistic) false, false, optimistic, inplace)
case int64: case int64:
res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), res, err = set(jstr, path, strconv.FormatInt(int64(v), 10),
false, false, optimistic) false, false, optimistic, inplace)
case uint8: case uint8:
res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10),
false, false, optimistic) false, false, optimistic, inplace)
case uint16: case uint16:
res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10),
false, false, optimistic) false, false, optimistic, inplace)
case uint32: case uint32:
res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10),
false, false, optimistic) false, false, optimistic, inplace)
case uint64: case uint64:
res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10),
false, false, optimistic) false, false, optimistic, inplace)
case float32: case float32:
res, err = set(jstr, path, strconv.FormatFloat(float64(v), 'f', -1, 64), res, err = set(jstr, path, strconv.FormatFloat(float64(v), 'f', -1, 64),
false, false, optimistic) false, false, optimistic, inplace)
case float64: case float64:
res, err = set(jstr, path, strconv.FormatFloat(float64(v), 'f', -1, 64), res, err = set(jstr, path, strconv.FormatFloat(float64(v), 'f', -1, 64),
false, false, optimistic) false, false, optimistic, inplace)
} }
if err == errNoChange { if err == errNoChange {
return json, nil return json, nil
@ -551,7 +605,7 @@ func SetRawOptions(json, path, value string, opts *Options) (string, error) {
if opts != nil { if opts != nil {
optimistic = opts.Optimistic optimistic = opts.Optimistic
} }
res, err := set(json, path, value, false, false, optimistic) res, err := set(json, path, value, false, false, optimistic, false)
if err == errNoChange { if err == errNoChange {
return json, nil return json, nil
} }
@ -572,11 +626,12 @@ func SetRawBytesOptions(json []byte, path string, value []byte,
opts *Options) ([]byte, error) { opts *Options) ([]byte, error) {
jstr := *(*string)(unsafe.Pointer(&json)) jstr := *(*string)(unsafe.Pointer(&json))
vstr := *(*string)(unsafe.Pointer(&value)) vstr := *(*string)(unsafe.Pointer(&value))
var optimistic bool var optimistic, inplace bool
if opts != nil { if opts != nil {
optimistic = opts.Optimistic optimistic = opts.Optimistic
inplace = opts.ReplaceInPlace
} }
res, err := set(jstr, path, vstr, false, false, optimistic) res, err := set(jstr, path, vstr, false, false, optimistic, inplace)
if err == errNoChange { if err == errNoChange {
return json, nil return json, nil
} }

View File

@ -180,7 +180,7 @@ var json = `
}, },
"committer": { "committer": {
"name": "Tom Tom Anderson", "name": "Tom Tom Anderson",
"email": "jeff@anderson.edu", "email": "jeffditto@anderson.edu",
"date": "2013-06-22T16:30:59Z" "date": "2013-06-22T16:30:59Z"
}, },
"message": "Merge pull request #162 from stedolan/utf8-fixes\n\nUtf8 fixes. Closes #161", "message": "Merge pull request #162 from stedolan/utf8-fixes\n\nUtf8 fixes. Closes #161",
@ -197,10 +197,12 @@ var path = "commit.committer.email"
var value = "tomtom@anderson.com" var value = "tomtom@anderson.com"
var rawValue = `"tomtom@anderson.com"` var rawValue = `"tomtom@anderson.com"`
var rawValueBytes = []byte(rawValue) var rawValueBytes = []byte(rawValue)
var expect = strings.Replace(json, "jeff@anderson.edu", "tomtom@anderson.com", 1) var expect = strings.Replace(json, "jeffditto@anderson.edu", "tomtom@anderson.com", 1)
var jsonBytes = []byte(json) var jsonBytes = []byte(json)
var jsonBytes2 = []byte(json)
var expectBytes = []byte(expect) var expectBytes = []byte(expect)
var opts = &Options{Optimistic: true} var opts = &Options{Optimistic: true}
var optsInPlace = &Options{Optimistic: true, ReplaceInPlace: true}
func BenchmarkSet(t *testing.B) { func BenchmarkSet(t *testing.B) {
t.ReportAllocs() t.ReportAllocs()
@ -253,6 +255,7 @@ func BenchmarkSetRawBytes(t *testing.B) {
} }
} }
} }
func BenchmarkSetOptimistic(t *testing.B) { func BenchmarkSetOptimistic(t *testing.B) {
t.ReportAllocs() t.ReportAllocs()
for i := 0; i < t.N; i++ { for i := 0; i < t.N; i++ {
@ -266,6 +269,19 @@ func BenchmarkSetOptimistic(t *testing.B) {
} }
} }
func BenchmarkSetInPlace(t *testing.B) {
t.ReportAllocs()
for i := 0; i < t.N; i++ {
res, err := SetOptions(json, path, value, optsInPlace)
if err != nil {
t.Fatal(err)
}
if res != expect {
t.Fatal("expected '%v', got '%v'", expect, res)
}
}
}
func BenchmarkSetRawOptimistic(t *testing.B) { func BenchmarkSetRawOptimistic(t *testing.B) {
t.ReportAllocs() t.ReportAllocs()
for i := 0; i < t.N; i++ { for i := 0; i < t.N; i++ {
@ -278,6 +294,20 @@ func BenchmarkSetRawOptimistic(t *testing.B) {
} }
} }
} }
func BenchmarkSetRawInPlace(t *testing.B) {
t.ReportAllocs()
for i := 0; i < t.N; i++ {
res, err := SetRawOptions(json, path, rawValue, optsInPlace)
if err != nil {
t.Fatal(err)
}
if res != expect {
t.Fatal("expected '%v', got '%v'", expect, res)
}
}
}
func BenchmarkSetBytesOptimistic(t *testing.B) { func BenchmarkSetBytesOptimistic(t *testing.B) {
t.ReportAllocs() t.ReportAllocs()
for i := 0; i < t.N; i++ { for i := 0; i < t.N; i++ {
@ -291,6 +321,20 @@ func BenchmarkSetBytesOptimistic(t *testing.B) {
} }
} }
func BenchmarkSetBytesInPlace(t *testing.B) {
t.ReportAllocs()
for i := 0; i < t.N; i++ {
copy(jsonBytes2, jsonBytes)
res, err := SetBytesOptions(jsonBytes2, path, value, optsInPlace)
if err != nil {
t.Fatal(err)
}
if bytes.Compare(res, expectBytes) != 0 {
t.Fatal("expected '%v', got '%v'", string(expectBytes), string(res))
}
}
}
func BenchmarkSetRawBytesOptimistic(t *testing.B) { func BenchmarkSetRawBytesOptimistic(t *testing.B) {
t.ReportAllocs() t.ReportAllocs()
for i := 0; i < t.N; i++ { for i := 0; i < t.N; i++ {
@ -303,3 +347,17 @@ func BenchmarkSetRawBytesOptimistic(t *testing.B) {
} }
} }
} }
func BenchmarkSetRawBytesInPlace(t *testing.B) {
t.ReportAllocs()
for i := 0; i < t.N; i++ {
copy(jsonBytes2, jsonBytes)
res, err := SetRawBytesOptions(jsonBytes2, path, rawValueBytes, optsInPlace)
if err != nil {
t.Fatal(err)
}
if bytes.Compare(res, expectBytes) != 0 {
t.Fatal("expected '%v', got '%v'", string(expectBytes), string(res))
}
}
}