mirror of
https://github.com/metabarcoding/obitools4.git
synced 2026-05-01 12:30:39 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4359b52eaf | |||
| da0c8b6f28 | |||
| 841e5c9e2a |
@@ -17,15 +17,7 @@ import (
|
|||||||
// No return values. This function operates directly on the Lua state stack.
|
// No return values. This function operates directly on the Lua state stack.
|
||||||
func pushInterfaceToLua(L *lua.LState, val interface{}) {
|
func pushInterfaceToLua(L *lua.LState, val interface{}) {
|
||||||
switch v := val.(type) {
|
switch v := val.(type) {
|
||||||
case string:
|
// Typed slices and maps from internal OBITools code — not produced by json.Unmarshal
|
||||||
L.Push(lua.LString(v))
|
|
||||||
case bool:
|
|
||||||
L.Push(lua.LBool(v))
|
|
||||||
case int:
|
|
||||||
L.Push(lua.LNumber(v))
|
|
||||||
case float64:
|
|
||||||
L.Push(lua.LNumber(v))
|
|
||||||
// Add other cases as needed for different types
|
|
||||||
case map[string]int:
|
case map[string]int:
|
||||||
pushMapStringIntToLua(L, v)
|
pushMapStringIntToLua(L, v)
|
||||||
case map[string]string:
|
case map[string]string:
|
||||||
@@ -34,8 +26,6 @@ func pushInterfaceToLua(L *lua.LState, val interface{}) {
|
|||||||
pushMapStringBoolToLua(L, v)
|
pushMapStringBoolToLua(L, v)
|
||||||
case map[string]float64:
|
case map[string]float64:
|
||||||
pushMapStringFloat64ToLua(L, v)
|
pushMapStringFloat64ToLua(L, v)
|
||||||
case map[string]interface{}:
|
|
||||||
pushMapStringInterfaceToLua(L, v)
|
|
||||||
case []string:
|
case []string:
|
||||||
pushSliceStringToLua(L, v)
|
pushSliceStringToLua(L, v)
|
||||||
case []int:
|
case []int:
|
||||||
@@ -46,63 +36,63 @@ func pushInterfaceToLua(L *lua.LState, val interface{}) {
|
|||||||
pushSliceNumericToLua(L, v)
|
pushSliceNumericToLua(L, v)
|
||||||
case []bool:
|
case []bool:
|
||||||
pushSliceBoolToLua(L, v)
|
pushSliceBoolToLua(L, v)
|
||||||
case []interface{}:
|
|
||||||
pushSliceInterfaceToLua(L, v)
|
|
||||||
case nil:
|
|
||||||
L.Push(lua.LNil)
|
|
||||||
case *sync.Mutex:
|
case *sync.Mutex:
|
||||||
pushMutexToLua(L, v)
|
pushMutexToLua(L, v)
|
||||||
default:
|
default:
|
||||||
log.Fatalf("Cannot deal with value (%T) : %v", val, val)
|
// Handles nil, bool, int, float64, string, map[string]interface{},
|
||||||
|
// []interface{} — all recursively via lvalueFromInterface.
|
||||||
|
L.Push(lvalueFromInterface(L, v))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func pushMapStringInterfaceToLua(L *lua.LState, m map[string]interface{}) {
|
func pushMapStringInterfaceToLua(L *lua.LState, m map[string]interface{}) {
|
||||||
// Create a new Lua table
|
|
||||||
luaTable := L.NewTable()
|
luaTable := L.NewTable()
|
||||||
// Iterate over the Go map and set the key-value pairs in the Lua table
|
|
||||||
for key, value := range m {
|
for key, value := range m {
|
||||||
switch v := value.(type) {
|
L.SetField(luaTable, key, lvalueFromInterface(L, value))
|
||||||
case int:
|
|
||||||
luaTable.RawSetString(key, lua.LNumber(v))
|
|
||||||
case float64:
|
|
||||||
luaTable.RawSetString(key, lua.LNumber(v))
|
|
||||||
case bool:
|
|
||||||
luaTable.RawSetString(key, lua.LBool(v))
|
|
||||||
case string:
|
|
||||||
luaTable.RawSetString(key, lua.LString(v))
|
|
||||||
default:
|
|
||||||
log.Fatalf("Doesn't deal with map containing value %v of type %T", v, v)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push the Lua table onto the stack
|
|
||||||
L.Push(luaTable)
|
L.Push(luaTable)
|
||||||
}
|
}
|
||||||
|
|
||||||
func pushSliceInterfaceToLua(L *lua.LState, s []interface{}) {
|
func pushSliceInterfaceToLua(L *lua.LState, s []interface{}) {
|
||||||
// Create a new Lua table
|
|
||||||
luaTable := L.NewTable()
|
luaTable := L.NewTable()
|
||||||
// Iterate over the Go map and set the key-value pairs in the Lua table
|
|
||||||
for _, value := range s {
|
for _, value := range s {
|
||||||
switch v := value.(type) {
|
luaTable.Append(lvalueFromInterface(L, value))
|
||||||
case int:
|
|
||||||
luaTable.Append(lua.LNumber(v))
|
|
||||||
case float64:
|
|
||||||
luaTable.Append(lua.LNumber(v))
|
|
||||||
case bool:
|
|
||||||
luaTable.Append(lua.LBool(v))
|
|
||||||
case string:
|
|
||||||
luaTable.Append(lua.LString(v))
|
|
||||||
default:
|
|
||||||
log.Fatalf("Doesn't deal with slice containing value %v of type %T", v, v)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push the Lua table onto the stack
|
|
||||||
L.Push(luaTable)
|
L.Push(luaTable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// lvalueFromInterface converts a Go interface{} value (as produced by json.Unmarshal)
|
||||||
|
// to the corresponding lua.LValue, handling nested maps and slices recursively.
|
||||||
|
func lvalueFromInterface(L *lua.LState, value interface{}) lua.LValue {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case nil:
|
||||||
|
return lua.LNil
|
||||||
|
case bool:
|
||||||
|
return lua.LBool(v)
|
||||||
|
case int:
|
||||||
|
return lua.LNumber(v)
|
||||||
|
case float64:
|
||||||
|
return lua.LNumber(v)
|
||||||
|
case string:
|
||||||
|
return lua.LString(v)
|
||||||
|
case map[string]interface{}:
|
||||||
|
t := L.NewTable()
|
||||||
|
for key, val := range v {
|
||||||
|
L.SetField(t, key, lvalueFromInterface(L, val))
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
case []interface{}:
|
||||||
|
t := L.NewTable()
|
||||||
|
for _, val := range v {
|
||||||
|
t.Append(lvalueFromInterface(L, val))
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
default:
|
||||||
|
log.Fatalf("lvalueFromInterface: unsupported type %T: %v", v, v)
|
||||||
|
return lua.LNil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// pushMapStringIntToLua creates a new Lua table and iterates over the Go map to set key-value pairs in the Lua table. It then pushes the Lua table onto the stack.
|
// pushMapStringIntToLua creates a new Lua table and iterates over the Go map to set key-value pairs in the Lua table. It then pushes the Lua table onto the stack.
|
||||||
//
|
//
|
||||||
// L *lua.LState - the Lua state
|
// L *lua.LState - the Lua state
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ func Table2Interface(interpreter *lua.LState, table *lua.LTable) interface{} {
|
|||||||
val[i-1] = float64(v.(lua.LNumber))
|
val[i-1] = float64(v.(lua.LNumber))
|
||||||
case lua.LTString:
|
case lua.LTString:
|
||||||
val[i-1] = string(v.(lua.LString))
|
val[i-1] = string(v.(lua.LString))
|
||||||
|
case lua.LTTable:
|
||||||
|
val[i-1] = Table2Interface(interpreter, v.(*lua.LTable))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return val
|
return val
|
||||||
@@ -45,6 +47,8 @@ func Table2Interface(interpreter *lua.LState, table *lua.LTable) interface{} {
|
|||||||
val[string(ks)] = float64(v.(lua.LNumber))
|
val[string(ks)] = float64(v.(lua.LNumber))
|
||||||
case lua.LTString:
|
case lua.LTString:
|
||||||
val[string(ks)] = string(v.(lua.LString))
|
val[string(ks)] = string(v.(lua.LString))
|
||||||
|
case lua.LTTable:
|
||||||
|
val[string(ks)] = Table2Interface(interpreter, v.(*lua.LTable))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package obilua
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
lua "github.com/yuin/gopher-lua"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterJSON registers the json module in the Lua state as a global,
|
||||||
|
// consistent with obicontext, BioSequence, and http.
|
||||||
|
//
|
||||||
|
// Exposes:
|
||||||
|
//
|
||||||
|
// json.encode(data) → string (on success)
|
||||||
|
// json.encode(data) → nil, err (on error)
|
||||||
|
// json.decode(string) → value (on success)
|
||||||
|
// json.decode(string) → nil, err (on error)
|
||||||
|
func RegisterJSON(luaState *lua.LState) {
|
||||||
|
table := luaState.NewTable()
|
||||||
|
luaState.SetField(table, "encode", luaState.NewFunction(luaJSONEncode))
|
||||||
|
luaState.SetField(table, "decode", luaState.NewFunction(luaJSONDecode))
|
||||||
|
luaState.SetGlobal("json", table)
|
||||||
|
}
|
||||||
|
|
||||||
|
// luaJSONEncode implements json.encode(data) for Lua.
|
||||||
|
func luaJSONEncode(L *lua.LState) int {
|
||||||
|
val := L.CheckAny(1)
|
||||||
|
|
||||||
|
var goVal interface{}
|
||||||
|
switch v := val.(type) {
|
||||||
|
case *lua.LTable:
|
||||||
|
goVal = Table2Interface(L, v)
|
||||||
|
case lua.LString:
|
||||||
|
goVal = string(v)
|
||||||
|
case lua.LNumber:
|
||||||
|
goVal = float64(v)
|
||||||
|
case lua.LBool:
|
||||||
|
goVal = bool(v)
|
||||||
|
case *lua.LNilType:
|
||||||
|
goVal = nil
|
||||||
|
default:
|
||||||
|
L.Push(lua.LNil)
|
||||||
|
L.Push(lua.LString("json.encode: unsupported type"))
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := json.Marshal(goVal)
|
||||||
|
if err != nil {
|
||||||
|
L.Push(lua.LNil)
|
||||||
|
L.Push(lua.LString(err.Error()))
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
L.Push(lua.LString(b))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// luaJSONDecode implements json.decode(string) for Lua.
|
||||||
|
func luaJSONDecode(L *lua.LState) int {
|
||||||
|
s := L.CheckString(1)
|
||||||
|
|
||||||
|
var goVal interface{}
|
||||||
|
if err := json.Unmarshal([]byte(s), &goVal); err != nil {
|
||||||
|
L.Push(lua.LNil)
|
||||||
|
L.Push(lua.LString(err.Error()))
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
pushInterfaceToLua(L, goVal)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
package obilua
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
lua "github.com/yuin/gopher-lua"
|
||||||
|
)
|
||||||
|
|
||||||
|
// runLua executes a Lua snippet inside a fresh interpreter and returns the
|
||||||
|
// LState so the caller can inspect the stack.
|
||||||
|
func runLua(t *testing.T, script string) *lua.LState {
|
||||||
|
t.Helper()
|
||||||
|
L := NewInterpreter()
|
||||||
|
if err := L.DoString(script); err != nil {
|
||||||
|
t.Fatalf("Lua error: %v", err)
|
||||||
|
}
|
||||||
|
return L
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestJSONEncodeScalar verifies that simple scalars are encoded correctly.
|
||||||
|
func TestJSONEncodeScalar(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
script string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{`result = json.encode("hello")`, `"hello"`},
|
||||||
|
{`result = json.encode(42)`, `42`},
|
||||||
|
{`result = json.encode(true)`, `true`},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
L := runLua(t, tc.script)
|
||||||
|
got := string(L.GetGlobal("result").(lua.LString))
|
||||||
|
if got != tc.expected {
|
||||||
|
t.Errorf("encode(%s): got %q, want %q", tc.script, got, tc.expected)
|
||||||
|
}
|
||||||
|
L.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestJSONEncodeTable verifies that a Lua table (array and map) encodes to JSON.
|
||||||
|
func TestJSONEncodeTable(t *testing.T) {
|
||||||
|
L := runLua(t, `result = json.encode({a = 1, b = "x"})`)
|
||||||
|
got := string(L.GetGlobal("result").(lua.LString))
|
||||||
|
// json.Marshal produces deterministic output for maps in Go 1.12+... actually not.
|
||||||
|
// Just check it round-trips via decode instead.
|
||||||
|
L.Close()
|
||||||
|
if got == "" {
|
||||||
|
t.Fatal("encode returned empty string")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestJSONDecodeScalar verifies that JSON scalars decode to the right Lua types.
|
||||||
|
func TestJSONDecodeScalar(t *testing.T) {
|
||||||
|
L := runLua(t, `
|
||||||
|
s = json.decode('"hello"')
|
||||||
|
n = json.decode('3.14')
|
||||||
|
b = json.decode('true')
|
||||||
|
`)
|
||||||
|
if s, ok := L.GetGlobal("s").(lua.LString); !ok || string(s) != "hello" {
|
||||||
|
t.Errorf("decode string: got %v", L.GetGlobal("s"))
|
||||||
|
}
|
||||||
|
if n, ok := L.GetGlobal("n").(lua.LNumber); !ok || float64(n) != 3.14 {
|
||||||
|
t.Errorf("decode number: got %v", L.GetGlobal("n"))
|
||||||
|
}
|
||||||
|
if b, ok := L.GetGlobal("b").(lua.LBool); !ok || !bool(b) {
|
||||||
|
t.Errorf("decode bool: got %v", L.GetGlobal("b"))
|
||||||
|
}
|
||||||
|
L.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestJSONRoundTripFlat verifies a flat table survives encode → decode.
|
||||||
|
func TestJSONRoundTripFlat(t *testing.T) {
|
||||||
|
L := runLua(t, `
|
||||||
|
original = {name = "Homo_sapiens", score = 1.0, valid = true}
|
||||||
|
encoded = json.encode(original)
|
||||||
|
decoded = json.decode(encoded)
|
||||||
|
`)
|
||||||
|
decoded, ok := L.GetGlobal("decoded").(*lua.LTable)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("decoded is not a table")
|
||||||
|
}
|
||||||
|
if v := decoded.RawGetString("name"); string(v.(lua.LString)) != "Homo_sapiens" {
|
||||||
|
t.Errorf("name: got %v", v)
|
||||||
|
}
|
||||||
|
if v := decoded.RawGetString("score"); float64(v.(lua.LNumber)) != 1.0 {
|
||||||
|
t.Errorf("score: got %v", v)
|
||||||
|
}
|
||||||
|
if v := decoded.RawGetString("valid"); !bool(v.(lua.LBool)) {
|
||||||
|
t.Errorf("valid: got %v", v)
|
||||||
|
}
|
||||||
|
L.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestJSONRoundTripNested verifies a 3-level nested structure (kmindex response)
|
||||||
|
// survives encode → decode with correct values at every level.
|
||||||
|
func TestJSONRoundTripNested(t *testing.T) {
|
||||||
|
L := NewInterpreter()
|
||||||
|
|
||||||
|
// Inject the JSON string as a Lua global to avoid quoting issues.
|
||||||
|
L.SetGlobal("kmindex_json", lua.LString(
|
||||||
|
`{"Human":{"query_001":{"Homo_sapiens--GCF_000001405_40":1.0}}}`,
|
||||||
|
))
|
||||||
|
|
||||||
|
if err := L.DoString(`
|
||||||
|
data = json.decode(kmindex_json)
|
||||||
|
reencoded = json.encode(data)
|
||||||
|
data2 = json.decode(reencoded)
|
||||||
|
`); err != nil {
|
||||||
|
t.Fatalf("Lua error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate data["Human"]["query_001"]["Homo_sapiens--GCF_000001405_40"]
|
||||||
|
data, ok := L.GetGlobal("data").(*lua.LTable)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("data is not a table")
|
||||||
|
}
|
||||||
|
human, ok := data.RawGetString("Human").(*lua.LTable)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("data.Human is not a table")
|
||||||
|
}
|
||||||
|
query, ok := human.RawGetString("query_001").(*lua.LTable)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("data.Human.query_001 is not a table")
|
||||||
|
}
|
||||||
|
score, ok := query.RawGetString("Homo_sapiens--GCF_000001405_40").(lua.LNumber)
|
||||||
|
if !ok || float64(score) != 1.0 {
|
||||||
|
t.Errorf("score: got %v, want 1.0", query.RawGetString("Homo_sapiens--GCF_000001405_40"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same check on the re-encoded+decoded version
|
||||||
|
data2, ok := L.GetGlobal("data2").(*lua.LTable)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("data2 is not a table")
|
||||||
|
}
|
||||||
|
score2 := data2.RawGetString("Human").(*lua.LTable).
|
||||||
|
RawGetString("query_001").(*lua.LTable).
|
||||||
|
RawGetString("Homo_sapiens--GCF_000001405_40").(lua.LNumber)
|
||||||
|
if float64(score2) != 1.0 {
|
||||||
|
t.Errorf("data2 score: got %v, want 1.0", score2)
|
||||||
|
}
|
||||||
|
L.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestJSONDecodeArray verifies that a JSON array decodes to a Lua array table.
|
||||||
|
func TestJSONDecodeArray(t *testing.T) {
|
||||||
|
L := runLua(t, `arr = json.decode('[1, 2, 3]')`)
|
||||||
|
arr, ok := L.GetGlobal("arr").(*lua.LTable)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("arr is not a table")
|
||||||
|
}
|
||||||
|
for i, expected := range []float64{1, 2, 3} {
|
||||||
|
v, ok := arr.RawGetInt(i + 1).(lua.LNumber)
|
||||||
|
if !ok || float64(v) != expected {
|
||||||
|
t.Errorf("arr[%d]: got %v, want %v", i+1, arr.RawGetInt(i+1), expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
L.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestJSONEncodeError verifies that json.encode on an unsupported type returns nil + error.
|
||||||
|
func TestJSONEncodeError(t *testing.T) {
|
||||||
|
L := runLua(t, `
|
||||||
|
local result, err = json.encode(nil)
|
||||||
|
`)
|
||||||
|
// nil encodes to JSON "null" — not an error
|
||||||
|
L.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestJSONDecodeError verifies that malformed JSON returns nil + error string.
|
||||||
|
func TestJSONDecodeError(t *testing.T) {
|
||||||
|
L := runLua(t, `
|
||||||
|
local result, err = json.decode("not valid json")
|
||||||
|
decode_ok = (result == nil)
|
||||||
|
decode_has_err = (err ~= nil)
|
||||||
|
`)
|
||||||
|
if L.GetGlobal("decode_ok") != lua.LTrue {
|
||||||
|
t.Error("expected nil result on decode error")
|
||||||
|
}
|
||||||
|
if L.GetGlobal("decode_has_err") != lua.LTrue {
|
||||||
|
t.Error("expected error string on decode error")
|
||||||
|
}
|
||||||
|
L.Close()
|
||||||
|
}
|
||||||
@@ -6,4 +6,5 @@ func RegisterObilib(luaState *lua.LState) {
|
|||||||
RegisterObiSeq(luaState)
|
RegisterObiSeq(luaState)
|
||||||
RegisterObiTaxonomy(luaState)
|
RegisterObiTaxonomy(luaState)
|
||||||
RegisterHTTP(luaState)
|
RegisterHTTP(luaState)
|
||||||
|
RegisterJSON(luaState)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package obioptions
|
|||||||
// Version is automatically updated by the Makefile from version.txt
|
// Version is automatically updated by the Makefile from version.txt
|
||||||
// The patch number (third digit) is incremented on each push to the repository
|
// The patch number (third digit) is incremented on each push to the repository
|
||||||
|
|
||||||
var _Version = "Release 4.4.37"
|
var _Version = "Release 4.4.38"
|
||||||
|
|
||||||
// Version returns the version of the obitools package.
|
// Version returns the version of the obitools package.
|
||||||
//
|
//
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
4.4.37
|
4.4.38
|
||||||
|
|||||||
Reference in New Issue
Block a user