diff --git a/gjson/api.go b/gjson/api.go index 9dc0487..ae21ab4 100644 --- a/gjson/api.go +++ b/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 { diff --git a/gjson/config.go b/gjson/config.go index 4135ef0..a318557 100644 --- a/gjson/config.go +++ b/gjson/config.go @@ -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, } } diff --git a/gjson/encode.go b/gjson/encode.go index a001598..70804c8 100644 --- a/gjson/encode.go +++ b/gjson/encode.go @@ -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 } diff --git a/gjson/gjson.go b/gjson/gjson.go index a4ca591..eb632d5 100644 --- a/gjson/gjson.go +++ b/gjson/gjson.go @@ -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)) diff --git a/gjson/gjson_test.go b/gjson/gjson_test.go index 5432ec4..94847c2 100644 --- a/gjson/gjson_test.go +++ b/gjson/gjson_test.go @@ -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) + } +} diff --git a/gjson/types.go b/gjson/types.go index 27bfe42..17527c0 100644 --- a/gjson/types.go +++ b/gjson/types.go @@ -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