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>
This commit is contained in:
parent
62be8bc557
commit
ce80352e79
17
gjson/api.go
17
gjson/api.go
@ -111,6 +111,23 @@ func (g *GJSON) cfgEncodeKeepBuffer(L *glua.LState) int {
|
||||
return 1
|
||||
}
|
||||
|
||||
// cfgEncodeSortKeys configures whether to sort object keys for stable output.
|
||||
// Lua: gjson.encode_sort_keys([sort])
|
||||
// Returns current value when called without arguments.
|
||||
// When enabled, object keys are sorted alphabetically for deterministic output.
|
||||
// When disabled (default), keys are output in arbitrary order for better performance.
|
||||
func (g *GJSON) cfgEncodeSortKeys(L *glua.LState) int {
|
||||
if L.GetTop() == 0 {
|
||||
L.Push(glua.LBool(g.config.encodeSortKeys))
|
||||
return 1
|
||||
}
|
||||
|
||||
g.config.encodeSortKeys = L.CheckBool(1)
|
||||
|
||||
L.Push(glua.LBool(g.config.encodeSortKeys))
|
||||
return 1
|
||||
}
|
||||
|
||||
// encode is the Lua function for gjson.encode(value).
|
||||
// Returns (json_string, nil) on success or (nil, error_message) on failure.
|
||||
func (g *GJSON) encode(L *glua.LState) int {
|
||||
|
||||
@ -9,6 +9,7 @@ const (
|
||||
defaultDecodeMaxDepth = 1000
|
||||
defaultEncodeNumberPrecision = 14
|
||||
defaultEncodeKeepBuffer = true
|
||||
defaultEncodeSortKeys = false // 默认不排序,保持高性能
|
||||
)
|
||||
|
||||
// sparseArrayConfig controls sparse array handling during encoding.
|
||||
@ -25,6 +26,7 @@ type Config struct {
|
||||
decodeMaxDepth int
|
||||
encodeNumberPrecision int
|
||||
encodeKeepBuffer bool
|
||||
encodeSortKeys bool // 是否对 object 键排序(稳定输出)
|
||||
}
|
||||
|
||||
// defaultConfig returns a new Config with default values.
|
||||
@ -39,6 +41,7 @@ func defaultConfig() *Config {
|
||||
decodeMaxDepth: defaultDecodeMaxDepth,
|
||||
encodeNumberPrecision: defaultEncodeNumberPrecision,
|
||||
encodeKeepBuffer: defaultEncodeKeepBuffer,
|
||||
encodeSortKeys: defaultEncodeSortKeys,
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,5 +57,6 @@ func (c *Config) Clone() *Config {
|
||||
decodeMaxDepth: c.decodeMaxDepth,
|
||||
encodeNumberPrecision: c.encodeNumberPrecision,
|
||||
encodeKeepBuffer: c.encodeKeepBuffer,
|
||||
encodeSortKeys: c.encodeSortKeys,
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package gjson
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@ -159,7 +160,51 @@ func encodeArray(L *glua.LState, tbl *glua.LTable, maxIndex int, config *Config,
|
||||
|
||||
// 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)
|
||||
// 快速路径:不需要排序时直接编码
|
||||
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) {
|
||||
@ -174,28 +219,34 @@ func encodeObject(L *glua.LState, tbl *glua.LTable, config *Config, depth int) (
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@ -63,6 +63,7 @@ func Loader(L *glua.LState) int {
|
||||
L.SetField(mod, "decode_max_depth", L.NewFunction(instance.cfgDecodeMaxDepth))
|
||||
L.SetField(mod, "encode_number_precision", L.NewFunction(instance.cfgEncodeNumberPrecision))
|
||||
L.SetField(mod, "encode_keep_buffer", L.NewFunction(instance.cfgEncodeKeepBuffer))
|
||||
L.SetField(mod, "encode_sort_keys", L.NewFunction(instance.cfgEncodeSortKeys))
|
||||
L.SetField(mod, "new", L.NewFunction(gjsonNew))
|
||||
|
||||
// Set gjson.null (lightuserdata representing JSON null)
|
||||
@ -94,6 +95,7 @@ func RegisterGlobal(L *glua.LState) {
|
||||
L.SetField(mod, "decode_max_depth", L.NewFunction(instance.cfgDecodeMaxDepth))
|
||||
L.SetField(mod, "encode_number_precision", L.NewFunction(instance.cfgEncodeNumberPrecision))
|
||||
L.SetField(mod, "encode_keep_buffer", L.NewFunction(instance.cfgEncodeKeepBuffer))
|
||||
L.SetField(mod, "encode_sort_keys", L.NewFunction(instance.cfgEncodeSortKeys))
|
||||
L.SetField(mod, "new", L.NewFunction(gjsonNew))
|
||||
L.SetField(mod, "null", instance.null)
|
||||
L.SetField(mod, "_NAME", glua.LString(ModuleName))
|
||||
|
||||
@ -349,3 +349,40 @@ func TestEncodeSparseArray(t *testing.T) {
|
||||
t.Fatalf("test failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeSortKeys(t *testing.T) {
|
||||
L := glua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
Preload(L)
|
||||
|
||||
err := L.DoString(`
|
||||
local gjson = require("gjson")
|
||||
|
||||
-- Test default (no sorting)
|
||||
local default_sort = gjson.encode_sort_keys()
|
||||
assert(default_sort == false, "default should be false")
|
||||
|
||||
-- Enable sorting (returns new value)
|
||||
local new_val = gjson.encode_sort_keys(true)
|
||||
assert(new_val == true, "should return new value")
|
||||
assert(gjson.encode_sort_keys() == true, "should be true now")
|
||||
|
||||
-- Test sorted output
|
||||
local data = {c = 3, a = 1, b = 2}
|
||||
local result = gjson.encode(data)
|
||||
-- Keys should be in alphabetical order: a, b, c
|
||||
local a_pos = string.find(result, '"a"')
|
||||
local b_pos = string.find(result, '"b"')
|
||||
local c_pos = string.find(result, '"c"')
|
||||
assert(a_pos < b_pos, "a should come before b")
|
||||
assert(b_pos < c_pos, "b should come before c")
|
||||
|
||||
-- Disable sorting (back to fast mode)
|
||||
gjson.encode_sort_keys(false)
|
||||
assert(gjson.encode_sort_keys() == false, "should be false again")
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatalf("test failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,6 +49,7 @@ func gjsonNew(L *glua.LState) int {
|
||||
L.SetField(tbl, "decode_max_depth", L.NewFunction(instance.cfgDecodeMaxDepth))
|
||||
L.SetField(tbl, "encode_number_precision", L.NewFunction(instance.cfgEncodeNumberPrecision))
|
||||
L.SetField(tbl, "encode_keep_buffer", L.NewFunction(instance.cfgEncodeKeepBuffer))
|
||||
L.SetField(tbl, "encode_sort_keys", L.NewFunction(instance.cfgEncodeSortKeys))
|
||||
L.SetField(tbl, "new", L.NewFunction(gjsonNew))
|
||||
|
||||
// Set null
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user