Skip to content

Commit 426cbb1

Browse files
authored
Added yaml.encode and a few test cases for it (#18)
1 parent 8996a09 commit 426cbb1

File tree

8 files changed

+229
-11
lines changed

8 files changed

+229
-11
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ require (
1414
github.com/mitchellh/mapstructure v1.3.2 // indirect
1515
github.com/montanaflynn/stats v0.6.3
1616
github.com/prometheus/client_golang v1.5.1
17+
github.com/stretchr/testify v1.5.1
1718
github.com/technoweenie/multipartstreamer v1.0.1
1819
github.com/yuin/gluamapper v0.0.0-20150323120927-d836955830e7
1920
github.com/yuin/gopher-lua v0.0.0-20200603152657-dc2b0ca8b37e

go.sum

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ github.com/cbroglie/mustache v1.0.1 h1:ivMg8MguXq/rrz2eu3tw6g3b16+PQhoTn6EZAhst2
1414
github.com/cbroglie/mustache v1.0.1/go.mod h1:R/RUa+SobQ14qkP4jtx5Vke5sDytONDQXNLPY/PO69g=
1515
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
1616
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
17-
github.com/cheggaaa/pb v1.0.29 h1:FckUN5ngEk2LpvuG0fw1GEFx6LtyY2pWI/Z2QgCnEYo=
1817
github.com/cheggaaa/pb/v3 v3.0.5 h1:lmZOti7CraK9RSjzExsY53+WWfub9Qv13B5m4ptEoPE=
1918
github.com/cheggaaa/pb/v3 v3.0.5/go.mod h1:X1L61/+36nz9bjIsrDU52qHKOQukUQe2Ge+YvGuquCw=
2019
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=

yaml/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Usage
44

5+
### decode
56
```lua
67
local yaml = require("yaml")
78
local inspect = require("inspect")
@@ -18,3 +19,14 @@ print(inspect(result, {newline="", indent=""}))
1819
-- {a = {b = 1}}
1920
```
2021

22+
### encode
23+
```lua
24+
local yaml = require("yaml")
25+
local encoded, err = yaml.encode({a = {b = 1}})
26+
if err then error(err) end
27+
print(encoded)
28+
-- Output:
29+
-- a:
30+
-- b: 1
31+
--
32+
```

yaml/api.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
package yaml
33

44
import (
5+
"fmt"
6+
57
lua "github.com/yuin/gopher-lua"
68
yaml "gopkg.in/yaml.v2"
79
)
@@ -21,6 +23,80 @@ func Decode(L *lua.LState) int {
2123
return 1
2224
}
2325

26+
// Encode lua yaml.encode(any) returns (string, error)
27+
func Encode(L *lua.LState) int {
28+
arg := L.CheckAny(1)
29+
var value interface{}
30+
err := L.GPCall(func(L *lua.LState) int {
31+
visited := make(map[*lua.LTable]bool)
32+
value = toYAML(L, visited, arg)
33+
return 0
34+
}, lua.LNil)
35+
if err != nil {
36+
L.Push(lua.LNil)
37+
L.Push(lua.LString(err.Error()))
38+
return 2
39+
}
40+
data, err := yaml.Marshal(value)
41+
if err != nil {
42+
L.Push(lua.LNil)
43+
L.Push(lua.LString(err.Error()))
44+
return 2
45+
}
46+
L.Push(lua.LString(data))
47+
return 1
48+
}
49+
50+
func tableIsSlice(table *lua.LTable) bool {
51+
expectedKey := lua.LNumber(1)
52+
for key, _ := table.Next(lua.LNil); key != lua.LNil; key, _ = table.Next(key) {
53+
if expectedKey != key {
54+
return false
55+
}
56+
expectedKey++
57+
}
58+
return true
59+
}
60+
61+
func toYAML(L *lua.LState, visited map[*lua.LTable]bool, value lua.LValue) interface{} {
62+
switch value.Type() {
63+
case lua.LTNil:
64+
return nil
65+
case lua.LTBool:
66+
return lua.LVAsBool(value)
67+
case lua.LTNumber:
68+
num := float64(lua.LVAsNumber(value))
69+
intNum := int64(num)
70+
if num != float64(intNum) {
71+
return num
72+
}
73+
return intNum
74+
case lua.LTString:
75+
return lua.LVAsString(value)
76+
case lua.LTTable:
77+
valueTable := value.(*lua.LTable)
78+
if visited[valueTable] {
79+
L.RaiseError("nested table %s", valueTable)
80+
}
81+
visited[valueTable] = true
82+
if tableIsSlice(valueTable) {
83+
ret := make([]interface{}, 0, valueTable.Len())
84+
valueTable.ForEach(func(_ lua.LValue, tValue lua.LValue) {
85+
ret = append(ret, toYAML(L, visited, tValue))
86+
})
87+
return ret
88+
}
89+
ret := make(map[interface{}]interface{})
90+
valueTable.ForEach(func(tKey lua.LValue, tValue lua.LValue) {
91+
ret[toYAML(L, visited, tKey)] = toYAML(L, visited, tValue)
92+
})
93+
return ret
94+
default:
95+
L.RaiseError(fmt.Sprintf("cannot encode values with %s in them", value.Type()))
96+
return nil
97+
}
98+
}
99+
24100
func fromYAML(L *lua.LState, value interface{}) lua.LValue {
25101
switch converted := value.(type) {
26102
case bool:

yaml/api_test.go

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,38 @@ package yaml
33
import (
44
"testing"
55

6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
69
lua "github.com/yuin/gopher-lua"
710
)
811

912
func TestApi(t *testing.T) {
10-
state := lua.NewState()
11-
Preload(state)
12-
if err := state.DoFile("./test/test_api.lua"); err != nil {
13-
t.Fatalf("execute test: %s\n", err.Error())
14-
}
13+
// Setup the VM
14+
L := lua.NewState()
15+
defer L.Close()
16+
Preload(L)
17+
18+
// Load the test cases from file
19+
err := L.DoFile("./test/test_api.lua")
20+
require.NoError(t, err)
21+
test := L.CheckTable(1)
22+
L.Pop(1)
23+
24+
// For each method on the returned test object, invoke it safely with PCall.
25+
testCount := 0
26+
test.ForEach(func(key lua.LValue, value lua.LValue) {
27+
if value.Type() != lua.LTFunction {
28+
return
29+
}
30+
testCount++
31+
t.Run(lua.LVAsString(key), func(t *testing.T) {
32+
L.Push(value)
33+
L.Push(test)
34+
require.NoError(t, L.PCall(1, 0, nil))
35+
})
36+
})
37+
38+
// Ensure we ran non-zero tests.
39+
assert.NotEqual(t, 0, testCount, "test should not be empty")
1540
}

yaml/example_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,22 @@ a:
2929
// Output:
3030
// {a = {b = 1}}
3131
}
32+
33+
func ExampleEncode() {
34+
state := lua.NewState()
35+
Preload(state)
36+
inspect.Preload(state)
37+
source := `
38+
local yaml = require("yaml")
39+
local encoded, err = yaml.encode({a = {b = 1}})
40+
if err then error(err) end
41+
print(encoded)
42+
`
43+
if err := state.DoString(source); err != nil {
44+
log.Fatal(err.Error())
45+
}
46+
// Output:
47+
// a:
48+
// b: 1
49+
//
50+
}

yaml/loader.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@ func Loader(L *lua.LState) int {
2222

2323
var api = map[string]lua.LGFunction{
2424
"decode": Decode,
25+
"encode": Encode,
2526
}

yaml/test/test_api.lua

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,95 @@
11
local yaml = require("yaml")
2-
local text = [[
2+
local test = {}
3+
4+
-- test decode
5+
function test:decode()
6+
local text = [[
7+
a:
8+
b: 1
9+
]]
10+
local result, err = yaml.decode(text)
11+
assert(not err, tostring(err))
12+
assert(result["a"]["b"] == 1, tostring(result["a"]["b"]))
13+
print("done: yaml.decode()")
14+
end
15+
16+
-- test decode with no args throws exception
17+
function test:decode_no_args()
18+
local ok, errMsg = pcall(yaml.decode)
19+
assert(not ok)
20+
assert(errMsg)
21+
assert(errMsg:find("(string expected, got nil)"), tostring(errMsg))
22+
end
23+
24+
-- test encode of decoded(text) == text
25+
function test:decoded_text_equals_text()
26+
local text = [[
327
a:
428
b: 1
529
]]
6-
local result, err = yaml.decode(text)
7-
if err then error(err) end
8-
if not(result["a"]["b"] == 1) then error("decode error") end
30+
local result = { a = { b = 1 } }
31+
local encodedResult, err = yaml.encode(result)
32+
assert(not err, tostring(err))
33+
assert(encodedResult == text, tostring(encodedResult)
34+
)
35+
end
36+
37+
-- test encode(slice) works
38+
function test:encode_slice_works()
39+
local encodedSlice = yaml.encode({ "foo", "bar", "baz" })
40+
assert(encodedSlice == [[
41+
- foo
42+
- bar
43+
- baz
44+
]], tostring(encodedSlice))
45+
end
46+
47+
-- test encode(sparse slice) works
48+
function test:encode_sparse_slice_returns_map()
49+
local slice = { [0] = "foo", [1] = "bar", [2] = "baz" }
50+
local encodedSlice = yaml.encode(slice)
51+
assert(encodedSlice == [[
52+
0: foo
53+
1: bar
54+
2: baz
55+
]], tostring(encodedSlice))
56+
end
57+
58+
-- test encode(map) works
59+
function test:encode_map_returns_map()
60+
local map = { foo = "bar", bar = { 1, 2, 3.45 } }
61+
local encodedMap = yaml.encode(map)
62+
assert(encodedMap == [[
63+
bar:
64+
- 1
65+
- 2
66+
- 3.45
67+
foo: bar
68+
]], tostring(encodedMap))
69+
end
70+
71+
-- test encode(function) fails
72+
function test:encode_function_fails()
73+
local _, errMsg = yaml.encode(function()
74+
return ""
75+
end)
76+
assert(errMsg)
77+
assert(errMsg:find("cannot encode values with function in them"), errMsg)
78+
79+
-- test encode with no args throws exception
80+
local ok, errMsg = pcall(yaml.encode)
81+
assert(not ok)
82+
assert(errMsg:find("(value expected)"), tostring(errMsg))
83+
end
84+
85+
-- test cycles
86+
function test:cycles_return_error()
87+
local t1 = {}
88+
local t2 = { t1 = t1 }
89+
t1[t2] = t2
90+
local _, errMsg = yaml.encode(t1)
91+
assert(errMsg)
92+
assert(errMsg:find("nested table"), tostring(errMsg))
93+
end
994

10-
print("done: yaml.decode()")
95+
return test

0 commit comments

Comments
 (0)