mirror of https://github.com/tidwall/tile38.git
381 lines
7.6 KiB
Go
381 lines
7.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 isJSONNumber(data string) bool {
|
|
// Returns true if the given string can be encoded as a JSON number value.
|
|
// See:
|
|
// https://json.org
|
|
// http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf
|
|
if data == "" {
|
|
return false
|
|
}
|
|
i := 0
|
|
// sign
|
|
if data[i] == '-' {
|
|
i++
|
|
}
|
|
if i == len(data) {
|
|
return false
|
|
}
|
|
// int
|
|
if data[i] == '0' {
|
|
i++
|
|
} else {
|
|
for ; i < len(data); i++ {
|
|
if data[i] >= '0' && data[i] <= '9' {
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
}
|
|
// frac
|
|
if i == len(data) {
|
|
return true
|
|
}
|
|
if data[i] == '.' {
|
|
i++
|
|
if i == len(data) {
|
|
return false
|
|
}
|
|
if data[i] < '0' || data[i] > '9' {
|
|
return false
|
|
}
|
|
i++
|
|
for ; i < len(data); i++ {
|
|
if data[i] >= '0' && data[i] <= '9' {
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
}
|
|
// exp
|
|
if i == len(data) {
|
|
return true
|
|
}
|
|
if data[i] == 'e' || data[i] == 'E' {
|
|
i++
|
|
if i == len(data) {
|
|
return false
|
|
}
|
|
if data[i] == '+' || data[i] == '-' {
|
|
i++
|
|
}
|
|
if i == len(data) {
|
|
return false
|
|
}
|
|
if data[i] < '0' || data[i] > '9' {
|
|
return false
|
|
}
|
|
i++
|
|
for ; i < len(data); i++ {
|
|
if data[i] >= '0' && data[i] <= '9' {
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
}
|
|
return i == len(data)
|
|
}
|
|
|
|
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()
|
|
z := extractZCoordinate(o)
|
|
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 (s *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, _ := s.cols.Get(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.Since(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 (s *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:
|
|
raw = isJSONNumber(val)
|
|
case "true", "false", "null":
|
|
raw = true
|
|
}
|
|
}
|
|
col, _ := s.cols.Get(key)
|
|
var createcol bool
|
|
if col == nil {
|
|
col = collection.New()
|
|
createcol = true
|
|
}
|
|
var json string
|
|
var geoobj bool
|
|
o, fields, _, 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 s.cmdSET(&nmsg)
|
|
}
|
|
if createcol {
|
|
s.cols.Set(key, col)
|
|
}
|
|
|
|
d.key = key
|
|
d.id = id
|
|
d.obj = collection.String(json)
|
|
d.timestamp = time.Now()
|
|
d.updated = true
|
|
|
|
col.Set(d.id, d.obj, fields, 0)
|
|
switch msg.OutputType {
|
|
case JSON:
|
|
var buf bytes.Buffer
|
|
buf.WriteString(`{"ok":true`)
|
|
buf.WriteString(`,"elapsed":"` + time.Since(start).String() + "\"}")
|
|
return resp.StringValue(buf.String()), d, nil
|
|
case RESP:
|
|
return resp.SimpleStringValue("OK"), d, nil
|
|
}
|
|
return NOMessage, d, nil
|
|
}
|
|
|
|
func (s *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, _ := s.cols.Get(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, fields, _, 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 s.cmdSET(&nmsg)
|
|
}
|
|
|
|
d.key = key
|
|
d.id = id
|
|
d.obj = collection.String(json)
|
|
d.timestamp = time.Now()
|
|
d.updated = true
|
|
col.Set(d.id, d.obj, fields, 0)
|
|
switch msg.OutputType {
|
|
case JSON:
|
|
var buf bytes.Buffer
|
|
buf.WriteString(`{"ok":true`)
|
|
buf.WriteString(`,"elapsed":"` + time.Since(start).String() + "\"}")
|
|
return resp.StringValue(buf.String()), d, nil
|
|
case RESP:
|
|
return resp.IntegerValue(1), d, nil
|
|
}
|
|
return NOMessage, d, nil
|
|
}
|