소스 검색

feat(json): add yyjson-backed parse_to_nlohmann bridge helper

fszontagh 2 달 전
부모
커밋
f059034f0a
5개의 변경된 파일238개의 추가작업 그리고 0개의 파일을 삭제
  1. 1 0
      service/CMakeLists.txt
  2. 88 0
      service/src/json_parse.cpp
  3. 16 0
      service/src/json_parse.hpp
  4. 29 0
      tests/CMakeLists.txt
  5. 104 0
      tests/test_json_parse.cpp

+ 1 - 0
service/CMakeLists.txt

@@ -31,6 +31,7 @@ set(DATABASE_SERVICE_SOURCES
     src/main.cpp
     src/database_service.cpp
     src/database_grpc_impl.cpp
+    src/json_parse.cpp
     src/memory_store.cpp
     src/persistence/persistence_manager.cpp
     src/persistence/wal.cpp

+ 88 - 0
service/src/json_parse.cpp

@@ -0,0 +1,88 @@
+#include "json_parse.hpp"
+
+#include <yyjson.h>
+
+#include <stdexcept>
+#include <string>
+
+namespace smartbotic::db {
+
+namespace {
+
+nlohmann::json convert(yyjson_val* v) {
+    if (!v) {
+        return nullptr;
+    }
+    switch (yyjson_get_type(v)) {
+        case YYJSON_TYPE_NULL:
+            return nullptr;
+        case YYJSON_TYPE_BOOL:
+            return yyjson_get_bool(v);
+        case YYJSON_TYPE_NUM:
+            switch (yyjson_get_subtype(v)) {
+                case YYJSON_SUBTYPE_UINT:
+                    return yyjson_get_uint(v);
+                case YYJSON_SUBTYPE_SINT:
+                    return yyjson_get_sint(v);
+                case YYJSON_SUBTYPE_REAL:
+                default:
+                    return yyjson_get_real(v);
+            }
+        case YYJSON_TYPE_STR:
+            return std::string(yyjson_get_str(v), yyjson_get_len(v));
+        case YYJSON_TYPE_ARR: {
+            nlohmann::json out = nlohmann::json::array();
+            size_t idx, max;
+            yyjson_val* elem;
+            yyjson_arr_foreach(v, idx, max, elem) {
+                out.push_back(convert(elem));
+            }
+            return out;
+        }
+        case YYJSON_TYPE_OBJ: {
+            nlohmann::json out = nlohmann::json::object();
+            size_t idx, max;
+            yyjson_val* key;
+            yyjson_val* val;
+            yyjson_obj_foreach(v, idx, max, key, val) {
+                out[std::string(yyjson_get_str(key), yyjson_get_len(key))] = convert(val);
+            }
+            return out;
+        }
+        default:
+            return nullptr;
+    }
+}
+
+}  // namespace
+
+nlohmann::json parse_to_nlohmann(const char* data, size_t length) {
+    if (!data || length == 0) {
+        throw nlohmann::json::parse_error::create(
+            101, 0, "empty input", nullptr);
+    }
+    yyjson_read_err err{};
+    yyjson_doc* doc = yyjson_read_opts(
+        const_cast<char*>(data), length,
+        YYJSON_READ_NOFLAG, nullptr, &err);
+    if (!doc) {
+        std::string msg = "yyjson parse error: ";
+        msg += err.msg ? err.msg : "unknown";
+        throw nlohmann::json::parse_error::create(
+            101, static_cast<size_t>(err.pos), msg, nullptr);
+    }
+    try {
+        nlohmann::json out = convert(yyjson_doc_get_root(doc));
+        yyjson_doc_free(doc);
+        return out;
+    } catch (...) {
+        yyjson_doc_free(doc);
+        throw;
+    }
+}
+
+nlohmann::json parse_to_nlohmann(std::string_view bytes) {
+    return parse_to_nlohmann(bytes.data(), bytes.size());
+}
+
+}  // namespace smartbotic::db

+ 16 - 0
service/src/json_parse.hpp

@@ -0,0 +1,16 @@
+#pragma once
+
+#include <string_view>
+#include <nlohmann/json.hpp>
+
+namespace smartbotic::db {
+
+// Parses a JSON document using yyjson (read-only DOM) and materialises an
+// nlohmann::json tree from it. Drop-in replacement for nlohmann::json::parse
+// at hot paths where parse speed matters but we still want nlohmann semantics
+// downstream. Throws nlohmann::json::parse_error on malformed input so existing
+// catch sites keep working.
+nlohmann::json parse_to_nlohmann(std::string_view bytes);
+nlohmann::json parse_to_nlohmann(const char* data, size_t length);
+
+}  // namespace smartbotic::db

+ 29 - 0
tests/CMakeLists.txt

@@ -191,6 +191,34 @@ endif()
 find_package(Threads REQUIRED)
 target_link_libraries(test_eviction PRIVATE Threads::Threads)
 
+# yyjson → nlohmann bridge helper test (v1.10 Phase A T2)
+# Exercises parse_to_nlohmann() in isolation against nlohmann::json::parse
+# for a corpus of production-shaped docs. yyjson is configured at the
+# service/ scope; re-resolve it here so the test target can find it
+# independently of the service build.
+if(NOT yyjson_FOUND)
+    find_package(PkgConfig REQUIRED)
+    pkg_check_modules(yyjson REQUIRED yyjson)
+endif()
+
+add_executable(test_json_parse
+    test_json_parse.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/../service/src/json_parse.cpp
+)
+
+target_include_directories(test_json_parse PRIVATE
+    ${CMAKE_CURRENT_SOURCE_DIR}/../service/src
+    ${yyjson_INCLUDE_DIRS}
+)
+
+if(TARGET nlohmann_json::nlohmann_json)
+    target_link_libraries(test_json_parse PRIVATE nlohmann_json::nlohmann_json)
+else()
+    target_include_directories(test_json_parse PRIVATE ${NLOHMANN_JSON_INCLUDE_DIRS})
+endif()
+
+target_link_libraries(test_json_parse PRIVATE ${yyjson_LIBRARIES})
+
 # Register with CTest
 enable_testing()
 add_test(NAME vector_storage COMMAND test_vector_storage)
@@ -199,3 +227,4 @@ add_test(NAME timestamp_precision COMMAND test_timestamp_precision)
 add_test(NAME snapshot_durability COMMAND test_snapshot_durability)
 add_test(NAME config_dropins COMMAND test_config_dropins)
 add_test(NAME eviction COMMAND test_eviction)
+add_test(NAME json_parse COMMAND test_json_parse)

+ 104 - 0
tests/test_json_parse.cpp

@@ -0,0 +1,104 @@
+#include <cassert>
+#include <cstring>
+#include <iostream>
+#include <string>
+#include <nlohmann/json.hpp>
+#include "json_parse.hpp"
+
+using nlohmann::json;
+using smartbotic::db::parse_to_nlohmann;
+
+namespace {
+
+void check(bool cond, const char* msg) {
+    if (!cond) {
+        std::cerr << "FAIL: " << msg << "\n";
+        std::abort();
+    }
+}
+
+void test_empty_object() {
+    auto j = parse_to_nlohmann("{}");
+    check(j.is_object() && j.empty(), "empty object");
+}
+
+void test_empty_array() {
+    auto j = parse_to_nlohmann("[]");
+    check(j.is_array() && j.empty(), "empty array");
+}
+
+void test_primitives() {
+    check(parse_to_nlohmann("true").get<bool>() == true, "true");
+    check(parse_to_nlohmann("false").get<bool>() == false, "false");
+    check(parse_to_nlohmann("null").is_null(), "null");
+    check(parse_to_nlohmann("42").get<int64_t>() == 42, "int");
+    check(parse_to_nlohmann("-7").get<int64_t>() == -7, "negative int");
+    check(parse_to_nlohmann("3.14").get<double>() == 3.14, "double");
+    check(parse_to_nlohmann("\"hello\"").get<std::string>() == "hello", "string");
+}
+
+void test_nested() {
+    std::string s = R"({"a":1,"b":[2,3,{"c":"x"}],"d":null,"e":true})";
+    auto y = parse_to_nlohmann(s);
+    auto n = json::parse(s);
+    check(y == n, "nested equality with nlohmann");
+}
+
+void test_unicode_string() {
+    std::string s = R"({"name":"Árvíztűrő tükörfúrógép","emoji":"🚀"})";
+    auto y = parse_to_nlohmann(s);
+    auto n = json::parse(s);
+    check(y == n, "unicode equality");
+}
+
+void test_large_int() {
+    auto j = parse_to_nlohmann("9223372036854775807");
+    check(j.get<int64_t>() == 9223372036854775807LL, "int64 max");
+}
+
+void test_string_view_overload() {
+    std::string s = "{\"k\":1}";
+    auto j = parse_to_nlohmann(std::string_view(s));
+    check(j["k"].get<int>() == 1, "string_view overload");
+}
+
+void test_invalid_json_throws() {
+    bool threw = false;
+    try {
+        (void)parse_to_nlohmann("{not json");
+    } catch (const std::exception&) {
+        threw = true;
+    }
+    check(threw, "invalid json throws");
+}
+
+void test_corpus_equivalence() {
+    // Sample of doc shapes the production service sees.
+    const char* corpus[] = {
+        R"({"_id":"abc","name":"x","tags":["a","b"],"count":0})",
+        R"({"_id":"xyz","_vector":[0.1,-0.2,0.3,0.4],"meta":{"k":"v"}})",
+        R"({"_id":"e1","_event":"call.ended","at":1731628800123})",
+        R"([])",
+        R"([1,2,3,4,5,6,7,8,9,10])",
+        R"({"deeply":{"nested":{"object":{"is":{"fine":true}}}}})",
+    };
+    for (const char* s : corpus) {
+        check(parse_to_nlohmann(s) == json::parse(s), s);
+    }
+}
+
+}  // namespace
+
+int main() {
+    test_empty_object();
+    test_empty_array();
+    test_primitives();
+    test_nested();
+    test_unicode_string();
+    test_large_int();
+    test_string_view_overload();
+    test_invalid_json_throws();
+    test_corpus_equivalence();
+    std::cout << "test_json_parse: all passed\n";
+    return 0;
+}