tile38/internal/server/json.go

316 lines
6.6 KiB
Go

package server
import (
"bytes"
"encoding/json"
"strconv"
"strings"
"time"
"github.com/tidwall/geojson"
"github.com/tidwall/gjson"
"github.com/tidwall/resp"
"github.com/tidwall/sjson"
"github.com/tidwall/tile38/internal/collection"
)
func appendJSONString(b []byte, s string) []byte {
for i := 0; i < len(s); i++ {
if s[i] < ' ' || s[i] == '\\' || s[i] == '"' || s[i] > 126 {
d, _ := json.Marshal(s)
return append(b, string(d)...)
}
}
b = append(b, '"')
b = append(b, s...)
b = append(b, '"')
return b
}
func jsonString(s string) string {
for i := 0; i < len(s); i++ {
if s[i] < ' ' || s[i] == '\\' || s[i] == '"' || s[i] > 126 {
d, _ := json.Marshal(s)
return string(d)
}
}
b := make([]byte, len(s)+2)
b[0] = '"'
copy(b[1:], s)
b[len(b)-1] = '"'
return string(b)
}
func appendJSONSimpleBounds(dst []byte, o geojson.Object) []byte {
bbox := o.Rect()
dst = append(dst, `{"sw":{"lat":`...)
dst = strconv.AppendFloat(dst, bbox.Min.Y, 'f', -1, 64)
dst = append(dst, `,"lon":`...)
dst = strconv.AppendFloat(dst, bbox.Min.X, 'f', -1, 64)
dst = append(dst, `},"ne":{"lat":`...)
dst = strconv.AppendFloat(dst, bbox.Max.Y, 'f', -1, 64)
dst = append(dst, `,"lon":`...)
dst = strconv.AppendFloat(dst, bbox.Max.X, 'f', -1, 64)
dst = append(dst, `}}`...)
return dst
}
func appendJSONSimplePoint(dst []byte, o geojson.Object) []byte {
point := o.Center()
var z float64
if gPoint, ok := o.(*geojson.Point); ok {
z = gPoint.Z()
}
dst = append(dst, `{"lat":`...)
dst = strconv.AppendFloat(dst, point.Y, 'f', -1, 64)
dst = append(dst, `,"lon":`...)
dst = strconv.AppendFloat(dst, point.X, 'f', -1, 64)
if z != 0 {
dst = append(dst, `,"z":`...)
dst = strconv.AppendFloat(dst, z, 'f', -1, 64)
}
dst = append(dst, '}')
return dst
}
func appendJSONTimeFormat(b []byte, t time.Time) []byte {
b = append(b, '"')
b = t.AppendFormat(b, "2006-01-02T15:04:05.999999999Z07:00")
b = append(b, '"')
return b
}
func jsonTimeFormat(t time.Time) string {
var b []byte
b = appendJSONTimeFormat(b, t)
return string(b)
}
func (c *Server) cmdJget(msg *Message) (resp.Value, error) {
start := time.Now()
if len(msg.Args) < 3 {
return NOMessage, errInvalidNumberOfArguments
}
if len(msg.Args) > 5 {
return NOMessage, errInvalidNumberOfArguments
}
key := msg.Args[1]
id := msg.Args[2]
var doget bool
var path string
var raw bool
if len(msg.Args) > 3 {
doget = true
path = msg.Args[3]
if len(msg.Args) == 5 {
if strings.ToLower(msg.Args[4]) == "raw" {
raw = true
} else {
return NOMessage, errInvalidArgument(msg.Args[4])
}
}
}
col := c.getCol(key)
if col == nil {
if msg.OutputType == RESP {
return resp.NullValue(), nil
}
return NOMessage, errKeyNotFound
}
o, _, ok := col.Get(id)
if !ok {
if msg.OutputType == RESP {
return resp.NullValue(), nil
}
return NOMessage, errIDNotFound
}
var res gjson.Result
if doget {
res = gjson.Get(o.String(), path)
} else {
res = gjson.Parse(o.String())
}
var val string
if raw {
val = res.Raw
} else {
val = res.String()
}
var buf bytes.Buffer
if msg.OutputType == JSON {
buf.WriteString(`{"ok":true`)
}
switch msg.OutputType {
case JSON:
if res.Exists() {
buf.WriteString(`,"value":` + jsonString(val))
}
buf.WriteString(`,"elapsed":"` + time.Now().Sub(start).String() + "\"}")
return resp.StringValue(buf.String()), nil
case RESP:
if !res.Exists() {
return resp.NullValue(), nil
}
return resp.StringValue(val), nil
}
return NOMessage, nil
}
func (c *Server) cmdJset(msg *Message) (res resp.Value, d commandDetails, err error) {
// JSET key path value [RAW]
start := time.Now()
var raw, str bool
switch len(msg.Args) {
default:
return NOMessage, d, errInvalidNumberOfArguments
case 5:
case 6:
switch strings.ToLower(msg.Args[5]) {
default:
return NOMessage, d, errInvalidArgument(msg.Args[5])
case "raw":
raw = true
case "str":
str = true
}
}
key := msg.Args[1]
id := msg.Args[2]
path := msg.Args[3]
val := msg.Args[4]
if !str && !raw {
switch val {
default:
if len(val) > 0 {
if (val[0] >= '0' && val[0] <= '9') || val[0] == '-' {
if _, err := strconv.ParseFloat(val, 64); err == nil {
raw = true
}
}
}
case "true", "false", "null":
raw = true
}
}
col := c.getCol(key)
var createcol bool
if col == nil {
col = collection.New()
createcol = true
}
var json string
var geoobj bool
o, _, ok := col.Get(id)
if ok {
geoobj = objIsSpatial(o)
json = o.String()
}
if raw {
// set as raw block
json, err = sjson.SetRaw(json, path, val)
} else {
// set as a string
json, err = sjson.Set(json, path, val)
}
if err != nil {
return NOMessage, d, err
}
if geoobj {
nmsg := *msg
nmsg.Args = []string{"SET", key, id, "OBJECT", json}
// SET key id OBJECT json
return c.cmdSet(&nmsg)
}
if createcol {
c.setCol(key, col)
}
d.key = key
d.id = id
d.obj = collection.String(json)
d.timestamp = time.Now()
d.updated = true
c.clearIDExpires(key, id)
col.Set(d.id, d.obj, nil, nil)
switch msg.OutputType {
case JSON:
var buf bytes.Buffer
buf.WriteString(`{"ok":true`)
buf.WriteString(`,"elapsed":"` + time.Now().Sub(start).String() + "\"}")
return resp.StringValue(buf.String()), d, nil
case RESP:
return resp.SimpleStringValue("OK"), d, nil
}
return NOMessage, d, nil
}
func (c *Server) cmdJdel(msg *Message) (res resp.Value, d commandDetails, err error) {
start := time.Now()
if len(msg.Args) != 4 {
return NOMessage, d, errInvalidNumberOfArguments
}
key := msg.Args[1]
id := msg.Args[2]
path := msg.Args[3]
col := c.getCol(key)
if col == nil {
if msg.OutputType == RESP {
return resp.IntegerValue(0), d, nil
}
return NOMessage, d, errKeyNotFound
}
var json string
var geoobj bool
o, _, ok := col.Get(id)
if ok {
geoobj = objIsSpatial(o)
json = o.String()
}
njson, err := sjson.Delete(json, path)
if err != nil {
return NOMessage, d, err
}
if njson == json {
switch msg.OutputType {
case JSON:
return NOMessage, d, errPathNotFound
case RESP:
return resp.IntegerValue(0), d, nil
}
return NOMessage, d, nil
}
json = njson
if geoobj {
nmsg := *msg
nmsg.Args = []string{"SET", key, id, "OBJECT", json}
// SET key id OBJECT json
return c.cmdSet(&nmsg)
}
d.key = key
d.id = id
d.obj = collection.String(json)
d.timestamp = time.Now()
d.updated = true
c.clearIDExpires(d.key, d.id)
col.Set(d.id, d.obj, nil, nil)
switch msg.OutputType {
case JSON:
var buf bytes.Buffer
buf.WriteString(`{"ok":true`)
buf.WriteString(`,"elapsed":"` + time.Now().Sub(start).String() + "\"}")
return resp.StringValue(buf.String()), d, nil
case RESP:
return resp.IntegerValue(1), d, nil
}
return NOMessage, d, nil
}