有序编码:在 Decode-Modify-Encode 流程中保持 Key 顺序
概述
增强 qjson 的 lazy table API,使其在修改后编码时保持原始 JSON 的 key 顺序。这支持 API 网关场景:JSON 请求被解码、轻量修改、再编码,同时保持 key 顺序一致性。
需求
支持的场景
- 修改现有值 — 修改已有 key 的值;key 顺序不变
- 删除 key — 设置
t.key = nil 从输出中移除该 key;剩余 key 保持顺序
- 新增 key — 新 key 追加到对象末尾
语义
t.key = nil 表示删除(与 lua-cjson 兼容),而非 JSON null
- 删除后重新添加的 key 出现在末尾(视为新 key)
- 只保持 key 顺序;空白和数字格式可能变化
- 数组保持索引顺序(无需修改)
设计
方案:有序 Key 列表 + Value 映射
当 LazyObject 首次被修改时,物化为有序结构:
{
_dirty = true,
_keys = {"a", "b", "c"}, -- 有序 key 列表
_values = {a = 1, b = 2, c = 3}, -- key -> value 映射
-- 保留原有字段用于未修改子树的 buffer 切片
_doc, _cur, _cur_box, _bs, _be, _parent
}
实现变更
1. LazyObject.__newindex
首次写入时:
- 通过
qjson_cursor_object_entry_at 按顺序遍历原始 JSON 的所有 key
- 构建
_keys 列表和 _values table
- 应用当前赋值操作
后续写入时:
- 已有 key:更新
_values[key]
- 新 key:追加到
_keys,设置 _values[key]
- 删除(
= nil):从 _keys 移除,从 _values 删除
LazyObject.__newindex = function(t, k, v)
-- 向上标记 dirty(现有逻辑)
local cur = t
while cur do
local mt = getmetatable(cur)
if mt ~= LazyObject and mt ~= LazyArray then break end
rawset(cur, "_dirty", true)
cur = rawget(cur, "_parent")
end
local keys = rawget(t, "_keys")
if not keys then
-- 首次修改:物化 key 顺序
keys = {}
local values = {}
local i = 0
while true do
local rc = C.qjson_cursor_object_entry_at(t._cur, i, strp_box, size_box, child_box)
if rc == QJSON_NOT_FOUND then break end
check(rc)
local key = ffi.string(strp_box[0], size_box[0])
keys[#keys + 1] = key
values[key] = decode_cursor(t, child_box)
i = i + 1
end
rawset(t, "_keys", keys)
rawset(t, "_values", values)
end
local values = rawget(t, "_values")
if v == nil then
-- 删除:从 _keys 移除
for i, key in ipairs(keys) do
if key == k then
table.remove(keys, i)
break
end
end
values[k] = nil
elseif values[k] == nil then
-- 新 key:追加到 _keys
keys[#keys + 1] = k
values[k] = v
else
-- 已有 key:只更新值
values[k] = v
end
end
2. LazyObject.__index
物化后从 _values 读取。现有的 read_object_field 函数保持不变,因为:
- 物化前:使用 cursor 路径(现有行为)
- 物化后:
_values 包含物化步骤中解码的所有值
这里不需要代码变更 — read_object_field 中现有的 rawget(self, key) 缓存检查不会触发,因为我们使用 _values 作为存储,而非直接 rawset 到 table。__index 元方法在检查 _keys 存在后自然会回退到 _values 查找:
local function read_object_field(self, key)
if type(key) ~= "string" then return nil end
-- 检查是否已物化(新增)
local values = rawget(self, "_values")
if values then
return values[key]
end
-- 原有 lazy 路径通过 cursor(不变)
local rc = C.qjson_cursor_field(self._cur, key, #key, child_box)
if not check(rc) then return nil end
local v = decode_cursor(self, child_box)
if type(v) == "table" then rawset(self, key, v) end
return v
end
3. LazyObject.__pairs
物化后按 _keys 顺序迭代:
function LazyObject.__pairs(t)
local keys = rawget(t, "_keys")
if keys then
local values = rawget(t, "_values")
local i = 0
return function()
i = i + 1
local k = keys[i]
if k then return k, values[k] end
end
end
-- 原有 lazy 迭代器
return lazy_object_iter, { view = t, i = 0 }, nil
end
4. encode_lazy_object_walking
使用 _keys 顺序编码:
local function encode_lazy_object_walking(t)
local keys = rawget(t, "_keys")
if keys then
-- 已物化:使用 _keys 顺序
local values = rawget(t, "_values")
local parts = {}
for _, k in ipairs(keys) do
local v = values[k]
if v ~= nil then
parts[#parts + 1] = encode_string(k) .. ":" .. encode(v)
end
end
return "{" .. table.concat(parts, ",") .. "}"
end
-- 原有基于 cursor 的遍历(用于仅子节点 dirty 的情况)
local parts = {}
local i = 0
while true do
local rc = C.qjson_cursor_object_entry_at(t._cur, i, strp_box, size_box, child_box)
if rc == QJSON_NOT_FOUND then break end
check(rc)
local k = ffi.string(strp_box[0], size_box[0])
local cached = rawget(t, k)
local v = cached ~= nil and not INTERNAL_KEYS[k] and cached or decode_cursor(t, child_box)
parts[#parts + 1] = encode_string(k) .. ":" .. encode(v)
i = i + 1
end
return "{" .. table.concat(parts, ",") .. "}"
end
内部 Key 更新
将新的内部 key 添加到排除集合:
local INTERNAL_KEYS = {
_doc = true, _cur_box = true, _cur = true, _bs = true, _be = true,
_parent = true, _dirty = true,
_keys = true, _values = true, -- 新增
}
边界情况
| 场景 |
行为 |
| 读取后修改同一 key |
正常工作:物化时值已在 _values 中 |
| 修改嵌套对象 |
子对象独立 dirty;父对象通过 encode() 编码子对象 |
空对象 {} 新增 key |
_keys = {} 初始化,然后追加 |
| key 名与内部字段冲突 |
_values 隔离用户数据;无冲突 |
| 多次删除 |
每次从 _keys 移除;剩余 key 顺序保持 |
| 删除所有 key |
_keys = {},编码为 {} |
性能特征
- 只读路径:无变化(无
_keys/_values 开销)
- 首次修改:O(n) 物化所有 key/value
- 后续修改:删除 O(n)(线性扫描
_keys),新增/更新 O(1)
- 编码:O(n) 遍历
_keys
对于目标用例(decode、修改少量字段、encode),这是可接受的。
测试
新增测试用例
describe("ordered encode", function()
it("preserves key order on value modification", function()
local t = qjson.decode('{"c":3,"a":1,"b":2}')
t.a = 100
assert.are.equal('{"c":3,"a":100,"b":2}', qjson.encode(t))
end)
it("preserves order when deleting a key", function()
local t = qjson.decode('{"c":3,"a":1,"b":2}')
t.a = nil
assert.are.equal('{"c":3,"b":2}', qjson.encode(t))
end)
it("appends new keys to the end", function()
local t = qjson.decode('{"c":3,"a":1}')
t.b = 2
assert.are.equal('{"c":3,"a":1,"b":2}', qjson.encode(t))
end)
it("deleted then re-added key appears at end", function()
local t = qjson.decode('{"a":1,"b":2,"c":3}')
t.b = nil
t.b = 999
assert.are.equal('{"a":1,"c":3,"b":999}', qjson.encode(t))
end)
it("handles nested object modification", function()
local t = qjson.decode('{"x":1,"nested":{"a":1,"b":2},"y":2}')
t.nested.a = 100
local out = qjson.encode(t)
-- x 和 y 顺序保持,nested 内部有序
assert.truthy(out:find('"x":1'))
assert.truthy(out:find('"y":2'))
assert.truthy(out:find('"a":100'))
end)
it("handles empty object with additions", function()
local t = qjson.decode('{}')
t.a = 1
t.b = 2
assert.are.equal('{"a":1,"b":2}', qjson.encode(t))
end)
it("handles delete all keys", function()
local t = qjson.decode('{"a":1,"b":2}')
t.a = nil
t.b = nil
assert.are.equal('{}', qjson.encode(t))
end)
it("read before modify works correctly", function()
local t = qjson.decode('{"a":1,"b":2,"c":3}')
local _ = t.b -- 先读取
t.b = 999
assert.are.equal('{"a":1,"b":999,"c":3}', qjson.encode(t))
end)
end)
需修改的文件
-
lua/qjson/table.lua — 所有变更都在此文件:
INTERNAL_KEYS — 添加 _keys、_values
LazyObject.__newindex — 重写为使用有序 key 列表
read_object_field / LazyObject.__index — 检查物化状态
LazyObject.__pairs — 支持物化后的迭代
encode_lazy_object_walking — 物化后使用 _keys 顺序
-
tests/lua/ — 新增测试文件 ordered_encode_spec.lua
不在范围内
- 保留原始空白/格式
- 控制新 key 的插入位置(始终追加)
- 数组元素删除语义(遵循现有 lua-cjson 行为)
- Rust 侧变更(所有变更都在 Lua 层)
有序编码:在 Decode-Modify-Encode 流程中保持 Key 顺序
概述
增强 qjson 的 lazy table API,使其在修改后编码时保持原始 JSON 的 key 顺序。这支持 API 网关场景:JSON 请求被解码、轻量修改、再编码,同时保持 key 顺序一致性。
需求
支持的场景
t.key = nil从输出中移除该 key;剩余 key 保持顺序语义
t.key = nil表示删除(与 lua-cjson 兼容),而非 JSON null设计
方案:有序 Key 列表 + Value 映射
当
LazyObject首次被修改时,物化为有序结构:{ _dirty = true, _keys = {"a", "b", "c"}, -- 有序 key 列表 _values = {a = 1, b = 2, c = 3}, -- key -> value 映射 -- 保留原有字段用于未修改子树的 buffer 切片 _doc, _cur, _cur_box, _bs, _be, _parent }实现变更
1. LazyObject.__newindex
首次写入时:
qjson_cursor_object_entry_at按顺序遍历原始 JSON 的所有 key_keys列表和_valuestable后续写入时:
_values[key]_keys,设置_values[key]= nil):从_keys移除,从_values删除2. LazyObject.__index
物化后从
_values读取。现有的read_object_field函数保持不变,因为:_values包含物化步骤中解码的所有值这里不需要代码变更 —
read_object_field中现有的rawget(self, key)缓存检查不会触发,因为我们使用_values作为存储,而非直接 rawset 到 table。__index元方法在检查_keys存在后自然会回退到_values查找:3. LazyObject.__pairs
物化后按
_keys顺序迭代:4. encode_lazy_object_walking
使用
_keys顺序编码:内部 Key 更新
将新的内部 key 添加到排除集合:
边界情况
_values中encode()编码子对象{}新增 key_keys = {}初始化,然后追加_values隔离用户数据;无冲突_keys移除;剩余 key 顺序保持_keys = {},编码为{}性能特征
_keys/_values开销)_keys),新增/更新 O(1)_keys对于目标用例(decode、修改少量字段、encode),这是可接受的。
测试
新增测试用例
需修改的文件
lua/qjson/table.lua— 所有变更都在此文件:INTERNAL_KEYS— 添加_keys、_valuesLazyObject.__newindex— 重写为使用有序 key 列表read_object_field/LazyObject.__index— 检查物化状态LazyObject.__pairs— 支持物化后的迭代encode_lazy_object_walking— 物化后使用_keys顺序tests/lua/— 新增测试文件ordered_encode_spec.lua不在范围内