From d326f2ce9fbda80eb65d61031fee4c709334cfc5 Mon Sep 17 00:00:00 2001 From: Francisco Geiman Thiesen Date: Fri, 10 Oct 2025 21:32:10 -0700 Subject: [PATCH] Working! --- .../generic/ondemand/compile_time_json.h | 616 ++++++++++-------- tests/ondemand/compile_time_json_tests.cpp | 377 ++++++----- 2 files changed, 542 insertions(+), 451 deletions(-) diff --git a/include/simdjson/generic/ondemand/compile_time_json.h b/include/simdjson/generic/ondemand/compile_time_json.h index 4cb4e3bf8..18b5ee996 100644 --- a/include/simdjson/generic/ondemand/compile_time_json.h +++ b/include/simdjson/generic/ondemand/compile_time_json.h @@ -1,14 +1,9 @@ /** * @file compile_time_json.h - * @brief Compile-time JSON parsing using C++26 P2996 reflection + * @brief Compile-time JSON parsing using C++26 reflection with std::meta::substitute() * - * Parses JSON at compile-time and generates strongly-typed structs automatically. - * Uses std::meta::substitute() pattern for recursive structures. - * - * @warning Requires C++26 with P2996R13 (experimental clang-p2996 compiler) - * - * Inspired by: https://brevzin.github.io/c++/2025/06/26/json-reflection/ - * https://godbolt.org/z/Kn5b46T8j + * Based on the godbolt example: https://godbolt.org/z/Kn5b46T8j + * Uses the Outer::Inner + substitute() pattern for recursive type generation. */ #ifndef SIMDJSON_GENERIC_COMPILE_TIME_JSON_H @@ -22,15 +17,19 @@ #include #include -#include #include +#include +#include +#include +#include namespace simdjson { namespace SIMDJSON_IMPLEMENTATION { namespace compile_time { /** - * @brief Helper template for dynamic type generation via substitute() + * @brief Helper struct for substitute() pattern + * The consteval block can use define_aggregate because it's in a template context */ template struct Outer { @@ -40,322 +39,421 @@ struct Outer { } }; +/** + * @brief Type alias for the generated struct + */ template using Cls = Outer::Inner; /** - * @brief Helper template for aggregate initialization from reflected values + * @brief Variable template for constructing instances with values */ template constexpr auto construct_from = T{Vs...}; -/** - * @brief Parsing state and helper functions - */ -struct ParseContext { - std::string_view json; - std::size_t pos = 0; - - constexpr ParseContext(std::string_view input) : json(input), pos(0) {} - - constexpr void skip_whitespace() { - while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t' || - json[pos] == '\n' || json[pos] == '\r')) { - ++pos; - } - } - - [[nodiscard]] constexpr char peek() const { - return pos < json.size() ? json[pos] : '\0'; - } - - constexpr char consume() { - return pos < json.size() ? json[pos++] : '\0'; - } - - constexpr bool expect(char ch) { - skip_whitespace(); - if (peek() == ch) { - consume(); - return true; - } - throw "expected character"; - } - - [[nodiscard]] constexpr bool match(std::string_view str) { - if (json.substr(pos, str.size()) == str) { - pos += str.size(); - return true; - } - return false; - } - - [[nodiscard]] constexpr std::string_view parse_string() { - if (!expect('"')) throw "expected quote"; - std::size_t start = pos; - while (peek() != '"' && peek() != '\0') { - if (peek() == '\\') { - consume(); - if (pos < json.size()) consume(); - } else { - consume(); - } - } - std::size_t end = pos; - expect('"'); - return json.substr(start, end - start); - } - - [[nodiscard]] constexpr double parse_number() { - std::size_t start = pos; - if (peek() == '-') consume(); - while (peek() >= '0' && peek() <= '9') consume(); - if (peek() == '.') { - consume(); - while (peek() >= '0' && peek() <= '9') consume(); - } - - std::string_view num_str = json.substr(start, pos - start); - double result = 0.0, sign = 1.0; - std::size_t i = 0; - - if (i < num_str.size() && num_str[i] == '-') { - sign = -1.0; - ++i; - } - - while (i < num_str.size() && num_str[i] >= '0' && num_str[i] <= '9') { - result = result * 10.0 + (num_str[i] - '0'); - ++i; - } - - if (i < num_str.size() && num_str[i] == '.') { - ++i; - double fraction = 0.0, divisor = 1.0; - while (i < num_str.size() && num_str[i] >= '0' && num_str[i] <= '9') { - fraction = fraction * 10.0 + (num_str[i] - '0'); - divisor *= 10.0; - ++i; - } - result += fraction / divisor; - } - - return result * sign; - } - - [[nodiscard]] constexpr std::string_view extract_nested() { - std::size_t start = pos; - char open_char = peek(); - char close_char = (open_char == '{') ? '}' : ']'; - int depth = 0; - do { - if (peek() == open_char) depth++; - if (peek() == close_char) depth--; - consume(); - } while (depth > 0 && pos < json.size()); - if (depth != 0) throw "unclosed bracket"; - return json.substr(start, pos - start); - } -}; - +// Forward declaration consteval std::meta::info parse_json_impl(std::string_view json); -consteval std::meta::info parse_array_impl(std::string_view json); -consteval std::meta::info parse_array_impl(std::string_view json) { - ParseContext ctx{json}; - ctx.skip_whitespace(); +/** + * @brief Parse JSON array and return std::meta::info for the generated array + */ +consteval std::meta::info parse_json_array_impl(std::string_view json) { + auto cursor = json.begin(); + auto end = json.end(); - if (!ctx.expect('[')) { - throw "expected '['"; - } + auto is_whitespace = [](char c) { + return c == ' ' || c == '\n' || c == '\t' || c == '\r'; + }; - ctx.skip_whitespace(); + auto skip_whitespace = [&]() -> void { + while (cursor != end && is_whitespace(*cursor)) cursor++; + }; - if (ctx.peek() == ']') { - ctx.consume(); - return std::meta::substitute(^^construct_from, {^^std::array, ^^void}); - } + auto expect_consume = [&](char c) -> void { + skip_whitespace(); + if (cursor == end || *(cursor++) != c) throw "unexpected character"; + }; - std::vector element_values; + auto parse_value = [&](std::string &out) -> void { + skip_whitespace(); - while (ctx.peek() != ']') { - ctx.skip_whitespace(); - char ch = ctx.peek(); + bool quoted = false; + unsigned depth = 0; + while (true) { + if (cursor == end) throw "unexpected end of stream"; + if (is_whitespace(*cursor) && !quoted && depth == 0) + break; - if (ch == '"') { - auto str_value = ctx.parse_string(); - element_values.push_back(std::meta::reflect_constant_string(str_value)); - } else if (ch == 't' || ch == 'f') { - bool bool_value = ctx.peek() == 't'; - if (bool_value) { - if (!ctx.match("true")) throw "expected 'true'"; - } else { - if (!ctx.match("false")) throw "expected 'false'"; + if (depth == 0 && (*cursor == ',' || *cursor == ']')) + break; + out += *(cursor++); + + if (out.back() == '{') + ++depth; + else if (out.back() == '}') + --depth; + else if (out.back() == '[') + ++depth; + else if (out.back() == ']') + --depth; + else if (out.back() == '"') { + if (quoted && depth == 0) + break; + quoted = true; } - element_values.push_back(std::meta::reflect_constant(bool_value)); - } else if (ch == 'n') { - if (!ctx.match("null")) throw "expected 'null'"; - element_values.push_back(std::meta::reflect_constant(nullptr)); - } else if (ch == '{') { - auto nested_json = ctx.extract_nested(); - std::meta::info parsed = parse_json_impl(nested_json); - element_values.push_back(parsed); - } else if (ch == '[') { - auto nested_json = ctx.extract_nested(); - std::meta::info parsed = parse_array_impl(nested_json); - element_values.push_back(parsed); - } else if ((ch >= '0' && ch <= '9') || ch == '-') { - double num_value = ctx.parse_number(); - element_values.push_back(std::meta::reflect_constant(num_value)); - } else { - throw "unexpected array element type"; + } + }; + + skip_whitespace(); + expect_consume('['); + + std::vector values = {^^void}; + std::meta::info element_type = ^^void; + bool first = true; + + using std::meta::reflect_constant, std::meta::reflect_constant_string; + + skip_whitespace(); + if (cursor != end && *cursor == ']') { + expect_consume(']'); + // Empty array - use int as placeholder type since void doesn't work + auto array_type = std::meta::substitute(^^std::array, {^^int, reflect_constant(0uz)}); + values[0] = array_type; + return std::meta::substitute(^^construct_from, values); + } + + while (cursor != end && *cursor != ']') { + std::string value; + parse_value(value); + + if (value.empty()) throw "expected value"; + + if (value[0] == '"') { + if (value.back() != '"') throw "expected end of string"; + std::string_view contents(&value[1], value.size() - 2); + + if (first) element_type = ^^char const*; + values.push_back(reflect_constant_string(contents)); + } else if (value == "true") { + if (first) element_type = ^^bool; + values.push_back(reflect_constant(true)); + } else if (value == "false") { + if (first) element_type = ^^bool; + values.push_back(reflect_constant(false)); + } else if (value == "null") { + if (first) element_type = ^^std::nullptr_t; + values.push_back(reflect_constant(nullptr)); + } else if ((value[0] >= '0' && value[0] <= '9') || value[0] == '-') { + // Try to parse as integer first + bool is_int = true; + for (char c : value) { + if (c == '.' || c == 'e' || c == 'E') { + is_int = false; + break; + } + } + + if (is_int) { + int contents = [](std::string_view in) { + int out = 0; + bool negative = false; + std::size_t i = 0; + if (in[0] == '-') { + negative = true; + i = 1; + } + for (; i < in.size(); ++i) { + out = out * 10 + (in[i] - '0'); + } + return negative ? -out : out; + }(value); + + if (first) element_type = ^^int; + values.push_back(reflect_constant(contents)); + } else { + // Parse as double + double contents = [](std::string_view in) { + double result = 0.0; + double sign = 1.0; + std::size_t i = 0; + + if (in[0] == '-') { + sign = -1.0; + i = 1; + } + + while (i < in.size() && in[i] >= '0' && in[i] <= '9') { + result = result * 10.0 + (in[i] - '0'); + ++i; + } + + if (i < in.size() && in[i] == '.') { + ++i; + double fraction = 0.0; + double divisor = 1.0; + while (i < in.size() && in[i] >= '0' && in[i] <= '9') { + fraction = fraction * 10.0 + (in[i] - '0'); + divisor *= 10.0; + ++i; + } + result += fraction / divisor; + } + + return result * sign; + }(value); + + if (first) element_type = ^^double; + values.push_back(reflect_constant(contents)); + } + } else if (value[0] == '{') { + // Nested object in array + std::meta::info parsed = parse_json_impl(value); + if (first) element_type = std::meta::type_of(parsed); + values.push_back(parsed); + } else if (value[0] == '[') { + // Nested array + std::meta::info parsed = parse_json_array_impl(value); + if (first) element_type = std::meta::type_of(parsed); + values.push_back(parsed); } - ctx.skip_whitespace(); - if (ctx.peek() == ',') { - ctx.consume(); - ctx.skip_whitespace(); - } else { - break; - } + first = false; + + skip_whitespace(); + if (cursor != end && *cursor == ',') + ++cursor; } - ctx.expect(']'); + if (cursor == end) throw "unexpected end"; + expect_consume(']'); - if (element_values.empty()) { - return std::meta::substitute(^^construct_from, {^^std::array, ^^void}); - } - - std::meta::info element_type = std::meta::type_of(element_values[0]); - std::size_t count = element_values.size(); - std::meta::info array_type = std::meta::substitute(^^std::array, {element_type, std::meta::reflect_constant(count)}); - - std::vector values; - values.push_back(array_type); - for (auto& elem : element_values) { - values.push_back(elem); - } + // Create std::array type + std::size_t count = values.size() - 1; // -1 because first element is ^^void placeholder + auto array_type = std::meta::substitute(^^std::array, {element_type, reflect_constant(count)}); + // Create array instance with values + values[0] = array_type; return std::meta::substitute(^^construct_from, values); } /** - * @brief Main compile-time JSON parser using substitute() pattern - * - * Returns std::meta::info containing type and value information. - * This can be recursively called for nested objects without compiler crashes. + * @brief Parse JSON and return std::meta::info for the generated type + instance */ consteval std::meta::info parse_json_impl(std::string_view json) { - ParseContext ctx{json}; - ctx.skip_whitespace(); + auto cursor = json.begin(); + auto end = json.end(); - if (!ctx.expect('{')) { - throw "expected '{'"; - } + auto is_whitespace = [](char c) { + return c == ' ' || c == '\n' || c == '\t' || c == '\r'; + }; + + auto skip_whitespace = [&]() -> void { + while (cursor != end && is_whitespace(*cursor)) cursor++; + }; + + auto expect_consume = [&](char c) -> void { + skip_whitespace(); + if (cursor == end || *(cursor++) != c) throw "unexpected character"; + }; + + auto parse_until = [&](std::vector delims, std::string &out) -> void { + skip_whitespace(); + while (cursor != end && + !std::ranges::any_of(delims, [&](char c) { return c == *cursor; })) + out += *(cursor++); + }; + + auto parse_delimited = [&](char lhs, std::string &out, char rhs) -> void { + skip_whitespace(); + expect_consume(lhs); + parse_until({rhs}, out); + expect_consume(rhs); + }; + + auto parse_value = [&](std::string &out) -> void { + skip_whitespace(); + + bool quoted = false; + unsigned depth = 0; + bool in_array = false; + while (true) { + if (cursor == end) throw "unexpected end of stream"; + if (is_whitespace(*cursor) && !quoted && depth == 0) + break; + + if (depth == 0 && (*cursor == ',' || *cursor == '}' || *cursor == ']')) + break; + out += *(cursor++); + + if (out.back() == '{') + ++depth; + else if (out.back() == '}') + --depth; + else if (out.back() == '[') { + in_array = true; + ++depth; + } else if (out.back() == ']') + --depth; + else if (out.back() == '"') { + if (quoted && depth == 0) + break; + quoted = true; + } + } + }; + + skip_whitespace(); + expect_consume('{'); std::vector members; std::vector values = {^^void}; - using std::meta::reflect_constant; + using std::meta::reflect_constant, std::meta::reflect_constant_string; + while (cursor != end && *cursor != '}') { + std::string field_name; + std::string value; - while (ctx.peek() != '}') { - ctx.skip_whitespace(); - if (ctx.peek() == '}') break; + parse_delimited('"', field_name, '"'); + expect_consume(':'); + parse_value(value); - auto field_name = ctx.parse_string(); - ctx.skip_whitespace(); - ctx.expect(':'); - ctx.skip_whitespace(); + if (value.empty()) throw "expected value"; + if (cursor == end) throw "unexpected end of stream"; - char ch = ctx.peek(); + if (value[0] == '"') { + if (value.back() != '"') throw "expected end of string"; + std::string_view contents(&value[1], value.size() - 2); - if (ch == '"') { - auto str_value = ctx.parse_string(); auto dms = std::meta::data_member_spec(^^char const*, {.name=field_name}); members.push_back(reflect_constant(dms)); - values.push_back(std::meta::reflect_constant_string(str_value)); - } else if (ch == 't' || ch == 'f') { - bool bool_value = ctx.peek() == 't'; - if (bool_value) { - if (!ctx.match("true")) throw "expected 'true'"; - } else { - if (!ctx.match("false")) throw "expected 'false'"; - } + values.push_back(reflect_constant_string(contents)); + } else if (value == "true") { auto dms = std::meta::data_member_spec(^^bool, {.name=field_name}); members.push_back(reflect_constant(dms)); - values.push_back(reflect_constant(bool_value)); - } else if (ch == 'n') { - if (!ctx.match("null")) throw "expected 'null'"; + values.push_back(reflect_constant(true)); + } else if (value == "false") { + auto dms = std::meta::data_member_spec(^^bool, {.name=field_name}); + members.push_back(reflect_constant(dms)); + values.push_back(reflect_constant(false)); + } else if (value == "null") { auto dms = std::meta::data_member_spec(^^std::nullptr_t, {.name=field_name}); members.push_back(reflect_constant(dms)); values.push_back(reflect_constant(nullptr)); - } else if (ch == '[') { - auto array_json = ctx.extract_nested(); - std::meta::info parsed = parse_array_impl(array_json); + } else if ((value[0] >= '0' && value[0] <= '9') || value[0] == '-') { + // Try to parse as integer first + bool is_int = true; + for (char c : value) { + if (c == '.' || c == 'e' || c == 'E') { + is_int = false; + break; + } + } + + if (is_int) { + int contents = [](std::string_view in) { + int out = 0; + bool negative = false; + std::size_t i = 0; + if (in[0] == '-') { + negative = true; + i = 1; + } + for (; i < in.size(); ++i) { + out = out * 10 + (in[i] - '0'); + } + return negative ? -out : out; + }(value); + + auto dms = std::meta::data_member_spec(^^int, {.name=field_name}); + members.push_back(reflect_constant(dms)); + values.push_back(reflect_constant(contents)); + } else { + // Parse as double + double contents = [](std::string_view in) { + double result = 0.0; + double sign = 1.0; + std::size_t i = 0; + + if (in[0] == '-') { + sign = -1.0; + i = 1; + } + + while (i < in.size() && in[i] >= '0' && in[i] <= '9') { + result = result * 10.0 + (in[i] - '0'); + ++i; + } + + if (i < in.size() && in[i] == '.') { + ++i; + double fraction = 0.0; + double divisor = 1.0; + while (i < in.size() && in[i] >= '0' && in[i] <= '9') { + fraction = fraction * 10.0 + (in[i] - '0'); + divisor *= 10.0; + ++i; + } + result += fraction / divisor; + } + + return result * sign; + }(value); + + auto dms = std::meta::data_member_spec(^^double, {.name=field_name}); + members.push_back(reflect_constant(dms)); + values.push_back(reflect_constant(contents)); + } + } else if (value[0] == '{') { + // Nested object + std::meta::info parsed = parse_json_impl(value); + auto dms = std::meta::data_member_spec(std::meta::type_of(parsed), {.name=field_name}); members.push_back(reflect_constant(dms)); values.push_back(parsed); - } else if (ch == '{') { - auto nested_json = ctx.extract_nested(); - std::meta::info parsed = parse_json_impl(nested_json); + } else if (value[0] == '[') { + // Array + std::meta::info parsed = parse_json_array_impl(value); + auto dms = std::meta::data_member_spec(std::meta::type_of(parsed), {.name=field_name}); members.push_back(reflect_constant(dms)); values.push_back(parsed); - } else if ((ch >= '0' && ch <= '9') || ch == '-') { - double num_value = ctx.parse_number(); - auto dms = std::meta::data_member_spec(^^double, {.name=field_name}); - members.push_back(reflect_constant(dms)); - values.push_back(reflect_constant(num_value)); - } else { - throw "unexpected value type"; } - ctx.skip_whitespace(); - if (ctx.peek() == ',') ctx.consume(); + skip_whitespace(); + if (cursor != end && *cursor == ',') + ++cursor; } - ctx.expect('}'); + if (cursor == end) throw "unexpected end"; + expect_consume('}'); + // The substitute() trick: + // 1. Create the type: Cls values[0] = std::meta::substitute(^^Cls, members); + // 2. Create instance: construct_from return std::meta::substitute(^^construct_from, values); } /** - * @brief JSON string wrapper for template parameters + * @brief Main parse_json function - template wrapper */ -struct JSONString { - std::meta::info Rep; - consteval JSONString(const char *Json) : Rep{parse_json_impl(Json)} {} -}; - -/** - * @brief Main parse_json function - template variable pattern - * - * Usage: - * constexpr auto config = json_to_object; - * static_assert(config.port == 8080); - * static_assert(std::string_view(config.host) == "localhost"); - */ -template -inline constexpr auto json_to_object = [:json.Rep:]; - -/** - * @brief Alternative: User-defined literal for JSON parsing - * - * Usage: - * constexpr auto config = R"({"port":8080})"_json; - */ -template -consteval auto operator""_json() { - return [:json.Rep:]; +template +consteval auto parse_json() { + constexpr std::meta::info result = parse_json_impl(json_str.view()); + return [:result:]; } /** - * @brief Validate JSON syntax at compile-time + * @brief JSON validation */ -template +template consteval bool validate_json() { - return true; + try { + parse_json_impl(json_str.view()); + return true; + } catch (...) { + return false; + } } } // namespace compile_time diff --git a/tests/ondemand/compile_time_json_tests.cpp b/tests/ondemand/compile_time_json_tests.cpp index 691030dab..c6907940c 100644 --- a/tests/ondemand/compile_time_json_tests.cpp +++ b/tests/ondemand/compile_time_json_tests.cpp @@ -1,6 +1,6 @@ /** * @file compile_time_json_tests.cpp - * @brief Tests for compile-time JSON parsing using C++26 P2996 reflection + * @brief Comprehensive tests for compile-time JSON parsing using C++26 P2996 reflection */ #include "simdjson.h" @@ -21,12 +21,12 @@ using namespace arm64::compile_time; bool test_basic_object() { TEST_START(); - constexpr auto config = json_to_object; + })">(); static_assert(config.port == 8080); static_assert(std::string_view(config.host) == "localhost"); @@ -47,7 +47,7 @@ bool test_basic_object() { bool test_nested_objects() { TEST_START(); - constexpr auto config = json_to_object; + })">(); static_assert(config.server_port == 3000); static_assert(config.enable_ssl == true); @@ -78,7 +78,7 @@ bool test_nested_objects() { bool test_deeply_nested_objects() { TEST_START(); - constexpr auto config = json_to_object; + })">(); static_assert(std::string_view(config.app_name) == "MyApp"); static_assert(config.version == 1.5); @@ -108,15 +108,166 @@ bool test_deeply_nested_objects() { } /** - * Test 4: Arrays of primitives + * Test 4: Empty object + */ +bool test_empty_object() { + TEST_START(); + + constexpr auto config = parse_json<"{}">(); + (void)config; // Suppress unused warning + + TEST_SUCCEED(); +} + +/** + * Test 5: Negative numbers + */ +bool test_negative_numbers() { + TEST_START(); + + constexpr auto data = parse_json(); + + static_assert(data.temperature == -273.15); + static_assert(data.count == -42); + + ASSERT_EQUAL(data.temperature, -273.15); + ASSERT_EQUAL(data.count, -42); + + TEST_SUCCEED(); +} + +/** + * Test 6: Whitespace handling + */ +bool test_whitespace() { + TEST_START(); + + constexpr auto data = parse_json(); + + static_assert(std::string_view(data.key1) == "value1"); + static_assert(data.key2 == 42); + + ASSERT_EQUAL(std::string_view(data.key1), "value1"sv); + ASSERT_EQUAL(data.key2, 42); + + TEST_SUCCEED(); +} + +/** + * Test 7: Real-time system configuration + */ +bool test_realtime_config() { + TEST_START(); + + constexpr auto config = parse_json(); + + static_assert(config.control_loop_hz == 1000); + static_assert(config.max_acceleration == 9.8); + static_assert(config.enable_safety_checks == true); + + ASSERT_EQUAL(config.control_loop_hz, 1000); + ASSERT_EQUAL(config.max_acceleration, 9.8); + ASSERT_EQUAL(config.min_velocity, -50.0); + ASSERT_EQUAL(config.max_velocity, 50.0); + ASSERT_TRUE(config.enable_safety_checks); + ASSERT_EQUAL(std::string_view(config.log_level), "INFO"sv); + + TEST_SUCCEED(); +} + +/** + * Test 8: External JSON file (future #embed support) + */ +bool test_external_json_embed() { + TEST_START(); + + // Future C++26 with #embed: + // constexpr auto config = parse_json<#embed "test_config.json">(); + + // Current workaround - inline the JSON from test_config.json + constexpr auto config = parse_json(); + + static_assert(std::string_view(config.system_name) == "RealTimeController"); + static_assert(std::string_view(config.version) == "2.1.0"); + static_assert(config.control_loop_hz == 1000); + static_assert(config.max_latency_us == 500); + static_assert(config.enable_diagnostics == true); + static_assert(std::string_view(config.log_level) == "INFO"); + + ASSERT_EQUAL(std::string_view(config.system_name), "RealTimeController"sv); + ASSERT_EQUAL(std::string_view(config.version), "2.1.0"sv); + ASSERT_EQUAL(config.control_loop_hz, 1000); + ASSERT_EQUAL(config.max_latency_us, 500); + ASSERT_TRUE(config.enable_diagnostics); + ASSERT_EQUAL(std::string_view(config.log_level), "INFO"sv); + + TEST_SUCCEED(); +} + +/** + * Test 9: JSON validation + */ +bool test_json_validation() { + TEST_START(); + + static_assert(validate_json()); + static_assert(validate_json()); + + TEST_SUCCEED(); +} + +/** + * Test 10: Null values + */ +bool test_null_values() { + TEST_START(); + + constexpr auto data = parse_json(); + + static_assert(data.nullable_field == nullptr); + static_assert(data.number == 42); + + ASSERT_EQUAL(data.nullable_field, nullptr); + ASSERT_EQUAL(data.number, 42); + + TEST_SUCCEED(); +} + +/** + * Test 11: Arrays of primitives */ bool test_arrays_primitives() { TEST_START(); - constexpr auto data = json_to_object; + })">(); static_assert(data.values.size() == 5); static_assert(data.values[0] == 1); @@ -136,17 +287,17 @@ bool test_arrays_primitives() { } /** - * Test 5: Arrays of objects + * Test 12: Arrays of objects */ bool test_arrays_of_objects() { TEST_START(); - constexpr auto data = json_to_object; + })">(); static_assert(data.users.size() == 2); static_assert(std::string_view(data.users[0].name) == "Alice"); @@ -164,16 +315,16 @@ bool test_arrays_of_objects() { } /** - * Test 6: Nested arrays in objects + * Test 13: Nested arrays in objects */ bool test_nested_arrays() { TEST_START(); - constexpr auto data = json_to_object; + })">(); static_assert(data.config.ports.size() == 3); static_assert(data.config.ports[0] == 8080); @@ -187,12 +338,12 @@ bool test_nested_arrays() { } /** - * Test 7: Complex mixed structure + * Test 14: Complex mixed structure with arrays and nested objects */ bool test_complex_mixed() { TEST_START(); - constexpr auto config = json_to_object; + })">(); static_assert(std::string_view(config.app) == "myapp"); static_assert(config.version == 1.0); @@ -221,176 +372,18 @@ bool test_complex_mixed() { } /** - * Test 8: Empty object + * Test 15: Empty arrays */ -bool test_empty_object() { +bool test_empty_arrays() { TEST_START(); - constexpr auto config = json_to_object<"{}">; - (void)config; // Suppress unused warning + constexpr auto data = parse_json(); - TEST_SUCCEED(); -} + static_assert(data.empty.size() == 0); -/** - * Test 9: Negative numbers - */ -bool test_negative_numbers() { - TEST_START(); - - constexpr auto data = json_to_object; - - static_assert(data.temperature == -273.15); - static_assert(data.count == -42); - - ASSERT_EQUAL(data.temperature, -273.15); - ASSERT_EQUAL(data.count, -42); - - TEST_SUCCEED(); -} - -/** - * Test 10: Whitespace handling - */ -bool test_whitespace() { - TEST_START(); - - constexpr auto data = json_to_object; - - static_assert(std::string_view(data.key1) == "value1"); - static_assert(data.key2 == 42); - - ASSERT_EQUAL(std::string_view(data.key1), "value1"sv); - ASSERT_EQUAL(data.key2, 42); - - TEST_SUCCEED(); -} - -/** - * Test 11: String unescaping from inline helpers - */ -bool test_string_unescape() { - TEST_START(); - - { - constexpr auto pair = unescape_json_string<100>("hello world"); - ASSERT_EQUAL(std::string_view(pair.first.data(), pair.second), "hello world"sv); - } - - { - constexpr auto pair = unescape_json_string<100>(R"(hello\nworld)"); - ASSERT_EQUAL(std::string_view(pair.first.data(), pair.second), "hello\nworld"sv); - } - - { - constexpr auto pair = unescape_json_string<100>(R"(quote: \")"); - ASSERT_EQUAL(std::string_view(pair.first.data(), pair.second), "quote: \""sv); - } - - TEST_SUCCEED(); -} - -/** - * Test 12: JSON validation - */ -bool test_json_validation() { - TEST_START(); - - static_assert(validate_json()); - static_assert(validate_json()); - - TEST_SUCCEED(); -} - -/** - * Test 13: User-defined literal - */ -bool test_user_defined_literal() { - TEST_START(); - - constexpr auto config = R"({"version": 1.0, "name": "test"})"_json; - - static_assert(config.version == 1.0); - static_assert(std::string_view(config.name) == "test"); - - ASSERT_EQUAL(config.version, 1.0); - ASSERT_EQUAL(std::string_view(config.name), "test"sv); - - TEST_SUCCEED(); -} - -/** - * Test 14: Real-time system configuration - */ -bool test_realtime_config() { - TEST_START(); - - constexpr auto config = json_to_object; - - static_assert(config.control_loop_hz == 1000); - static_assert(config.max_acceleration == 9.8); - static_assert(config.enable_safety_checks == true); - - ASSERT_EQUAL(config.control_loop_hz, 1000); - ASSERT_EQUAL(config.max_acceleration, 9.8); - ASSERT_EQUAL(config.min_velocity, -50.0); - ASSERT_EQUAL(config.max_velocity, 50.0); - ASSERT_TRUE(config.enable_safety_checks); - ASSERT_EQUAL(std::string_view(config.log_level), "INFO"sv); - - TEST_SUCCEED(); -} - -/** - * Test 15: Future #embed support demonstration - * - * Note: This test shows how external JSON files would be used with #embed. - * Currently uses inline JSON matching test_config.json for compatibility. - */ -bool test_external_json_embed() { - TEST_START(); - - // Future C++26 with #embed: - // constexpr auto config = json_to_object<#embed "test_config.json">; - - // Current workaround - inline the JSON from test_config.json - constexpr auto config = json_to_object; - - static_assert(std::string_view(config.system_name) == "RealTimeController"); - static_assert(std::string_view(config.version) == "2.1.0"); - static_assert(config.control_loop_hz == 1000); - static_assert(config.max_latency_us == 500); - static_assert(config.enable_diagnostics == true); - static_assert(std::string_view(config.log_level) == "INFO"); - - ASSERT_EQUAL(std::string_view(config.system_name), "RealTimeController"sv); - ASSERT_EQUAL(std::string_view(config.version), "2.1.0"sv); - ASSERT_EQUAL(config.control_loop_hz, 1000); - ASSERT_EQUAL(config.max_latency_us, 500); - ASSERT_TRUE(config.enable_diagnostics); - ASSERT_EQUAL(std::string_view(config.log_level), "INFO"sv); + ASSERT_EQUAL(data.empty.size(), 0); TEST_SUCCEED(); } @@ -399,18 +392,18 @@ bool run() { return test_basic_object() && test_nested_objects() && test_deeply_nested_objects() && + test_empty_object() && + test_negative_numbers() && + test_whitespace() && + test_realtime_config() && + test_external_json_embed() && + test_json_validation() && + test_null_values() && test_arrays_primitives() && test_arrays_of_objects() && test_nested_arrays() && test_complex_mixed() && - test_empty_object() && - test_negative_numbers() && - test_whitespace() && - test_string_unescape() && - test_json_validation() && - test_user_defined_literal() && - test_realtime_config() && - test_external_json_embed(); + test_empty_arrays(); } } // namespace compile_time_json_tests