lolly/gjson/encode.go
xfy ce80352e79 feat(gjson): add encode_sort_keys option for stable JSON output
Add configurable key sorting for JSON object encoding. When enabled,
object keys are sorted alphabetically for deterministic output.
Default is disabled for better performance.

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

271 lines
6.4 KiB
Go

package gjson
import (
"fmt"
"sort"
"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) {
// 快速路径:不需要排序时直接编码
if !config.encodeSortKeys {
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:
keyStr = formatNumber(float64(k), config.encodeNumberPrecision)
keyStr = "\"" + keyStr + "\""
default:
return
}
// 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
}
// 排序路径:收集所有键值对后排序
type kv struct {
key string
value glua.LValue
}
pairs := make([]kv, 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:
keyStr = formatNumber(float64(k), config.encodeNumberPrecision)
keyStr = "\"" + keyStr + "\""
default:
return
}
pairs = append(pairs, kv{key: keyStr, value: value})
})
if encodeErr != nil {
return "", encodeErr
}
// 按键排序(保证输出顺序稳定)
sort.Slice(pairs, func(i, j int) bool {
return pairs[i].key < pairs[j].key
})
// 编码值
elements := make([]string, 0, len(pairs))
for _, p := range pairs {
valStr, err := encodeLuaValue(L, p.value, config, depth+1)
if err != nil {
return "", err
}
elements = append(elements, p.key+":"+valStr)
}
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)
}