Merge pull request #110 from metabarcoding/push-tstsmnkomnoo

Push tstsmnkomnoo
This commit is contained in:
coissac
2026-04-13 17:57:38 +02:00
committed by GitHub
7 changed files with 300 additions and 50 deletions
+37 -47
View File
@@ -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,61 +36,61 @@ 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)
} }
L.Push(luaTable)
} }
// Push the Lua table onto the stack // lvalueFromInterface converts a Go interface{} value (as produced by json.Unmarshal)
L.Push(luaTable) // 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.
+4
View File
@@ -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))
} }
} }
}) })
+71
View File
@@ -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
}
+184
View File
@@ -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()
}
+1
View File
@@ -6,4 +6,5 @@ func RegisterObilib(luaState *lua.LState) {
RegisterObiSeq(luaState) RegisterObiSeq(luaState)
RegisterObiTaxonomy(luaState) RegisterObiTaxonomy(luaState)
RegisterHTTP(luaState) RegisterHTTP(luaState)
RegisterJSON(luaState)
} }
+1 -1
View File
@@ -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
View File
@@ -1 +1 @@
4.4.37 4.4.38