├── .github └── FUNDING.yml ├── fixtures ├── empty.lua ├── update.lua ├── any.lua ├── echo.lua ├── hash.lua ├── sleep.lua ├── sum.lua ├── batch.lua ├── demo.lua ├── error.lua ├── error1.lua ├── fib.lua ├── json.lua ├── print.lua ├── joinmap.lua ├── enrich.lua ├── join.lua ├── module.lua └── array.lua ├── go.mod ├── gen ├── binary.go ├── unary.go ├── binary_test.go └── unary_in_test.go ├── LICENSE ├── go.sum ├── z_unary.go ├── json ├── json_test.go └── json.go ├── z_binary.go ├── convert_test.go ├── README.md ├── convert.go ├── value_test.go ├── module.go ├── z_unary_test.go ├── module_test.go ├── script.go ├── script_test.go ├── z_binary_test.go └── value.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [kelindar] 2 | -------------------------------------------------------------------------------- /fixtures/empty.lua: -------------------------------------------------------------------------------- 1 | function main() 2 | local x = 1 3 | end -------------------------------------------------------------------------------- /fixtures/update.lua: -------------------------------------------------------------------------------- 1 | function main(input) 2 | input.Name = "Updated" 3 | return input.Name 4 | end -------------------------------------------------------------------------------- /fixtures/any.lua: -------------------------------------------------------------------------------- 1 | local api = require("test") 2 | function main(input) 3 | return api.toNumbers(input) 4 | end -------------------------------------------------------------------------------- /fixtures/echo.lua: -------------------------------------------------------------------------------- 1 | local api = require("test") 2 | 3 | function main(input) 4 | return api.echo(input) 5 | end -------------------------------------------------------------------------------- /fixtures/hash.lua: -------------------------------------------------------------------------------- 1 | local api = require("test") 2 | 3 | function main(input) 4 | return api.hash(input) 5 | end -------------------------------------------------------------------------------- /fixtures/sleep.lua: -------------------------------------------------------------------------------- 1 | local api = require("test") 2 | 3 | function main(input) 4 | return api.sleep(10) 5 | end -------------------------------------------------------------------------------- /fixtures/sum.lua: -------------------------------------------------------------------------------- 1 | local api = require("test") 2 | 3 | function main(a, b) 4 | return api.sum(a, b) 5 | end -------------------------------------------------------------------------------- /fixtures/batch.lua: -------------------------------------------------------------------------------- 1 | local api = require("test") 2 | 3 | function main(input) 4 | return api.batch(input) 5 | end -------------------------------------------------------------------------------- /fixtures/demo.lua: -------------------------------------------------------------------------------- 1 | local demo = require("demo_mod") 2 | 3 | function main(input) 4 | return demo.Mult(5, 5) 5 | end -------------------------------------------------------------------------------- /fixtures/error.lua: -------------------------------------------------------------------------------- 1 | local api = require("test") 2 | 3 | function main(input) 4 | return api.error("roman") 5 | end -------------------------------------------------------------------------------- /fixtures/error1.lua: -------------------------------------------------------------------------------- 1 | local api = require("test") 2 | 3 | function main(input) 4 | return api.error1(input) 5 | end -------------------------------------------------------------------------------- /fixtures/fib.lua: -------------------------------------------------------------------------------- 1 | function main(n) 2 | if n < 2 then return 1 end 3 | return main(n - 2) + main(n - 1) 4 | end -------------------------------------------------------------------------------- /fixtures/json.lua: -------------------------------------------------------------------------------- 1 | local json = require("json") 2 | 3 | function main(input) 4 | return json.encode(input) 5 | end -------------------------------------------------------------------------------- /fixtures/print.lua: -------------------------------------------------------------------------------- 1 | function main(input) 2 | local text = "Hello, " .. input.Name .. "!" 3 | return text 4 | end -------------------------------------------------------------------------------- /fixtures/joinmap.lua: -------------------------------------------------------------------------------- 1 | local api = require("test") 2 | 3 | function main(input) 4 | return api.joinMap(input) 5 | end -------------------------------------------------------------------------------- /fixtures/enrich.lua: -------------------------------------------------------------------------------- 1 | local api = require("test") 2 | 3 | function main(input) 4 | return api.enrich("roman", input) 5 | end -------------------------------------------------------------------------------- /fixtures/join.lua: -------------------------------------------------------------------------------- 1 | local api = require("test") 2 | 3 | function main() 4 | return api.join({'apples', 'oranges', 'watermelons'}) 5 | end -------------------------------------------------------------------------------- /fixtures/module.lua: -------------------------------------------------------------------------------- 1 | local demo_mod = {} -- The main table 2 | 3 | function demo_mod.Mult(a, b) 4 | return a * b 5 | end 6 | 7 | return demo_mod -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kelindar/lua 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/cheekybits/genny v1.0.0 7 | github.com/stretchr/testify v1.8.0 8 | github.com/yuin/gopher-lua v1.1.1 9 | layeh.com/gopher-luar v1.0.10 10 | ) 11 | 12 | require ( 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/pmezard/go-difflib v1.0.0 // indirect 15 | gopkg.in/yaml.v3 v3.0.1 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /fixtures/array.lua: -------------------------------------------------------------------------------- 1 | local json = require("json") 2 | 3 | function main() 4 | return { 5 | ["empty"] = json.array(), 6 | ["empty_map"] = json.array({}), 7 | ["array"] = json.array({1, 2, 3}), 8 | ["table"] = json.array({["apple"] = 5}), 9 | ["str"] = json.array("hello"), 10 | ["int"] = json.array(12), 11 | ["bool"] = json.array(true), 12 | ["float"] = json.array(12.34), 13 | ["empties"] = json.array(json.array()) 14 | } 15 | end -------------------------------------------------------------------------------- /gen/binary.go: -------------------------------------------------------------------------------- 1 | package lua 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/cheekybits/genny/generic" 7 | lua "github.com/yuin/gopher-lua" 8 | ) 9 | 10 | //go:generate genny -in=$GOFILE -out=../z_binary.go gen "TIn=String,Number,Bool TOut=String,Number,Bool" 11 | 12 | // TIn is the generic type. 13 | type TIn generic.Type 14 | 15 | // TOut is the generic type. 16 | type TOut generic.Type 17 | 18 | func init() { 19 | typ := reflect.TypeOf((*func(TIn) (TOut, error))(nil)).Elem() 20 | builtin[typ] = func(v any) lua.LGFunction { 21 | f := v.(func(TIn) (TOut, error)) 22 | return func(state *lua.LState) int { 23 | v, err := f(TIn(state.CheckTIn(1))) 24 | if err != nil { 25 | state.RaiseError(err.Error()) 26 | return 0 27 | } 28 | 29 | state.Push(lua.LTOut(v)) 30 | return 1 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /gen/unary.go: -------------------------------------------------------------------------------- 1 | package lua 2 | 3 | import ( 4 | "reflect" 5 | 6 | lua "github.com/yuin/gopher-lua" 7 | ) 8 | 9 | //go:generate genny -in=$GOFILE -out=../z_unary.go gen "TIn=String,Number,Bool" 10 | 11 | func init() { 12 | typ := reflect.TypeOf((*func(TIn) error)(nil)).Elem() 13 | builtin[typ] = func(v any) lua.LGFunction { 14 | f := v.(func(TIn) error) 15 | return func(state *lua.LState) int { 16 | if err := f(TIn(state.CheckTIn(1))); err != nil { 17 | state.RaiseError(err.Error()) 18 | } 19 | return 0 20 | } 21 | } 22 | } 23 | 24 | func init() { 25 | typ := reflect.TypeOf((*func() (TIn, error))(nil)).Elem() 26 | builtin[typ] = func(v any) lua.LGFunction { 27 | f := v.(func() (TIn, error)) 28 | return func(state *lua.LState) int { 29 | v, err := f() 30 | if err != nil { 31 | state.RaiseError(err.Error()) 32 | return 0 33 | } 34 | 35 | state.Push(lua.LTIn(v)) 36 | return 1 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Roman Atachiants 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /gen/binary_test.go: -------------------------------------------------------------------------------- 1 | package lua 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | //go:generate genny -in=$GOFILE -out=../z_binary_test.go gen "TIn=String,Number,Bool TOut=String,Number,Bool" 12 | 13 | func Test_TInTOut(t *testing.T) { 14 | m := &NativeModule{Name: "test"} 15 | m.Register("test1", func(v TIn) (TOut, error) { 16 | return newTestValue(TypeTOut).(TOut), nil 17 | }) 18 | m.Register("test2", func(v TIn) (TOut, error) { 19 | return newTestValue(TypeTOut).(TOut), errors.New("boom") 20 | }) 21 | 22 | { // Happy path 23 | s, err := FromString("", ` 24 | local api = require("test") 25 | function main(input) 26 | return api.test1(input) 27 | end`, m) 28 | assert.NotNil(t, s) 29 | assert.NoError(t, err) 30 | _, err = s.Run(context.Background(), newTestValue(TypeTIn).(TIn)) 31 | assert.NoError(t, err) 32 | } 33 | 34 | { // Invalid argument 35 | s, err := FromString("", ` 36 | local api = require("test") 37 | function main(input) 38 | return api.test2(input) 39 | end`, m) 40 | assert.NotNil(t, s) 41 | assert.NoError(t, err) 42 | _, err = s.Run(context.Background(), newTestValue(TypeTIn).(TIn)) 43 | assert.Error(t, err) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /gen/unary_in_test.go: -------------------------------------------------------------------------------- 1 | package lua 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | //go:generate genny -in=$GOFILE -out=../z_unary_test.go gen "TIn=String,Number,Bool" 12 | 13 | func Test_In_TIn(t *testing.T) { 14 | m := &NativeModule{Name: "test"} 15 | m.Register("test1", func(v TIn) error { 16 | return nil 17 | }) 18 | m.Register("test2", func(v TIn) error { 19 | return errors.New("boom") 20 | }) 21 | 22 | { // Happy path 23 | s, err := FromString("", ` 24 | local api = require("test") 25 | function main(input) 26 | return api.test1(input) 27 | end`, m) 28 | assert.NotNil(t, s) 29 | assert.NoError(t, err) 30 | _, err = s.Run(context.Background(), newTestValue(TypeTIn).(TIn)) 31 | assert.NoError(t, err) 32 | } 33 | 34 | { // Invalid argument 35 | s, err := FromString("", ` 36 | local api = require("test") 37 | function main(input) 38 | return api.test2(input) 39 | end`, m) 40 | assert.NotNil(t, s) 41 | assert.NoError(t, err) 42 | _, err = s.Run(context.Background(), newTestValue(TypeTIn).(TIn)) 43 | assert.Error(t, err) 44 | } 45 | } 46 | 47 | func Test_Out_TIn(t *testing.T) { 48 | m := &NativeModule{Name: "test"} 49 | m.Register("test1", func() (TIn, error) { 50 | return newTestValue(TypeTIn).(TIn), nil 51 | }) 52 | m.Register("test2", func() (TIn, error) { 53 | return newTestValue(TypeTIn).(TIn), errors.New("boom") 54 | }) 55 | 56 | { // Happy path 57 | s, err := FromString("", ` 58 | local api = require("test") 59 | function main(input) 60 | return api.test1(input) 61 | end`, m) 62 | assert.NotNil(t, s) 63 | assert.NoError(t, err) 64 | _, err = s.Run(context.Background()) 65 | assert.NoError(t, err) 66 | } 67 | 68 | { // Invalid argument 69 | s, err := FromString("", ` 70 | local api = require("test") 71 | function main(input) 72 | return api.test2(input) 73 | end`, m) 74 | assert.NotNil(t, s) 75 | assert.NoError(t, err) 76 | _, err = s.Run(context.Background()) 77 | assert.Error(t, err) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= 2 | github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= 3 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 4 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 5 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 12 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 13 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 14 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 15 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 16 | github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= 17 | github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= 18 | github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= 19 | golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 20 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 21 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 22 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 23 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 24 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 25 | layeh.com/gopher-luar v1.0.10 h1:55b0mpBhN9XSshEd2Nz6WsbYXctyBT35azk4POQNSXo= 26 | layeh.com/gopher-luar v1.0.10/go.mod h1:TPnIVCZ2RJBndm7ohXyaqfhzjlZ+OA2SZR/YwL8tECk= 27 | -------------------------------------------------------------------------------- /z_unary.go: -------------------------------------------------------------------------------- 1 | // This file was automatically generated by genny. 2 | // Any changes will be lost if this file is regenerated. 3 | // see https://github.com/cheekybits/genny 4 | 5 | package lua 6 | 7 | import ( 8 | "reflect" 9 | 10 | lua "github.com/yuin/gopher-lua" 11 | ) 12 | 13 | func init() { 14 | typ := reflect.TypeOf((*func(String) error)(nil)).Elem() 15 | builtin[typ] = func(v any) lua.LGFunction { 16 | f := v.(func(String) error) 17 | return func(state *lua.LState) int { 18 | if err := f(String(state.CheckString(1))); err != nil { 19 | state.RaiseError(err.Error()) 20 | } 21 | return 0 22 | } 23 | } 24 | } 25 | 26 | func init() { 27 | typ := reflect.TypeOf((*func() (String, error))(nil)).Elem() 28 | builtin[typ] = func(v any) lua.LGFunction { 29 | f := v.(func() (String, error)) 30 | return func(state *lua.LState) int { 31 | v, err := f() 32 | if err != nil { 33 | state.RaiseError(err.Error()) 34 | return 0 35 | } 36 | 37 | state.Push(lua.LString(v)) 38 | return 1 39 | } 40 | } 41 | } 42 | 43 | func init() { 44 | typ := reflect.TypeOf((*func(Number) error)(nil)).Elem() 45 | builtin[typ] = func(v any) lua.LGFunction { 46 | f := v.(func(Number) error) 47 | return func(state *lua.LState) int { 48 | if err := f(Number(state.CheckNumber(1))); err != nil { 49 | state.RaiseError(err.Error()) 50 | } 51 | return 0 52 | } 53 | } 54 | } 55 | 56 | func init() { 57 | typ := reflect.TypeOf((*func() (Number, error))(nil)).Elem() 58 | builtin[typ] = func(v any) lua.LGFunction { 59 | f := v.(func() (Number, error)) 60 | return func(state *lua.LState) int { 61 | v, err := f() 62 | if err != nil { 63 | state.RaiseError(err.Error()) 64 | return 0 65 | } 66 | 67 | state.Push(lua.LNumber(v)) 68 | return 1 69 | } 70 | } 71 | } 72 | 73 | func init() { 74 | typ := reflect.TypeOf((*func(Bool) error)(nil)).Elem() 75 | builtin[typ] = func(v any) lua.LGFunction { 76 | f := v.(func(Bool) error) 77 | return func(state *lua.LState) int { 78 | if err := f(Bool(state.CheckBool(1))); err != nil { 79 | state.RaiseError(err.Error()) 80 | } 81 | return 0 82 | } 83 | } 84 | } 85 | 86 | func init() { 87 | typ := reflect.TypeOf((*func() (Bool, error))(nil)).Elem() 88 | builtin[typ] = func(v any) lua.LGFunction { 89 | f := v.(func() (Bool, error)) 90 | return func(state *lua.LState) int { 91 | v, err := f() 92 | if err != nil { 93 | state.RaiseError(err.Error()) 94 | return 0 95 | } 96 | 97 | state.Push(lua.LBool(v)) 98 | return 1 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /json/json_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Roman Atachiants and contributors. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | // This is a fork of https://github.com/layeh/gopher-json, licensed under The Unlicense 5 | 6 | package json 7 | 8 | import ( 9 | "encoding/json" 10 | "fmt" 11 | "testing" 12 | 13 | "github.com/stretchr/testify/assert" 14 | lua "github.com/yuin/gopher-lua" 15 | ) 16 | 17 | func TestSimple(t *testing.T) { 18 | const str = ` 19 | local json = require("json") 20 | assert(type(json) == "table") 21 | assert(type(json.decode) == "function") 22 | assert(type(json.encode) == "function") 23 | assert(json.encode(true) == "true") 24 | assert(json.encode(1) == "1") 25 | assert(json.encode(-10) == "-10") 26 | assert(json.encode(nil) == "null") 27 | assert(json.encode({}) == "[]") 28 | assert(json.encode({1, 2, 3}) == "[1,2,3]") 29 | local _, err = json.encode({1, 2, [10] = 3}) 30 | assert(string.find(err, "sparse array")) 31 | local _, err = json.encode({1, 2, 3, name = "Tim"}) 32 | assert(string.find(err, "mixed or invalid key types")) 33 | local _, err = json.encode({name = "Tim", [false] = 123}) 34 | assert(string.find(err, "mixed or invalid key types")) 35 | local obj = {"a",1,"b",2,"c",3} 36 | local jsonStr = json.encode(obj) 37 | local jsonObj = json.decode(jsonStr) 38 | for i = 1, #obj do 39 | assert(obj[i] == jsonObj[i]) 40 | end 41 | local obj = {name="Tim",number=12345} 42 | local jsonStr = json.encode(obj) 43 | local jsonObj = json.decode(jsonStr) 44 | assert(obj.name == jsonObj.name) 45 | assert(obj.number == jsonObj.number) 46 | assert(json.decode("null") == nil) 47 | assert(json.decode(json.encode({person={name = "tim",}})).person.name == "tim") 48 | local obj = { 49 | abc = 123, 50 | def = nil, 51 | } 52 | local obj2 = { 53 | obj = obj, 54 | } 55 | obj.obj2 = obj2 56 | assert(json.encode(obj) == nil) 57 | local a = {} 58 | for i=1, 5 do 59 | a[i] = i 60 | end 61 | assert(json.encode(a) == "[1,2,3,4,5]") 62 | ` 63 | s := lua.NewState() 64 | defer s.Close() 65 | 66 | s.PreloadModule("json", Loader) 67 | assert.NotPanics(t, func() { 68 | s.DoString(str) 69 | }) 70 | 71 | } 72 | 73 | func TestCustomRequire(t *testing.T) { 74 | const str = ` 75 | local j = require("JSON") 76 | assert(type(j) == "table") 77 | assert(type(j.decode) == "function") 78 | assert(type(j.encode) == "function") 79 | ` 80 | s := lua.NewState() 81 | defer s.Close() 82 | 83 | s.PreloadModule("JSON", Loader) 84 | if err := s.DoString(str); err != nil { 85 | t.Error(err) 86 | } 87 | } 88 | 89 | func TestDecodeValue_jsonNumber(t *testing.T) { 90 | s := lua.NewState() 91 | defer s.Close() 92 | 93 | v := DecodeValue(s, json.Number("124.11")) 94 | if v.Type() != lua.LTString || v.String() != "124.11" { 95 | t.Fatalf("expecting LString, got %T", v) 96 | } 97 | } 98 | 99 | func TestArrayCoerce(t *testing.T) { 100 | const require = `local json = require("json")` 101 | tests := [][2]string{ 102 | {"", "[]"}, 103 | {"{}", "[]"}, 104 | {"{1, 2}", "[1,2]"}, 105 | {`"hello"`, `[\"hello\"]`}, 106 | {`1.2`, `[1.2]`}, 107 | {`true`, `[true]`}, 108 | } 109 | 110 | for _, tc := range tests { 111 | t.Run(tc[0], func(t *testing.T) { 112 | s := lua.NewState() 113 | defer s.Close() 114 | 115 | script := fmt.Sprintf("%s\n"+ 116 | "print(json.array(%s))\n"+ 117 | "assert(json.encode(json.array(%s)) == \"%s\")", 118 | require, tc[0], tc[0], tc[1]) 119 | 120 | s.PreloadModule("json", Loader) 121 | assert.NoError(t, s.DoString(script)) 122 | }) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /z_binary.go: -------------------------------------------------------------------------------- 1 | // This file was automatically generated by genny. 2 | // Any changes will be lost if this file is regenerated. 3 | // see https://github.com/cheekybits/genny 4 | 5 | package lua 6 | 7 | import ( 8 | "reflect" 9 | 10 | lua "github.com/yuin/gopher-lua" 11 | ) 12 | 13 | func init() { 14 | typ := reflect.TypeOf((*func(String) (String, error))(nil)).Elem() 15 | builtin[typ] = func(v any) lua.LGFunction { 16 | f := v.(func(String) (String, error)) 17 | return func(state *lua.LState) int { 18 | v, err := f(String(state.CheckString(1))) 19 | if err != nil { 20 | state.RaiseError(err.Error()) 21 | return 0 22 | } 23 | 24 | state.Push(lua.LString(v)) 25 | return 1 26 | } 27 | } 28 | } 29 | 30 | func init() { 31 | typ := reflect.TypeOf((*func(String) (Number, error))(nil)).Elem() 32 | builtin[typ] = func(v any) lua.LGFunction { 33 | f := v.(func(String) (Number, error)) 34 | return func(state *lua.LState) int { 35 | v, err := f(String(state.CheckString(1))) 36 | if err != nil { 37 | state.RaiseError(err.Error()) 38 | return 0 39 | } 40 | 41 | state.Push(lua.LNumber(v)) 42 | return 1 43 | } 44 | } 45 | } 46 | 47 | func init() { 48 | typ := reflect.TypeOf((*func(String) (Bool, error))(nil)).Elem() 49 | builtin[typ] = func(v any) lua.LGFunction { 50 | f := v.(func(String) (Bool, error)) 51 | return func(state *lua.LState) int { 52 | v, err := f(String(state.CheckString(1))) 53 | if err != nil { 54 | state.RaiseError(err.Error()) 55 | return 0 56 | } 57 | 58 | state.Push(lua.LBool(v)) 59 | return 1 60 | } 61 | } 62 | } 63 | 64 | func init() { 65 | typ := reflect.TypeOf((*func(Number) (String, error))(nil)).Elem() 66 | builtin[typ] = func(v any) lua.LGFunction { 67 | f := v.(func(Number) (String, error)) 68 | return func(state *lua.LState) int { 69 | v, err := f(Number(state.CheckNumber(1))) 70 | if err != nil { 71 | state.RaiseError(err.Error()) 72 | return 0 73 | } 74 | 75 | state.Push(lua.LString(v)) 76 | return 1 77 | } 78 | } 79 | } 80 | 81 | func init() { 82 | typ := reflect.TypeOf((*func(Number) (Number, error))(nil)).Elem() 83 | builtin[typ] = func(v any) lua.LGFunction { 84 | f := v.(func(Number) (Number, error)) 85 | return func(state *lua.LState) int { 86 | v, err := f(Number(state.CheckNumber(1))) 87 | if err != nil { 88 | state.RaiseError(err.Error()) 89 | return 0 90 | } 91 | 92 | state.Push(lua.LNumber(v)) 93 | return 1 94 | } 95 | } 96 | } 97 | 98 | func init() { 99 | typ := reflect.TypeOf((*func(Number) (Bool, error))(nil)).Elem() 100 | builtin[typ] = func(v any) lua.LGFunction { 101 | f := v.(func(Number) (Bool, error)) 102 | return func(state *lua.LState) int { 103 | v, err := f(Number(state.CheckNumber(1))) 104 | if err != nil { 105 | state.RaiseError(err.Error()) 106 | return 0 107 | } 108 | 109 | state.Push(lua.LBool(v)) 110 | return 1 111 | } 112 | } 113 | } 114 | 115 | func init() { 116 | typ := reflect.TypeOf((*func(Bool) (String, error))(nil)).Elem() 117 | builtin[typ] = func(v any) lua.LGFunction { 118 | f := v.(func(Bool) (String, error)) 119 | return func(state *lua.LState) int { 120 | v, err := f(Bool(state.CheckBool(1))) 121 | if err != nil { 122 | state.RaiseError(err.Error()) 123 | return 0 124 | } 125 | 126 | state.Push(lua.LString(v)) 127 | return 1 128 | } 129 | } 130 | } 131 | 132 | func init() { 133 | typ := reflect.TypeOf((*func(Bool) (Number, error))(nil)).Elem() 134 | builtin[typ] = func(v any) lua.LGFunction { 135 | f := v.(func(Bool) (Number, error)) 136 | return func(state *lua.LState) int { 137 | v, err := f(Bool(state.CheckBool(1))) 138 | if err != nil { 139 | state.RaiseError(err.Error()) 140 | return 0 141 | } 142 | 143 | state.Push(lua.LNumber(v)) 144 | return 1 145 | } 146 | } 147 | } 148 | 149 | func init() { 150 | typ := reflect.TypeOf((*func(Bool) (Bool, error))(nil)).Elem() 151 | builtin[typ] = func(v any) lua.LGFunction { 152 | f := v.(func(Bool) (Bool, error)) 153 | return func(state *lua.LState) int { 154 | v, err := f(Bool(state.CheckBool(1))) 155 | if err != nil { 156 | state.RaiseError(err.Error()) 157 | return 0 158 | } 159 | 160 | state.Push(lua.LBool(v)) 161 | return 1 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /convert_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Roman Atachiants and contributors. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | package lua 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | lua "github.com/yuin/gopher-lua" 11 | ) 12 | 13 | /* 14 | BenchmarkConvert/table-10 1862884 649.8 ns/op 920 B/op 16 allocs/op 15 | */ 16 | func BenchmarkConvert(b *testing.B) { 17 | b.ReportAllocs() 18 | l := lua.NewState() 19 | defer l.Close() 20 | 21 | b.Run("table", func(b *testing.B) { 22 | v := Table{ 23 | "hello": String("world"), 24 | "next": Bool(true), 25 | "age": Number(10), 26 | } 27 | 28 | b.ResetTimer() 29 | for i := 0; i < b.N; i++ { 30 | _ = v.lvalue(l) 31 | } 32 | }) 33 | } 34 | 35 | func TestValueOf(t *testing.T) { 36 | type myNil struct{} 37 | tests := []struct { 38 | input any 39 | output Value 40 | }{ 41 | {input: complex64(1), output: Nil{}}, 42 | {input: Number(1), output: Number(1)}, 43 | {input: Bool(true), output: Bool(true)}, 44 | {input: String("hi"), output: String("hi")}, 45 | {input: int(1), output: Number(1)}, 46 | {input: int8(1), output: Number(1)}, 47 | {input: int16(1), output: Number(1)}, 48 | {input: int32(1), output: Number(1)}, 49 | {input: int64(1), output: Number(1)}, 50 | {input: uint(1), output: Number(1)}, 51 | {input: uint8(1), output: Number(1)}, 52 | {input: uint16(1), output: Number(1)}, 53 | {input: uint32(1), output: Number(1)}, 54 | {input: uint64(1), output: Number(1)}, 55 | {input: float32(1), output: Number(1)}, 56 | {input: float64(1), output: Number(1)}, 57 | {input: true, output: Bool(true)}, 58 | {input: "hi", output: String("hi")}, 59 | {input: []string{"a", "b"}, output: Strings{"a", "b"}}, 60 | {input: []int{1, 2, 3}, output: Numbers{1, 2, 3}}, 61 | {input: []int8{1, 2, 3}, output: Numbers{1, 2, 3}}, 62 | {input: []int16{1, 2, 3}, output: Numbers{1, 2, 3}}, 63 | {input: []int32{1, 2, 3}, output: Numbers{1, 2, 3}}, 64 | {input: []int64{1, 2, 3}, output: Numbers{1, 2, 3}}, 65 | {input: []uint{1, 2, 3}, output: Numbers{1, 2, 3}}, 66 | {input: []uint8{1, 2, 3}, output: Numbers{1, 2, 3}}, 67 | {input: []uint16{1, 2, 3}, output: Numbers{1, 2, 3}}, 68 | {input: []uint32{1, 2, 3}, output: Numbers{1, 2, 3}}, 69 | {input: []uint64{1, 2, 3}, output: Numbers{1, 2, 3}}, 70 | {input: []float32{1, 2, 3}, output: Numbers{1, 2, 3}}, 71 | {input: []float64{1, 2, 3}, output: Numbers{1, 2, 3}}, 72 | {input: []bool{false, true}, output: Bools{false, true}}, 73 | {input: nil, output: Nil{}}, 74 | {input: struct{}{}, output: Nil{}}, 75 | {input: myNil{}, output: Nil{}}, 76 | {input: Nil{}, output: Nil{}}, 77 | {input: map[string]any{ 78 | "A": "foo", 79 | "B": "bar", 80 | }, output: Table{ 81 | "A": String("foo"), 82 | "B": String("bar"), 83 | }}, 84 | } 85 | 86 | for _, tc := range tests { 87 | assert.Equal(t, tc.output, ValueOf(tc.input)) 88 | assert.NotEmpty(t, tc.output.String()) 89 | } 90 | } 91 | 92 | func TestResultOfArray(t *testing.T) { 93 | mp := []map[string]any{ 94 | {"hello": []float64{1, 2, 3}}, 95 | {"next": true}, 96 | {"hello": "world"}, 97 | } 98 | l := lua.NewState() 99 | val := lvalueOf(l, mp) 100 | res := resultOf(val) 101 | array, ok := res.(Array) 102 | assert.True(t, ok) 103 | assert.Len(t, array, 3) 104 | 105 | resMp := array.Native().([]any) 106 | for i, val := range resMp { 107 | assert.Equal(t, mp[i], val.(map[string]any)) 108 | } 109 | } 110 | 111 | func TestResultComplexMap(t *testing.T) { 112 | mp := []map[string]any{ 113 | {"map": []map[string]any{ 114 | {"e": "f", "g": "h"}}, 115 | }, 116 | } 117 | l := lua.NewState() 118 | val := lvalueOf(l, mp) 119 | res := resultOf(val) 120 | array, ok := res.(Array) 121 | assert.True(t, ok) 122 | resMp := array.Native().([]any) 123 | assert.Len(t, resMp, len(mp)) 124 | } 125 | 126 | func TestResultOfMap(t *testing.T) { 127 | mp := map[string]any{ 128 | "string": "aj", 129 | "numbers": []float64{1, 2, 3}, 130 | "bool": true, 131 | } 132 | l := lua.NewState() 133 | val := lvalueOf(l, mp) 134 | res := resultOf(val) 135 | table, ok := res.(Table) 136 | assert.True(t, ok) 137 | assert.Len(t, table, 3) 138 | resMp, ok := table.Native().(map[string]any) 139 | assert.True(t, ok) 140 | 141 | for _, key := range []string{"string", "numbers", "bool"} { 142 | assert.Equal(t, mp[key], resMp[key]) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Concurrent LUA Executor 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/kelindar/lua)](https://goreportcard.com/report/github.com/kelindar/lua) 4 | [![GoDoc](https://godoc.org/github.com/kelindar/lua?status.svg)](https://godoc.org/github.com/kelindar/lua) 5 | 6 | This repository contains a concurrent LUA executor that is designed to keep running a same (but updateable) set of scripts over a long period of time. The design of the library is quite opinionated, as it requires the `main()` function to be present in the script in order to run it. It also maintains a pool of VMs per `Script` instance to increase throughput per script. 7 | 8 | Under the hood, it uses [gopher-lua](https://github.com/yuin/gopher-lua) library but abstracts it away, in order to be easily replaced in future if required. 9 | 10 | 11 | ## Usage Example 12 | Below is the usage example which runs the fibonacci LUA script with input `10`. 13 | 14 | ```go 15 | // Load the script 16 | s, err := FromString("test.lua", ` 17 | function main(n) 18 | if n < 2 then return 1 end 19 | return main(n - 2) + main(n - 1) 20 | end 21 | `) 22 | 23 | // Run the main() function with 10 as argument 24 | result, err := s.Run(context.Background(), 10) 25 | println(result.String()) // Output: 89 26 | ``` 27 | 28 | The library also supports passing complex data types, thanks to [gopher-luar](https://github.com/layeh/gopher-luar). In the example below we create a `Person` struct and update its name in LUA as a side-effect of the script. It also returns the updated name back as a string. 29 | 30 | ```go 31 | // Load the script 32 | s, err := FromString("test.lua", ` 33 | function main(input) 34 | input.Name = "Updated" 35 | return input.Name 36 | end 37 | `) 38 | 39 | input := &Person{ Name: "Roman" } 40 | out, err := s.Run(context.Background(), input) 41 | println(out) // Outputs: "Updated" 42 | println(input.Name) // Outputs: "Updated" 43 | ``` 44 | 45 | ## Native Modules 46 | 47 | This library also supports and abstracts modules, which allows you to provide one or multiple native libraries which can be used by the script. These things are just ensembles of functions which are implemented in pure Go. 48 | 49 | Such functions must comply to a specific interface - they should have their arguments as the library's values (e.g. `Number`, `String` or `Bool`) and the result can be either a value and `error` or just an `error`. Here's an example of such function: 50 | ```go 51 | func hash(s lua.String) (lua.Number, error) { 52 | h := fnv.New32a() 53 | h.Write([]byte(s)) 54 | 55 | return lua.Number(h.Sum32()), nil 56 | } 57 | ``` 58 | 59 | In order to use it, the functions should be registered into a `NativeModule` which then is loaded when script is created. 60 | ```go 61 | // Create a test module which provides hash function 62 | module := &NativeModule{ 63 | Name: "test", 64 | Version: "1.0.0", 65 | } 66 | module.Register("hash", hash) 67 | 68 | // Load the script 69 | s, err := FromString("test.lua", ` 70 | local api = require("test") 71 | 72 | function main(input) 73 | return api.hash(input) 74 | end 75 | `, module) // <- attach the module 76 | 77 | out, err := s.Run(context.Background(), "abcdef") 78 | println(out) // Output: 4282878506 79 | 80 | ``` 81 | 82 | ## Script Modules 83 | 84 | Similarly to native modules, the library also supports LUA script modules. In order to use it, first you need to create a script which contains a module and returns a table with the functions. Then, create a `ScriptModule` which points to the script with `Name` which can be used in the `require` statement. 85 | 86 | ```go 87 | moduleCode, err := FromString("module.lua", ` 88 | local demo_mod = {} -- The main table 89 | 90 | function demo_mod.Mult(a, b) 91 | return a * b 92 | end 93 | 94 | return demo_mod 95 | `) 96 | 97 | // Create a test module which provides hash function 98 | module := &ScriptModule{ 99 | Script: moduleCode, 100 | Name: "demo_mod", 101 | Version: "1.0.0", 102 | } 103 | ``` 104 | 105 | Finally, attach the module to the script as with native modules. 106 | 107 | ```go 108 | // Load the script 109 | s, err := FromString("test.lua", ` 110 | local demo = require("demo_mod") 111 | 112 | function main(input) 113 | return demo.Mult(5, 5) 114 | end 115 | `, module) // <- attach the module 116 | 117 | out, err := s.Run(context.Background()) 118 | println(out) // Output: 25 119 | ``` 120 | 121 | 122 | ## Benchmarks 123 | 124 | ``` 125 | Benchmark_Serial/fib-8 5870025 203 ns/op 16 B/op 2 allocs/op 126 | Benchmark_Serial/empty-8 8592448 137 ns/op 0 B/op 0 allocs/op 127 | Benchmark_Serial/update-8 1000000 1069 ns/op 224 B/op 14 allocs/op 128 | ``` 129 | -------------------------------------------------------------------------------- /convert.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Roman Atachiants and contributors. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | package lua 5 | 6 | import ( 7 | "encoding/json" 8 | "reflect" 9 | 10 | lua "github.com/yuin/gopher-lua" 11 | luar "layeh.com/gopher-luar" 12 | ) 13 | 14 | // ValueOf converts the type to our value 15 | func ValueOf(v any) Value { 16 | if v == nil || reflect.TypeOf(v).Size() == 0 { 17 | return Nil{} 18 | } 19 | 20 | switch v := v.(type) { 21 | case Number: 22 | return v 23 | case String: 24 | return v 25 | case Bool: 26 | return v 27 | case Numbers: 28 | return v 29 | case Strings: 30 | return v 31 | case Bools: 32 | return v 33 | case Table: 34 | return v 35 | case Array: 36 | return v 37 | case int: 38 | return Number(v) 39 | case int8: 40 | return Number(v) 41 | case int16: 42 | return Number(v) 43 | case int32: 44 | return Number(v) 45 | case int64: 46 | return Number(v) 47 | case uint: 48 | return Number(v) 49 | case uint8: 50 | return Number(v) 51 | case uint16: 52 | return Number(v) 53 | case uint32: 54 | return Number(v) 55 | case uint64: 56 | return Number(v) 57 | case float32: 58 | return Number(v) 59 | case float64: 60 | return Number(v) 61 | case bool: 62 | return Bool(v) 63 | case string: 64 | return String(v) 65 | case []int: 66 | return numbersOf(v) 67 | case []int8: 68 | return numbersOf(v) 69 | case []int16: 70 | return numbersOf(v) 71 | case []int32: 72 | return numbersOf(v) 73 | case []int64: 74 | return numbersOf(v) 75 | case []uint: 76 | return numbersOf(v) 77 | case []uint8: 78 | return numbersOf(v) 79 | case []uint16: 80 | return numbersOf(v) 81 | case []uint32: 82 | return numbersOf(v) 83 | case []uint64: 84 | return numbersOf(v) 85 | case []float32: 86 | return numbersOf(v) 87 | case []float64: 88 | return Numbers(v) 89 | case []bool: 90 | return Bools(v) 91 | case []string: 92 | return Strings(v) 93 | case map[string]any: 94 | return mapAsTable(v) 95 | case []any: 96 | return sliceAsArray(v) 97 | case nil, Nil: 98 | return Nil{} 99 | case struct{}: 100 | return Nil{} 101 | default: 102 | out, err := json.Marshal(v) 103 | if err != nil { 104 | return Nil{} 105 | } 106 | 107 | var resp any 108 | if err := json.Unmarshal(out, &resp); err != nil { 109 | return Nil{} 110 | } 111 | 112 | return ValueOf(resp) 113 | } 114 | } 115 | 116 | // resultOf converts a value to a LUA-friendly one. 117 | func resultOf(v lua.LValue) Value { 118 | switch v := v.(type) { 119 | case lua.LNumber: 120 | return Number(v) 121 | case lua.LString: 122 | return String(v) 123 | case lua.LBool: 124 | return Bool(v) 125 | case *lua.LTable: 126 | 127 | // slice cases 128 | if top := v.RawGetInt(1); top != nil { 129 | switch top.Type() { 130 | case lua.LTNumber: 131 | return asNumbers(v) 132 | case lua.LTString: 133 | return asStrings(v) 134 | case lua.LTBool: 135 | return asBools(v) 136 | case lua.LTTable: 137 | return asArrays(v) 138 | } 139 | } 140 | // map case 141 | tbl := asTable(v) 142 | if len(tbl) > 0 { 143 | return tbl 144 | } 145 | return Nil{} 146 | case *lua.LUserData: 147 | return ValueOf(v.Value) 148 | default: 149 | return Nil{} 150 | } 151 | } 152 | 153 | func asNumbers(t *lua.LTable) (out Numbers) { 154 | t.ForEach(func(_, v lua.LValue) { 155 | out = append(out, float64(v.(lua.LNumber))) 156 | }) 157 | return 158 | } 159 | 160 | func asStrings(t *lua.LTable) (out Strings) { 161 | t.ForEach(func(_, v lua.LValue) { 162 | out = append(out, string(v.(lua.LString))) 163 | }) 164 | return 165 | } 166 | 167 | func asBools(t *lua.LTable) (out Bools) { 168 | t.ForEach(func(_, v lua.LValue) { 169 | out = append(out, bool(v.(lua.LBool))) 170 | }) 171 | return 172 | } 173 | 174 | func asTable(t *lua.LTable) Table { 175 | out := make(Table) 176 | t.ForEach(func(k, v lua.LValue) { 177 | if k.Type() == lua.LTString { 178 | out[k.String()] = resultOf(v) 179 | } 180 | }) 181 | return out 182 | } 183 | 184 | func asArrays(t *lua.LTable) Array { 185 | out, index := make(Array, t.Len()), 0 186 | t.ForEach(func(_ lua.LValue, v lua.LValue) { 187 | out[index] = resultOf(v) 188 | index += 1 189 | }) 190 | return out 191 | } 192 | 193 | func sliceAsArray(input []any) Array { 194 | arr := make(Array, 0, len(input)) 195 | for _, v := range input { 196 | arr = append(arr, ValueOf(v)) 197 | } 198 | return arr 199 | } 200 | 201 | func mapAsTable(input map[string]any) Table { 202 | t := make(Table, len(input)) 203 | for k, v := range input { 204 | t[k] = ValueOf(v) 205 | } 206 | return t 207 | } 208 | 209 | // -------------------------------------------------------------------- 210 | 211 | // lvalueOf converts the script input into a valid lua value 212 | func lvalueOf(exec *lua.LState, value any) lua.LValue { 213 | if value == nil { 214 | return lua.LNil 215 | } 216 | 217 | switch x := value.(type) { 218 | case Value: 219 | return x.lvalue(exec) 220 | default: 221 | switch val := reflect.ValueOf(value); val.Kind() { 222 | case reflect.Map, reflect.Slice: 223 | return ValueOf(val.Interface()).lvalue(exec) 224 | default: 225 | return luar.New(exec, value) 226 | } 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /value_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Roman Atachiants and contributors. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | package lua 5 | 6 | import ( 7 | "encoding/json" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | lua "github.com/yuin/gopher-lua" 12 | ) 13 | 14 | func newTestValue(typ Type) Value { 15 | switch typ { 16 | case TypeNumber: 17 | return Number(1) 18 | case TypeString: 19 | return String("x") 20 | case TypeBool: 21 | return Bool(true) 22 | default: 23 | return Nil{} 24 | } 25 | } 26 | 27 | func TestTableConvert(t *testing.T) { 28 | expect := newTestMap() 29 | input := ValueOf(expect) 30 | assert.EqualValues(t, expect, input.Native()) 31 | } 32 | 33 | func TestComplexTable(t *testing.T) { 34 | expect := newComplexMap() 35 | input := ValueOf(expect) 36 | output := input.Native().([]any) 37 | assert.Len(t, output, len(expect)) 38 | } 39 | 40 | // Test map[string][]float64 41 | func TestTableCodec(t *testing.T) { 42 | expect := newTestMap() 43 | encoded, err := json.Marshal(expect) 44 | assert.NoError(t, err) 45 | 46 | // Decode a table 47 | var decoded Table 48 | assert.NoError(t, json.Unmarshal(encoded, &decoded)) 49 | 50 | // re encode it back to json and compare strings 51 | reincoded, err := json.Marshal(decoded) 52 | assert.NoError(t, err) 53 | assert.Equal(t, encoded, reincoded) 54 | } 55 | 56 | func TestArrayMap(t *testing.T) { 57 | mp := []map[string]any{ 58 | {"hello": []float64{1, 2, 3}}, 59 | {"next": true}, 60 | {"map": map[string]any{ 61 | "a": []int64{1, 2}, 62 | "b": 1, 63 | "c": false, 64 | "d": []map[string]any{ 65 | {"e": "f", "g": "h"}, 66 | }, 67 | }}, 68 | {"hello": "world"}, 69 | } 70 | l := lua.NewState() 71 | val := lvalueOf(l, mp) 72 | tbl, ok := val.(*lua.LTable) 73 | assert.True(t, ok) 74 | 75 | mp1, ok := tbl.RawGetInt(1).(*lua.LTable) 76 | assert.True(t, ok) 77 | assert.Equal(t, 3, mp1.RawGetString("hello").(*lua.LTable).Len()) 78 | 79 | mp2, ok := tbl.RawGetInt(2).(*lua.LTable) 80 | assert.True(t, ok) 81 | assert.Equal(t, lua.LTrue, mp2.RawGetString("next")) 82 | 83 | mp3, ok := tbl.RawGetInt(3).(*lua.LTable) 84 | assert.True(t, ok) 85 | mp3d, ok := mp3.RawGetString("map").(*lua.LTable).RawGetString("d").(*lua.LTable) 86 | assert.True(t, ok) 87 | assert.Equal(t, 1, mp3d.Len()) 88 | 89 | mp4, ok := tbl.RawGetInt(4).(*lua.LTable) 90 | assert.True(t, ok) 91 | assert.Equal(t, lua.LString("world"), mp4.RawGetString("hello")) 92 | } 93 | 94 | func TestArraySlice(t *testing.T) { 95 | s := [][]int{ 96 | {1, 2, 3}, 97 | {4, 5, 6}, 98 | } 99 | l := lua.NewState() 100 | val := lvalueOf(l, s) 101 | tbl, ok := val.(*lua.LTable) 102 | assert.True(t, ok) 103 | 104 | s1, ok := tbl.RawGetInt(1).(*lua.LTable) 105 | assert.Equal(t, 3, s1.Len()) 106 | 107 | s1.ForEach(func(key, val lua.LValue) { 108 | assert.Equal(t, key, val) 109 | }) 110 | } 111 | 112 | func TestNumbers(t *testing.T) { 113 | n := []int{1, 2, 3} 114 | l := lua.NewState() 115 | val := lvalueOf(l, n) 116 | tbl, ok := val.(*lua.LTable) 117 | assert.True(t, ok) 118 | assert.Equal(t, 3, tbl.Len()) 119 | 120 | tbl.ForEach(func(key, val lua.LValue) { 121 | assert.Equal(t, key, val) 122 | }) 123 | } 124 | 125 | func TestBools(t *testing.T) { 126 | n := []bool{true, false, true} 127 | l := lua.NewState() 128 | val := lvalueOf(l, n) 129 | tbl, ok := val.(*lua.LTable) 130 | assert.True(t, ok) 131 | assert.Equal(t, 3, tbl.Len()) 132 | 133 | tbl.ForEach(func(key, val lua.LValue) { 134 | switch key { 135 | case lua.LNumber(1), lua.LNumber(3): 136 | assert.Equal(t, lua.LTrue, val) 137 | default: 138 | assert.Equal(t, lua.LFalse, val) 139 | } 140 | }) 141 | } 142 | 143 | func TestStrings(t *testing.T) { 144 | n := []string{"aj", "roman", "abdo"} 145 | l := lua.NewState() 146 | val := lvalueOf(l, n) 147 | tbl, ok := val.(*lua.LTable) 148 | assert.True(t, ok) 149 | assert.Equal(t, 3, tbl.Len()) 150 | 151 | tbl.ForEach(func(key, val lua.LValue) { 152 | switch key { 153 | case lua.LNumber(1): 154 | assert.Equal(t, lua.LString("aj"), val) 155 | case lua.LNumber(2): 156 | assert.Equal(t, lua.LString("roman"), val) 157 | case lua.LNumber(3): 158 | assert.Equal(t, lua.LString("abdo"), val) 159 | default: 160 | assert.Equal(t, lua.LFalse, val) 161 | } 162 | }) 163 | } 164 | 165 | func TestMap(t *testing.T) { 166 | mp := map[string]any{ 167 | "string": "aj", 168 | "numbers": []float64{1, 2, 3}, 169 | "bool": true, 170 | } 171 | l := lua.NewState() 172 | val := lvalueOf(l, mp) 173 | tbl, ok := val.(*lua.LTable) 174 | assert.True(t, ok) 175 | tbl.ForEach(func(key, val lua.LValue) { 176 | switch key { 177 | case lua.LString("string"): 178 | assert.Equal(t, lua.LString("aj"), val) 179 | case lua.LString("numbers"): 180 | tbl1, ok := val.(*lua.LTable) 181 | assert.True(t, ok) 182 | assert.Equal(t, 3, tbl1.Len()) 183 | case lua.LString("bool"): 184 | assert.Equal(t, lua.LTrue, val) 185 | } 186 | }) 187 | } 188 | 189 | func must(err error) { 190 | if err != nil { 191 | panic(err) 192 | } 193 | } 194 | 195 | func newTestMap() map[string]any { 196 | return map[string]any{ 197 | "user": "Roman", 198 | "age": 37.0, 199 | "dev": true, 200 | "bitmap": []bool{true, false, true}, 201 | "floats": []float64{1, 2, 3, 4, 5}, 202 | } 203 | } 204 | 205 | func newComplexMap() []map[string]any { 206 | return []map[string]any{ 207 | { 208 | "args": []float64{2, 4}, 209 | }, { 210 | "args": []float64{2, 4}, 211 | }, 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /module.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Roman Atachiants and contributors. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | package lua 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "reflect" 10 | "sync" 11 | 12 | lua "github.com/yuin/gopher-lua" 13 | ) 14 | 15 | var ( 16 | errFuncInput = errors.New("lua: function input arguments must be of type lua.Value") 17 | errFuncOutput = errors.New("lua: function return values must be either (error) or (lua.Value, error)") 18 | errorInterface = reflect.TypeOf((*error)(nil)).Elem() 19 | ) 20 | 21 | var builtin = make(map[reflect.Type]func(any) lua.LGFunction, 8) 22 | 23 | // Module represents a loadable module. 24 | type Module interface { 25 | inject(state *lua.LState) error 26 | } 27 | 28 | // -------------------------------------------------------------------- 29 | 30 | // ScriptModule represents a loadable module written in LUA itself. 31 | type ScriptModule struct { 32 | Script *Script // The script that contains the module 33 | Name string // The name of the module 34 | Version string // The module version string 35 | } 36 | 37 | // Inject loads the module into the state 38 | func (m *ScriptModule) inject(runtime *lua.LState) error { 39 | 40 | // Inject the prerequisite modules of the module 41 | if err := m.Script.loadModules(runtime); err != nil { 42 | return err 43 | } 44 | 45 | // Push the function to the runtime 46 | codeFn := runtime.NewFunctionFromProto(m.Script.code) 47 | preload := runtime.GetField(runtime.GetField(runtime.Get(lua.EnvironIndex), "package"), "preload") 48 | if _, ok := preload.(*lua.LTable); !ok { 49 | return errors.New("package.preload must be a table") 50 | 51 | } 52 | runtime.SetField(preload, m.Name, codeFn) 53 | return nil 54 | } 55 | 56 | // -------------------------------------------------------------------- 57 | 58 | // NativeModule represents a loadable native module. 59 | type NativeModule struct { 60 | lock sync.Mutex 61 | funcs map[string]fngen 62 | Name string // The name of the module 63 | Version string // The module version string 64 | } 65 | 66 | type fngen struct { 67 | name string 68 | code any 69 | } 70 | 71 | // Generate generates a function 72 | func (g *fngen) generate() lua.LGFunction { 73 | rv := reflect.ValueOf(g.code) 74 | rt := rv.Type() 75 | if maker, ok := builtin[rt]; ok { 76 | return maker(g.code) 77 | } 78 | 79 | name := g.name 80 | args := make([]reflect.Value, 0, rt.NumIn()) 81 | return func(state *lua.LState) int { 82 | if state.GetTop() != rt.NumIn() { 83 | state.RaiseError("%s expects %d arguments, but got %d", name, rt.NumIn(), state.GetTop()) 84 | return 0 85 | } 86 | 87 | // Convert the arguments 88 | args = args[:0] 89 | for i := 0; i < rt.NumIn(); i++ { 90 | args = append(args, reflect.ValueOf(resultOf(state.Get(i+1)))) 91 | } 92 | 93 | // Call the function 94 | out := rv.Call(args) 95 | switch len(out) { 96 | case 1: 97 | if err := out[0]; !err.IsNil() { 98 | state.RaiseError(err.Interface().(error).Error()) 99 | } 100 | return 0 101 | default: 102 | if err := out[1]; !err.IsNil() { 103 | state.RaiseError(err.Interface().(error).Error()) 104 | return 0 105 | } 106 | state.Push(lvalueOf(state, out[0].Interface())) 107 | return 1 108 | } 109 | } 110 | } 111 | 112 | // Register registers a function into the module. 113 | func (m *NativeModule) Register(name string, function any) error { 114 | m.lock.Lock() 115 | defer m.lock.Unlock() 116 | 117 | // Lazily create the function map 118 | if m.funcs == nil { 119 | m.funcs = make(map[string]fngen, 2) 120 | } 121 | 122 | // Validate the function 123 | if err := validate(function); err != nil { 124 | return err 125 | } 126 | 127 | m.funcs[name] = fngen{name: name, code: function} 128 | return nil 129 | } 130 | 131 | // Unregister unregisters a function from the module. 132 | func (m *NativeModule) Unregister(name string) { 133 | m.lock.Lock() 134 | defer m.lock.Unlock() 135 | delete(m.funcs, name) 136 | } 137 | 138 | // Inject loads the module into the state 139 | func (m *NativeModule) inject(state *lua.LState) error { 140 | table := make(map[string]lua.LGFunction, len(m.funcs)) 141 | for name, g := range m.funcs { 142 | table[name] = g.generate() 143 | } 144 | 145 | state.PreloadModule(m.Name, func(state *lua.LState) int { 146 | mod := state.SetFuncs(state.NewTable(), table) 147 | state.SetField(mod, "version", lua.LString(m.Version)) 148 | state.Push(mod) 149 | return 1 150 | }) 151 | return nil 152 | } 153 | 154 | // validate validates the function type 155 | func validate(function any) error { 156 | rv := reflect.ValueOf(function) 157 | rt := rv.Type() 158 | if rt.Kind() != reflect.Func { 159 | return fmt.Errorf("lua: input is a %s, not a function", rt.Kind().String()) 160 | } 161 | 162 | // Validate the input 163 | for i := 0; i < rt.NumIn(); i++ { 164 | if _, ok := typeMap[rt.In(i)]; !ok { 165 | return errFuncInput 166 | } 167 | } 168 | 169 | // Validate the output 170 | switch { 171 | case rt.NumOut() == 1 && isError(rt, 0): 172 | case rt.NumOut() == 2 && isValid(rt, 0) && isError(rt, 1): 173 | default: 174 | return errFuncOutput 175 | } 176 | return nil 177 | } 178 | 179 | func isError(rt reflect.Type, at int) bool { 180 | return rt.Out(at).Implements(typeError) 181 | } 182 | 183 | func isValid(rt reflect.Type, at int) bool { 184 | switch rt.Out(at) { 185 | case typeString, typeNumber, typeBool, typeNumbers, typeStrings, typeBools, typeTable, typeArray, typeValue: 186 | return true 187 | default: 188 | return false 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /z_unary_test.go: -------------------------------------------------------------------------------- 1 | // This file was automatically generated by genny. 2 | // Any changes will be lost if this file is regenerated. 3 | // see https://github.com/cheekybits/genny 4 | 5 | package lua 6 | 7 | import ( 8 | "context" 9 | "errors" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func Test_In_String(t *testing.T) { 16 | m := &NativeModule{Name: "test"} 17 | m.Register("test1", func(v String) error { 18 | return nil 19 | }) 20 | m.Register("test2", func(v String) error { 21 | return errors.New("boom") 22 | }) 23 | 24 | { // Happy path 25 | s, err := FromString("", ` 26 | local api = require("test") 27 | function main(input) 28 | return api.test1(input) 29 | end`, m) 30 | assert.NotNil(t, s) 31 | assert.NoError(t, err) 32 | _, err = s.Run(context.Background(), newTestValue(TypeString).(String)) 33 | assert.NoError(t, err) 34 | } 35 | 36 | { // Invalid argument 37 | s, err := FromString("", ` 38 | local api = require("test") 39 | function main(input) 40 | return api.test2(input) 41 | end`, m) 42 | assert.NotNil(t, s) 43 | assert.NoError(t, err) 44 | _, err = s.Run(context.Background(), newTestValue(TypeString).(String)) 45 | assert.Error(t, err) 46 | } 47 | } 48 | 49 | func Test_Out_String(t *testing.T) { 50 | m := &NativeModule{Name: "test"} 51 | m.Register("test1", func() (String, error) { 52 | return newTestValue(TypeString).(String), nil 53 | }) 54 | m.Register("test2", func() (String, error) { 55 | return newTestValue(TypeString).(String), errors.New("boom") 56 | }) 57 | 58 | { // Happy path 59 | s, err := FromString("", ` 60 | local api = require("test") 61 | function main(input) 62 | return api.test1(input) 63 | end`, m) 64 | assert.NotNil(t, s) 65 | assert.NoError(t, err) 66 | _, err = s.Run(context.Background()) 67 | assert.NoError(t, err) 68 | } 69 | 70 | { // Invalid argument 71 | s, err := FromString("", ` 72 | local api = require("test") 73 | function main(input) 74 | return api.test2(input) 75 | end`, m) 76 | assert.NotNil(t, s) 77 | assert.NoError(t, err) 78 | _, err = s.Run(context.Background()) 79 | assert.Error(t, err) 80 | } 81 | } 82 | 83 | func Test_In_Number(t *testing.T) { 84 | m := &NativeModule{Name: "test"} 85 | m.Register("test1", func(v Number) error { 86 | return nil 87 | }) 88 | m.Register("test2", func(v Number) error { 89 | return errors.New("boom") 90 | }) 91 | 92 | { // Happy path 93 | s, err := FromString("", ` 94 | local api = require("test") 95 | function main(input) 96 | return api.test1(input) 97 | end`, m) 98 | assert.NotNil(t, s) 99 | assert.NoError(t, err) 100 | _, err = s.Run(context.Background(), newTestValue(TypeNumber).(Number)) 101 | assert.NoError(t, err) 102 | } 103 | 104 | { // Invalid argument 105 | s, err := FromString("", ` 106 | local api = require("test") 107 | function main(input) 108 | return api.test2(input) 109 | end`, m) 110 | assert.NotNil(t, s) 111 | assert.NoError(t, err) 112 | _, err = s.Run(context.Background(), newTestValue(TypeNumber).(Number)) 113 | assert.Error(t, err) 114 | } 115 | } 116 | 117 | func Test_Out_Number(t *testing.T) { 118 | m := &NativeModule{Name: "test"} 119 | m.Register("test1", func() (Number, error) { 120 | return newTestValue(TypeNumber).(Number), nil 121 | }) 122 | m.Register("test2", func() (Number, error) { 123 | return newTestValue(TypeNumber).(Number), errors.New("boom") 124 | }) 125 | 126 | { // Happy path 127 | s, err := FromString("", ` 128 | local api = require("test") 129 | function main(input) 130 | return api.test1(input) 131 | end`, m) 132 | assert.NotNil(t, s) 133 | assert.NoError(t, err) 134 | _, err = s.Run(context.Background()) 135 | assert.NoError(t, err) 136 | } 137 | 138 | { // Invalid argument 139 | s, err := FromString("", ` 140 | local api = require("test") 141 | function main(input) 142 | return api.test2(input) 143 | end`, m) 144 | assert.NotNil(t, s) 145 | assert.NoError(t, err) 146 | _, err = s.Run(context.Background()) 147 | assert.Error(t, err) 148 | } 149 | } 150 | 151 | func Test_In_Bool(t *testing.T) { 152 | m := &NativeModule{Name: "test"} 153 | m.Register("test1", func(v Bool) error { 154 | return nil 155 | }) 156 | m.Register("test2", func(v Bool) error { 157 | return errors.New("boom") 158 | }) 159 | 160 | { // Happy path 161 | s, err := FromString("", ` 162 | local api = require("test") 163 | function main(input) 164 | return api.test1(input) 165 | end`, m) 166 | assert.NotNil(t, s) 167 | assert.NoError(t, err) 168 | _, err = s.Run(context.Background(), newTestValue(TypeBool).(Bool)) 169 | assert.NoError(t, err) 170 | } 171 | 172 | { // Invalid argument 173 | s, err := FromString("", ` 174 | local api = require("test") 175 | function main(input) 176 | return api.test2(input) 177 | end`, m) 178 | assert.NotNil(t, s) 179 | assert.NoError(t, err) 180 | _, err = s.Run(context.Background(), newTestValue(TypeBool).(Bool)) 181 | assert.Error(t, err) 182 | } 183 | } 184 | 185 | func Test_Out_Bool(t *testing.T) { 186 | m := &NativeModule{Name: "test"} 187 | m.Register("test1", func() (Bool, error) { 188 | return newTestValue(TypeBool).(Bool), nil 189 | }) 190 | m.Register("test2", func() (Bool, error) { 191 | return newTestValue(TypeBool).(Bool), errors.New("boom") 192 | }) 193 | 194 | { // Happy path 195 | s, err := FromString("", ` 196 | local api = require("test") 197 | function main(input) 198 | return api.test1(input) 199 | end`, m) 200 | assert.NotNil(t, s) 201 | assert.NoError(t, err) 202 | _, err = s.Run(context.Background()) 203 | assert.NoError(t, err) 204 | } 205 | 206 | { // Invalid argument 207 | s, err := FromString("", ` 208 | local api = require("test") 209 | function main(input) 210 | return api.test2(input) 211 | end`, m) 212 | assert.NotNil(t, s) 213 | assert.NoError(t, err) 214 | _, err = s.Run(context.Background()) 215 | assert.Error(t, err) 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /json/json.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Roman Atachiants and contributors. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | // This is a fork of https://github.com/layeh/gopher-json, licensed under The Unlicense 5 | 6 | package json 7 | 8 | import ( 9 | "encoding/json" 10 | "errors" 11 | 12 | lua "github.com/yuin/gopher-lua" 13 | ) 14 | 15 | var ( 16 | errNested = errors.New("cannot encode recursively nested tables to JSON") 17 | errSparseArray = errors.New("cannot encode sparse array") 18 | errInvalidKeys = errors.New("cannot encode mixed or invalid key types") 19 | ) 20 | 21 | // Loader is the module loader function. 22 | func Loader(L *lua.LState) int { 23 | t := L.NewTable() 24 | L.SetFuncs(t, api) 25 | L.Push(t) 26 | return 1 27 | } 28 | 29 | var api = map[string]lua.LGFunction{ 30 | "decode": apiDecode, 31 | "encode": apiEncode, 32 | "array": apiArray, 33 | } 34 | 35 | func apiDecode(state *lua.LState) int { 36 | str := state.CheckString(1) 37 | 38 | value, err := Decode(state, []byte(str)) 39 | if err != nil { 40 | state.Push(lua.LNil) 41 | state.Push(lua.LString(err.Error())) 42 | return 2 43 | } 44 | state.Push(value) 45 | return 1 46 | } 47 | 48 | func apiEncode(state *lua.LState) int { 49 | value := state.CheckAny(1) 50 | 51 | data, err := Encode(value) 52 | if err != nil { 53 | state.Push(lua.LNil) 54 | state.RaiseError(err.Error()) 55 | return 1 56 | } 57 | state.Push(lua.LString(string(data))) 58 | return 1 59 | } 60 | 61 | // -------------------------------------------------------------------- 62 | 63 | // EmptyArray is a marker for an empty array. 64 | var EmptyArray = &lua.LUserData{Value: []any(nil)} 65 | 66 | // apiArray creates an array from the arguments. 67 | func apiArray(state *lua.LState) int { 68 | switch state.GetTop() { 69 | case 0: 70 | state.Push(EmptyArray) 71 | return 1 72 | case 1: 73 | // If it's not a table, or empty return a marker 74 | table, ok := state.CheckAny(1).(*lua.LTable) 75 | switch { 76 | case ok && table.Len() > 0: // array 77 | state.Push(table) 78 | return 1 79 | case ok: // check if it's an empty map 80 | k, v := table.Next(lua.LNil) 81 | if k == lua.LNil && v == lua.LNil { 82 | state.Push(EmptyArray) 83 | return 1 84 | } 85 | } 86 | 87 | // Wrap user data in an array 88 | if custom, ok := state.CheckAny(1).(*lua.LUserData); ok { 89 | state.Push(&lua.LUserData{Value: []any{custom.Value}}) 90 | return 1 91 | } 92 | 93 | // Otherwise, return an array 94 | fallthrough 95 | default: 96 | table := state.CreateTable(state.GetTop(), 0) 97 | for i := 1; i <= state.GetTop(); i++ { 98 | table.RawSetInt(i, state.Get(i)) 99 | } 100 | 101 | // Return the table 102 | state.Push(table) 103 | return 1 104 | } 105 | } 106 | 107 | // -------------------------------------------------------------------- 108 | 109 | type invalidTypeError lua.LValueType 110 | 111 | func (i invalidTypeError) Error() string { 112 | return `cannot encode ` + lua.LValueType(i).String() + ` to JSON` 113 | } 114 | 115 | // Encode returns the JSON encoding of value. 116 | func Encode(value lua.LValue) ([]byte, error) { 117 | return json.Marshal(jsonValue{ 118 | LValue: value, 119 | visited: make(map[*lua.LTable]bool), 120 | }) 121 | } 122 | 123 | type jsonValue struct { 124 | lua.LValue 125 | visited map[*lua.LTable]bool 126 | } 127 | 128 | func (j jsonValue) MarshalJSON() (data []byte, err error) { 129 | switch converted := j.LValue.(type) { 130 | case lua.LBool: 131 | data, err = json.Marshal(bool(converted)) 132 | case lua.LNumber: 133 | data, err = json.Marshal(float64(converted)) 134 | case *lua.LNilType: 135 | data = []byte(`null`) 136 | case *lua.LUserData: 137 | switch { 138 | case converted == EmptyArray: 139 | data = []byte(`[]`) 140 | default: 141 | data, err = json.Marshal(converted.Value) 142 | } 143 | case lua.LString: 144 | data, err = json.Marshal(string(converted)) 145 | case *lua.LTable: 146 | if j.visited[converted] { 147 | return nil, errNested 148 | } 149 | j.visited[converted] = true 150 | 151 | key, value := converted.Next(lua.LNil) 152 | 153 | switch key.Type() { 154 | case lua.LTNil: // empty table 155 | data = []byte(`[]`) 156 | case lua.LTNumber: 157 | arr := make([]jsonValue, 0, converted.Len()) 158 | expectedKey := lua.LNumber(1) 159 | for key != lua.LNil { 160 | if key.Type() != lua.LTNumber { 161 | err = errInvalidKeys 162 | return 163 | } 164 | if expectedKey != key { 165 | err = errSparseArray 166 | return 167 | } 168 | arr = append(arr, jsonValue{value, j.visited}) 169 | expectedKey++ 170 | key, value = converted.Next(key) 171 | } 172 | data, err = json.Marshal(arr) 173 | case lua.LTString: 174 | obj := make(map[string]jsonValue) 175 | for key != lua.LNil { 176 | if key.Type() != lua.LTString { 177 | err = errInvalidKeys 178 | return 179 | } 180 | obj[key.String()] = jsonValue{value, j.visited} 181 | key, value = converted.Next(key) 182 | } 183 | data, err = json.Marshal(obj) 184 | default: 185 | err = errInvalidKeys 186 | } 187 | default: 188 | err = invalidTypeError(j.LValue.Type()) 189 | } 190 | return 191 | } 192 | 193 | // Decode converts the JSON encoded data to Lua values. 194 | func Decode(L *lua.LState, data []byte) (lua.LValue, error) { 195 | var value any 196 | err := json.Unmarshal(data, &value) 197 | if err != nil { 198 | return nil, err 199 | } 200 | return DecodeValue(L, value), nil 201 | } 202 | 203 | // DecodeValue converts the value to a Lua value. 204 | // 205 | // This function only converts values that the encoding/json package decodes to. 206 | // All other values will return lua.LNil. 207 | func DecodeValue(L *lua.LState, value any) lua.LValue { 208 | switch converted := value.(type) { 209 | case bool: 210 | return lua.LBool(converted) 211 | case float64: 212 | return lua.LNumber(converted) 213 | case string: 214 | return lua.LString(converted) 215 | case json.Number: 216 | return lua.LString(converted) 217 | case []any: 218 | arr := L.CreateTable(len(converted), 0) 219 | for _, item := range converted { 220 | arr.Append(DecodeValue(L, item)) 221 | } 222 | return arr 223 | case map[string]any: 224 | tbl := L.CreateTable(0, len(converted)) 225 | for key, item := range converted { 226 | tbl.RawSetH(lua.LString(key), DecodeValue(L, item)) 227 | } 228 | return tbl 229 | case nil: 230 | return lua.LNil 231 | } 232 | 233 | return lua.LNil 234 | } 235 | -------------------------------------------------------------------------------- /module_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Roman Atachiants and contributors. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | package lua 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "fmt" 10 | "hash/fnv" 11 | "strings" 12 | "testing" 13 | "time" 14 | 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | func testModule() Module { 19 | m := &NativeModule{ 20 | Name: "test", 21 | Version: "1.0.0", 22 | } 23 | 24 | must(m.Register("hash", hash)) 25 | must(m.Register("echo", echo)) 26 | must(m.Register("sum", sum)) 27 | must(m.Register("join", join)) 28 | must(m.Register("sleep", sleep)) 29 | must(m.Register("joinMap", joinMap)) 30 | must(m.Register("enrich", enrich)) 31 | must(m.Register("error", errorfunc)) 32 | must(m.Register("error1", errorfunc1)) 33 | must(m.Register("toNumbers", toNumbers)) 34 | must(m.Register("value", value)) 35 | return m 36 | } 37 | 38 | func sum(a, b Number) (Number, error) { 39 | return a + b, nil 40 | } 41 | 42 | func echo(v String) (String, error) { 43 | return v, nil 44 | } 45 | 46 | func errorfunc(v String) (String, error) { 47 | return "", fmt.Errorf("error with input (%v)", v) 48 | } 49 | 50 | func errorfunc1(_ Table) (String, error) { 51 | return "", errors.New("throwing error") 52 | } 53 | 54 | func hash(s Value) (Number, error) { 55 | h := fnv.New32a() 56 | h.Write([]byte(s.(String))) 57 | 58 | return Number(h.Sum32()), nil 59 | } 60 | 61 | func join(v Strings) (String, error) { 62 | return String(strings.Join([]string(v), ", ")), nil 63 | } 64 | 65 | func sleep(v Number) error { 66 | time.Sleep(time.Duration(v) * time.Millisecond) 67 | return nil 68 | } 69 | 70 | func joinMap(table Table) (String, error) { 71 | var sb strings.Builder 72 | for k, v := range table { 73 | sb.WriteString(k + ": " + v.String() + ", ") 74 | } 75 | return String(sb.String()), nil 76 | } 77 | 78 | func enrich(name String, request Table) (Table, error) { 79 | request["name"] = name 80 | request["age"] = Number(30) 81 | return request, nil 82 | } 83 | 84 | func toNumbers(v Value) (Numbers, error) { 85 | switch t := v.(type) { 86 | case Numbers: 87 | return t, nil 88 | default: 89 | return nil, fmt.Errorf("unsupported type %T", v) 90 | } 91 | } 92 | 93 | func value(v Value) (Value, error) { 94 | return v, nil 95 | } 96 | 97 | func Test_Join(t *testing.T) { 98 | s, err := newScript("fixtures/join.lua") 99 | assert.NoError(t, err) 100 | 101 | out, err := s.Run(context.Background()) 102 | assert.NoError(t, err) 103 | assert.Equal(t, TypeString, out.Type()) 104 | assert.Equal(t, "apples, oranges, watermelons", string(out.(String))) 105 | } 106 | 107 | func Test_Hash(t *testing.T) { 108 | s, err := newScript("fixtures/hash.lua") 109 | assert.NoError(t, err) 110 | 111 | out, err := s.Run(context.Background(), "abcdef") 112 | assert.NoError(t, err) 113 | assert.Equal(t, TypeNumber, out.Type()) 114 | assert.Equal(t, int64(4282878506), int64(out.(Number))) 115 | } 116 | 117 | func Test_Sum(t *testing.T) { 118 | s, err := newScript("fixtures/sum.lua") 119 | assert.NoError(t, err) 120 | 121 | out, err := s.Run(context.Background(), 2, 3) 122 | assert.NoError(t, err) 123 | assert.Equal(t, TypeNumber, out.Type()) 124 | assert.Equal(t, int64(5), int64(out.(Number))) 125 | } 126 | 127 | func Test_JoinMap(t *testing.T) { 128 | s, err := newScript("fixtures/joinMap.lua") 129 | assert.NoError(t, err) 130 | 131 | out, err := s.Run(context.Background(), map[string]any{ 132 | "A": "apples", 133 | "B": "oranges", 134 | }) 135 | assert.NoError(t, err) 136 | assert.Equal(t, TypeString, out.Type()) 137 | assert.Contains(t, string(out.(String)), "A: apples") 138 | assert.Contains(t, string(out.(String)), "B: oranges") 139 | } 140 | 141 | func Test_NotAFunc(t *testing.T) { 142 | m := &NativeModule{ 143 | Name: "test", 144 | Version: "1.0.0", 145 | } 146 | assert.Error(t, m.Register("xxx", 123)) 147 | assert.NoError(t, m.Register("hash", hash)) 148 | assert.Equal(t, 1, len(m.funcs)) 149 | m.Unregister("hash") 150 | assert.Equal(t, 0, len(m.funcs)) 151 | } 152 | 153 | func Test_ScriptModule(t *testing.T) { 154 | 155 | m, err := newScript("fixtures/module.lua") 156 | assert.NoError(t, err) 157 | 158 | s, err := newScript("fixtures/demo.lua", &ScriptModule{ 159 | Script: m, 160 | Name: "demo_mod", 161 | Version: "1.0.0", 162 | }) 163 | assert.NoError(t, err) 164 | 165 | out, err := s.Run(context.Background(), 10, m) 166 | assert.NoError(t, err) 167 | assert.Equal(t, TypeNumber, out.Type()) 168 | assert.Equal(t, Number(25), out.(Number)) 169 | assert.Equal(t, "25", out.String()) 170 | 171 | err = s.Close() 172 | assert.NoError(t, err) 173 | } 174 | 175 | func TestEnrich(t *testing.T) { 176 | s, err := newScript("fixtures/enrich.lua") 177 | assert.NoError(t, err) 178 | 179 | out, err := s.Run(context.Background(), map[string]any{ 180 | "A": "apples", 181 | "B": "oranges", 182 | }) 183 | assert.NoError(t, err) 184 | assert.Equal(t, TypeTable, out.Type()) 185 | assert.EqualValues(t, map[string]any{ 186 | "A": "apples", 187 | "B": "oranges", 188 | "age": 30.0, 189 | "name": "roman", 190 | }, out.(Table).Native()) 191 | } 192 | 193 | func TestErrorMessage(t *testing.T) { 194 | s, err := newScript("fixtures/error.lua") 195 | assert.NoError(t, err) 196 | 197 | _, err = s.Run(context.Background(), nil) 198 | assert.Error(t, err) 199 | assert.Contains(t, err.Error(), "error with input (roman)") 200 | } 201 | 202 | func TestErrorMessage1(t *testing.T) { 203 | s, err := newScript("fixtures/error1.lua") 204 | assert.NoError(t, err) 205 | 206 | _, err = s.Run(context.Background(), map[string]string{ 207 | "test1": "default", 208 | }) 209 | assert.Error(t, err) 210 | assert.Contains(t, err.Error(), "throwing error") 211 | } 212 | 213 | func TestEnrichComplexTable(t *testing.T) { 214 | s, err := newScript("fixtures/enrich.lua") 215 | assert.NoError(t, err) 216 | 217 | v, err := s.Run(context.Background(), map[string][]float64{ 218 | "A": {1, 2, 3}, 219 | "B": {1, 2, 3}, 220 | }) 221 | 222 | assert.NoError(t, err) 223 | assert.NotNil(t, v) 224 | assert.Equal(t, Table{ 225 | "A": Numbers{1, 2, 3}, 226 | "B": Numbers{1, 2, 3}, 227 | "age": Number(30), 228 | "name": String("roman"), 229 | }, v) 230 | } 231 | 232 | func TestUserdata(t *testing.T) { 233 | s, err := FromString("sandbox", ` 234 | function main(request) 235 | return type(request) 236 | end 237 | `) 238 | assert.NoError(t, err) 239 | 240 | out, err := s.Run(context.Background(), []map[string]any{ 241 | {"a": 1.0, "b": 2.0}, 242 | {"a": 10.0, "b": 20.0}, 243 | }) 244 | 245 | assert.NoError(t, err) 246 | assert.Equal(t, String("table"), out) 247 | } 248 | 249 | func Test_Any(t *testing.T) { 250 | s, err := newScript("fixtures/any.lua") 251 | assert.NoError(t, err) 252 | 253 | out, err := s.Run(context.Background(), []float64{1.1, 2.1}) 254 | assert.NoError(t, err) 255 | assert.Equal(t, TypeNumbers, out.Type()) 256 | assert.Equal(t, []float64{1.1, 2.1}, out.Native()) 257 | } 258 | -------------------------------------------------------------------------------- /script.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Roman Atachiants and contributors. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | package lua 5 | 6 | import ( 7 | "bufio" 8 | "bytes" 9 | "context" 10 | "errors" 11 | "fmt" 12 | "io" 13 | "math" 14 | "runtime" 15 | "sync" 16 | 17 | "github.com/kelindar/lua/json" 18 | lua "github.com/yuin/gopher-lua" 19 | "github.com/yuin/gopher-lua/parse" 20 | ) 21 | 22 | var ( 23 | errInvalidScript = errors.New("lua: script is not in a valid state") 24 | ) 25 | 26 | // Script represents a LUA script 27 | type Script struct { 28 | lock sync.RWMutex 29 | name string // The name of the script 30 | conc int // The concurrency setting for the VM pool 31 | pool pool // The pool of runtimes for concurrent use 32 | mods []Module // The injected modules 33 | code *lua.FunctionProto // The precompiled code 34 | } 35 | 36 | // New creates a new script from an io.Reader 37 | func New(name string, source io.Reader, concurrency int, modules ...Module) (*Script, error) { 38 | if concurrency <= 0 { 39 | concurrency = defaultConcurrency 40 | } 41 | 42 | script := &Script{ 43 | name: name, 44 | mods: modules, 45 | conc: concurrency, 46 | } 47 | return script, script.Update(source) 48 | } 49 | 50 | // FromReader reads a script fron an io.Reader 51 | func FromReader(name string, r io.Reader, modules ...Module) (*Script, error) { 52 | return New(name, r, 0, modules...) 53 | } 54 | 55 | // FromString reads a script fron a string 56 | func FromString(name, code string, modules ...Module) (*Script, error) { 57 | return New(name, bytes.NewBufferString(code), 0, modules...) 58 | } 59 | 60 | // Name returns the name of the script 61 | func (s *Script) Name() string { 62 | return s.name 63 | } 64 | 65 | // Concurrency returns the concurrency setting of the script 66 | func (s *Script) Concurrency() int { 67 | return s.conc 68 | } 69 | 70 | // Run runs the main function of the script with arguments. 71 | func (s *Script) Run(ctx context.Context, args ...any) (Value, error) { 72 | 73 | // Protect swapping of the pools when the script is updated. 74 | s.lock.RLock() 75 | defer s.lock.RUnlock() 76 | 77 | // Acquire and release the pool of VMs, given our read lock we can still 78 | // enter here concurrently so the pool must also be thread-safe. 79 | vm := s.pool.Acquire() 80 | defer s.pool.Release(vm) 81 | 82 | // Run the script 83 | return vm.Run(ctx, args) 84 | } 85 | 86 | // Update updates the content of the script. 87 | func (s *Script) Update(r io.Reader) (err error) { 88 | code, err := s.compile(r) 89 | if err != nil { 90 | return err 91 | } 92 | 93 | // Protect from now on, as we need to update the script while loading 94 | s.lock.Lock() 95 | defer s.lock.Unlock() 96 | 97 | // Create a new pool of VMs 98 | s.code = code 99 | s.pool, err = newPool(s, s.conc) 100 | return 101 | } 102 | 103 | // Compile compiles a script into a function that can be shared. 104 | func (s *Script) compile(r io.Reader) (*lua.FunctionProto, error) { 105 | reader := bufio.NewReader(r) 106 | chunk, err := parse.Parse(reader, s.name) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | // Compile into a function 112 | return lua.Compile(chunk, s.name) 113 | } 114 | 115 | // LoadModules loads in the prerequisite modules 116 | func (s *Script) loadModules(runtime *lua.LState) error { 117 | runtime.PreloadModule("json", json.Loader) 118 | for _, m := range s.mods { 119 | if err := m.inject(runtime); err != nil { 120 | return err 121 | } 122 | } 123 | 124 | return nil 125 | } 126 | 127 | // Close closes the script and cleanly disposes of its resources. 128 | func (s *Script) Close() error { 129 | return nil 130 | } 131 | 132 | // findFunction extracts a global function 133 | func findFunction(runtime *lua.LState, name string) (*lua.LFunction, error) { 134 | fn := runtime.GetGlobal(name) 135 | if fn == nil || fn.Type() != lua.LTFunction { 136 | return nil, fmt.Errorf("lua: %s() function not found", name) 137 | } 138 | 139 | return fn.(*lua.LFunction), nil 140 | } 141 | 142 | // -------------------------------------------------------------------- 143 | 144 | // VM represents a single VM which can only be ran serially. 145 | type vm struct { 146 | argn int // The number of arguments 147 | exec *lua.LState // The pool of runtimes for concurrent use 148 | main *lua.LFunction // The main function 149 | } 150 | 151 | // newVM creates a new VM for a script 152 | func newVM(s *Script) (*vm, error) { 153 | l := newState() 154 | v := &vm{ 155 | exec: l, 156 | argn: 0, 157 | main: nil, 158 | } 159 | 160 | // Push the function to the runtime 161 | codeFn := v.exec.NewFunctionFromProto(s.code) 162 | v.exec.Push(codeFn) 163 | 164 | // Inject the modules 165 | if err := s.loadModules(v.exec); err != nil { 166 | return nil, err 167 | } 168 | 169 | // Initialize by calling the script 170 | if err := v.exec.PCall(0, lua.MultRet, nil); err != nil { 171 | return nil, err 172 | } 173 | 174 | // If we have a main function, set it 175 | if mainFn, err := findFunction(v.exec, "main"); err == nil { 176 | v.argn = int(mainFn.Proto.NumParameters) 177 | v.main = mainFn 178 | } 179 | return v, nil 180 | } 181 | 182 | // Run runs the main function of the script with arguments. 183 | func (v *vm) Run(ctx context.Context, args []any) (Value, error) { 184 | if v.main == nil { 185 | return nil, errInvalidScript 186 | } 187 | 188 | // Push the arguments into the state 189 | exec := v.exec 190 | exec.SetContext(ctx) 191 | exec.Push(v.main) 192 | for _, arg := range args { 193 | exec.Push(lvalueOf(exec, arg)) 194 | } 195 | 196 | // Call the main function 197 | if err := exec.PCall(len(args), 1, nil); err != nil { 198 | return nil, err 199 | } 200 | 201 | // Pop the returned value 202 | result := exec.Get(-1) 203 | exec.Pop(1) 204 | return resultOf(result), nil 205 | } 206 | 207 | // newState creates a new LUA state 208 | func newState() *lua.LState { 209 | return lua.NewState(lua.Options{ 210 | RegistrySize: 1024 * 4, // this is the initial size of the registry 211 | RegistryMaxSize: 1024 * 128, // this is the maximum size that the registry can grow to. If set to `0` (the default) then the registry will not auto grow 212 | RegistryGrowStep: 32, // this is how much to step up the registry by each time it runs out of space. The default is `32`. 213 | CallStackSize: 64, // this is the maximum callstack size of this LState 214 | MinimizeStackMemory: true, // Defaults to `false` if not specified. If set, the callstack will auto grow and shrink as needed up to a max of `CallStackSize`. If not set, the callstack will be fixed at `CallStackSize`. 215 | }) 216 | } 217 | 218 | // -------------------------------------------------------------------- 219 | 220 | // defaultConcurrency sets the default concurrency for the VM pool 221 | var defaultConcurrency = int(math.Min( 222 | float64(runtime.GOMAXPROCS(-1)), float64(runtime.NumCPU()), 223 | )) 224 | 225 | // Pool holds a pool of runtimes. 226 | type pool chan *vm 227 | 228 | // newPool creates a new pool of runtimes. 229 | func newPool(s *Script, concurrency int) (pool, error) { 230 | pool := make(pool, concurrency) 231 | for i := 0; i < concurrency; i++ { 232 | vm, err := newVM(s) 233 | if err != nil { 234 | return nil, err 235 | } 236 | 237 | pool <- vm 238 | } 239 | 240 | return pool, nil 241 | } 242 | 243 | // Acquire gets a state from the pool. 244 | func (p pool) Acquire() (vm *vm) { 245 | return <-p // Wait until we have a VM 246 | } 247 | 248 | // Release returns a state to the pool. 249 | func (p pool) Release(vm *vm) { 250 | select { 251 | case p <- vm: 252 | default: // Discard 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /script_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Roman Atachiants and contributors. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | package lua 5 | 6 | import ( 7 | "context" 8 | "encoding/json" 9 | "os" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func newScript(file string, mods ...Module) (*Script, error) { 16 | f, _ := os.Open(file) 17 | mods = append(mods, testModule()) 18 | return FromReader("test.lua", f, mods...) 19 | } 20 | 21 | type Person struct { 22 | Name string 23 | } 24 | 25 | /* 26 | Benchmark_Serial/fib-10 7079620 169.1 ns/op 16 B/op 2 allocs/op 27 | Benchmark_Serial/empty-10 9799070 122.8 ns/op 0 B/op 0 allocs/op 28 | Benchmark_Serial/update-10 1353627 880.6 ns/op 224 B/op 14 allocs/op 29 | Benchmark_Serial/table-10 3123861 386.4 ns/op 57 B/op 3 allocs/op 30 | Benchmark_Serial/sleep-sigle-10 100 11010793 ns/op 0 B/op 0 allocs/op 31 | Benchmark_Serial/sleep-multi-10 1093 1108391 ns/op 0 B/op 0 allocs/op 32 | */ 33 | func Benchmark_Serial(b *testing.B) { 34 | b.Run("fib", func(b *testing.B) { 35 | s, _ := newScript("fixtures/fib.lua") 36 | b.ReportAllocs() 37 | b.ResetTimer() 38 | for i := 0; i < b.N; i++ { 39 | s.Run(context.Background(), 1) 40 | } 41 | }) 42 | 43 | b.Run("empty", func(b *testing.B) { 44 | s, _ := newScript("fixtures/empty.lua") 45 | b.ReportAllocs() 46 | b.ResetTimer() 47 | for i := 0; i < b.N; i++ { 48 | s.Run(context.Background()) 49 | } 50 | }) 51 | 52 | b.Run("update", func(b *testing.B) { 53 | s, _ := newScript("fixtures/update.lua") 54 | input := &Person{Name: "Roman"} 55 | b.ReportAllocs() 56 | b.ResetTimer() 57 | for i := 0; i < b.N; i++ { 58 | s.Run(context.Background(), input) 59 | } 60 | }) 61 | 62 | b.Run("table", func(b *testing.B) { 63 | s, _ := newScript("fixtures/empty.lua") 64 | input := Table{ 65 | "hello": String("world"), 66 | "next": Bool(true), 67 | "age": Number(10), 68 | } 69 | 70 | b.ReportAllocs() 71 | b.ResetTimer() 72 | for i := 0; i < b.N; i++ { 73 | s.Run(context.Background(), input) 74 | } 75 | }) 76 | 77 | b.Run("sleep-sigle", func(b *testing.B) { 78 | s, _ := newScript("fixtures/sleep.lua") 79 | b.ReportAllocs() 80 | b.ResetTimer() 81 | for i := 0; i < b.N; i++ { 82 | s.Run(context.Background()) 83 | } 84 | }) 85 | 86 | b.Run("sleep-multi", func(b *testing.B) { 87 | s, _ := newScript("fixtures/sleep.lua") 88 | b.RunParallel(func(pb *testing.PB) { 89 | b.ReportAllocs() 90 | b.ResetTimer() 91 | for pb.Next() { 92 | s.Run(context.Background()) 93 | } 94 | }) 95 | }) 96 | 97 | } 98 | 99 | /* 100 | cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz 101 | Benchmark_Module/echo-12 2925387 405.3 ns/op 48 B/op 3 allocs/op 102 | Benchmark_Module/hash-12 3037982 388.2 ns/op 32 B/op 3 allocs/op 103 | */ 104 | func Benchmark_Module(b *testing.B) { 105 | b.Run("echo", func(b *testing.B) { 106 | s, _ := newScript("fixtures/echo.lua") 107 | b.ReportAllocs() 108 | b.ResetTimer() 109 | for i := 0; i < b.N; i++ { 110 | s.Run(context.Background(), "abc") 111 | } 112 | }) 113 | 114 | b.Run("hash", func(b *testing.B) { 115 | s, _ := newScript("fixtures/hash.lua") 116 | b.ReportAllocs() 117 | b.ResetTimer() 118 | for i := 0; i < b.N; i++ { 119 | s.Run(context.Background(), "abc") 120 | } 121 | }) 122 | } 123 | 124 | // Benchmark_Fib_Parallel-8 3893732 268 ns/op 16 B/op 2 allocs/op 125 | func Benchmark_Fib_Parallel(b *testing.B) { 126 | s, _ := newScript("fixtures/fib.lua") 127 | b.ReportAllocs() 128 | b.ResetTimer() 129 | b.RunParallel(func(pb *testing.PB) { 130 | for pb.Next() { 131 | s.Run(context.Background(), 1) 132 | } 133 | }) 134 | } 135 | 136 | func Test_Update(t *testing.T) { 137 | s, err := newScript("fixtures/update.lua") 138 | assert.NoError(t, err) 139 | assert.Equal(t, "test.lua", s.Name()) 140 | 141 | input := &Person{ 142 | Name: "Roman", 143 | } 144 | out, err := s.Run(context.Background(), input) 145 | assert.NoError(t, err) 146 | assert.Equal(t, TypeString, out.Type()) 147 | assert.Equal(t, "Updated", input.Name) 148 | assert.Equal(t, "Updated", out.String()) 149 | } 150 | 151 | func Test_Fib(t *testing.T) { 152 | 153 | s, err := newScript("fixtures/fib.lua") 154 | assert.NoError(t, err) 155 | 156 | out, err := s.Run(context.Background(), 10) 157 | assert.NoError(t, err) 158 | assert.Equal(t, TypeNumber, out.Type()) 159 | assert.Equal(t, Number(89), out.(Number)) 160 | assert.Equal(t, "89", out.String()) 161 | 162 | err = s.Close() 163 | assert.NoError(t, err) 164 | } 165 | 166 | func Test_Empty(t *testing.T) { 167 | s, err := newScript("fixtures/empty.lua") 168 | assert.NoError(t, err) 169 | 170 | out, err := s.Run(context.Background()) 171 | assert.NoError(t, err) 172 | assert.Equal(t, TypeNil, out.Type()) 173 | } 174 | 175 | func Test_Print(t *testing.T) { 176 | s, err := newScript("fixtures/print.lua") 177 | assert.NoError(t, err) 178 | 179 | out, err := s.Run(context.Background(), &Person{ 180 | Name: "Roman", 181 | }) 182 | assert.NoError(t, err) 183 | assert.Equal(t, TypeString, out.Type()) 184 | assert.Equal(t, "Hello, Roman!", out.String()) 185 | } 186 | 187 | func Test_InvalidScript(t *testing.T) { 188 | _, err := FromString("", ` 189 | xxx main() 190 | local x = 1 191 | end`) 192 | assert.Error(t, err) 193 | } 194 | 195 | func Test_NoMain(t *testing.T) { 196 | 197 | { 198 | s, err := FromString("", `main = 1`) 199 | assert.NoError(t, err) 200 | 201 | _, err = s.Run(context.Background()) 202 | assert.Error(t, err) 203 | } 204 | 205 | { 206 | s, err := FromString("", ` 207 | function notmain() 208 | local x = 1 209 | end`) 210 | assert.NoError(t, err) 211 | 212 | _, err = s.Run(context.Background()) 213 | assert.Error(t, err) 214 | } 215 | 216 | { 217 | s, err := FromString("", ` 218 | function xxx() 219 | local x = 1 220 | end`) 221 | assert.NoError(t, err) 222 | 223 | _, err = s.Run(context.Background()) 224 | assert.Error(t, err) 225 | } 226 | } 227 | 228 | func Test_Error(t *testing.T) { 229 | { 230 | _, err := FromString("", ` 231 | error() 232 | function main() 233 | local x = 1 234 | end`) 235 | assert.Error(t, err) 236 | } 237 | 238 | { 239 | s, err := FromString("", ` 240 | function main() 241 | error() 242 | end`) 243 | assert.NoError(t, err) 244 | _, err = s.Run(context.Background()) 245 | assert.Error(t, err) 246 | } 247 | } 248 | 249 | func Test_JSON(t *testing.T) { 250 | input := map[string]any{ 251 | "a": 123, 252 | "b": "hello", 253 | "c": 10.15, 254 | "d": true, 255 | "e": &Person{Name: "Roman"}, 256 | } 257 | 258 | s, err := newScript("fixtures/json.lua") 259 | assert.NoError(t, err) 260 | 261 | out, err := s.Run(context.Background(), input) 262 | assert.NoError(t, err) 263 | assert.Equal(t, TypeString, out.Type()) 264 | assert.Equal(t, `{"a":123,"b":"hello","c":10.15,"d":true,"e":{"Name":"Roman"}}`, 265 | out.String()) 266 | } 267 | 268 | func TestPooledTables(t *testing.T) { 269 | input := []any{ 270 | Table{"hello": String("world")}, 271 | Array{String("hello"), String("world")}, 272 | Strings{"hello", "world"}, 273 | Bools{true, false}, 274 | Numbers{1, 2, 3}, 275 | } 276 | 277 | s, err := newScript("fixtures/json.lua") 278 | assert.NoError(t, err) 279 | 280 | out, err := s.Run(context.Background(), input...) 281 | assert.NoError(t, err) 282 | assert.Equal(t, TypeString, out.Type()) 283 | assert.Equal(t, `{"hello":"world"}`, 284 | out.String()) 285 | } 286 | 287 | func TestNewScript(t *testing.T) { 288 | f, _ := os.Open("fixtures/json.lua") 289 | s, err := New("test.lua", f, 10) 290 | assert.NoError(t, err) 291 | assert.Equal(t, 10, s.Concurrency()) 292 | } 293 | 294 | func TestEmptyArray(t *testing.T) { 295 | s, err := newScript("fixtures/array.lua") 296 | assert.NoError(t, err) 297 | 298 | out, err := s.Run(context.Background()) 299 | b, err := json.Marshal(out) 300 | assert.NoError(t, err) 301 | 302 | assert.JSONEq(t, `{ 303 | "empty": [], 304 | "empty_map": [], 305 | "array": [1, 2, 3], 306 | "table": [{"apple": 5}], 307 | "str": ["hello"], 308 | "int": [12], 309 | "bool": [true], 310 | "float": [12.34], 311 | "empties": [[]] 312 | }`, string(b)) 313 | } 314 | -------------------------------------------------------------------------------- /z_binary_test.go: -------------------------------------------------------------------------------- 1 | // This file was automatically generated by genny. 2 | // Any changes will be lost if this file is regenerated. 3 | // see https://github.com/cheekybits/genny 4 | 5 | package lua 6 | 7 | import ( 8 | "context" 9 | "errors" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func Test_StringString(t *testing.T) { 16 | m := &NativeModule{Name: "test"} 17 | m.Register("test1", func(v String) (String, error) { 18 | return newTestValue(TypeString).(String), nil 19 | }) 20 | m.Register("test2", func(v String) (String, error) { 21 | return newTestValue(TypeString).(String), errors.New("boom") 22 | }) 23 | 24 | { // Happy path 25 | s, err := FromString("", ` 26 | local api = require("test") 27 | function main(input) 28 | return api.test1(input) 29 | end`, m) 30 | assert.NotNil(t, s) 31 | assert.NoError(t, err) 32 | _, err = s.Run(context.Background(), newTestValue(TypeString).(String)) 33 | assert.NoError(t, err) 34 | } 35 | 36 | { // Invalid argument 37 | s, err := FromString("", ` 38 | local api = require("test") 39 | function main(input) 40 | return api.test2(input) 41 | end`, m) 42 | assert.NotNil(t, s) 43 | assert.NoError(t, err) 44 | _, err = s.Run(context.Background(), newTestValue(TypeString).(String)) 45 | assert.Error(t, err) 46 | } 47 | } 48 | 49 | func Test_StringNumber(t *testing.T) { 50 | m := &NativeModule{Name: "test"} 51 | m.Register("test1", func(v String) (Number, error) { 52 | return newTestValue(TypeNumber).(Number), nil 53 | }) 54 | m.Register("test2", func(v String) (Number, error) { 55 | return newTestValue(TypeNumber).(Number), errors.New("boom") 56 | }) 57 | 58 | { // Happy path 59 | s, err := FromString("", ` 60 | local api = require("test") 61 | function main(input) 62 | return api.test1(input) 63 | end`, m) 64 | assert.NotNil(t, s) 65 | assert.NoError(t, err) 66 | _, err = s.Run(context.Background(), newTestValue(TypeString).(String)) 67 | assert.NoError(t, err) 68 | } 69 | 70 | { // Invalid argument 71 | s, err := FromString("", ` 72 | local api = require("test") 73 | function main(input) 74 | return api.test2(input) 75 | end`, m) 76 | assert.NotNil(t, s) 77 | assert.NoError(t, err) 78 | _, err = s.Run(context.Background(), newTestValue(TypeString).(String)) 79 | assert.Error(t, err) 80 | } 81 | } 82 | 83 | func Test_StringBool(t *testing.T) { 84 | m := &NativeModule{Name: "test"} 85 | m.Register("test1", func(v String) (Bool, error) { 86 | return newTestValue(TypeBool).(Bool), nil 87 | }) 88 | m.Register("test2", func(v String) (Bool, error) { 89 | return newTestValue(TypeBool).(Bool), errors.New("boom") 90 | }) 91 | 92 | { // Happy path 93 | s, err := FromString("", ` 94 | local api = require("test") 95 | function main(input) 96 | return api.test1(input) 97 | end`, m) 98 | assert.NotNil(t, s) 99 | assert.NoError(t, err) 100 | _, err = s.Run(context.Background(), newTestValue(TypeString).(String)) 101 | assert.NoError(t, err) 102 | } 103 | 104 | { // Invalid argument 105 | s, err := FromString("", ` 106 | local api = require("test") 107 | function main(input) 108 | return api.test2(input) 109 | end`, m) 110 | assert.NotNil(t, s) 111 | assert.NoError(t, err) 112 | _, err = s.Run(context.Background(), newTestValue(TypeString).(String)) 113 | assert.Error(t, err) 114 | } 115 | } 116 | 117 | func Test_NumberString(t *testing.T) { 118 | m := &NativeModule{Name: "test"} 119 | m.Register("test1", func(v Number) (String, error) { 120 | return newTestValue(TypeString).(String), nil 121 | }) 122 | m.Register("test2", func(v Number) (String, error) { 123 | return newTestValue(TypeString).(String), errors.New("boom") 124 | }) 125 | 126 | { // Happy path 127 | s, err := FromString("", ` 128 | local api = require("test") 129 | function main(input) 130 | return api.test1(input) 131 | end`, m) 132 | assert.NotNil(t, s) 133 | assert.NoError(t, err) 134 | _, err = s.Run(context.Background(), newTestValue(TypeNumber).(Number)) 135 | assert.NoError(t, err) 136 | } 137 | 138 | { // Invalid argument 139 | s, err := FromString("", ` 140 | local api = require("test") 141 | function main(input) 142 | return api.test2(input) 143 | end`, m) 144 | assert.NotNil(t, s) 145 | assert.NoError(t, err) 146 | _, err = s.Run(context.Background(), newTestValue(TypeNumber).(Number)) 147 | assert.Error(t, err) 148 | } 149 | } 150 | 151 | func Test_NumberNumber(t *testing.T) { 152 | m := &NativeModule{Name: "test"} 153 | m.Register("test1", func(v Number) (Number, error) { 154 | return newTestValue(TypeNumber).(Number), nil 155 | }) 156 | m.Register("test2", func(v Number) (Number, error) { 157 | return newTestValue(TypeNumber).(Number), errors.New("boom") 158 | }) 159 | 160 | { // Happy path 161 | s, err := FromString("", ` 162 | local api = require("test") 163 | function main(input) 164 | return api.test1(input) 165 | end`, m) 166 | assert.NotNil(t, s) 167 | assert.NoError(t, err) 168 | _, err = s.Run(context.Background(), newTestValue(TypeNumber).(Number)) 169 | assert.NoError(t, err) 170 | } 171 | 172 | { // Invalid argument 173 | s, err := FromString("", ` 174 | local api = require("test") 175 | function main(input) 176 | return api.test2(input) 177 | end`, m) 178 | assert.NotNil(t, s) 179 | assert.NoError(t, err) 180 | _, err = s.Run(context.Background(), newTestValue(TypeNumber).(Number)) 181 | assert.Error(t, err) 182 | } 183 | } 184 | 185 | func Test_NumberBool(t *testing.T) { 186 | m := &NativeModule{Name: "test"} 187 | m.Register("test1", func(v Number) (Bool, error) { 188 | return newTestValue(TypeBool).(Bool), nil 189 | }) 190 | m.Register("test2", func(v Number) (Bool, error) { 191 | return newTestValue(TypeBool).(Bool), errors.New("boom") 192 | }) 193 | 194 | { // Happy path 195 | s, err := FromString("", ` 196 | local api = require("test") 197 | function main(input) 198 | return api.test1(input) 199 | end`, m) 200 | assert.NotNil(t, s) 201 | assert.NoError(t, err) 202 | _, err = s.Run(context.Background(), newTestValue(TypeNumber).(Number)) 203 | assert.NoError(t, err) 204 | } 205 | 206 | { // Invalid argument 207 | s, err := FromString("", ` 208 | local api = require("test") 209 | function main(input) 210 | return api.test2(input) 211 | end`, m) 212 | assert.NotNil(t, s) 213 | assert.NoError(t, err) 214 | _, err = s.Run(context.Background(), newTestValue(TypeNumber).(Number)) 215 | assert.Error(t, err) 216 | } 217 | } 218 | 219 | func Test_BoolString(t *testing.T) { 220 | m := &NativeModule{Name: "test"} 221 | m.Register("test1", func(v Bool) (String, error) { 222 | return newTestValue(TypeString).(String), nil 223 | }) 224 | m.Register("test2", func(v Bool) (String, error) { 225 | return newTestValue(TypeString).(String), errors.New("boom") 226 | }) 227 | 228 | { // Happy path 229 | s, err := FromString("", ` 230 | local api = require("test") 231 | function main(input) 232 | return api.test1(input) 233 | end`, m) 234 | assert.NotNil(t, s) 235 | assert.NoError(t, err) 236 | _, err = s.Run(context.Background(), newTestValue(TypeBool).(Bool)) 237 | assert.NoError(t, err) 238 | } 239 | 240 | { // Invalid argument 241 | s, err := FromString("", ` 242 | local api = require("test") 243 | function main(input) 244 | return api.test2(input) 245 | end`, m) 246 | assert.NotNil(t, s) 247 | assert.NoError(t, err) 248 | _, err = s.Run(context.Background(), newTestValue(TypeBool).(Bool)) 249 | assert.Error(t, err) 250 | } 251 | } 252 | 253 | func Test_BoolNumber(t *testing.T) { 254 | m := &NativeModule{Name: "test"} 255 | m.Register("test1", func(v Bool) (Number, error) { 256 | return newTestValue(TypeNumber).(Number), nil 257 | }) 258 | m.Register("test2", func(v Bool) (Number, error) { 259 | return newTestValue(TypeNumber).(Number), errors.New("boom") 260 | }) 261 | 262 | { // Happy path 263 | s, err := FromString("", ` 264 | local api = require("test") 265 | function main(input) 266 | return api.test1(input) 267 | end`, m) 268 | assert.NotNil(t, s) 269 | assert.NoError(t, err) 270 | _, err = s.Run(context.Background(), newTestValue(TypeBool).(Bool)) 271 | assert.NoError(t, err) 272 | } 273 | 274 | { // Invalid argument 275 | s, err := FromString("", ` 276 | local api = require("test") 277 | function main(input) 278 | return api.test2(input) 279 | end`, m) 280 | assert.NotNil(t, s) 281 | assert.NoError(t, err) 282 | _, err = s.Run(context.Background(), newTestValue(TypeBool).(Bool)) 283 | assert.Error(t, err) 284 | } 285 | } 286 | 287 | func Test_BoolBool(t *testing.T) { 288 | m := &NativeModule{Name: "test"} 289 | m.Register("test1", func(v Bool) (Bool, error) { 290 | return newTestValue(TypeBool).(Bool), nil 291 | }) 292 | m.Register("test2", func(v Bool) (Bool, error) { 293 | return newTestValue(TypeBool).(Bool), errors.New("boom") 294 | }) 295 | 296 | { // Happy path 297 | s, err := FromString("", ` 298 | local api = require("test") 299 | function main(input) 300 | return api.test1(input) 301 | end`, m) 302 | assert.NotNil(t, s) 303 | assert.NoError(t, err) 304 | _, err = s.Run(context.Background(), newTestValue(TypeBool).(Bool)) 305 | assert.NoError(t, err) 306 | } 307 | 308 | { // Invalid argument 309 | s, err := FromString("", ` 310 | local api = require("test") 311 | function main(input) 312 | return api.test2(input) 313 | end`, m) 314 | assert.NotNil(t, s) 315 | assert.NoError(t, err) 316 | _, err = s.Run(context.Background(), newTestValue(TypeBool).(Bool)) 317 | assert.Error(t, err) 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /value.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Roman Atachiants and contributors. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | package lua 5 | 6 | import ( 7 | "encoding/json" 8 | "fmt" 9 | "reflect" 10 | 11 | lua "github.com/yuin/gopher-lua" 12 | ) 13 | 14 | type numberType interface { 15 | ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64 16 | } 17 | 18 | type tableType interface { 19 | lcopy(*lua.LTable, *lua.LState) lua.LValue 20 | } 21 | 22 | var ( 23 | typeError = reflect.TypeOf((*error)(nil)).Elem() 24 | typeNumber = reflect.TypeOf(Number(0)) 25 | typeString = reflect.TypeOf(String("")) 26 | typeBool = reflect.TypeOf(Bool(true)) 27 | typeNumbers = reflect.TypeOf(Numbers(nil)) 28 | typeStrings = reflect.TypeOf(Strings(nil)) 29 | typeBools = reflect.TypeOf(Bools(nil)) 30 | typeTable = reflect.TypeOf(Table(nil)) 31 | typeArray = reflect.TypeOf(Array(nil)) 32 | typeValue = reflect.TypeOf((*Value)(nil)).Elem() 33 | ) 34 | 35 | var typeMap = map[reflect.Type]Type{ 36 | typeString: TypeString, 37 | typeNumber: TypeNumber, 38 | typeBool: TypeBool, 39 | typeStrings: TypeStrings, 40 | typeNumbers: TypeNumbers, 41 | typeBools: TypeBools, 42 | typeTable: TypeTable, 43 | typeArray: TypeArray, 44 | typeValue: TypeValue, 45 | } 46 | 47 | // Type represents a type of the value 48 | type Type byte 49 | 50 | // Various supported types 51 | const ( 52 | TypeNil = Type(iota) 53 | TypeBool 54 | TypeNumber 55 | TypeString 56 | TypeBools 57 | TypeNumbers 58 | TypeStrings 59 | TypeTable 60 | TypeArray 61 | TypeValue 62 | ) 63 | 64 | // Value represents a returned 65 | type Value interface { 66 | fmt.Stringer 67 | Type() Type 68 | Native() any 69 | lvalue(*lua.LState) lua.LValue 70 | } 71 | 72 | // -------------------------------------------------------------------- 73 | 74 | // Nil represents the nil value 75 | type Nil struct{} 76 | 77 | // Type returns the type of the value 78 | func (v Nil) Type() Type { 79 | return TypeNil 80 | } 81 | 82 | // String returns the string representation of the value 83 | func (v Nil) String() string { 84 | return "(nil)" 85 | } 86 | 87 | // Native returns value casted to native type 88 | func (v Nil) Native() any { 89 | return nil 90 | } 91 | 92 | // lvalue converts the value to a LUA value 93 | func (v Nil) lvalue(*lua.LState) lua.LValue { 94 | return lua.LNil 95 | } 96 | 97 | // -------------------------------------------------------------------- 98 | 99 | // Number represents the numerical value 100 | type Number float64 101 | 102 | // Type returns the type of the value 103 | func (v Number) Type() Type { 104 | return TypeNumber 105 | } 106 | 107 | // String returns the string representation of the value 108 | func (v Number) String() string { 109 | return lua.LNumber(v).String() 110 | } 111 | 112 | // Native returns value casted to native type 113 | func (v Number) Native() any { 114 | return float64(v) 115 | } 116 | 117 | // lvalue converts the value to a LUA value 118 | func (v Number) lvalue(*lua.LState) lua.LValue { 119 | return lua.LNumber(v) 120 | } 121 | 122 | // -------------------------------------------------------------------- 123 | 124 | // Numbers represents the number array value 125 | type Numbers []float64 126 | 127 | // numbersOf returns an array as a numbers array 128 | func numbersOf[T numberType](arr []T) Numbers { 129 | out := make([]float64, 0, len(arr)) 130 | for _, v := range arr { 131 | out = append(out, float64(v)) 132 | } 133 | return out 134 | } 135 | 136 | // Type returns the type of the value 137 | func (v Numbers) Type() Type { 138 | return TypeNumbers 139 | } 140 | 141 | // String returns the string representation of the value 142 | func (v Numbers) String() string { 143 | return fmt.Sprintf("%v", []float64(v)) 144 | } 145 | 146 | // Native returns value casted to native type 147 | func (v Numbers) Native() any { 148 | return []float64(v) 149 | } 150 | 151 | // lvalue converts the value to a LUA value 152 | func (v Numbers) lvalue(state *lua.LState) lua.LValue { 153 | tbl := state.CreateTable(len(v)+4, 0) 154 | return v.lcopy(tbl, state) 155 | } 156 | 157 | // lcopy copies the table to another table 158 | func (v Numbers) lcopy(dst *lua.LTable, state *lua.LState) lua.LValue { 159 | for _, item := range v { 160 | dst.Append(lua.LNumber(item)) 161 | } 162 | return dst 163 | } 164 | 165 | // -------------------------------------------------------------------- 166 | 167 | // String represents the string value 168 | type String string 169 | 170 | // Type returns the type of the value 171 | func (v String) Type() Type { 172 | return TypeString 173 | } 174 | 175 | // String returns the string representation of the value 176 | func (v String) String() string { 177 | return lua.LString(v).String() 178 | } 179 | 180 | // Native returns value casted to native type 181 | func (v String) Native() any { 182 | return string(v) 183 | } 184 | 185 | // lvalue converts the value to a LUA value 186 | func (v String) lvalue(*lua.LState) lua.LValue { 187 | return lua.LString(v) 188 | } 189 | 190 | // -------------------------------------------------------------------- 191 | 192 | // Strings represents the string array value 193 | type Strings []string 194 | 195 | // Type returns the type of the value 196 | func (v Strings) Type() Type { 197 | return TypeStrings 198 | } 199 | 200 | // String returns the string representation of the value 201 | func (v Strings) String() string { 202 | return fmt.Sprintf("%v", []string(v)) 203 | } 204 | 205 | // Native returns value casted to native type 206 | func (v Strings) Native() any { 207 | return []string(v) 208 | } 209 | 210 | // Table converts the slice to a lua table 211 | func (v Strings) table() *lua.LTable { 212 | tbl := new(lua.LTable) 213 | for _, item := range v { 214 | tbl.Append(lua.LString(item)) 215 | } 216 | return tbl 217 | } 218 | 219 | // lvalue converts the value to a LUA value 220 | func (v Strings) lvalue(state *lua.LState) lua.LValue { 221 | tbl := state.CreateTable(len(v)+4, 0) 222 | return v.lcopy(tbl, state) 223 | } 224 | 225 | // lcopy copies the table to another table 226 | func (v Strings) lcopy(dst *lua.LTable, state *lua.LState) lua.LValue { 227 | for _, item := range v { 228 | dst.Append(lua.LString(item)) 229 | } 230 | return dst 231 | } 232 | 233 | // -------------------------------------------------------------------- 234 | 235 | // Bool represents the boolean value 236 | type Bool bool 237 | 238 | // Type returns the type of the value 239 | func (v Bool) Type() Type { 240 | return TypeBool 241 | } 242 | 243 | // String returns the string representation of the value 244 | func (v Bool) String() string { 245 | return lua.LBool(v).String() 246 | } 247 | 248 | // Native returns value casted to native type 249 | func (v Bool) Native() any { 250 | return bool(v) 251 | } 252 | 253 | // lvalue converts the value to a LUA value 254 | func (v Bool) lvalue(*lua.LState) lua.LValue { 255 | return lua.LBool(v) 256 | } 257 | 258 | // -------------------------------------------------------------------- 259 | 260 | // Bools represents the boolean array value 261 | type Bools []bool 262 | 263 | // Type returns the type of the value 264 | func (v Bools) Type() Type { 265 | return TypeBools 266 | } 267 | 268 | // String returns the string representation of the value 269 | func (v Bools) String() string { 270 | return fmt.Sprintf("%v", []bool(v)) 271 | } 272 | 273 | // Native returns value casted to native type 274 | func (v Bools) Native() any { 275 | return []bool(v) 276 | } 277 | 278 | // lvalue converts the value to a LUA value 279 | func (v Bools) lvalue(state *lua.LState) lua.LValue { 280 | tbl := state.CreateTable(len(v)+4, 0) 281 | return v.lcopy(tbl, state) 282 | } 283 | 284 | // lcopy copies the table to another table 285 | func (v Bools) lcopy(dst *lua.LTable, state *lua.LState) lua.LValue { 286 | for _, item := range v { 287 | dst.Append(lua.LBool(item)) 288 | } 289 | return dst 290 | } 291 | 292 | // -------------------------------------------------------------------- 293 | 294 | // Table represents a map of string to value 295 | type Table map[string]Value 296 | 297 | // Type returns the type of the value 298 | func (v Table) Type() Type { 299 | return TypeTable 300 | } 301 | 302 | // String returns the string representation of the value 303 | func (v Table) String() string { 304 | return fmt.Sprintf("%v", map[string]Value(v)) 305 | } 306 | 307 | // Native returns value casted to native type 308 | func (v Table) Native() any { 309 | out := make(map[string]any, len(v)) 310 | for key, elem := range v { 311 | out[key] = elem.Native() 312 | } 313 | return out 314 | } 315 | 316 | // lvalue converts the value to a LUA value 317 | func (v Table) lvalue(state *lua.LState) lua.LValue { 318 | tbl := state.CreateTable(0, len(v)+4) 319 | return v.lcopy(tbl, state) 320 | } 321 | 322 | // lcopy copies the table to another table 323 | func (v Table) lcopy(dst *lua.LTable, state *lua.LState) lua.LValue { 324 | for k, item := range v { 325 | dst.RawSetString(k, item.lvalue(state)) 326 | } 327 | return dst 328 | } 329 | 330 | // UnmarshalJSON unmarshals the type from JSON 331 | func (v *Table) UnmarshalJSON(b []byte) error { 332 | var data map[string]any 333 | if err := json.Unmarshal(b, &data); err != nil { 334 | return err 335 | } 336 | 337 | *v = mapAsTable(data) 338 | return nil 339 | } 340 | 341 | // -------------------------------------------------------------------- 342 | 343 | // Array represents the array of values 344 | type Array []Value 345 | 346 | // Type returns the type of the value 347 | func (v Array) Type() Type { 348 | return TypeArray 349 | } 350 | 351 | // String returns the string representation of the value 352 | func (v Array) String() string { 353 | return fmt.Sprintf("%+v", []Value(v)) 354 | } 355 | 356 | // Native returns value casted to native type 357 | func (v Array) Native() any { 358 | out := make([]any, len(v)) 359 | for i, elem := range v { 360 | out[i] = elem.Native() 361 | } 362 | return out 363 | } 364 | 365 | // lvalue converts the value to a LUA value 366 | func (v Array) lvalue(state *lua.LState) lua.LValue { 367 | tbl := state.CreateTable(len(v)+4, 0) 368 | return v.lcopy(tbl, state) 369 | } 370 | 371 | // lcopy copies the table to another table 372 | func (v Array) lcopy(dst *lua.LTable, state *lua.LState) lua.LValue { 373 | for _, item := range v { 374 | dst.Append(item.lvalue(state)) 375 | } 376 | return dst 377 | } 378 | --------------------------------------------------------------------------------