lolly/gjson/encode.go
xfy 376b9fd33c feat(gjson): add Lua JSON encoding/decoding library
Add gjson package providing high-performance JSON encoding/decoding
for gopher-lua with lua-cjson API compatibility. Uses goccy/go-json
as the underlying JSON engine.

Features:
- Full lua-cjson API compatibility for OpenResty migration
- Sparse array detection and handling
- Maximum nesting depth control for encode/decode
- Number precision control
- Independent configuration instances via gjson.new()
- gjson.null sentinel for JSON null values

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:44:31 +08:00

220 lines
5.4 KiB
Go

package gjson
import (
"fmt"
"strconv"
"strings"
json "github.com/goccy/go-json"
glua "github.com/yuin/gopher-lua"
)
// encodeLuaValue converts a Lua value to a JSON string.
func encodeLuaValue(L *glua.LState, value glua.LValue, config *Config, depth int) (string, error) {
switch value.Type() {
case glua.LTNil:
return "null", nil
case glua.LTBool:
if value == glua.LTrue {
return "true", nil
}
return "false", nil
case glua.LTNumber:
num := float64(value.(glua.LNumber))
return formatNumber(num, config.encodeNumberPrecision), nil
case glua.LTString:
str := string(value.(glua.LString))
// Use go-json for proper string escaping
result, err := json.Marshal(str)
if err != nil {
return "", fmt.Errorf("failed to encode string: %w", err)
}
return string(result), nil
case glua.LTTable:
return encodeTable(L, value.(*glua.LTable), config, depth)
case glua.LTUserData:
if isNull(value) {
return "null", nil
}
return "", fmt.Errorf("cannot encode userdata (not gjson.null)")
case glua.LTFunction:
return "", fmt.Errorf("cannot encode function")
case glua.LTThread:
return "", fmt.Errorf("cannot encode thread")
case glua.LTChannel:
return "", fmt.Errorf("cannot encode channel")
default:
return "", fmt.Errorf("cannot encode unknown type: %s", value.Type())
}
}
// encodeTable converts a Lua table to JSON (array or object).
func encodeTable(L *glua.LState, tbl *glua.LTable, config *Config, depth int) (string, error) {
// Check depth limit
if depth >= config.encodeMaxDepth {
return "", fmt.Errorf("maximum nesting depth %d exceeded", config.encodeMaxDepth)
}
// Determine if table is an array or object
isArray, maxIndex, _ := checkArrayType(tbl, config)
if isArray {
return encodeArray(L, tbl, maxIndex, config, depth)
}
return encodeObject(L, tbl, config, depth)
}
// checkArrayType determines if a table should be encoded as an array.
// Returns: (isArray, maxIndex, count)
func checkArrayType(tbl *glua.LTable, config *Config) (bool, int, int) {
maxIndex := 0
count := 0
hasStringKey := false
tbl.ForEach(func(key, value glua.LValue) {
switch k := key.(type) {
case glua.LNumber:
// Protect against integer overflow - only accept positive integers within int range
floatIdx := float64(k)
if floatIdx < 1 || floatIdx > float64(int(^uint(0)>>1)) {
hasStringKey = true // Treat out-of-range as object key
return
}
idx := int(k)
if idx > maxIndex {
maxIndex = idx
}
count++
case glua.LString:
hasStringKey = true
}
})
// If there are string keys, it's an object
if hasStringKey {
return false, maxIndex, count
}
// Empty table is encoded as empty object (lua-cjson behavior)
if count == 0 {
return false, 0, 0
}
// Check for sparse array
// Sparse condition: ratio > 0 && maxIndex > safe && maxIndex > count * ratio
if config.encodeSparseArray.ratio > 0 &&
maxIndex > config.encodeSparseArray.safe &&
maxIndex > count*config.encodeSparseArray.ratio {
// Sparse array detected
if !config.encodeSparseArray.convert {
// Would return error, but we return false to indicate object encoding
return false, maxIndex, count
}
// Convert to object
return false, maxIndex, count
}
// Check if keys are sequential starting from 1
// Use MaxN() for quick check
maxN := tbl.MaxN()
if maxN == count && maxN == maxIndex {
return true, maxIndex, count
}
// Non-sequential keys -> object
return false, maxIndex, count
}
// encodeArray encodes a Lua table as a JSON array.
func encodeArray(L *glua.LState, tbl *glua.LTable, maxIndex int, config *Config, depth int) (string, error) {
elements := make([]string, 0, maxIndex)
for i := 1; i <= maxIndex; i++ {
val := tbl.RawGetInt(i)
if val == glua.LNil {
// Missing element in sparse array - encode as null
elements = append(elements, "null")
continue
}
encoded, err := encodeLuaValue(L, val, config, depth+1)
if err != nil {
return "", err
}
elements = append(elements, encoded)
}
return "[" + strings.Join(elements, ",") + "]", nil
}
// encodeObject encodes a Lua table as a JSON object.
func encodeObject(L *glua.LState, tbl *glua.LTable, config *Config, depth int) (string, error) {
elements := make([]string, 0)
var encodeErr error
tbl.ForEach(func(key, value glua.LValue) {
if encodeErr != nil {
return
}
// Encode key
var keyStr string
switch k := key.(type) {
case glua.LString:
encoded, _ := json.Marshal(string(k))
keyStr = string(encoded)
case glua.LNumber:
// Numeric keys are converted to strings
keyStr = formatNumber(float64(k), config.encodeNumberPrecision)
// Quote the numeric key for JSON object
keyStr = "\"" + keyStr + "\""
default:
return // Skip invalid keys
}
// Encode value
valStr, err := encodeLuaValue(L, value, config, depth+1)
if err != nil {
encodeErr = err
return
}
elements = append(elements, keyStr+":"+valStr)
})
if encodeErr != nil {
return "", encodeErr
}
return "{" + strings.Join(elements, ",") + "}", nil
}
// formatNumber formats a number with the specified precision.
func formatNumber(n float64, precision int) string {
if precision <= 0 {
precision = 14
}
if precision > 14 {
precision = 14
}
// Check if it's an integer
if n == float64(int64(n)) && n >= -9007199254740992 && n <= 9007199254740992 {
return strconv.FormatInt(int64(n), 10)
}
// Use 'g' format for floating point
return strconv.FormatFloat(n, 'g', precision, 64)
}