|
@@ -0,0 +1,155 @@
|
|
|
|
|
+#include <cassert>
|
|
|
|
|
+#include <iostream>
|
|
|
|
|
+#include <optional>
|
|
|
|
|
+#include <string>
|
|
|
|
|
+
|
|
|
|
|
+#include <nlohmann/json.hpp>
|
|
|
|
|
+
|
|
|
|
|
+#include "doc_binary.hpp"
|
|
|
|
|
+
|
|
|
|
|
+using nlohmann::json;
|
|
|
|
|
+using smartbotic::db::doc_binary::Doc;
|
|
|
|
|
+using smartbotic::db::doc_binary::encode;
|
|
|
|
|
+using smartbotic::db::doc_binary::decode;
|
|
|
|
|
+using smartbotic::db::doc_binary::get_field;
|
|
|
|
|
+using smartbotic::db::doc_binary::to_json_text;
|
|
|
|
|
+
|
|
|
|
|
+namespace {
|
|
|
|
|
+
|
|
|
|
|
+void check(bool cond, const char* msg) {
|
|
|
|
|
+ if (!cond) {
|
|
|
|
|
+ std::cerr << "FAIL: " << msg << "\n";
|
|
|
|
|
+ std::abort();
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void roundtrip(const json& j, const char* label) {
|
|
|
|
|
+ auto doc = encode(j);
|
|
|
|
|
+ auto back = decode(doc);
|
|
|
|
|
+ check(back == j, label);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void test_default_doc_is_empty() {
|
|
|
|
|
+ Doc d;
|
|
|
|
|
+ check(d.empty(), "default-constructed Doc is empty");
|
|
|
|
|
+ check(d.root() == nullptr, "empty Doc has null root");
|
|
|
|
|
+ auto back = decode(d);
|
|
|
|
|
+ check(back.is_null(), "decoding empty Doc returns null");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void test_primitives_roundtrip() {
|
|
|
|
|
+ roundtrip(json(nullptr), "null");
|
|
|
|
|
+ roundtrip(json(true), "true");
|
|
|
|
|
+ roundtrip(json(false), "false");
|
|
|
|
|
+ roundtrip(json(0), "int zero");
|
|
|
|
|
+ roundtrip(json(42), "int 42");
|
|
|
|
|
+ roundtrip(json(-7), "negative int");
|
|
|
|
|
+ roundtrip(json(9223372036854775807LL), "int64 max");
|
|
|
|
|
+ roundtrip(json(3.14), "double");
|
|
|
|
|
+ roundtrip(json("hello"), "string");
|
|
|
|
|
+ roundtrip(json(""), "empty string");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void test_collections_roundtrip() {
|
|
|
|
|
+ roundtrip(json::array(), "empty array");
|
|
|
|
|
+ roundtrip(json::object(), "empty object");
|
|
|
|
|
+ roundtrip(json::array({1, 2, 3}), "int array");
|
|
|
|
|
+ roundtrip(json::array({1, "two", 3.0, true, nullptr}), "mixed array");
|
|
|
|
|
+ roundtrip(json({{"k", "v"}, {"n", 1}}), "small object");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void test_zoe_shape_roundtrip() {
|
|
|
|
|
+ json zoe = {
|
|
|
|
|
+ {"_id", "doc-1"},
|
|
|
|
|
+ {"_created_at", 1731628800123LL},
|
|
|
|
|
+ {"name", "Zoe"},
|
|
|
|
|
+ {"tags", json::array({"hot", "pinned"})},
|
|
|
|
|
+ {"_vector", json::array({0.1, -0.2, 0.3, 0.4})},
|
|
|
|
|
+ {"meta", {{"flag", true}, {"score", 0.93}}},
|
|
|
|
|
+ };
|
|
|
|
|
+ roundtrip(zoe, "Zoe-shape");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void test_unicode_roundtrip() {
|
|
|
|
|
+ roundtrip(
|
|
|
|
|
+ json({{"hu", "Árvíztűrő tükörfúrógép"}, {"emoji", "🚀"}}),
|
|
|
|
|
+ "unicode strings");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void test_get_field_present() {
|
|
|
|
|
+ json j = {{"name", "Zoe"}, {"count", 42}, {"flag", true}};
|
|
|
|
|
+ auto doc = encode(j);
|
|
|
|
|
+
|
|
|
|
|
+ auto name = get_field(doc, "name");
|
|
|
|
|
+ check(name.has_value(), "name present");
|
|
|
|
|
+ check(name->get<std::string>() == "Zoe", "name value");
|
|
|
|
|
+
|
|
|
|
|
+ auto count = get_field(doc, "count");
|
|
|
|
|
+ check(count.has_value(), "count present");
|
|
|
|
|
+ check(count->get<int>() == 42, "count value");
|
|
|
|
|
+
|
|
|
|
|
+ auto flag = get_field(doc, "flag");
|
|
|
|
|
+ check(flag.has_value(), "flag present");
|
|
|
|
|
+ check(flag->get<bool>() == true, "flag value");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void test_get_field_missing() {
|
|
|
|
|
+ json j = {{"name", "Zoe"}};
|
|
|
|
|
+ auto doc = encode(j);
|
|
|
|
|
+ auto absent = get_field(doc, "nonexistent");
|
|
|
|
|
+ check(!absent.has_value(), "missing field returns nullopt");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void test_get_field_on_non_object() {
|
|
|
|
|
+ auto doc = encode(json::array({1, 2, 3}));
|
|
|
|
|
+ auto v = get_field(doc, "anything");
|
|
|
|
|
+ check(!v.has_value(), "get_field on array returns nullopt");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void test_get_field_on_empty() {
|
|
|
|
|
+ Doc d;
|
|
|
|
|
+ auto v = get_field(d, "anything");
|
|
|
|
|
+ check(!v.has_value(), "get_field on empty Doc returns nullopt");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void test_to_json_text_roundtrip() {
|
|
|
|
|
+ json j = {{"a", 1}, {"b", "two"}, {"c", json::array({1, 2})}};
|
|
|
|
|
+ auto doc = encode(j);
|
|
|
|
|
+ std::string text = to_json_text(doc);
|
|
|
|
|
+ json parsed = json::parse(text);
|
|
|
|
|
+ check(parsed == j, "to_json_text round-trips through nlohmann::json::parse");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void test_to_json_text_empty() {
|
|
|
|
|
+ Doc d;
|
|
|
|
|
+ check(to_json_text(d) == "null", "to_json_text of empty Doc is \"null\"");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void test_doc_is_move_only() {
|
|
|
|
|
+ auto d1 = encode(json({{"a", 1}}));
|
|
|
|
|
+ check(!d1.empty(), "encoded doc not empty");
|
|
|
|
|
+ Doc d2 = std::move(d1);
|
|
|
|
|
+ check(d1.empty(), "moved-from Doc is empty");
|
|
|
|
|
+ check(!d2.empty(), "moved-to Doc holds the data");
|
|
|
|
|
+ auto field = get_field(d2, "a");
|
|
|
|
|
+ check(field.has_value() && field->get<int>() == 1, "moved Doc still readable");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+} // namespace
|
|
|
|
|
+
|
|
|
|
|
+int main() {
|
|
|
|
|
+ test_default_doc_is_empty();
|
|
|
|
|
+ test_primitives_roundtrip();
|
|
|
|
|
+ test_collections_roundtrip();
|
|
|
|
|
+ test_zoe_shape_roundtrip();
|
|
|
|
|
+ test_unicode_roundtrip();
|
|
|
|
|
+ test_get_field_present();
|
|
|
|
|
+ test_get_field_missing();
|
|
|
|
|
+ test_get_field_on_non_object();
|
|
|
|
|
+ test_get_field_on_empty();
|
|
|
|
|
+ test_to_json_text_roundtrip();
|
|
|
|
|
+ test_to_json_text_empty();
|
|
|
|
|
+ test_doc_is_move_only();
|
|
|
|
|
+ std::cout << "test_doc_binary: all passed\n";
|
|
|
|
|
+ return 0;
|
|
|
|
|
+}
|