Compare commits

...

22 Commits

Author SHA1 Message Date
Eric Coissac 4359b52eaf Release 4.4.38 2026-04-13 17:57:00 +02:00
Eric Coissac da0c8b6f28 ♻️ refactor lua_push_interface and add json module
Refactor pushInterfaceToLua to delegate unsupported types (nil, bool/int/float/string/map/slice) recursively via new lvalueFromInterface helper. Simplify typed slice and map handlers, remove explicit nil case (now handled by lvalueFromInterface), eliminate redundant type switches in pushMapStringIntToLua and similar functions. Add new luajson.go with RegisterJSON, lua.JSONEncode/Decode bindings using lvalueFromInterface and Table2 Interface for bidirectional round-trips. Include comprehensive tests covering scalars, nested structures (e.g., kmindex response), arrays and error cases.
2026-04-13 17:56:58 +02:00
coissac 841e5c9e2a Merge pull request #109 from metabarcoding/push-okvqknqnvmnl
Push okvqknqnvmnl
2026-04-13 17:19:41 +02:00
Eric Coissac e298daeef9 [v4.5] Bugfix for 3-base sequence handling and utility refactoring
- **Bug fix**: Corrected logic in 4-mer calculation to properly handle sequences of length exactly three. Previously, such cases could produce invalid or unexpected results due to an incomplete guard condition (`length < 0`) which failed for ` length == 3` (where computed step size was zero). The fix ensures all sequences shorter than four bases are safely excluded.

- **Refactor**: Introduced a new internal utility function (`inverser_chaine`) to centralize string reversal logic, improving code maintainability and test coverage without affecting user-facing behavior.
2026-04-13 17:18:53 +02:00
Eric Coissac d9e6f67a6e chore: bump version to 4.4.36
Update package and file versions from v4.4.35 to 4.4.36.
2026-04-13 17:18:48 +02:00
Eric Coissac f036c7fa96 ⬆️ version bump to v4.5
- Update `version.txt` from "v3" to v4.5
- Bump Go constant `_Version = 'Release 4.x.y'` accordingly
2026-04-13 17:18:34 +02:00
Eric Coissac e33665e716 Refactor: Extract utility function for string reversal
- Introduce `inverser_chaine()` helper to centralize logic
 - Update tests and documentation accordingly
2026-04-13 17:18:34 +02:00
Eric Coissac c955a614ca chore: bump version to 4.4.35
Update obioptions/version.go and version.txt to reflect release 4.4.35.
2026-04-13 17:18:34 +02:00
Eric Coissac f19065261e We kept 2026-04-13 17:18:34 +02:00
coissac 3e349e92e1 Merge pull request #104 from theo-krueger/master
Bugfix: result of 0 4mers not caught if sequence length == 3
2026-04-13 16:39:08 +02:00
coissac a4ce24a418 Merge pull request #108 from metabarcoding/push-qlxnulxwokxo
Push qlxnulxwokxo
2026-04-13 16:27:43 +02:00
Eric Coissac 960ad1531d [4.4.34] HTTP client thread-safety and CI infrastructure updates
- Improved concurrency safety by replacing the global HTTP client with a thread-safe, lazy-initialized instance using `sync.Once`. The new implementation enables connection pooling (`MaxIdleConnsPerHost`, connections per host) and dynamically configures pool size based on `obidefault.ParallelWorkers()`, ensuring robust behavior in multi-threaded Lua environments.
- Updated GitHub Actions workflows to the latest stable versions of `actions/setup-go` and ` actions/checkout`, improving build reliability.
- Removed outdated Go dependency checksums for buger/jsonparser v1.1.x to keep the build clean and consistent.
2026-04-13 16:27:14 +02:00
Eric Coissac 137f49d1d1 🔧 refactor(http): use thread-safe lazy-initialized HTTP client with connection pooling
- Replace global _httpClient variable by a sync.Once-based lazy initialization
- Add getHTTPClient() function to safely initialize client with connection pooling settings (MaxIdleConnsPerHost, Max Con ns/Conn per host)
- Set connection pool size based on obidefault.ParallelWorkers()

This ensures safe concurrent access and better resource management in multi-threaded Lua environments.
2026-04-13 16:27:09 +02:00
Eric Coissac 083a92e13d ⬆️ update GitHub Actions to latest versions
- Upgrade actions/setup-go from v2/v4 (depending on workflow) to latest stable version
- Update all actions/checkout from v3/v4 (depending on workflow) to latest stable version
- Clean up outdated go.sum entries for buger/jsonparser v1.1.x
2026-04-13 14:41:47 +02:00
coissac 67683435e8 Merge pull request #107 from metabarcoding/push-oyzynqqnturm
Push oyzynqqnturm
2026-04-13 14:29:44 +02:00
Eric Coissac f32b29db4f Release 4.4.33 2026-04-13 14:29:18 +02:00
Eric Coissac 10f49fe64b 📝 Clarify RegisterHTTP global registration intent
//
// Registers the http module in Lua state as a global,
// aligning with obicontext and BioSequence conventions.
The change ensures consistent module exposure across Lua environments.
2026-04-13 14:29:16 +02:00
coissac d257917748 Merge pull request #106 from metabarcoding/push-qoqotlnktvls
Push qoqotlnktvls
2026-04-13 14:08:42 +02:00
Eric Coissac fec078c04c Release 4.4.32 2026-04-13 14:08:16 +02:00
Eric Coissac a92393dd51 ⬆️ update go.mod dependencies and improve error messages
- Bump github.com/buger/jsonparser from v1.1.1 to v1.2
- Add error details in log.Fatalf calls for better debugging
2026-04-13 14:08:13 +02:00
coissac 7e76698490 Merge pull request #105 from metabarcoding/push-pnqoquxmpqpq
Push pnqoquxmpqpq
2026-04-13 13:36:13 +02:00
theo-krueger c7816973a6 Bugfix: result of 0 4mers not caught if sequence length == 3
In the 4mer calculation:
length := slength - 3

- for sequences with <4 bases, length is <=0

The check to stop did only catch <0, so sequences lengths 2 or less, leaving sequence lengths of 3 unguarded
if length < 0 {
		return nil
	}
2026-04-10 14:05:30 +02:00
14 changed files with 422 additions and 70 deletions
+2 -2
View File
@@ -10,10 +10,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v2 uses: actions/setup-go@v5
with: with:
go-version: '1.23' go-version: '1.23'
- name: Checkout obitools4 project - name: Checkout obitools4 project
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Run tests - name: Run tests
run: make githubtests run: make githubtests
+3 -3
View File
@@ -18,7 +18,7 @@ jobs:
with: with:
go-version: "1.26" go-version: "1.26"
- name: Checkout obitools4 project - name: Checkout obitools4 project
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Run tests - name: Run tests
run: make githubtests run: make githubtests
@@ -49,7 +49,7 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
@@ -107,7 +107,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
+1 -1
View File
@@ -6,7 +6,7 @@ require (
github.com/DavidGamba/go-getoptions v0.33.0 github.com/DavidGamba/go-getoptions v0.33.0
github.com/PaesslerAG/gval v1.2.4 github.com/PaesslerAG/gval v1.2.4
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df
github.com/buger/jsonparser v1.1.1 github.com/buger/jsonparser v1.1.2
github.com/chen3feng/stl4go v0.1.1 github.com/chen3feng/stl4go v0.1.1
github.com/dlclark/regexp2 v1.11.5 github.com/dlclark/regexp2 v1.11.5
github.com/goccy/go-json v0.10.6 github.com/goccy/go-json v0.10.6
+2 -2
View File
@@ -6,8 +6,8 @@ github.com/PaesslerAG/jsonpath v0.1.0 h1:gADYeifvlqK3R3i2cR5B4DGgxLXIPb3TRTH1mGi
github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8= github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8=
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df h1:GSoSVRLoBaFpOOds6QyY1L8AX7uoY+Ln3BHc22W40X0= github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df h1:GSoSVRLoBaFpOOds6QyY1L8AX7uoY+Ln3BHc22W40X0=
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df/go.mod h1:hiVxq5OP2bUGBRNS3Z/bt/reCLFNbdcST6gISi1fiOM= github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df/go.mod h1:hiVxq5OP2bUGBRNS3Z/bt/reCLFNbdcST6gISi1fiOM=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/chen3feng/stl4go v0.1.1 h1:0L1+mDw7pomftKDruM23f1mA7miavOj6C6MZeadzN2Q= github.com/chen3feng/stl4go v0.1.1 h1:0L1+mDw7pomftKDruM23f1mA7miavOj6C6MZeadzN2Q=
github.com/chen3feng/stl4go v0.1.1/go.mod h1:5ml3psLgETJjRJnMbPE+JiHLrCpt+Ajc2weeTECXzWU= github.com/chen3feng/stl4go v0.1.1/go.mod h1:5ml3psLgETJjRJnMbPE+JiHLrCpt+Ajc2weeTECXzWU=
github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM= github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=
+1 -1
View File
@@ -47,7 +47,7 @@ func Encode4mer(seq *obiseq.BioSequence, buffer *[]byte) []byte {
length := slength - 3 length := slength - 3
rawseq := seq.Sequence() rawseq := seq.Sequence()
if length < 0 { if length <= 0 {
return nil return nil
} }
+90 -7
View File
@@ -91,7 +91,7 @@ func LuaWorker(proto *lua.FunctionProto) obiseq.SeqWorker {
err := interpreter.PCall(0, lua.MultRet, nil) err := interpreter.PCall(0, lua.MultRet, nil)
if err != nil { if err != nil {
log.Fatalf("Error in executing the lua script") log.Fatalf("Error in executing the lua script: %v", err)
} }
result := interpreter.GetGlobal("worker") result := interpreter.GetGlobal("worker")
@@ -141,6 +141,69 @@ func LuaWorker(proto *lua.FunctionProto) obiseq.SeqWorker {
return nil return nil
} }
// LuaSliceWorker creates a SeqSliceWorker that calls the Lua function
// named "slice_worker". Unlike LuaWorker, the entire batch (BioSequenceSlice)
// is passed to the Lua function at once, enabling batch-level processing
// (e.g. a single HTTP request per batch instead of one per sequence).
//
// The Lua function signature:
//
// function slice_worker(slice) -- receives a BioSequenceSlice
// -- process the batch
// return slice -- returns a BioSequenceSlice (or nil)
// end
func LuaSliceWorker(proto *lua.FunctionProto) obiseq.SeqSliceWorker {
interpreter := NewInterpreter()
lfunc := interpreter.NewFunctionFromProto(proto)
interpreter.Push(lfunc)
err := interpreter.PCall(0, lua.MultRet, nil)
if err != nil {
log.Fatalf("Error in executing the lua script: %v", err)
}
result := interpreter.GetGlobal("slice_worker")
if lua_worker, ok := result.(*lua.LFunction); ok {
f := func(slice obiseq.BioSequenceSlice) (obiseq.BioSequenceSlice, error) {
if err := interpreter.CallByParam(lua.P{
Fn: lua_worker,
NRet: 1,
Protect: true,
}, obiseqslice2Lua(interpreter, &slice)); err != nil {
log.Fatal(err)
}
lreponse := interpreter.Get(-1)
defer interpreter.Pop(1)
if reponse, ok := lreponse.(*lua.LUserData); ok {
s := reponse.Value
switch val := s.(type) {
case *obiseq.BioSequenceSlice:
return *val, nil
case *obiseq.BioSequence:
return obiseq.BioSequenceSlice{val}, nil
default:
r := reflect.TypeOf(val)
return nil, fmt.Errorf("slice_worker function doesn't return the correct type %s", r)
}
}
if _, ok = lreponse.(*lua.LNilType); ok {
return nil, nil
}
return nil, fmt.Errorf("slice_worker function doesn't return the correct type %T", lreponse)
}
return f
}
log.Fatalf("The slice_worker object is not a function")
return nil
}
// LuaProcessor processes a Lua script on a sequence iterator and returns a new iterator. // LuaProcessor processes a Lua script on a sequence iterator and returns a new iterator.
// //
// Parameters: // Parameters:
@@ -173,7 +236,7 @@ func LuaProcessor(iterator obiiter.IBioSequence, name, program string, breakOnEr
err = interpreter.PCall(0, lua.MultRet, nil) err = interpreter.PCall(0, lua.MultRet, nil)
if err != nil { if err != nil {
log.Fatalf("Error in executing the lua script") log.Fatalf("Error in executing the lua script: %v", err)
} }
result := interpreter.GetGlobal("begin") result := interpreter.GetGlobal("begin")
@@ -198,7 +261,7 @@ func LuaProcessor(iterator obiiter.IBioSequence, name, program string, breakOnEr
err = interpreter.PCall(0, lua.MultRet, nil) err = interpreter.PCall(0, lua.MultRet, nil)
if err != nil { if err != nil {
log.Fatalf("Error in executing the lua script") log.Fatalf("Error in executing the lua script: %v", err)
} }
result := interpreter.GetGlobal("finish") result := interpreter.GetGlobal("finish")
@@ -216,11 +279,27 @@ func LuaProcessor(iterator obiiter.IBioSequence, name, program string, breakOnEr
}() }()
ff := func(iterator obiiter.IBioSequence) { // Detect whether the script defines slice_worker (batch-level) or worker (per-sequence).
w := LuaWorker(proto) hasSliceWorker := func() bool {
sw := obiseq.SeqToSliceWorker(w, false) interpreter := NewInterpreter()
lfunc := interpreter.NewFunctionFromProto(proto)
interpreter.Push(lfunc)
if err := interpreter.PCall(0, lua.MultRet, nil); err != nil {
return false
}
result := interpreter.GetGlobal("slice_worker")
interpreter.Close()
_, ok := result.(*lua.LFunction)
return ok
}()
// iterator = iterator.SortBatches() ff := func(iterator obiiter.IBioSequence) {
var sw obiseq.SeqSliceWorker
if hasSliceWorker {
sw = LuaSliceWorker(proto)
} else {
sw = obiseq.SeqToSliceWorker(LuaWorker(proto), false)
}
for iterator.Next() { for iterator.Next() {
seqs := iterator.Get() seqs := iterator.Get()
@@ -235,6 +314,10 @@ func LuaProcessor(iterator obiiter.IBioSequence, name, program string, breakOnEr
} }
} }
if ns == nil {
ns = obiseq.BioSequenceSlice{}
}
newIter.Push(obiiter.MakeBioSequenceBatch(seqs.Source(), seqs.Order(), ns)) newIter.Push(obiiter.MakeBioSequenceBatch(seqs.Source(), seqs.Order(), ns))
} }
+38 -48
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,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
+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))
} }
} }
}) })
+23 -4
View File
@@ -4,18 +4,37 @@ import (
"io" "io"
"net/http" "net/http"
"strings" "strings"
"sync"
"time" "time"
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault"
lua "github.com/yuin/gopher-lua" lua "github.com/yuin/gopher-lua"
) )
const httpClientTimeout = 30 * time.Second const httpClientTimeout = 30 * time.Second
var _httpClient = &http.Client{ var (
Timeout: httpClientTimeout, _httpClient *http.Client
_httpClientOnce sync.Once
)
func getHTTPClient() *http.Client {
_httpClientOnce.Do(func() {
conns := 2 * obidefault.ParallelWorkers()
_httpClient = &http.Client{
Transport: &http.Transport{
MaxIdleConnsPerHost: conns,
MaxConnsPerHost: conns,
IdleConnTimeout: 90 * time.Second,
},
Timeout: httpClientTimeout,
}
})
return _httpClient
} }
// RegisterHTTP registers the http module in the Lua state. // RegisterHTTP registers the http module in the Lua state as a global,
// consistent with obicontext and BioSequence.
// //
// Exposes: // Exposes:
// //
@@ -45,7 +64,7 @@ func luaHTTPPost(L *lua.LState) int {
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
resp, err := _httpClient.Do(req) resp, err := getHTTPClient().Do(req)
if err != nil { if err != nil {
L.Push(lua.LNil) L.Push(lua.LNil)
L.Push(lua.LString(err.Error())) L.Push(lua.LString(err.Error()))
+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.31" 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.31 4.4.38