Add admin get/set config keys API (#6113)
This PR adds two new admin APIs in Minio server and madmin package: - GetConfigKeys(keys []string) ([]byte, error) - SetConfigKeys(params map[string]string) (err error) A key is a path in Minio configuration file, (e.g. notify.webhook.1) The user will always send a string value when setting it in the config file, the API will know how to convert the value to the appropriate type. The user is also able to set a raw json. Before setting a new config, Minio will validate all fields and try to connect to notification targets if available.master
parent
fd1b8491db
commit
3099af70a3
@ -0,0 +1,54 @@ |
||||
// +build ignore
|
||||
|
||||
/* |
||||
* Minio Cloud Storage, (C) 2017 Minio, Inc. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
* |
||||
*/ |
||||
|
||||
package main |
||||
|
||||
import ( |
||||
"bytes" |
||||
"encoding/json" |
||||
"log" |
||||
|
||||
"github.com/minio/minio/pkg/madmin" |
||||
) |
||||
|
||||
func main() { |
||||
// Note: YOUR-ACCESSKEYID, YOUR-SECRETACCESSKEY are
|
||||
// dummy values, please replace them with original values.
|
||||
|
||||
// API requests are secure (HTTPS) if secure=true and insecure (HTTPS) otherwise.
|
||||
// New returns an Minio Admin client object.
|
||||
madmClnt, err := madmin.New("your-minio.example.com:9000", "YOUR-ACCESSKEYID", "YOUR-SECRETACCESSKEY", true) |
||||
if err != nil { |
||||
log.Fatalln(err) |
||||
} |
||||
|
||||
configBytes, err := madmClnt.GetConfigKeys([]string{"notify.amqp.1", "version"}) |
||||
if err != nil { |
||||
log.Fatalf("failed due to: %v", err) |
||||
} |
||||
|
||||
// Pretty-print config received as json.
|
||||
var buf bytes.Buffer |
||||
err = json.Indent(&buf, configBytes, "", "\t") |
||||
if err != nil { |
||||
log.Fatalf("failed due to: %v", err) |
||||
} |
||||
|
||||
log.Println("config received successfully: ", string(buf.Bytes())) |
||||
} |
@ -0,0 +1,53 @@ |
||||
// +build ignore
|
||||
|
||||
/* |
||||
* Minio Cloud Storage, (C) 2017 Minio, Inc. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
* |
||||
*/ |
||||
|
||||
package main |
||||
|
||||
import ( |
||||
"fmt" |
||||
"log" |
||||
|
||||
"github.com/minio/minio/pkg/madmin" |
||||
) |
||||
|
||||
func main() { |
||||
// Note: YOUR-ACCESSKEYID, YOUR-SECRETACCESSKEY are
|
||||
// dummy values, please replace them with original values.
|
||||
|
||||
// Note: YOUR-ACCESSKEYID, YOUR-SECRETACCESSKEY are
|
||||
// dummy values, please replace them with original values.
|
||||
|
||||
// API requests are secure (HTTPS) if secure=true and insecure (HTTPS) otherwise.
|
||||
// New returns an Minio Admin client object.
|
||||
madmClnt, err := madmin.New("your-minio.example.com:9000", "YOUR-ACCESSKEYID", "YOUR-SECRETACCESSKEY", true) |
||||
if err != nil { |
||||
log.Fatalln(err) |
||||
} |
||||
|
||||
err = madmClnt.SetConfigKeys(map[string]string{ |
||||
"domain": "example.com", |
||||
"notify.webhook.1": "{\"enable\": true, \"endpoint\": \"http://example.com/api/object-notifications\"}", |
||||
}) |
||||
|
||||
if err != nil { |
||||
log.Fatalln(err) |
||||
} |
||||
|
||||
fmt.Println("Setting new configuration successfully executed.") |
||||
} |
@ -0,0 +1,21 @@ |
||||
The MIT License (MIT) |
||||
|
||||
Copyright (c) 2016 Josh Baker |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of |
||||
this software and associated documentation files (the "Software"), to deal in |
||||
the Software without restriction, including without limitation the rights to |
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of |
||||
the Software, and to permit persons to whom the Software is furnished to do so, |
||||
subject to the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be included in all |
||||
copies or substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS |
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR |
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER |
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
||||
|
@ -0,0 +1,278 @@ |
||||
<p align="center"> |
||||
<img |
||||
src="logo.png" |
||||
width="240" height="78" border="0" alt="SJSON"> |
||||
<br> |
||||
<a href="https://travis-ci.org/tidwall/sjson"><img src="https://img.shields.io/travis/tidwall/sjson.svg?style=flat-square" alt="Build Status"></a> |
||||
<a href="https://godoc.org/github.com/tidwall/sjson"><img src="https://img.shields.io/badge/api-reference-blue.svg?style=flat-square" alt="GoDoc"></a> |
||||
</p> |
||||
|
||||
<p align="center">set a json value quickly</a></p> |
||||
|
||||
SJSON is a Go package that provides a [very fast](#performance) and simple way to set a value in a json document. The purpose for this library is to provide efficient json updating for the [SummitDB](https://github.com/tidwall/summitdb) project. |
||||
For quickly retrieving json values check out [GJSON](https://github.com/tidwall/gjson). |
||||
|
||||
For a command line interface check out [JSONed](https://github.com/tidwall/jsoned). |
||||
|
||||
Getting Started |
||||
=============== |
||||
|
||||
Installing |
||||
---------- |
||||
|
||||
To start using SJSON, install Go and run `go get`: |
||||
|
||||
```sh |
||||
$ go get -u github.com/tidwall/sjson |
||||
``` |
||||
|
||||
This will retrieve the library. |
||||
|
||||
Set a value |
||||
----------- |
||||
Set sets the value for the specified path. |
||||
A path is in dot syntax, such as "name.last" or "age". |
||||
This function expects that the json is well-formed and validated. |
||||
Invalid json will not panic, but it may return back unexpected results. |
||||
Invalid paths may return an error. |
||||
|
||||
```go |
||||
package main |
||||
|
||||
import "github.com/tidwall/sjson" |
||||
|
||||
const json = `{"name":{"first":"Janet","last":"Prichard"},"age":47}` |
||||
|
||||
func main() { |
||||
value, _ := sjson.Set(json, "name.last", "Anderson") |
||||
println(value) |
||||
} |
||||
``` |
||||
|
||||
This will print: |
||||
|
||||
```json |
||||
{"name":{"first":"Janet","last":"Anderson"},"age":47} |
||||
``` |
||||
|
||||
Path syntax |
||||
----------- |
||||
|
||||
A path is a series of keys separated by a dot. |
||||
The dot and colon characters can be escaped with '\'. |
||||
|
||||
```json |
||||
{ |
||||
"name": {"first": "Tom", "last": "Anderson"}, |
||||
"age":37, |
||||
"children": ["Sara","Alex","Jack"], |
||||
"fav.movie": "Deer Hunter", |
||||
"friends": [ |
||||
{"first": "James", "last": "Murphy"}, |
||||
{"first": "Roger", "last": "Craig"} |
||||
] |
||||
} |
||||
``` |
||||
``` |
||||
"name.last" >> "Anderson" |
||||
"age" >> 37 |
||||
"children.1" >> "Alex" |
||||
"friends.1.last" >> "Craig" |
||||
``` |
||||
|
||||
The `-1` key can be used to append a value to an existing array: |
||||
|
||||
``` |
||||
"children.-1" >> appends a new value to the end of the children array |
||||
``` |
||||
|
||||
Normally number keys are used to modify arrays, but it's possible to force a numeric object key by using the colon character: |
||||
|
||||
```json |
||||
{ |
||||
"users":{ |
||||
"2313":{"name":"Sara"}, |
||||
"7839":{"name":"Andy"} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
A colon path would look like: |
||||
|
||||
``` |
||||
"users.:2313.name" >> "Sara" |
||||
``` |
||||
|
||||
Supported types |
||||
--------------- |
||||
|
||||
Pretty much any type is supported: |
||||
|
||||
```go |
||||
sjson.Set(`{"key":true}`, "key", nil) |
||||
sjson.Set(`{"key":true}`, "key", false) |
||||
sjson.Set(`{"key":true}`, "key", 1) |
||||
sjson.Set(`{"key":true}`, "key", 10.5) |
||||
sjson.Set(`{"key":true}`, "key", "hello") |
||||
sjson.Set(`{"key":true}`, "key", map[string]interface{}{"hello":"world"}) |
||||
``` |
||||
|
||||
When a type is not recognized, SJSON will fallback to the `encoding/json` Marshaller. |
||||
|
||||
|
||||
Examples |
||||
-------- |
||||
|
||||
Set a value from empty document: |
||||
```go |
||||
value, _ := sjson.Set("", "name", "Tom") |
||||
println(value) |
||||
|
||||
// Output: |
||||
// {"name":"Tom"} |
||||
``` |
||||
|
||||
Set a nested value from empty document: |
||||
```go |
||||
value, _ := sjson.Set("", "name.last", "Anderson") |
||||
println(value) |
||||
|
||||
// Output: |
||||
// {"name":{"last":"Anderson"}} |
||||
``` |
||||
|
||||
Set a new value: |
||||
```go |
||||
value, _ := sjson.Set(`{"name":{"last":"Anderson"}}`, "name.first", "Sara") |
||||
println(value) |
||||
|
||||
// Output: |
||||
// {"name":{"first":"Sara","last":"Anderson"}} |
||||
``` |
||||
|
||||
Update an existing value: |
||||
```go |
||||
value, _ := sjson.Set(`{"name":{"last":"Anderson"}}`, "name.last", "Smith") |
||||
println(value) |
||||
|
||||
// Output: |
||||
// {"name":{"last":"Smith"}} |
||||
``` |
||||
|
||||
Set a new array value: |
||||
```go |
||||
value, _ := sjson.Set(`{"friends":["Andy","Carol"]}`, "friends.2", "Sara") |
||||
println(value) |
||||
|
||||
// Output: |
||||
// {"friends":["Andy","Carol","Sara"] |
||||
``` |
||||
|
||||
Append an array value by using the `-1` key in a path: |
||||
```go |
||||
value, _ := sjson.Set(`{"friends":["Andy","Carol"]}`, "friends.-1", "Sara") |
||||
println(value) |
||||
|
||||
// Output: |
||||
// {"friends":["Andy","Carol","Sara"] |
||||
``` |
||||
|
||||
Append an array value that is past the end: |
||||
```go |
||||
value, _ := sjson.Set(`{"friends":["Andy","Carol"]}`, "friends.4", "Sara") |
||||
println(value) |
||||
|
||||
// Output: |
||||
// {"friends":["Andy","Carol",null,null,"Sara"] |
||||
``` |
||||
|
||||
Delete a value: |
||||
```go |
||||
value, _ := sjson.Delete(`{"name":{"first":"Sara","last":"Anderson"}}`, "name.first") |
||||
println(value) |
||||
|
||||
// Output: |
||||
// {"name":{"last":"Anderson"}} |
||||
``` |
||||
|
||||
Delete an array value: |
||||
```go |
||||
value, _ := sjson.Delete(`{"friends":["Andy","Carol"]}`, "friends.1") |
||||
println(value) |
||||
|
||||
// Output: |
||||
// {"friends":["Andy"]} |
||||
``` |
||||
|
||||
Delete the last array value: |
||||
```go |
||||
value, _ := sjson.Delete(`{"friends":["Andy","Carol"]}`, "friends.-1") |
||||
println(value) |
||||
|
||||
// Output: |
||||
// {"friends":["Andy"]} |
||||
``` |
||||
|
||||
## Performance |
||||
|
||||
Benchmarks of SJSON alongside [encoding/json](https://golang.org/pkg/encoding/json/), |
||||
[ffjson](https://github.com/pquerna/ffjson), |
||||
[EasyJSON](https://github.com/mailru/easyjson), |
||||
and [Gabs](https://github.com/Jeffail/gabs) |
||||
|
||||
``` |
||||
Benchmark_SJSON-8 3000000 805 ns/op 1077 B/op 3 allocs/op |
||||
Benchmark_SJSON_ReplaceInPlace-8 3000000 449 ns/op 0 B/op 0 allocs/op |
||||
Benchmark_JSON_Map-8 300000 21236 ns/op 6392 B/op 150 allocs/op |
||||
Benchmark_JSON_Struct-8 300000 14691 ns/op 1789 B/op 24 allocs/op |
||||
Benchmark_Gabs-8 300000 21311 ns/op 6752 B/op 150 allocs/op |
||||
Benchmark_FFJSON-8 300000 17673 ns/op 3589 B/op 47 allocs/op |
||||
Benchmark_EasyJSON-8 1500000 3119 ns/op 1061 B/op 13 allocs/op |
||||
``` |
||||
|
||||
JSON document used: |
||||
|
||||
```json |
||||
{ |
||||
"widget": { |
||||
"debug": "on", |
||||
"window": { |
||||
"title": "Sample Konfabulator Widget", |
||||
"name": "main_window", |
||||
"width": 500, |
||||
"height": 500 |
||||
}, |
||||
"image": { |
||||
"src": "Images/Sun.png", |
||||
"hOffset": 250, |
||||
"vOffset": 250, |
||||
"alignment": "center" |
||||
}, |
||||
"text": { |
||||
"data": "Click Here", |
||||
"size": 36, |
||||
"style": "bold", |
||||
"vOffset": 100, |
||||
"alignment": "center", |
||||
"onMouseUp": "sun1.opacity = (sun1.opacity / 100) * 90;" |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
Each operation was rotated though one of the following search paths: |
||||
|
||||
``` |
||||
widget.window.name |
||||
widget.image.hOffset |
||||
widget.text.onMouseUp |
||||
``` |
||||
|
||||
*These benchmarks were run on a MacBook Pro 15" 2.8 GHz Intel Core i7 using Go 1.7.* |
||||
|
||||
## Contact |
||||
Josh Baker [@tidwall](http://twitter.com/tidwall) |
||||
|
||||
## License |
||||
|
||||
SJSON source code is available under the MIT [License](/LICENSE). |
After Width: | Height: | Size: 16 KiB |
@ -0,0 +1,653 @@ |
||||
// Package sjson provides setting json values.
|
||||
package sjson |
||||
|
||||
import ( |
||||
jsongo "encoding/json" |
||||
"reflect" |
||||
"strconv" |
||||
"unsafe" |
||||
|
||||
"github.com/tidwall/gjson" |
||||
) |
||||
|
||||
type errorType struct { |
||||
msg string |
||||
} |
||||
|
||||
func (err *errorType) Error() string { |
||||
return err.msg |
||||
} |
||||
|
||||
// Options represents additional options for the Set and Delete functions.
|
||||
type Options struct { |
||||
// Optimistic is a hint that the value likely exists which
|
||||
// allows for the sjson to perform a fast-track search and replace.
|
||||
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
|
||||
// In the case when the destination slice doesn't have enough free
|
||||
// bytes to replace the data in place, a new bytes slice will be
|
||||
// created under the hood.
|
||||
// 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 { |
||||
part string // current key part
|
||||
path string // remaining path
|
||||
force bool // force a string key
|
||||
more bool // there is more path to parse
|
||||
} |
||||
|
||||
func parsePath(path string) (pathResult, error) { |
||||
var r pathResult |
||||
if len(path) > 0 && path[0] == ':' { |
||||
r.force = true |
||||
path = path[1:] |
||||
} |
||||
for i := 0; i < len(path); i++ { |
||||
if path[i] == '.' { |
||||
r.part = path[:i] |
||||
r.path = path[i+1:] |
||||
r.more = true |
||||
return r, nil |
||||
} |
||||
if path[i] == '*' || path[i] == '?' { |
||||
return r, &errorType{"wildcard characters not allowed in path"} |
||||
} else if path[i] == '#' { |
||||
return r, &errorType{"array access character not allowed in path"} |
||||
} |
||||
if path[i] == '\\' { |
||||
// go into escape mode. this is a slower path that
|
||||
// strips off the escape character from the part.
|
||||
epart := []byte(path[:i]) |
||||
i++ |
||||
if i < len(path) { |
||||
epart = append(epart, path[i]) |
||||
i++ |
||||
for ; i < len(path); i++ { |
||||
if path[i] == '\\' { |
||||
i++ |
||||
if i < len(path) { |
||||
epart = append(epart, path[i]) |
||||
} |
||||
continue |
||||
} else if path[i] == '.' { |
||||
r.part = string(epart) |
||||
r.path = path[i+1:] |
||||
r.more = true |
||||
return r, nil |
||||
} else if path[i] == '*' || path[i] == '?' { |
||||
return r, &errorType{ |
||||
"wildcard characters not allowed in path"} |
||||
} else if path[i] == '#' { |
||||
return r, &errorType{ |
||||
"array access character not allowed in path"} |
||||
} |
||||
epart = append(epart, path[i]) |
||||
} |
||||
} |
||||
// append the last part
|
||||
r.part = string(epart) |
||||
return r, nil |
||||
} |
||||
} |
||||
r.part = path |
||||
return r, nil |
||||
} |
||||
|
||||
func mustMarshalString(s string) bool { |
||||
for i := 0; i < len(s); i++ { |
||||
if s[i] < ' ' || s[i] > 0x7f || s[i] == '"' { |
||||
return true |
||||
} |
||||
} |
||||
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, s...) |
||||
buf = append(buf, '"') |
||||
return buf |
||||
} |
||||
|
||||
// appendBuild builds a json block from a json path.
|
||||
func appendBuild(buf []byte, array bool, paths []pathResult, raw string, |
||||
stringify bool) []byte { |
||||
if !array { |
||||
buf = appendStringify(buf, paths[0].part) |
||||
buf = append(buf, ':') |
||||
} |
||||
if len(paths) > 1 { |
||||
n, numeric := atoui(paths[1]) |
||||
if numeric || (!paths[1].force && paths[1].part == "-1") { |
||||
buf = append(buf, '[') |
||||
buf = appendRepeat(buf, "null,", n) |
||||
buf = appendBuild(buf, true, paths[1:], raw, stringify) |
||||
buf = append(buf, ']') |
||||
} else { |
||||
buf = append(buf, '{') |
||||
buf = appendBuild(buf, false, paths[1:], raw, stringify) |
||||
buf = append(buf, '}') |
||||
} |
||||
} else { |
||||
if stringify { |
||||
buf = appendStringify(buf, raw) |
||||
} else { |
||||
buf = append(buf, raw...) |
||||
} |
||||
} |
||||
return buf |
||||
} |
||||
|
||||
// atoui does a rip conversion of string -> unigned int.
|
||||
func atoui(r pathResult) (n int, ok bool) { |
||||
if r.force { |
||||
return 0, false |
||||
} |
||||
for i := 0; i < len(r.part); i++ { |
||||
if r.part[i] < '0' || r.part[i] > '9' { |
||||
return 0, false |
||||
} |
||||
n = n*10 + int(r.part[i]-'0') |
||||
} |
||||
return n, true |
||||
} |
||||
|
||||
// appendRepeat repeats string "n" times and appends to buf.
|
||||
func appendRepeat(buf []byte, s string, n int) []byte { |
||||
for i := 0; i < n; i++ { |
||||
buf = append(buf, s...) |
||||
} |
||||
return buf |
||||
} |
||||
|
||||
// trim does a rip trim
|
||||
func trim(s string) string { |
||||
for len(s) > 0 { |
||||
if s[0] <= ' ' { |
||||
s = s[1:] |
||||
continue |
||||
} |
||||
break |
||||
} |
||||
for len(s) > 0 { |
||||
if s[len(s)-1] <= ' ' { |
||||
s = s[:len(s)-1] |
||||
continue |
||||
} |
||||
break |
||||
} |
||||
return s |
||||
} |
||||
|
||||
// deleteTailItem deletes the previous key or comma.
|
||||
func deleteTailItem(buf []byte) ([]byte, bool) { |
||||
loop: |
||||
for i := len(buf) - 1; i >= 0; i-- { |
||||
// look for either a ',',':','['
|
||||
switch buf[i] { |
||||
case '[': |
||||
return buf, true |
||||
case ',': |
||||
return buf[:i], false |
||||
case ':': |
||||
// delete tail string
|
||||
i-- |
||||
for ; i >= 0; i-- { |
||||
if buf[i] == '"' { |
||||
i-- |
||||
for ; i >= 0; i-- { |
||||
if buf[i] == '"' { |
||||
i-- |
||||
if i >= 0 && i == '\\' { |
||||
i-- |
||||
continue |
||||
} |
||||
for ; i >= 0; i-- { |
||||
// look for either a ',','{'
|
||||
switch buf[i] { |
||||
case '{': |
||||
return buf[:i+1], true |
||||
case ',': |
||||
return buf[:i], false |
||||
} |
||||
} |
||||
} |
||||
} |
||||
break |
||||
} |
||||
} |
||||
break loop |
||||
} |
||||
} |
||||
return buf, false |
||||
} |
||||
|
||||
var errNoChange = &errorType{"no change"} |
||||
|
||||
func appendRawPaths(buf []byte, jstr string, paths []pathResult, raw string, |
||||
stringify, del bool) ([]byte, error) { |
||||
var err error |
||||
var res gjson.Result |
||||
var found bool |
||||
if del { |
||||
if paths[0].part == "-1" && !paths[0].force { |
||||
res = gjson.Get(jstr, "#") |
||||
if res.Int() > 0 { |
||||
res = gjson.Get(jstr, strconv.FormatInt(int64(res.Int()-1), 10)) |
||||
found = true |
||||
} |
||||
} |
||||
} |
||||
if !found { |
||||
res = gjson.Get(jstr, paths[0].part) |
||||
} |
||||
if res.Index > 0 { |
||||
if len(paths) > 1 { |
||||
buf = append(buf, jstr[:res.Index]...) |
||||
buf, err = appendRawPaths(buf, res.Raw, paths[1:], raw, |
||||
stringify, del) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
buf = append(buf, jstr[res.Index+len(res.Raw):]...) |
||||
return buf, nil |
||||
} |
||||
buf = append(buf, jstr[:res.Index]...) |
||||
var exidx int // additional forward stripping
|
||||
if del { |
||||
var delNextComma bool |
||||
buf, delNextComma = deleteTailItem(buf) |
||||
if delNextComma { |
||||
i, j := res.Index+len(res.Raw), 0 |
||||
for ; i < len(jstr); i, j = i+1, j+1 { |
||||
if jstr[i] <= ' ' { |
||||
continue |
||||
} |
||||
if jstr[i] == ',' { |
||||
exidx = j + 1 |
||||
} |
||||
break |
||||
} |
||||
} |
||||
} else { |
||||
if stringify { |
||||
buf = appendStringify(buf, raw) |
||||
} else { |
||||
buf = append(buf, raw...) |
||||
} |
||||
} |
||||
buf = append(buf, jstr[res.Index+len(res.Raw)+exidx:]...) |
||||
return buf, nil |
||||
} |
||||
if del { |
||||
return nil, errNoChange |
||||
} |
||||
n, numeric := atoui(paths[0]) |
||||
isempty := true |
||||
for i := 0; i < len(jstr); i++ { |
||||
if jstr[i] > ' ' { |
||||
isempty = false |
||||
break |
||||
} |
||||
} |
||||
if isempty { |
||||
if numeric { |
||||
jstr = "[]" |
||||
} else { |
||||
jstr = "{}" |
||||
} |
||||
} |
||||
jsres := gjson.Parse(jstr) |
||||
if jsres.Type != gjson.JSON { |
||||
if numeric { |
||||
jstr = "[]" |
||||
} else { |
||||
jstr = "{}" |
||||
} |
||||
jsres = gjson.Parse(jstr) |
||||
} |
||||
var comma bool |
||||
for i := 1; i < len(jsres.Raw); i++ { |
||||
if jsres.Raw[i] <= ' ' { |
||||
continue |
||||
} |
||||
if jsres.Raw[i] == '}' || jsres.Raw[i] == ']' { |
||||
break |
||||
} |
||||
comma = true |
||||
break |
||||
} |
||||
switch jsres.Raw[0] { |
||||
default: |
||||
return nil, &errorType{"json must be an object or array"} |
||||
case '{': |
||||
buf = append(buf, '{') |
||||
buf = appendBuild(buf, false, paths, raw, stringify) |
||||
if comma { |
||||
buf = append(buf, ',') |
||||
} |
||||
buf = append(buf, jsres.Raw[1:]...) |
||||
return buf, nil |
||||
case '[': |
||||
var appendit bool |
||||
if !numeric { |
||||
if paths[0].part == "-1" && !paths[0].force { |
||||
appendit = true |
||||
} else { |
||||
return nil, &errorType{ |
||||
"cannot set array element for non-numeric key '" + |
||||
paths[0].part + "'"} |
||||
} |
||||
} |
||||
if appendit { |
||||
njson := trim(jsres.Raw) |
||||
if njson[len(njson)-1] == ']' { |
||||
njson = njson[:len(njson)-1] |
||||
} |
||||
buf = append(buf, njson...) |
||||
if comma { |
||||
buf = append(buf, ',') |
||||
} |
||||
|
||||
buf = appendBuild(buf, true, paths, raw, stringify) |
||||
buf = append(buf, ']') |
||||
return buf, nil |
||||
} |
||||
buf = append(buf, '[') |
||||
ress := jsres.Array() |
||||
for i := 0; i < len(ress); i++ { |
||||
if i > 0 { |
||||
buf = append(buf, ',') |
||||
} |
||||
buf = append(buf, ress[i].Raw...) |
||||
} |
||||
if len(ress) == 0 { |
||||
buf = appendRepeat(buf, "null,", n-len(ress)) |
||||
} else { |
||||
buf = appendRepeat(buf, ",null", n-len(ress)) |
||||
if comma { |
||||
buf = append(buf, ',') |
||||
} |
||||
} |
||||
buf = appendBuild(buf, true, paths, raw, stringify) |
||||
buf = append(buf, ']') |
||||
return buf, nil |
||||
} |
||||
} |
||||
|
||||
func isOptimisticPath(path string) bool { |
||||
for i := 0; i < len(path); i++ { |
||||
if path[i] < '.' || path[i] > 'z' { |
||||
return false |
||||
} |
||||
if path[i] > '9' && path[i] < 'A' { |
||||
return false |
||||
} |
||||
if path[i] > 'z' { |
||||
return false |
||||
} |
||||
} |
||||
return true |
||||
} |
||||
|
||||
func set(jstr, path, raw string, |
||||
stringify, del, optimistic, inplace bool) ([]byte, error) { |
||||
if path == "" { |
||||
return nil, &errorType{"path cannot be empty"} |
||||
} |
||||
if !del && optimistic && isOptimisticPath(path) { |
||||
res := gjson.Get(jstr, path) |
||||
if res.Exists() && res.Index > 0 { |
||||
sz := len(jstr) - len(res.Raw) + len(raw) |
||||
if stringify { |
||||
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 = append(buf, jstr[:res.Index]...) |
||||
if stringify { |
||||
buf = appendStringify(buf, raw) |
||||
} else { |
||||
buf = append(buf, raw...) |
||||
} |
||||
buf = append(buf, jstr[res.Index+len(res.Raw):]...) |
||||
return buf, nil |
||||
} |
||||
} |
||||
// parse the path, make sure that it does not contain invalid characters
|
||||
// such as '#', '?', '*'
|
||||
paths := make([]pathResult, 0, 4) |
||||
r, err := parsePath(path) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
paths = append(paths, r) |
||||
for r.more { |
||||
if r, err = parsePath(r.path); err != nil { |
||||
return nil, err |
||||
} |
||||
paths = append(paths, r) |
||||
} |
||||
|
||||
njson, err := appendRawPaths(nil, jstr, paths, raw, stringify, del) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return njson, nil |
||||
} |
||||
|
||||
// Set sets a json value for the specified path.
|
||||
// A path is in dot syntax, such as "name.last" or "age".
|
||||
// This function expects that the json is well-formed, and does not validate.
|
||||
// Invalid json will not panic, but it may return back unexpected results.
|
||||
// An error is returned if the path is not valid.
|
||||
//
|
||||
// A path is a series of keys separated by a dot.
|
||||
//
|
||||
// {
|
||||
// "name": {"first": "Tom", "last": "Anderson"},
|
||||
// "age":37,
|
||||
// "children": ["Sara","Alex","Jack"],
|
||||
// "friends": [
|
||||
// {"first": "James", "last": "Murphy"},
|
||||
// {"first": "Roger", "last": "Craig"}
|
||||
// ]
|
||||
// }
|
||||
// "name.last" >> "Anderson"
|
||||
// "age" >> 37
|
||||
// "children.1" >> "Alex"
|
||||
//
|
||||
func Set(json, path string, value interface{}) (string, error) { |
||||
return SetOptions(json, path, value, nil) |
||||
} |
||||
|
||||
// SetOptions sets a json value for the specified path with options.
|
||||
// A path is in dot syntax, such as "name.last" or "age".
|
||||
// This function expects that the json is well-formed, and does not validate.
|
||||
// Invalid json will not panic, but it may return back unexpected results.
|
||||
// An error is returned if the path is not valid.
|
||||
func SetOptions(json, path string, value interface{}, |
||||
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)) |
||||
jsonbh := reflect.SliceHeader{Data: jsonh.Data, Len: jsonh.Len} |
||||
jsonb := *(*[]byte)(unsafe.Pointer(&jsonbh)) |
||||
res, err := SetBytesOptions(jsonb, path, value, opts) |
||||
return string(res), err |
||||
} |
||||
|
||||
// SetBytes sets a json value for the specified path.
|
||||
// If working with bytes, this method preferred over
|
||||
// Set(string(data), path, value)
|
||||
func SetBytes(json []byte, path string, value interface{}) ([]byte, error) { |
||||
return SetBytesOptions(json, path, value, nil) |
||||
} |
||||
|
||||
// SetBytesOptions sets a json value for the specified path with options.
|
||||
// If working with bytes, this method preferred over
|
||||
// SetOptions(string(data), path, value)
|
||||
func SetBytesOptions(json []byte, path string, value interface{}, |
||||
opts *Options) ([]byte, error) { |
||||
var optimistic, inplace bool |
||||
if opts != nil { |
||||
optimistic = opts.Optimistic |
||||
inplace = opts.ReplaceInPlace |
||||
} |
||||
jstr := *(*string)(unsafe.Pointer(&json)) |
||||
var res []byte |
||||
var err error |
||||
switch v := value.(type) { |
||||
default: |
||||
b, err := jsongo.Marshal(value) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
raw := *(*string)(unsafe.Pointer(&b)) |
||||
res, err = set(jstr, path, raw, false, false, optimistic, inplace) |
||||
case dtype: |
||||
res, err = set(jstr, path, "", false, true, optimistic, inplace) |
||||
case string: |
||||
res, err = set(jstr, path, v, true, false, optimistic, inplace) |
||||
case []byte: |
||||
raw := *(*string)(unsafe.Pointer(&v)) |
||||
res, err = set(jstr, path, raw, true, false, optimistic, inplace) |
||||
case bool: |
||||
if v { |
||||
res, err = set(jstr, path, "true", false, false, optimistic, inplace) |
||||
} else { |
||||
res, err = set(jstr, path, "false", false, false, optimistic, inplace) |
||||
} |
||||
case int8: |
||||
res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), |
||||
false, false, optimistic, inplace) |
||||
case int16: |
||||
res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), |
||||
false, false, optimistic, inplace) |
||||
case int32: |
||||
res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), |
||||
false, false, optimistic, inplace) |
||||
case int64: |
||||
res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), |
||||
false, false, optimistic, inplace) |
||||
case uint8: |
||||
res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), |
||||
false, false, optimistic, inplace) |
||||
case uint16: |
||||
res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), |
||||
false, false, optimistic, inplace) |
||||
case uint32: |
||||
res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), |
||||
false, false, optimistic, inplace) |
||||
case uint64: |
||||
res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), |
||||
false, false, optimistic, inplace) |
||||
case float32: |
||||
res, err = set(jstr, path, strconv.FormatFloat(float64(v), 'f', -1, 64), |
||||
false, false, optimistic, inplace) |
||||
case float64: |
||||
res, err = set(jstr, path, strconv.FormatFloat(float64(v), 'f', -1, 64), |
||||
false, false, optimistic, inplace) |
||||
} |
||||
if err == errNoChange { |
||||
return json, nil |
||||
} |
||||
return res, err |
||||
} |
||||
|
||||
// SetRaw sets a raw json value for the specified path.
|
||||
// This function works the same as Set except that the value is set as a
|
||||
// raw block of json. This allows for setting premarshalled json objects.
|
||||
func SetRaw(json, path, value string) (string, error) { |
||||
return SetRawOptions(json, path, value, nil) |
||||
} |
||||
|
||||
// SetRawOptions sets a raw json value for the specified path with options.
|
||||
// This furnction works the same as SetOptions except that the value is set
|
||||
// as a raw block of json. This allows for setting premarshalled json objects.
|
||||
func SetRawOptions(json, path, value string, opts *Options) (string, error) { |
||||
var optimistic bool |
||||
if opts != nil { |
||||
optimistic = opts.Optimistic |
||||
} |
||||
res, err := set(json, path, value, false, false, optimistic, false) |
||||
if err == errNoChange { |
||||
return json, nil |
||||
} |
||||
return string(res), err |
||||
} |
||||
|
||||
// SetRawBytes sets a raw json value for the specified path.
|
||||
// If working with bytes, this method preferred over
|
||||
// SetRaw(string(data), path, value)
|
||||
func SetRawBytes(json []byte, path string, value []byte) ([]byte, error) { |
||||
return SetRawBytesOptions(json, path, value, nil) |
||||
} |
||||
|
||||
// SetRawBytesOptions sets a raw json value for the specified path with options.
|
||||
// If working with bytes, this method preferred over
|
||||
// SetRawOptions(string(data), path, value, opts)
|
||||
func SetRawBytesOptions(json []byte, path string, value []byte, |
||||
opts *Options) ([]byte, error) { |
||||
jstr := *(*string)(unsafe.Pointer(&json)) |
||||
vstr := *(*string)(unsafe.Pointer(&value)) |
||||
var optimistic, inplace bool |
||||
if opts != nil { |
||||
optimistic = opts.Optimistic |
||||
inplace = opts.ReplaceInPlace |
||||
} |
||||
res, err := set(jstr, path, vstr, false, false, optimistic, inplace) |
||||
if err == errNoChange { |
||||
return json, nil |
||||
} |
||||
return res, err |
||||
} |
||||
|
||||
type dtype struct{} |
||||
|
||||
// Delete deletes a value from json for the specified path.
|
||||
func Delete(json, path string) (string, error) { |
||||
return Set(json, path, dtype{}) |
||||
} |
||||
|
||||
// DeleteBytes deletes a value from json for the specified path.
|
||||
func DeleteBytes(json []byte, path string) ([]byte, error) { |
||||
return SetBytes(json, path, dtype{}) |
||||
} |
Loading…
Reference in new issue