This commit is contained in:
Francisco Geiman Thiesen
2025-10-10 21:32:10 -07:00
parent ca42a49fba
commit d326f2ce9f
2 changed files with 542 additions and 451 deletions
@@ -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<Ms...>::Inner + substitute() pattern for recursive type generation.
*/
#ifndef SIMDJSON_GENERIC_COMPILE_TIME_JSON_H
@@ -22,15 +17,19 @@
#include <meta>
#include <string_view>
#include <vector>
#include <array>
#include <vector>
#include <cstdint>
#include <string>
#include <algorithm>
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 <std::meta::info ...Ms>
struct Outer {
@@ -40,322 +39,421 @@ struct Outer {
}
};
/**
* @brief Type alias for the generated struct
*/
template <std::meta::info ...Ms>
using Cls = Outer<Ms...>::Inner;
/**
* @brief Helper template for aggregate initialization from reflected values
* @brief Variable template for constructing instances with values
*/
template <typename T, auto ... Vs>
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, 0>, ^^void});
}
auto expect_consume = [&](char c) -> void {
skip_whitespace();
if (cursor == end || *(cursor++) != c) throw "unexpected character";
};
std::vector<std::meta::info> 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<std::meta::info> 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, 0>, ^^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<std::meta::info> values;
values.push_back(array_type);
for (auto& elem : element_values) {
values.push_back(elem);
}
// Create std::array<ElementType, Count> 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<char> 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<std::meta::info> members;
std::vector<std::meta::info> 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<member_specs...>
values[0] = std::meta::substitute(^^Cls, members);
// 2. Create instance: construct_from<Type, values...>
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<R"({"port":8080,"host":"localhost"})">;
* static_assert(config.port == 8080);
* static_assert(std::string_view(config.host) == "localhost");
*/
template <JSONString json>
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 <JSONString json>
consteval auto operator""_json() {
return [:json.Rep:];
template<constevalutil::fixed_string json_str>
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 <JSONString json>
template<constevalutil::fixed_string json_str>
consteval bool validate_json() {
return true;
try {
parse_json_impl(json_str.view());
return true;
} catch (...) {
return false;
}
}
} // namespace compile_time
+185 -192
View File
@@ -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<R"({
constexpr auto config = parse_json<R"({
"port": 8080,
"host": "localhost",
"debug": true,
"timeout": 30.5
})">;
})">();
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<R"({
constexpr auto config = parse_json<R"({
"server_port": 3000,
"enable_ssl": true,
"database": {
@@ -55,7 +55,7 @@ bool test_nested_objects() {
"port": 5432,
"timeout_sec": 30.0
}
})">;
})">();
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<R"({
constexpr auto config = parse_json<R"({
"app_name": "MyApp",
"version": 1.5,
"server": {
@@ -90,7 +90,7 @@ bool test_deeply_nested_objects() {
"min_version": 1.3
}
}
})">;
})">();
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<R"({
"temperature": -273.15,
"count": -42
})">();
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<R"(
{
"key1" : "value1" ,
"key2" : 42
}
)">();
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<R"({
"control_loop_hz": 1000,
"max_acceleration": 9.8,
"min_velocity": -50.0,
"max_velocity": 50.0,
"enable_safety_checks": true,
"log_level": "INFO"
})">();
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<R"({
"system_name": "RealTimeController",
"version": "2.1.0",
"control_loop_hz": 1000,
"max_latency_us": 500,
"enable_diagnostics": true,
"log_level": "INFO"
})">();
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<R"({"valid": true})">());
static_assert(validate_json<R"({"nested": {"deep": 42}})">());
TEST_SUCCEED();
}
/**
* Test 10: Null values
*/
bool test_null_values() {
TEST_START();
constexpr auto data = parse_json<R"({
"nullable_field": null,
"number": 42
})">();
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<R"({
constexpr auto data = parse_json<R"({
"values": [1, 2, 3, 4, 5],
"flags": [true, false, true]
})">;
})">();
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<R"({
constexpr auto data = parse_json<R"({
"users": [
{"name": "Alice", "age": 30},
{"name": "Bob", "age": 25}
]
})">;
})">();
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<R"({
constexpr auto data = parse_json<R"({
"config": {
"ports": [8080, 8081, 8082]
}
})">;
})">();
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<R"({
constexpr auto config = parse_json<R"({
"app": "myapp",
"version": 1.0,
"config": {
@@ -203,7 +354,7 @@ bool test_complex_mixed() {
{"host": "server1", "port": 3000},
{"host": "server2", "port": 3001}
]
})">;
})">();
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<R"({
"empty": []
})">();
TEST_SUCCEED();
}
static_assert(data.empty.size() == 0);
/**
* Test 9: Negative numbers
*/
bool test_negative_numbers() {
TEST_START();
constexpr auto data = json_to_object<R"({
"temperature": -273.15,
"count": -42
})">;
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<R"(
{
"key1" : "value1" ,
"key2" : 42
}
)">;
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<R"({"valid": true})">());
static_assert(validate_json<R"({"nested": {"deep": 42}})">());
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<R"({
"control_loop_hz": 1000,
"max_acceleration": 9.8,
"min_velocity": -50.0,
"max_velocity": 50.0,
"enable_safety_checks": true,
"log_level": "INFO"
})">;
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<R"({
"system_name": "RealTimeController",
"version": "2.1.0",
"control_loop_hz": 1000,
"max_latency_us": 500,
"enable_diagnostics": true,
"log_level": "INFO"
})">;
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