Skip to content

embed gopher-json package #398

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd_scripting.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import (
"strings"
"sync"

luajson "github.com/alicebob/gopher-json"
lua "github.com/yuin/gopher-lua"
"github.com/yuin/gopher-lua/parse"

luajson "github.com/alicebob/miniredis/v2/gopher-json"
"github.com/alicebob/miniredis/v2/server"
)

Expand Down
5 changes: 1 addition & 4 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
module github.com/alicebob/miniredis/v2

require (
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302
github.com/yuin/gopher-lua v1.1.1
)
require github.com/yuin/gopher-lua v1.1.1

go 1.17
4 changes: 0 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE=
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
Expand Down
24 changes: 24 additions & 0 deletions gopher-json/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.

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 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.

For more information, please refer to <http://unlicense.org/>
1 change: 1 addition & 0 deletions gopher-json/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Copied from https://github.com/layeh/gopher-json and https://github.com/alicebob/gopher-json
189 changes: 189 additions & 0 deletions gopher-json/json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package json

import (
"encoding/json"
"errors"

"github.com/yuin/gopher-lua"
)

// Preload adds json to the given Lua state's package.preload table. After it
// has been preloaded, it can be loaded using require:
//
// local json = require("json")
func Preload(L *lua.LState) {
L.PreloadModule("json", Loader)
}

// Loader is the module loader function.
func Loader(L *lua.LState) int {
t := L.NewTable()
L.SetFuncs(t, api)
L.Push(t)
return 1
}

var api = map[string]lua.LGFunction{
"decode": apiDecode,
"encode": apiEncode,
}

func apiDecode(L *lua.LState) int {
if L.GetTop() != 1 {
L.Error(lua.LString("bad argument #1 to decode"), 1)
return 0
}
str := L.CheckString(1)

value, err := Decode(L, []byte(str))
if err != nil {
L.Push(lua.LNil)
L.Push(lua.LString(err.Error()))
return 2
}
L.Push(value)
return 1
}

func apiEncode(L *lua.LState) int {
if L.GetTop() != 1 {
L.Error(lua.LString("bad argument #1 to encode"), 1)
return 0
}
value := L.CheckAny(1)

data, err := Encode(value)
if err != nil {
L.Push(lua.LNil)
L.Push(lua.LString(err.Error()))
return 2
}
L.Push(lua.LString(string(data)))
return 1
}

var (
errNested = errors.New("cannot encode recursively nested tables to JSON")
errSparseArray = errors.New("cannot encode sparse array")
errInvalidKeys = errors.New("cannot encode mixed or invalid key types")
)

type invalidTypeError lua.LValueType

func (i invalidTypeError) Error() string {
return `cannot encode ` + lua.LValueType(i).String() + ` to JSON`
}

// Encode returns the JSON encoding of value.
func Encode(value lua.LValue) ([]byte, error) {
return json.Marshal(jsonValue{
LValue: value,
visited: make(map[*lua.LTable]bool),
})
}

type jsonValue struct {
lua.LValue
visited map[*lua.LTable]bool
}

func (j jsonValue) MarshalJSON() (data []byte, err error) {
switch converted := j.LValue.(type) {
case lua.LBool:
data, err = json.Marshal(bool(converted))
case lua.LNumber:
data, err = json.Marshal(float64(converted))
case *lua.LNilType:
data = []byte(`null`)
case lua.LString:
data, err = json.Marshal(string(converted))
case *lua.LTable:
if j.visited[converted] {
return nil, errNested
}
j.visited[converted] = true

key, value := converted.Next(lua.LNil)

switch key.Type() {
case lua.LTNil: // empty table
data = []byte(`[]`)
case lua.LTNumber:
arr := make([]jsonValue, 0, converted.Len())
expectedKey := lua.LNumber(1)
for key != lua.LNil {
if key.Type() != lua.LTNumber {
err = errInvalidKeys
return
}
if expectedKey != key {
err = errSparseArray
return
}
arr = append(arr, jsonValue{value, j.visited})
expectedKey++
key, value = converted.Next(key)
}
data, err = json.Marshal(arr)
case lua.LTString:
obj := make(map[string]jsonValue)
for key != lua.LNil {
if key.Type() != lua.LTString {
err = errInvalidKeys
return
}
obj[key.String()] = jsonValue{value, j.visited}
key, value = converted.Next(key)
}
data, err = json.Marshal(obj)
default:
err = errInvalidKeys
}
default:
err = invalidTypeError(j.LValue.Type())
}
return
}

// Decode converts the JSON encoded data to Lua values.
func Decode(L *lua.LState, data []byte) (lua.LValue, error) {
var value interface{}
err := json.Unmarshal(data, &value)
if err != nil {
return nil, err
}
return DecodeValue(L, value), nil
}

// DecodeValue converts the value to a Lua value.
//
// This function only converts values that the encoding/json package decodes to.
// All other values will return lua.LNil.
func DecodeValue(L *lua.LState, value interface{}) lua.LValue {
switch converted := value.(type) {
case bool:
return lua.LBool(converted)
case float64:
return lua.LNumber(converted)
case string:
return lua.LString(converted)
case json.Number:
return lua.LString(converted)
case []interface{}:
arr := L.CreateTable(len(converted), 0)
for _, item := range converted {
arr.Append(DecodeValue(L, item))
}
return arr
case map[string]interface{}:
tbl := L.CreateTable(0, len(converted))
for key, item := range converted {
tbl.RawSetH(lua.LString(key), DecodeValue(L, item))
}
return tbl
case nil:
return lua.LNil
}

return lua.LNil
}
110 changes: 110 additions & 0 deletions gopher-json/json_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package json

import (
"encoding/json"
"testing"

lua "github.com/yuin/gopher-lua"
)

func TestSimple(t *testing.T) {
const str = `
local json = require("json")
assert(type(json) == "table")
assert(type(json.decode) == "function")
assert(type(json.encode) == "function")

assert(json.encode(true) == "true")
assert(json.encode(1) == "1")
assert(json.encode(-10) == "-10")
assert(json.encode(nil) == "null")
assert(json.encode({}) == "[]")
assert(json.encode({1, 2, 3}) == "[1,2,3]")

local _, err = json.encode({1, 2, [10] = 3})
assert(string.find(err, "sparse array"))

local _, err = json.encode({1, 2, 3, name = "Tim"})
assert(string.find(err, "mixed or invalid key types"))

local _, err = json.encode({name = "Tim", [false] = 123})
assert(string.find(err, "mixed or invalid key types"))

local obj = {"a",1,"b",2,"c",3}
local jsonStr = json.encode(obj)
local jsonObj = json.decode(jsonStr)
for i = 1, #obj do
assert(obj[i] == jsonObj[i])
end

local obj = {name="Tim",number=12345}
local jsonStr = json.encode(obj)
local jsonObj = json.decode(jsonStr)
assert(obj.name == jsonObj.name)
assert(obj.number == jsonObj.number)

assert(json.decode("null") == nil)

local status, err = pcall(function() json.decode() end)

assert(err == "<string>:38: bad argument #1 to decode", err)
local status, err = pcall(function() json.decode(1,2) end)
assert(err == "<string>:40: bad argument #1 to decode", err)
local status, err = pcall(function() json.encode() end)
assert(err == "<string>:42: bad argument #1 to encode", err)
local status, err = pcall(function() json.encode(1,2) end)
assert(err == "<string>:44: bad argument #1 to encode", err)

assert(json.decode(json.encode({person={name = "tim",}})).person.name == "tim")

local obj = {
abc = 123,
def = nil,
}
local obj2 = {
obj = obj,
}
obj.obj2 = obj2
assert(json.encode(obj) == nil)

local a = {}
for i=1, 5 do
a[i] = i
end
assert(json.encode(a) == "[1,2,3,4,5]")
`
s := lua.NewState()
defer s.Close()

t.Skip("broken")
Preload(s)
if err := s.DoString(str); err != nil {
t.Error(err)
}
}

func TestCustomRequire(t *testing.T) {
const str = `
local j = require("JSON")
assert(type(j) == "table")
assert(type(j.decode) == "function")
assert(type(j.encode) == "function")
`
s := lua.NewState()
defer s.Close()

s.PreloadModule("JSON", Loader)
if err := s.DoString(str); err != nil {
t.Error(err)
}
}

func TestDecodeValue_jsonNumber(t *testing.T) {
s := lua.NewState()
defer s.Close()

v := DecodeValue(s, json.Number("124.11"))
if v.Type() != lua.LTString || v.String() != "124.11" {
t.Fatalf("expecting LString, got %T", v)
}
}
Loading