mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-22 16:48:23 +01:00
636 lines
15 KiB
Go
636 lines
15 KiB
Go
// Copyright 2024, Command Line Inc.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
// implements incremental json format
|
|
package ijson
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
)
|
|
|
|
// ijson values are built out of standard go building blocks:
|
|
// string, float64, bool, nil, []any, map[string]any
|
|
|
|
// paths are arrays of strings and ints
|
|
|
|
const (
|
|
SetCommandStr = "set"
|
|
DelCommandStr = "del"
|
|
AppendCommandStr = "append"
|
|
)
|
|
|
|
type Command = map[string]any
|
|
type Path = []any
|
|
type M = map[string]any
|
|
type A = []any
|
|
|
|
// instead of defining structs for commands, we just define a command shape
|
|
// set: type, path, value
|
|
// del: type, path
|
|
// arrayappend: type, path, value
|
|
|
|
func MakeSetCommand(path Path, value any) Command {
|
|
return Command{
|
|
"type": SetCommandStr,
|
|
"path": path,
|
|
"data": value,
|
|
}
|
|
}
|
|
|
|
func MakeDelCommand(path Path) Command {
|
|
return Command{
|
|
"type": DelCommandStr,
|
|
"path": path,
|
|
}
|
|
}
|
|
|
|
func MakeAppendCommand(path Path, value any) Command {
|
|
return Command{
|
|
"type": AppendCommandStr,
|
|
"path": path,
|
|
"data": value,
|
|
}
|
|
}
|
|
|
|
type PathError struct {
|
|
Err string
|
|
}
|
|
|
|
func (e PathError) Error() string {
|
|
return "PathError: " + e.Err
|
|
}
|
|
|
|
func MakePathTypeError(path Path, index int) error {
|
|
return PathError{fmt.Sprintf("invalid path element type:%T at index:%d (%s)", path[index], index, FormatPath(path))}
|
|
}
|
|
|
|
func MakePathError(errStr string, path Path, index int) error {
|
|
return PathError{fmt.Sprintf("%s at index:%d (%s)", errStr, index, FormatPath(path))}
|
|
}
|
|
|
|
type SetTypeError struct {
|
|
Err string
|
|
}
|
|
|
|
func (e SetTypeError) Error() string {
|
|
return "SetTypeError: " + e.Err
|
|
}
|
|
|
|
func MakeSetTypeError(errStr string, path Path, index int) error {
|
|
return SetTypeError{fmt.Sprintf("%s at index:%d (%s)", errStr, index, FormatPath(path))}
|
|
}
|
|
|
|
type BudgetError struct {
|
|
Err string
|
|
}
|
|
|
|
func (e BudgetError) Error() string {
|
|
return "BudgetError: " + e.Err
|
|
}
|
|
|
|
func MakeBudgetError(errStr string, path Path, index int) error {
|
|
return BudgetError{fmt.Sprintf("%s at index:%d (%s)", errStr, index, FormatPath(path))}
|
|
}
|
|
|
|
var simplePathStrRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
|
|
|
|
func FormatPath(path Path) string {
|
|
if len(path) == 0 {
|
|
return "$"
|
|
}
|
|
var buf bytes.Buffer
|
|
buf.WriteByte('$')
|
|
for _, elem := range path {
|
|
switch elem := elem.(type) {
|
|
case string:
|
|
if simplePathStrRe.MatchString(elem) {
|
|
buf.WriteByte('.')
|
|
buf.WriteString(elem)
|
|
} else {
|
|
buf.WriteByte('[')
|
|
buf.WriteString(strconv.Quote(elem))
|
|
buf.WriteByte(']')
|
|
}
|
|
case int:
|
|
buf.WriteByte('[')
|
|
buf.WriteString(strconv.Itoa(elem))
|
|
buf.WriteByte(']')
|
|
default:
|
|
// a placeholder for a bad value
|
|
buf.WriteString(".*")
|
|
}
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
type pathWithPos struct {
|
|
Path Path
|
|
Index int
|
|
}
|
|
|
|
func (pp pathWithPos) isLast() bool {
|
|
return pp.Index == len(pp.Path)-1
|
|
}
|
|
|
|
func GetPath(data any, path []any) (any, error) {
|
|
return getPathInternal(data, pathWithPos{Path: path, Index: 0})
|
|
}
|
|
|
|
func getPathInternal(data any, pp pathWithPos) (any, error) {
|
|
if data == nil {
|
|
return nil, nil
|
|
}
|
|
if pp.Index >= len(pp.Path) {
|
|
return data, nil
|
|
}
|
|
pathElemAny := pp.Path[pp.Index]
|
|
switch pathElem := pathElemAny.(type) {
|
|
case string:
|
|
mapVal, ok := data.(map[string]any)
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
return getPathInternal(mapVal[pathElem], pathWithPos{Path: pp.Path, Index: pp.Index + 1})
|
|
case int:
|
|
if pathElem < 0 {
|
|
return nil, MakePathError("negative index", pp.Path, pp.Index)
|
|
}
|
|
arrVal, ok := data.([]any)
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
if pathElem >= len(arrVal) {
|
|
return nil, nil
|
|
}
|
|
return getPathInternal(arrVal[pathElem], pathWithPos{Path: pp.Path, Index: pp.Index + 1})
|
|
default:
|
|
return nil, MakePathTypeError(pp.Path, pp.Index)
|
|
}
|
|
}
|
|
|
|
type CombiningFunc func(curValue any, newValue any, pp pathWithPos, opts SetPathOpts) (any, error)
|
|
|
|
type SetPathOpts struct {
|
|
Budget int // Budget 0 is unlimited (to set a 0 value, use -1)
|
|
Force bool
|
|
Remove bool
|
|
CombineFn CombiningFunc
|
|
}
|
|
|
|
func SetPathNoErr(data any, path Path, value any, opts *SetPathOpts) any {
|
|
ret, _ := SetPath(data, path, value, opts)
|
|
return ret
|
|
}
|
|
|
|
func SetPath(data any, path Path, value any, opts *SetPathOpts) (any, error) {
|
|
if opts == nil {
|
|
opts = &SetPathOpts{}
|
|
}
|
|
if opts.Remove && opts.CombineFn != nil {
|
|
return nil, fmt.Errorf("SetPath: Remove and CombineFn are mutually exclusive")
|
|
}
|
|
if opts.Remove && value != nil {
|
|
return nil, fmt.Errorf("SetPath: Remove and value are mutually exclusive")
|
|
}
|
|
return setPathInternal(data, pathWithPos{Path: path, Index: 0}, value, *opts)
|
|
}
|
|
|
|
func checkAndModifyBudget(opts *SetPathOpts, pp pathWithPos, cost int) bool {
|
|
if opts.Budget == 0 {
|
|
return true
|
|
}
|
|
opts.Budget -= cost
|
|
if opts.Budget < 0 {
|
|
return false
|
|
}
|
|
if opts.Budget == 0 {
|
|
// 0 is weird since it means unlimited, so we set it to -1 to fail the next operation
|
|
opts.Budget = -1
|
|
}
|
|
return true
|
|
}
|
|
|
|
func CombineFn_ArrayAppend(data any, value any, pp pathWithPos, opts SetPathOpts) (any, error) {
|
|
if !checkAndModifyBudget(&opts, pp, 1) {
|
|
return nil, MakeBudgetError("trying to append to array", pp.Path, pp.Index)
|
|
}
|
|
if data == nil {
|
|
data = make([]any, 0)
|
|
}
|
|
arrVal, ok := data.([]any)
|
|
if !ok && !opts.Force {
|
|
return nil, MakeSetTypeError(fmt.Sprintf("expected array, but got %T", data), pp.Path, pp.Index)
|
|
}
|
|
if !ok {
|
|
arrVal = make([]any, 0)
|
|
}
|
|
arrVal = append(arrVal, value)
|
|
return arrVal, nil
|
|
}
|
|
|
|
func CombineFn_SetUnless(data any, value any, pp pathWithPos, opts SetPathOpts) (any, error) {
|
|
if data != nil {
|
|
return data, nil
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func CombineFn_Max(data any, value any, pp pathWithPos, opts SetPathOpts) (any, error) {
|
|
valueFloat, ok := value.(float64)
|
|
if !ok {
|
|
return nil, MakeSetTypeError(fmt.Sprintf("expected float64, but got %T", value), pp.Path, pp.Index)
|
|
}
|
|
if data == nil {
|
|
return value, nil
|
|
}
|
|
dataFloat, ok := data.(float64)
|
|
if !ok && !opts.Force {
|
|
return nil, MakeSetTypeError(fmt.Sprintf("expected float64, but got %T", data), pp.Path, pp.Index)
|
|
}
|
|
if !ok {
|
|
return value, nil
|
|
}
|
|
if dataFloat > valueFloat {
|
|
return data, nil
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func CombineFn_Min(data any, value any, pp pathWithPos, opts SetPathOpts) (any, error) {
|
|
valueFloat, ok := value.(float64)
|
|
if !ok {
|
|
return nil, MakeSetTypeError(fmt.Sprintf("expected float64, but got %T", value), pp.Path, pp.Index)
|
|
}
|
|
if data == nil {
|
|
return value, nil
|
|
}
|
|
dataFloat, ok := data.(float64)
|
|
if !ok && !opts.Force {
|
|
return nil, MakeSetTypeError(fmt.Sprintf("expected float64, but got %T", data), pp.Path, pp.Index)
|
|
}
|
|
if !ok {
|
|
return value, nil
|
|
}
|
|
if dataFloat < valueFloat {
|
|
return data, nil
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func CombineFn_Inc(data any, value any, pp pathWithPos, opts SetPathOpts) (any, error) {
|
|
valueFloat, ok := value.(float64)
|
|
if !ok {
|
|
return nil, MakeSetTypeError(fmt.Sprintf("expected float64, but got %T", value), pp.Path, pp.Index)
|
|
}
|
|
if data == nil {
|
|
return value, nil
|
|
}
|
|
dataFloat, ok := data.(float64)
|
|
if !ok && !opts.Force {
|
|
return nil, MakeSetTypeError(fmt.Sprintf("expected float64, but got %T", data), pp.Path, pp.Index)
|
|
}
|
|
if !ok {
|
|
return value, nil
|
|
}
|
|
return dataFloat + valueFloat, nil
|
|
}
|
|
|
|
// force will clobber existing values that don't conform to path
|
|
// so SetPath(5, ["a"], 6 true) would return {"a": 6}
|
|
func setPathInternal(data any, pp pathWithPos, value any, opts SetPathOpts) (any, error) {
|
|
if pp.Index >= len(pp.Path) {
|
|
if opts.CombineFn != nil {
|
|
return opts.CombineFn(data, value, pp, opts)
|
|
}
|
|
return value, nil
|
|
}
|
|
pathElemAny := pp.Path[pp.Index]
|
|
switch pathElem := pathElemAny.(type) {
|
|
case string:
|
|
if data == nil {
|
|
if opts.Remove {
|
|
return nil, nil
|
|
}
|
|
data = make(map[string]any)
|
|
}
|
|
mapVal, ok := data.(map[string]any)
|
|
if !ok && !opts.Force {
|
|
return nil, MakeSetTypeError(fmt.Sprintf("expected map, but got %T", data), pp.Path, pp.Index)
|
|
}
|
|
if !ok {
|
|
mapVal = make(map[string]any)
|
|
}
|
|
if opts.Remove && pp.isLast() {
|
|
delete(mapVal, pathElem)
|
|
if len(mapVal) == 0 {
|
|
return nil, nil
|
|
}
|
|
return mapVal, nil
|
|
}
|
|
if _, ok := mapVal[pathElem]; !ok {
|
|
if opts.Remove {
|
|
return mapVal, nil
|
|
}
|
|
if !checkAndModifyBudget(&opts, pp, 1) {
|
|
return nil, MakeBudgetError("trying to allocate map entry", pp.Path, pp.Index)
|
|
}
|
|
}
|
|
newVal, err := setPathInternal(mapVal[pathElem], pathWithPos{Path: pp.Path, Index: pp.Index + 1}, value, opts)
|
|
if opts.Remove && newVal == nil {
|
|
delete(mapVal, pathElem)
|
|
if len(mapVal) == 0 {
|
|
return nil, nil
|
|
}
|
|
return mapVal, nil
|
|
}
|
|
mapVal[pathElem] = newVal
|
|
return mapVal, err
|
|
case int:
|
|
if pathElem < 0 {
|
|
return nil, MakePathError("negative index", pp.Path, pp.Index)
|
|
}
|
|
if data == nil {
|
|
if opts.Remove {
|
|
return nil, nil
|
|
}
|
|
if !checkAndModifyBudget(&opts, pp, pathElem+1) {
|
|
return nil, MakeBudgetError(fmt.Sprintf("trying to allocate array with %d elements", pathElem+1), pp.Path, pp.Index)
|
|
}
|
|
data = make([]any, pathElem+1)
|
|
}
|
|
arrVal, ok := data.([]any)
|
|
if !ok && !opts.Force {
|
|
return nil, MakeSetTypeError(fmt.Sprintf("expected array, but got %T", data), pp.Path, pp.Index)
|
|
}
|
|
if !ok {
|
|
if opts.Remove {
|
|
return nil, nil
|
|
}
|
|
if !checkAndModifyBudget(&opts, pp, pathElem+1) {
|
|
return nil, MakeBudgetError(fmt.Sprintf("trying to allocate array with %d elements", pathElem+1), pp.Path, pp.Index)
|
|
}
|
|
arrVal = make([]any, pathElem+1)
|
|
}
|
|
if opts.Remove && pp.isLast() {
|
|
if pathElem == len(arrVal)-1 {
|
|
arrVal = arrVal[:pathElem]
|
|
if len(arrVal) == 0 {
|
|
return nil, nil
|
|
}
|
|
return arrVal, nil
|
|
}
|
|
arrVal[pathElem] = nil
|
|
return arrVal, nil
|
|
}
|
|
entriesToAdd := pathElem + 1 - len(arrVal)
|
|
if opts.Remove && entriesToAdd > 0 {
|
|
return nil, nil
|
|
}
|
|
if !checkAndModifyBudget(&opts, pp, entriesToAdd) {
|
|
return nil, MakeBudgetError(fmt.Sprintf("trying to add %d elements to array", entriesToAdd), pp.Path, pp.Index)
|
|
}
|
|
for len(arrVal) <= pathElem {
|
|
arrVal = append(arrVal, nil)
|
|
}
|
|
newVal, err := setPathInternal(arrVal[pathElem], pathWithPos{Path: pp.Path, Index: pp.Index + 1}, value, opts)
|
|
if opts.Remove && newVal == nil && pathElem == len(arrVal)-1 {
|
|
arrVal = arrVal[:pathElem]
|
|
if len(arrVal) == 0 {
|
|
return nil, nil
|
|
}
|
|
return arrVal, nil
|
|
}
|
|
arrVal[pathElem] = newVal
|
|
return arrVal, err
|
|
default:
|
|
return nil, PathError{fmt.Sprintf("invalid path element type %T", pathElem)}
|
|
}
|
|
}
|
|
|
|
func NormalizeNumbers(v any) any {
|
|
switch v := v.(type) {
|
|
case int:
|
|
return float64(v)
|
|
case float32:
|
|
return float64(v)
|
|
case int8:
|
|
return float64(v)
|
|
case int16:
|
|
return float64(v)
|
|
case int32:
|
|
return float64(v)
|
|
case int64:
|
|
return float64(v)
|
|
case uint:
|
|
return float64(v)
|
|
case uint8:
|
|
return float64(v)
|
|
case uint16:
|
|
return float64(v)
|
|
case uint32:
|
|
return float64(v)
|
|
case uint64:
|
|
return float64(v)
|
|
case []any:
|
|
for i, elem := range v {
|
|
v[i] = NormalizeNumbers(elem)
|
|
}
|
|
case map[string]any:
|
|
for k, elem := range v {
|
|
v[k] = NormalizeNumbers(elem)
|
|
}
|
|
}
|
|
return v
|
|
|
|
}
|
|
|
|
func DeepEqual(v1 any, v2 any) bool {
|
|
if v1 == nil && v2 == nil {
|
|
return true
|
|
}
|
|
if v1 == nil || v2 == nil {
|
|
return false
|
|
}
|
|
switch v1 := v1.(type) {
|
|
case bool:
|
|
v2, ok := v2.(bool)
|
|
return ok && v1 == v2
|
|
case float64:
|
|
v2, ok := v2.(float64)
|
|
return ok && v1 == v2
|
|
case string:
|
|
v2, ok := v2.(string)
|
|
return ok && v1 == v2
|
|
case []any:
|
|
v2, ok := v2.([]any)
|
|
if !ok || len(v1) != len(v2) {
|
|
return false
|
|
}
|
|
for i := range v1 {
|
|
if !DeepEqual(v1[i], v2[i]) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
case map[string]any:
|
|
v2, ok := v2.(map[string]any)
|
|
if !ok || len(v1) != len(v2) {
|
|
return false
|
|
}
|
|
for k, v := range v1 {
|
|
if !DeepEqual(v, v2[k]) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
default:
|
|
// invalid data type, so just return false
|
|
return false
|
|
}
|
|
}
|
|
|
|
func getCommandType(command Command) string {
|
|
typeVal, ok := command["type"]
|
|
if !ok {
|
|
return ""
|
|
}
|
|
typeStr, ok := typeVal.(string)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
return typeStr
|
|
}
|
|
|
|
func getCommandPath(command Command) []any {
|
|
pathVal, ok := command["path"]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
path, ok := pathVal.([]any)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return path
|
|
}
|
|
|
|
func ValidatePath(path any) error {
|
|
if path == nil {
|
|
// nil path is allowed (sets the root)
|
|
return nil
|
|
}
|
|
pathArr, ok := path.([]any)
|
|
if !ok {
|
|
return fmt.Errorf("path is not an array")
|
|
}
|
|
for idx, elem := range pathArr {
|
|
switch elem.(type) {
|
|
case string, int:
|
|
continue
|
|
default:
|
|
return fmt.Errorf("path element %d is not a string or int", idx)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ValidateAndMarshalCommand(command Command) ([]byte, error) {
|
|
cmdType := getCommandType(command)
|
|
if cmdType != SetCommandStr && cmdType != DelCommandStr && cmdType != AppendCommandStr {
|
|
return nil, fmt.Errorf("unknown ijson command type %q", cmdType)
|
|
}
|
|
path := getCommandPath(command)
|
|
err := ValidatePath(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
barr, err := json.Marshal(command)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error marshalling ijson command to json: %w", err)
|
|
}
|
|
return barr, nil
|
|
}
|
|
|
|
func ApplyCommand(data any, command Command, budget int) (any, error) {
|
|
commandType := getCommandType(command)
|
|
if commandType == "" {
|
|
return nil, fmt.Errorf("ApplyCommand: missing type field")
|
|
}
|
|
switch commandType {
|
|
case SetCommandStr:
|
|
path := getCommandPath(command)
|
|
return SetPath(data, path, command["data"], &SetPathOpts{Budget: budget})
|
|
case DelCommandStr:
|
|
path := getCommandPath(command)
|
|
return SetPath(data, path, nil, &SetPathOpts{Remove: true, Budget: budget})
|
|
case AppendCommandStr:
|
|
path := getCommandPath(command)
|
|
return SetPath(data, path, command["data"], &SetPathOpts{CombineFn: CombineFn_ArrayAppend, Budget: budget})
|
|
default:
|
|
return nil, fmt.Errorf("ApplyCommand: unknown command type %q", commandType)
|
|
}
|
|
}
|
|
|
|
func ApplyCommands(data any, commands []Command, budget int) (any, error) {
|
|
for _, command := range commands {
|
|
var err error
|
|
data, err = ApplyCommand(data, command, budget)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
func CompactIJson(fullData []byte, budget int) ([]byte, error) {
|
|
var newData any
|
|
for len(fullData) > 0 {
|
|
nlIdx := bytes.IndexByte(fullData, '\n')
|
|
var cmdData []byte
|
|
if nlIdx == -1 {
|
|
cmdData = fullData
|
|
fullData = nil
|
|
} else {
|
|
cmdData = fullData[:nlIdx]
|
|
fullData = fullData[nlIdx+1:]
|
|
}
|
|
var cmdMap Command
|
|
err := json.Unmarshal(cmdData, &cmdMap)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error unmarshalling ijson command: %w", err)
|
|
}
|
|
newData, err = ApplyCommand(newData, cmdMap, budget)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error applying ijson command: %w", err)
|
|
}
|
|
}
|
|
newRootCmd := MakeSetCommand(nil, newData)
|
|
return json.Marshal(newRootCmd)
|
|
}
|
|
|
|
// returns a list of commands
|
|
func ParseIJson(fullData []byte) ([]Command, error) {
|
|
var commands []Command
|
|
for len(fullData) > 0 {
|
|
nlIdx := bytes.IndexByte(fullData, '\n')
|
|
var cmdData []byte
|
|
if nlIdx == -1 {
|
|
cmdData = fullData
|
|
fullData = nil
|
|
} else {
|
|
cmdData = fullData[:nlIdx]
|
|
fullData = fullData[nlIdx+1:]
|
|
}
|
|
var cmdMap Command
|
|
err := json.Unmarshal(cmdData, &cmdMap)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error unmarshalling ijson command: %w", err)
|
|
}
|
|
commands = append(commands, cmdMap)
|
|
}
|
|
return commands, nil
|
|
}
|