From 67d898322abe2fb833388bca2a70eaf524761544 Mon Sep 17 00:00:00 2001 From: teseo Date: Wed, 13 May 2026 12:39:47 -0700 Subject: [PATCH 1/7] embedder? --- CMakeLists.txt | 6 + cmake/jse/jse_add_embedded_spec.cmake | 113 ++++++++++++ tests/CMakeLists.txt | 6 + tests/test_validator.cpp | 11 +- tools/jse_embed_spec.cpp | 247 ++++++++++++++++++++++++++ 5 files changed, 382 insertions(+), 1 deletion(-) create mode 100644 cmake/jse/jse_add_embedded_spec.cmake create mode 100644 tools/jse_embed_spec.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 8b368dd..ffa9f6c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,6 +59,7 @@ include(jse_use_colors) # IPC Toolkit utils include(jse_prepend_current_path) include(jse_set_source_group) +include(jse_add_embedded_spec) # Sort projects inside the solution set_property(GLOBAL PROPERTY USE_FOLDERS ON) @@ -105,6 +106,11 @@ target_link_libraries(jse PUBLIC nlohmann_json::nlohmann_json) # Use C++17 target_compile_features(jse PUBLIC cxx_std_17) +add_executable(jse_embed_spec_tool EXCLUDE_FROM_ALL tools/jse_embed_spec.cpp) +target_link_libraries(jse_embed_spec_tool PRIVATE jse::jse) +target_compile_features(jse_embed_spec_tool PRIVATE cxx_std_17) +set_target_properties(jse_embed_spec_tool PROPERTIES FOLDER "JSE") + ################################################################################ # Tests ################################################################################ diff --git a/cmake/jse/jse_add_embedded_spec.cmake b/cmake/jse/jse_add_embedded_spec.cmake new file mode 100644 index 0000000..e05cfaf --- /dev/null +++ b/cmake/jse/jse_add_embedded_spec.cmake @@ -0,0 +1,113 @@ +# jse_add_embedded_spec( +# INPUT root_spec.json +# OUTPUT generated/spec.hpp +# INCLUDE_DIRS include_dir_1 include_dir_2 +# ) +# +# Creates the fixed jse::embed library target containing generated .hpp/.cpp +# files. The generated jse::embed::spec() function returns a lazily parsed, +# include-expanded nlohmann::json spec. +function(jse_add_embedded_spec) + set(options) + set(one_value_args INPUT OUTPUT) + set(multi_value_args INCLUDE_DIRS) + cmake_parse_arguments(JSE_EMBED + "${options}" + "${one_value_args}" + "${multi_value_args}" + ${ARGN} + ) + + if(JSE_EMBED_UNPARSED_ARGUMENTS) + message(FATAL_ERROR "Unknown arguments for jse_add_embedded_spec: ${JSE_EMBED_UNPARSED_ARGUMENTS}") + endif() + + if(TARGET jse_embed) + message(FATAL_ERROR "jse_add_embedded_spec creates fixed target jse::embed and can only be called once") + endif() + + if(NOT JSE_EMBED_INPUT) + message(FATAL_ERROR "jse_add_embedded_spec requires INPUT") + endif() + + if(NOT JSE_EMBED_OUTPUT) + message(FATAL_ERROR "jse_add_embedded_spec requires OUTPUT") + endif() + + if(NOT TARGET jse_embed_spec_tool) + message(FATAL_ERROR "jse_add_embedded_spec requires the jse_embed_spec_tool generator target") + endif() + + get_filename_component(_jse_embed_input "${JSE_EMBED_INPUT}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}") + get_filename_component(_jse_embed_header "${JSE_EMBED_OUTPUT}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_BINARY_DIR}") + get_filename_component(_jse_embed_output_dir "${_jse_embed_header}" DIRECTORY) + get_filename_component(_jse_embed_output_stem "${_jse_embed_header}" NAME_WE) + set(_jse_embed_source "${_jse_embed_output_dir}/${_jse_embed_output_stem}.cpp") + + get_filename_component(_jse_embed_input_dir "${_jse_embed_input}" DIRECTORY) + set(_jse_embed_include_dirs "${_jse_embed_input_dir}" ${JSE_EMBED_INCLUDE_DIRS}) + set(_jse_embed_abs_include_dirs) + set(_jse_embed_include_args) + set(_jse_embed_depends "${_jse_embed_input}") + + foreach(_jse_embed_include_dir IN LISTS _jse_embed_include_dirs) + get_filename_component(_jse_embed_abs_include_dir "${_jse_embed_include_dir}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}") + list(APPEND _jse_embed_abs_include_dirs "${_jse_embed_abs_include_dir}") + endforeach() + + list(REMOVE_DUPLICATES _jse_embed_abs_include_dirs) + + foreach(_jse_embed_abs_include_dir IN LISTS _jse_embed_abs_include_dirs) + list(APPEND _jse_embed_include_args "--include-dir" "${_jse_embed_abs_include_dir}") + file(GLOB_RECURSE _jse_embed_json_depends + CONFIGURE_DEPENDS + "${_jse_embed_abs_include_dir}/*.json" + ) + list(APPEND _jse_embed_depends ${_jse_embed_json_depends}) + endforeach() + list(REMOVE_DUPLICATES _jse_embed_depends) + + file(MAKE_DIRECTORY "${_jse_embed_output_dir}") + + add_custom_command( + OUTPUT + "${_jse_embed_header}" + "${_jse_embed_source}" + COMMAND + jse_embed_spec_tool + --input "${_jse_embed_input}" + --output-header "${_jse_embed_header}" + --output-source "${_jse_embed_source}" + --namespace "jse::embed" + --function "spec" + ${_jse_embed_include_args} + DEPENDS + jse_embed_spec_tool + ${_jse_embed_depends} + COMMENT + "Generating embedded JSON spec ${_jse_embed_output_stem}" + VERBATIM + ) + + set_source_files_properties( + "${_jse_embed_header}" + "${_jse_embed_source}" + PROPERTIES GENERATED TRUE + ) + + add_library(jse_embed + "${_jse_embed_source}" + "${_jse_embed_header}" + ) + add_library(jse::embed ALIAS jse_embed) + + target_include_directories(jse_embed PUBLIC + "$" + ) + target_link_libraries(jse_embed PUBLIC nlohmann_json::nlohmann_json) + target_compile_features(jse_embed PUBLIC cxx_std_17) + source_group("Generated" FILES "${_jse_embed_source}" "${_jse_embed_header}") + + set(JSE_EMBED_HEADER "${_jse_embed_header}" PARENT_SCOPE) + set(JSE_EMBED_SOURCE "${_jse_embed_source}" PARENT_SCOPE) +endfunction() diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d010fa1..8e2610d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -8,6 +8,11 @@ set(test_sources ) add_executable(unit_tests ${test_sources}) +jse_add_embedded_spec( + INPUT "${CMAKE_SOURCE_DIR}/data/rules_04.json" + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/embedded_specs/rules_04.hpp" +) + ################################################################################ # Required Libraries ################################################################################ @@ -16,6 +21,7 @@ include(catch2) target_link_libraries(unit_tests PUBLIC Catch2::Catch2) target_link_libraries(unit_tests PUBLIC jse::jse) +target_link_libraries(unit_tests PUBLIC jse::embed) include(jse_warnings) target_link_libraries(unit_tests PRIVATE jse::warnings) diff --git a/tests/test_validator.cpp b/tests/test_validator.cpp index beb7ed6..a85a20d 100644 --- a/tests/test_validator.cpp +++ b/tests/test_validator.cpp @@ -1,5 +1,6 @@ ////////////////////////////////////////////////////////////////////////// #include +#include #include #include #include @@ -250,6 +251,14 @@ TEST_CASE("include_rule", "[validator]") REQUIRE(new_rules == matching); } +TEST_CASE("embedded_include_rule", "[validator]") +{ + std::ifstream ifs(root_path + "/rules_03.json"); + json matching = json::parse(ifs); + + REQUIRE(jse::embed::spec() == matching); +} + TEST_CASE("file_01", "[validator]") { std::ifstream ifs1(root_path + "/input_01.json"); @@ -655,4 +664,4 @@ TEST_CASE("null_as_nan", "[validator][inject]") CHECK(return_json["f2"].is_null()); input["f2"] = std::nan(""); -} \ No newline at end of file +} diff --git a/tools/jse_embed_spec.cpp b/tools/jse_embed_spec.cpp new file mode 100644 index 0000000..32c7f2f --- /dev/null +++ b/tools/jse_embed_spec.cpp @@ -0,0 +1,247 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + struct Options + { + std::filesystem::path input; + std::filesystem::path output_header; + std::filesystem::path output_source; + std::string namespace_name = "jse"; + std::string function_name = "rules"; + std::vector include_dirs; + }; + + void print_usage(const char *argv0) + { + std::cerr << "Usage: " << argv0 << " --input spec.json" + << " --output-header spec.hpp --output-source spec.cpp" + << " [--namespace name] [--function name]" + << " [--include-dir dir ...]\n"; + } + + bool is_identifier(const std::string &value) + { + if (value.empty()) + return false; + + const auto first = static_cast(value.front()); + if (!(std::isalpha(first) || value.front() == '_')) + return false; + + for (char c : value.substr(1)) + { + const auto uc = static_cast(c); + if (!(std::isalnum(uc) || c == '_')) + return false; + } + + return true; + } + + std::vector split_namespace(const std::string &namespace_name) + { + std::vector parts; + if (namespace_name.empty()) + return parts; + + std::size_t start = 0; + while (start <= namespace_name.size()) + { + const std::size_t end = namespace_name.find("::", start); + const std::string part = namespace_name.substr( + start, end == std::string::npos ? std::string::npos : end - start); + if (!is_identifier(part)) + throw std::runtime_error("Invalid namespace component: " + part); + + parts.push_back(part); + + if (end == std::string::npos) + break; + start = end + 2; + } + + return parts; + } + + Options parse_options(int argc, char *argv[]) + { + Options options; + + for (int i = 1; i < argc; ++i) + { + const std::string arg = argv[i]; + const auto require_value = [&](const std::string &option) -> std::string { + if (i + 1 >= argc) + throw std::runtime_error("Missing value for " + option); + return argv[++i]; + }; + + if (arg == "--input") + options.input = require_value(arg); + else if (arg == "--output-header") + options.output_header = require_value(arg); + else if (arg == "--output-source") + options.output_source = require_value(arg); + else if (arg == "--namespace") + options.namespace_name = require_value(arg); + else if (arg == "--function") + options.function_name = require_value(arg); + else if (arg == "--include-dir") + options.include_dirs.push_back(require_value(arg)); + else if (arg == "--help" || arg == "-h") + { + print_usage(argv[0]); + std::exit(EXIT_SUCCESS); + } + else + { + throw std::runtime_error("Unknown argument: " + arg); + } + } + + if (options.input.empty()) + throw std::runtime_error("Missing --input"); + if (options.output_header.empty()) + throw std::runtime_error("Missing --output-header"); + if (options.output_source.empty()) + throw std::runtime_error("Missing --output-source"); + if (!is_identifier(options.function_name)) + throw std::runtime_error("Invalid function name: " + options.function_name); + + split_namespace(options.namespace_name); + + return options; + } + + jse::json load_and_expand_rules(const Options &options) + { + std::ifstream input(options.input); + if (!input) + throw std::runtime_error("Failed to open input spec: " + options.input.string()); + + jse::json rules = jse::json::parse(input); + + jse::JSE engine; + engine.include_directories = options.include_dirs; + + const auto parent = std::filesystem::absolute(options.input).parent_path(); + if (!parent.empty()) + engine.include_directories.push_back(parent.string()); + + return engine.inject_include(rules); + } + + std::string raw_string_literal(const std::string &value) + { + for (int i = 0; i < 10000; ++i) + { + const std::string delimiter = i == 0 ? "JSE_JSON" : "JSE_JSON_" + std::to_string(i); + if (value.find(")" + delimiter + "\"") == std::string::npos) + return "R\"" + delimiter + "(\n" + value + "\n)" + delimiter + "\""; + } + + throw std::runtime_error("Failed to find a valid raw string delimiter."); + } + + void ensure_parent_directory(const std::filesystem::path &path) + { + const auto parent = path.parent_path(); + if (!parent.empty()) + std::filesystem::create_directories(parent); + } + + void write_file(const std::filesystem::path &path, const std::string &content) + { + ensure_parent_directory(path); + + std::ofstream output(path); + if (!output) + throw std::runtime_error("Failed to open output file: " + path.string()); + + output << content; + if (!output) + throw std::runtime_error("Failed to write output file: " + path.string()); + } + + void open_namespaces(std::ostream &os, const std::vector &namespaces) + { + for (const auto &name : namespaces) + os << "namespace " << name << "\n{\n"; + if (!namespaces.empty()) + os << "\n"; + } + + void close_namespaces(std::ostream &os, const std::vector &namespaces) + { + for (auto it = namespaces.rbegin(); it != namespaces.rend(); ++it) + os << "} // namespace " << *it << "\n"; + } + + std::string header_content(const Options &options) + { + const auto namespaces = split_namespace(options.namespace_name); + + std::ostringstream os; + os << "#pragma once\n\n"; + os << "#include \n\n"; + open_namespaces(os, namespaces); + os << "const nlohmann::json &" << options.function_name << "();\n"; + if (!namespaces.empty()) + os << "\n"; + close_namespaces(os, namespaces); + + return os.str(); + } + + std::string source_content(const Options &options, const jse::json &rules) + { + const auto namespaces = split_namespace(options.namespace_name); + const auto header_name = options.output_header.filename().string(); + const std::string rules_text = rules.dump(4, ' ', true); + + std::ostringstream os; + os << "#include \"" << header_name << "\"\n\n"; + open_namespaces(os, namespaces); + os << "const nlohmann::json &" << options.function_name << "()\n"; + os << "{\n"; + os << " static const nlohmann::json value = nlohmann::json::parse(" + << raw_string_literal(rules_text) << ");\n"; + os << " return value;\n"; + os << "}\n"; + if (!namespaces.empty()) + os << "\n"; + close_namespaces(os, namespaces); + + return os.str(); + } +} // namespace + +int main(int argc, char *argv[]) +{ + try + { + const Options options = parse_options(argc, argv); + const jse::json rules = load_and_expand_rules(options); + + write_file(options.output_header, header_content(options)); + write_file(options.output_source, source_content(options, rules)); + return EXIT_SUCCESS; + } + catch (const std::exception &error) + { + std::cerr << error.what() << std::endl; + print_usage(argv[0]); + return EXIT_FAILURE; + } +} From 447269c5b6490448f9119d18f10cf31b5d284b33 Mon Sep 17 00:00:00 2001 From: teseo Date: Wed, 13 May 2026 14:18:32 -0700 Subject: [PATCH 2/7] better spec integration --- cmake/jse/jse_add_embedded_spec.cmake | 34 +++++++++++++++++---------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/cmake/jse/jse_add_embedded_spec.cmake b/cmake/jse/jse_add_embedded_spec.cmake index e05cfaf..29889fa 100644 --- a/cmake/jse/jse_add_embedded_spec.cmake +++ b/cmake/jse/jse_add_embedded_spec.cmake @@ -4,9 +4,9 @@ # INCLUDE_DIRS include_dir_1 include_dir_2 # ) # -# Creates the fixed jse::embed library target containing generated .hpp/.cpp -# files. The generated jse::embed::spec() function returns a lazily parsed, -# include-expanded nlohmann::json spec. +# Adds generated .hpp/.cpp files to the fixed jse::embed library target. Each +# generated header exposes jse::embed::::spec(), where output_stem +# is the OUTPUT filename stem converted to a valid C++ identifier. function(jse_add_embedded_spec) set(options) set(one_value_args INPUT OUTPUT) @@ -18,14 +18,12 @@ function(jse_add_embedded_spec) ${ARGN} ) + MESSAGE(STATUS "Configuring embedded spec with input ${JSE_EMBED_INPUT} and output ${JSE_EMBED_OUTPUT} using include dirs ${JSE_EMBED_INCLUDE_DIRS}") + if(JSE_EMBED_UNPARSED_ARGUMENTS) message(FATAL_ERROR "Unknown arguments for jse_add_embedded_spec: ${JSE_EMBED_UNPARSED_ARGUMENTS}") endif() - if(TARGET jse_embed) - message(FATAL_ERROR "jse_add_embedded_spec creates fixed target jse::embed and can only be called once") - endif() - if(NOT JSE_EMBED_INPUT) message(FATAL_ERROR "jse_add_embedded_spec requires INPUT") endif() @@ -43,6 +41,13 @@ function(jse_add_embedded_spec) get_filename_component(_jse_embed_output_dir "${_jse_embed_header}" DIRECTORY) get_filename_component(_jse_embed_output_stem "${_jse_embed_header}" NAME_WE) set(_jse_embed_source "${_jse_embed_output_dir}/${_jse_embed_output_stem}.cpp") + string(MAKE_C_IDENTIFIER "${_jse_embed_output_stem}" _jse_embed_spec_namespace) + + get_property(_jse_embed_namespaces GLOBAL PROPERTY JSE_EMBED_SPEC_NAMESPACES) + if(_jse_embed_spec_namespace IN_LIST _jse_embed_namespaces) + message(FATAL_ERROR "jse_add_embedded_spec already has an embedded spec named ${_jse_embed_spec_namespace}") + endif() + set_property(GLOBAL APPEND PROPERTY JSE_EMBED_SPEC_NAMESPACES "${_jse_embed_spec_namespace}") get_filename_component(_jse_embed_input_dir "${_jse_embed_input}" DIRECTORY) set(_jse_embed_include_dirs "${_jse_embed_input_dir}" ${JSE_EMBED_INCLUDE_DIRS}) @@ -78,7 +83,7 @@ function(jse_add_embedded_spec) --input "${_jse_embed_input}" --output-header "${_jse_embed_header}" --output-source "${_jse_embed_source}" - --namespace "jse::embed" + --namespace "jse::embed::${_jse_embed_spec_namespace}" --function "spec" ${_jse_embed_include_args} DEPENDS @@ -95,17 +100,20 @@ function(jse_add_embedded_spec) PROPERTIES GENERATED TRUE ) - add_library(jse_embed + if(NOT TARGET jse_embed) + add_library(jse_embed) + add_library(jse::embed ALIAS jse_embed) + target_link_libraries(jse_embed PUBLIC nlohmann_json::nlohmann_json) + target_compile_features(jse_embed PUBLIC cxx_std_17) + endif() + + target_sources(jse_embed PRIVATE "${_jse_embed_source}" "${_jse_embed_header}" ) - add_library(jse::embed ALIAS jse_embed) - target_include_directories(jse_embed PUBLIC "$" ) - target_link_libraries(jse_embed PUBLIC nlohmann_json::nlohmann_json) - target_compile_features(jse_embed PUBLIC cxx_std_17) source_group("Generated" FILES "${_jse_embed_source}" "${_jse_embed_header}") set(JSE_EMBED_HEADER "${_jse_embed_header}" PARENT_SCOPE) From 26994459a4fb0b8fc5a37b6cb94c2b05fc2b866e Mon Sep 17 00:00:00 2001 From: teseo Date: Wed, 13 May 2026 14:18:43 -0700 Subject: [PATCH 3/7] optin breaking change --- src/jse/jse.cpp | 4 ++-- src/jse/jse.h | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/jse/jse.cpp b/src/jse/jse.cpp index 78c86a2..e7f63d2 100644 --- a/src/jse/jse.cpp +++ b/src/jse/jse.cpp @@ -367,7 +367,7 @@ namespace jse { assert(rule.at("type") == "float"); - if (!input.is_number() && !input.is_null()) + if (!input.is_number() && !(allow_null_numbers && input.is_null())) return false; if (rule.contains("min") && input < rule["min"]) @@ -383,7 +383,7 @@ namespace jse { assert(rule.at("type") == "int"); - if (!input.is_number_integer() && !input.is_null()) + if (!input.is_number_integer() && !(allow_null_numbers && input.is_null())) return false; if (rule.contains("min") && input < rule["min"]) diff --git a/src/jse/jse.h b/src/jse/jse.h index 4dccfa2..81752e1 100644 --- a/src/jse/jse.h +++ b/src/jse/jse.h @@ -40,6 +40,9 @@ namespace jse // if all rules fail for a basic type, try boxing it once and try again bool boxing_primitive = true; + // allow null values to satisfy int and float rules + bool allow_null_numbers = false; + // message list typedef std::pair log_item; std::vector log; From 233f84c44745f7b6c1f5fccabd3e881b089fdbb7 Mon Sep 17 00:00:00 2001 From: teseo Date: Wed, 13 May 2026 14:18:50 -0700 Subject: [PATCH 4/7] tests --- tests/CMakeLists.txt | 5 ++++ tests/test_validator.cpp | 54 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8e2610d..94dc3f4 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -13,6 +13,11 @@ jse_add_embedded_spec( OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/embedded_specs/rules_04.hpp" ) +jse_add_embedded_spec( + INPUT "${CMAKE_SOURCE_DIR}/data/rules_01.json" + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/embedded_specs/rules_01.hpp" +) + ################################################################################ # Required Libraries ################################################################################ diff --git a/tests/test_validator.cpp b/tests/test_validator.cpp index a85a20d..44beae9 100644 --- a/tests/test_validator.cpp +++ b/tests/test_validator.cpp @@ -1,5 +1,6 @@ ////////////////////////////////////////////////////////////////////////// #include +#include #include #include #include @@ -256,7 +257,57 @@ TEST_CASE("embedded_include_rule", "[validator]") std::ifstream ifs(root_path + "/rules_03.json"); json matching = json::parse(ifs); - REQUIRE(jse::embed::spec() == matching); + REQUIRE(jse::embed::rules_04::spec() == matching); +} + +TEST_CASE("embedded_multiple_specs", "[validator]") +{ + std::ifstream ifs(root_path + "/rules_01.json"); + json matching = json::parse(ifs); + + REQUIRE(jse::embed::rules_01::spec() == matching); +} + +TEST_CASE("null_number_rules_are_opt_in", "[validator]") +{ + nlohmann::json rules = R"( + [ + { + "pointer": "/", + "type": "object", + "optional": ["field"] + }, + { + "pointer": "/field", + "type": "int" + }, + { + "pointer": "/field", + "type": "object", + "optional": ["offset"], + "default": null + }, + { + "pointer": "/field/offset", + "type": "int", + "default": 0 + } + ] + )"_json; + + nlohmann::json input = R"( + { + "field": null + } + )"_json; + + JSE jse; + jse.strict = true; + + REQUIRE(jse.verify_json(input, rules)); + + jse.allow_null_numbers = true; + REQUIRE(!jse.verify_json(input, rules)); } TEST_CASE("file_01", "[validator]") @@ -651,6 +702,7 @@ TEST_CASE("null_as_nan", "[validator][inject]") JSE jse; jse.strict = true; + jse.allow_null_numbers = true; bool r = jse.verify_json(input, rules); REQUIRE(r); From 66f9df8eed337a75976a6184cc521873896c1b2d Mon Sep 17 00:00:00 2001 From: teseo Date: Wed, 13 May 2026 19:25:43 -0700 Subject: [PATCH 5/7] splitting string for windows --- tools/jse_embed_spec.cpp | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/tools/jse_embed_spec.cpp b/tools/jse_embed_spec.cpp index 32c7f2f..703ed28 100644 --- a/tools/jse_embed_spec.cpp +++ b/tools/jse_embed_spec.cpp @@ -148,12 +148,23 @@ namespace { const std::string delimiter = i == 0 ? "JSE_JSON" : "JSE_JSON_" + std::to_string(i); if (value.find(")" + delimiter + "\"") == std::string::npos) - return "R\"" + delimiter + "(\n" + value + "\n)" + delimiter + "\""; + return "R\"" + delimiter + "(" + value + ")" + delimiter + "\""; } throw std::runtime_error("Failed to find a valid raw string delimiter."); } + std::vector split_string(const std::string &value, const std::size_t chunk_size) + { + std::vector chunks; + chunks.reserve((value.size() + chunk_size - 1) / chunk_size); + + for (std::size_t start = 0; start < value.size(); start += chunk_size) + chunks.push_back(value.substr(start, chunk_size)); + + return chunks; + } + void ensure_parent_directory(const std::filesystem::path &path) { const auto parent = path.parent_path(); @@ -206,17 +217,26 @@ namespace std::string source_content(const Options &options, const jse::json &rules) { + constexpr std::size_t max_string_literal_chunk_size = 8000; + const auto namespaces = split_namespace(options.namespace_name); const auto header_name = options.output_header.filename().string(); - const std::string rules_text = rules.dump(4, ' ', true); + const std::string rules_text = "\n" + rules.dump(4, ' ', true) + "\n"; + const auto rules_text_chunks = split_string(rules_text, max_string_literal_chunk_size); std::ostringstream os; os << "#include \"" << header_name << "\"\n\n"; + os << "#include \n\n"; open_namespaces(os, namespaces); os << "const nlohmann::json &" << options.function_name << "()\n"; os << "{\n"; - os << " static const nlohmann::json value = nlohmann::json::parse(" - << raw_string_literal(rules_text) << ");\n"; + os << " static const nlohmann::json value = []() {\n"; + os << " std::string text;\n"; + os << " text.reserve(" << rules_text.size() << ");\n"; + for (const auto &chunk : rules_text_chunks) + os << " text += " << raw_string_literal(chunk) << ";\n"; + os << " return nlohmann::json::parse(text);\n"; + os << " }();\n"; os << " return value;\n"; os << "}\n"; if (!namespaces.empty()) From 005de33656c39bae0f2c0f51631431ef1c7baf68 Mon Sep 17 00:00:00 2001 From: teseo Date: Sat, 16 May 2026 19:57:50 -0700 Subject: [PATCH 6/7] better multi target specs --- CMakeLists.txt | 2 +- cmake/jse/jse_add_embedded_spec.cmake | 121 ------------ cmake/jse/jse_add_spec.cmake | 269 ++++++++++++++++++++++++++ tests/CMakeLists.txt | 13 +- tests/test_validator.cpp | 4 +- 5 files changed, 282 insertions(+), 127 deletions(-) delete mode 100644 cmake/jse/jse_add_embedded_spec.cmake create mode 100644 cmake/jse/jse_add_spec.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index ffa9f6c..ff42f8c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,7 +59,7 @@ include(jse_use_colors) # IPC Toolkit utils include(jse_prepend_current_path) include(jse_set_source_group) -include(jse_add_embedded_spec) +include(jse_add_spec) # Sort projects inside the solution set_property(GLOBAL PROPERTY USE_FOLDERS ON) diff --git a/cmake/jse/jse_add_embedded_spec.cmake b/cmake/jse/jse_add_embedded_spec.cmake deleted file mode 100644 index 29889fa..0000000 --- a/cmake/jse/jse_add_embedded_spec.cmake +++ /dev/null @@ -1,121 +0,0 @@ -# jse_add_embedded_spec( -# INPUT root_spec.json -# OUTPUT generated/spec.hpp -# INCLUDE_DIRS include_dir_1 include_dir_2 -# ) -# -# Adds generated .hpp/.cpp files to the fixed jse::embed library target. Each -# generated header exposes jse::embed::::spec(), where output_stem -# is the OUTPUT filename stem converted to a valid C++ identifier. -function(jse_add_embedded_spec) - set(options) - set(one_value_args INPUT OUTPUT) - set(multi_value_args INCLUDE_DIRS) - cmake_parse_arguments(JSE_EMBED - "${options}" - "${one_value_args}" - "${multi_value_args}" - ${ARGN} - ) - - MESSAGE(STATUS "Configuring embedded spec with input ${JSE_EMBED_INPUT} and output ${JSE_EMBED_OUTPUT} using include dirs ${JSE_EMBED_INCLUDE_DIRS}") - - if(JSE_EMBED_UNPARSED_ARGUMENTS) - message(FATAL_ERROR "Unknown arguments for jse_add_embedded_spec: ${JSE_EMBED_UNPARSED_ARGUMENTS}") - endif() - - if(NOT JSE_EMBED_INPUT) - message(FATAL_ERROR "jse_add_embedded_spec requires INPUT") - endif() - - if(NOT JSE_EMBED_OUTPUT) - message(FATAL_ERROR "jse_add_embedded_spec requires OUTPUT") - endif() - - if(NOT TARGET jse_embed_spec_tool) - message(FATAL_ERROR "jse_add_embedded_spec requires the jse_embed_spec_tool generator target") - endif() - - get_filename_component(_jse_embed_input "${JSE_EMBED_INPUT}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}") - get_filename_component(_jse_embed_header "${JSE_EMBED_OUTPUT}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_BINARY_DIR}") - get_filename_component(_jse_embed_output_dir "${_jse_embed_header}" DIRECTORY) - get_filename_component(_jse_embed_output_stem "${_jse_embed_header}" NAME_WE) - set(_jse_embed_source "${_jse_embed_output_dir}/${_jse_embed_output_stem}.cpp") - string(MAKE_C_IDENTIFIER "${_jse_embed_output_stem}" _jse_embed_spec_namespace) - - get_property(_jse_embed_namespaces GLOBAL PROPERTY JSE_EMBED_SPEC_NAMESPACES) - if(_jse_embed_spec_namespace IN_LIST _jse_embed_namespaces) - message(FATAL_ERROR "jse_add_embedded_spec already has an embedded spec named ${_jse_embed_spec_namespace}") - endif() - set_property(GLOBAL APPEND PROPERTY JSE_EMBED_SPEC_NAMESPACES "${_jse_embed_spec_namespace}") - - get_filename_component(_jse_embed_input_dir "${_jse_embed_input}" DIRECTORY) - set(_jse_embed_include_dirs "${_jse_embed_input_dir}" ${JSE_EMBED_INCLUDE_DIRS}) - set(_jse_embed_abs_include_dirs) - set(_jse_embed_include_args) - set(_jse_embed_depends "${_jse_embed_input}") - - foreach(_jse_embed_include_dir IN LISTS _jse_embed_include_dirs) - get_filename_component(_jse_embed_abs_include_dir "${_jse_embed_include_dir}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}") - list(APPEND _jse_embed_abs_include_dirs "${_jse_embed_abs_include_dir}") - endforeach() - - list(REMOVE_DUPLICATES _jse_embed_abs_include_dirs) - - foreach(_jse_embed_abs_include_dir IN LISTS _jse_embed_abs_include_dirs) - list(APPEND _jse_embed_include_args "--include-dir" "${_jse_embed_abs_include_dir}") - file(GLOB_RECURSE _jse_embed_json_depends - CONFIGURE_DEPENDS - "${_jse_embed_abs_include_dir}/*.json" - ) - list(APPEND _jse_embed_depends ${_jse_embed_json_depends}) - endforeach() - list(REMOVE_DUPLICATES _jse_embed_depends) - - file(MAKE_DIRECTORY "${_jse_embed_output_dir}") - - add_custom_command( - OUTPUT - "${_jse_embed_header}" - "${_jse_embed_source}" - COMMAND - jse_embed_spec_tool - --input "${_jse_embed_input}" - --output-header "${_jse_embed_header}" - --output-source "${_jse_embed_source}" - --namespace "jse::embed::${_jse_embed_spec_namespace}" - --function "spec" - ${_jse_embed_include_args} - DEPENDS - jse_embed_spec_tool - ${_jse_embed_depends} - COMMENT - "Generating embedded JSON spec ${_jse_embed_output_stem}" - VERBATIM - ) - - set_source_files_properties( - "${_jse_embed_header}" - "${_jse_embed_source}" - PROPERTIES GENERATED TRUE - ) - - if(NOT TARGET jse_embed) - add_library(jse_embed) - add_library(jse::embed ALIAS jse_embed) - target_link_libraries(jse_embed PUBLIC nlohmann_json::nlohmann_json) - target_compile_features(jse_embed PUBLIC cxx_std_17) - endif() - - target_sources(jse_embed PRIVATE - "${_jse_embed_source}" - "${_jse_embed_header}" - ) - target_include_directories(jse_embed PUBLIC - "$" - ) - source_group("Generated" FILES "${_jse_embed_source}" "${_jse_embed_header}") - - set(JSE_EMBED_HEADER "${_jse_embed_header}" PARENT_SCOPE) - set(JSE_EMBED_SOURCE "${_jse_embed_source}" PARENT_SCOPE) -endfunction() diff --git a/cmake/jse/jse_add_spec.cmake b/cmake/jse/jse_add_spec.cmake new file mode 100644 index 0000000..ead0c66 --- /dev/null +++ b/cmake/jse/jse_add_spec.cmake @@ -0,0 +1,269 @@ +# jse_add_spec( +# [ALIAS alias::target] +# [INPUT root_spec.json] +# [OUTPUT generated/spec.hpp] +# [INCLUDE_DIRS include_dir_1 include_dir_2] +# [LINK_SPECS dependency::spec ...] +# [NAMESPACE cxx::namespace] +# [FUNCTION function_name] +# ) +# +# Creates or extends a CMake target carrying JSON spec metadata. If INPUT and +# OUTPUT are provided, generated .hpp/.cpp files are added to the target. The +# generated header exposes jse::embed::::::spec() by default. +include_guard(GLOBAL) + +function(_jse_resolve_target OUTPUT_VARIABLE TARGET_NAME) + get_target_property(_jse_aliased_target "${TARGET_NAME}" ALIASED_TARGET) + if(_jse_aliased_target) + set(${OUTPUT_VARIABLE} "${_jse_aliased_target}" PARENT_SCOPE) + else() + set(${OUTPUT_VARIABLE} "${TARGET_NAME}" PARENT_SCOPE) + endif() +endfunction() + +function(_jse_target_usage_scope OUTPUT_VARIABLE TARGET_NAME) + get_target_property(_jse_target_type "${TARGET_NAME}" TYPE) + if(_jse_target_type STREQUAL "INTERFACE_LIBRARY") + set(${OUTPUT_VARIABLE} INTERFACE PARENT_SCOPE) + elseif(_jse_target_type STREQUAL "EXECUTABLE") + set(${OUTPUT_VARIABLE} PRIVATE PARENT_SCOPE) + else() + set(${OUTPUT_VARIABLE} PUBLIC PARENT_SCOPE) + endif() +endfunction() + +function(_jse_append_target_property_unique TARGET_NAME PROPERTY_NAME) + if(ARGC LESS 3) + return() + endif() + + get_target_property(_jse_values "${TARGET_NAME}" "${PROPERTY_NAME}") + if(NOT _jse_values) + set(_jse_values) + endif() + + list(APPEND _jse_values ${ARGN}) + list(REMOVE_DUPLICATES _jse_values) + set_property(TARGET "${TARGET_NAME}" PROPERTY "${PROPERTY_NAME}" "${_jse_values}") +endfunction() + +function(_jse_collect_spec_include_dirs OUTPUT_VARIABLE) + set(_jse_queue ${ARGN}) + set(_jse_seen) + set(_jse_result) + + while(_jse_queue) + list(GET _jse_queue 0 _jse_target) + list(REMOVE_AT _jse_queue 0) + + if(NOT TARGET "${_jse_target}") + message(FATAL_ERROR "Unknown JSE spec dependency target: ${_jse_target}") + endif() + + _jse_resolve_target(_jse_real_target "${_jse_target}") + if("${_jse_real_target}" IN_LIST _jse_seen) + continue() + endif() + list(APPEND _jse_seen "${_jse_real_target}") + + get_target_property(_jse_include_dirs "${_jse_real_target}" INTERFACE_JSE_SPEC_INCLUDE_DIRS) + if(_jse_include_dirs) + list(APPEND _jse_result ${_jse_include_dirs}) + endif() + + get_target_property(_jse_link_specs "${_jse_real_target}" INTERFACE_JSE_LINK_SPECS) + if(_jse_link_specs) + list(APPEND _jse_queue ${_jse_link_specs}) + endif() + endwhile() + + list(REMOVE_DUPLICATES _jse_result) + set(${OUTPUT_VARIABLE} "${_jse_result}" PARENT_SCOPE) +endfunction() + +function(jse_add_spec TARGET_NAME) + set(options) + set(one_value_args ALIAS INPUT OUTPUT NAMESPACE FUNCTION) + set(multi_value_args INCLUDE_DIRS LINK_SPECS) + cmake_parse_arguments(JSE_SPEC + "${options}" + "${one_value_args}" + "${multi_value_args}" + ${ARGN} + ) + + if(JSE_SPEC_UNPARSED_ARGUMENTS) + message(FATAL_ERROR "Unknown arguments for jse_add_spec: ${JSE_SPEC_UNPARSED_ARGUMENTS}") + endif() + + if("${TARGET_NAME}" STREQUAL "") + message(FATAL_ERROR "jse_add_spec requires a target name") + endif() + + if((JSE_SPEC_INPUT AND NOT JSE_SPEC_OUTPUT) OR (JSE_SPEC_OUTPUT AND NOT JSE_SPEC_INPUT)) + message(FATAL_ERROR "jse_add_spec requires both INPUT and OUTPUT when generating an embedded spec") + endif() + + set(_jse_generation_requested OFF) + if(JSE_SPEC_INPUT) + set(_jse_generation_requested ON) + endif() + + if(TARGET "${TARGET_NAME}") + _jse_resolve_target(_jse_spec_target "${TARGET_NAME}") + else() + if("${TARGET_NAME}" MATCHES "::") + message(FATAL_ERROR + "Cannot create target ${TARGET_NAME}; CMake target names containing :: must be aliases. " + "Use jse_add_spec(real_target ALIAS ${TARGET_NAME} ...)." + ) + endif() + + if(_jse_generation_requested) + add_library(${TARGET_NAME}) + else() + add_library(${TARGET_NAME} INTERFACE) + endif() + set(_jse_spec_target "${TARGET_NAME}") + endif() + + get_target_property(_jse_spec_target_type "${_jse_spec_target}" TYPE) + if(_jse_generation_requested AND "${_jse_spec_target_type}" STREQUAL "INTERFACE_LIBRARY") + message(FATAL_ERROR + "jse_add_spec cannot add generated sources to interface target ${_jse_spec_target}. " + "Create the spec target first with a call that has INPUT and OUTPUT, or use a separate " + "metadata-only spec target as a LINK_SPECS dependency." + ) + endif() + + if(JSE_SPEC_ALIAS) + if(TARGET "${JSE_SPEC_ALIAS}") + get_target_property(_jse_existing_alias_target "${JSE_SPEC_ALIAS}" ALIASED_TARGET) + if(NOT "${_jse_existing_alias_target}" STREQUAL "${_jse_spec_target}") + message(FATAL_ERROR "Target ${JSE_SPEC_ALIAS} already exists and is not an alias for ${_jse_spec_target}") + endif() + else() + add_library(${JSE_SPEC_ALIAS} ALIAS ${_jse_spec_target}) + endif() + endif() + + _jse_target_usage_scope(_jse_usage_scope "${_jse_spec_target}") + + foreach(_jse_link_spec IN LISTS JSE_SPEC_LINK_SPECS) + if(NOT TARGET "${_jse_link_spec}") + message(FATAL_ERROR "Unknown JSE spec dependency target: ${_jse_link_spec}") + endif() + endforeach() + + if(JSE_SPEC_LINK_SPECS) + _jse_append_target_property_unique(${_jse_spec_target} INTERFACE_JSE_LINK_SPECS ${JSE_SPEC_LINK_SPECS}) + endif() + + set(_jse_local_include_dirs ${JSE_SPEC_INCLUDE_DIRS}) + if(JSE_SPEC_INPUT) + get_filename_component(_jse_input_abs "${JSE_SPEC_INPUT}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}") + get_filename_component(_jse_input_dir "${_jse_input_abs}" DIRECTORY) + list(APPEND _jse_local_include_dirs "${_jse_input_dir}") + endif() + + set(_jse_abs_include_dirs) + foreach(_jse_include_dir IN LISTS _jse_local_include_dirs) + get_filename_component(_jse_abs_include_dir "${_jse_include_dir}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}") + list(APPEND _jse_abs_include_dirs "${_jse_abs_include_dir}") + endforeach() + if(_jse_abs_include_dirs) + list(REMOVE_DUPLICATES _jse_abs_include_dirs) + _jse_append_target_property_unique(${_jse_spec_target} INTERFACE_JSE_SPEC_INCLUDE_DIRS ${_jse_abs_include_dirs}) + endif() + + if(NOT _jse_generation_requested) + return() + endif() + + if(NOT TARGET jse_embed_spec_tool) + message(FATAL_ERROR "jse_add_spec requires the jse_embed_spec_tool generator target") + endif() + + get_filename_component(_jse_embed_header "${JSE_SPEC_OUTPUT}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_BINARY_DIR}") + get_filename_component(_jse_embed_output_dir "${_jse_embed_header}" DIRECTORY) + get_filename_component(_jse_embed_output_stem "${_jse_embed_header}" NAME_WE) + set(_jse_embed_source "${_jse_embed_output_dir}/${_jse_embed_output_stem}.cpp") + string(MAKE_C_IDENTIFIER "${_jse_spec_target}" _jse_embed_target_namespace) + string(MAKE_C_IDENTIFIER "${_jse_embed_output_stem}" _jse_embed_spec_namespace) + + if(JSE_SPEC_NAMESPACE) + set(_jse_embed_namespace "${JSE_SPEC_NAMESPACE}") + else() + set(_jse_embed_namespace "jse::embed::${_jse_embed_target_namespace}::${_jse_embed_spec_namespace}") + endif() + + if(JSE_SPEC_FUNCTION) + set(_jse_embed_function "${JSE_SPEC_FUNCTION}") + else() + set(_jse_embed_function "spec") + endif() + + get_property(_jse_embed_namespaces GLOBAL PROPERTY JSE_EMBED_SPEC_NAMESPACES) + if("${_jse_embed_namespace}" IN_LIST _jse_embed_namespaces) + message(FATAL_ERROR "jse_add_spec already has an embedded spec named ${_jse_embed_namespace}") + endif() + set_property(GLOBAL APPEND PROPERTY JSE_EMBED_SPEC_NAMESPACES "${_jse_embed_namespace}") + + _jse_collect_spec_include_dirs(_jse_embed_include_dirs "${_jse_spec_target}") + + set(_jse_embed_include_args) + set(_jse_embed_depends "${_jse_input_abs}") + foreach(_jse_embed_include_dir IN LISTS _jse_embed_include_dirs) + list(APPEND _jse_embed_include_args "--include-dir" "${_jse_embed_include_dir}") + file(GLOB_RECURSE _jse_embed_json_depends + CONFIGURE_DEPENDS + "${_jse_embed_include_dir}/*.json" + ) + list(APPEND _jse_embed_depends ${_jse_embed_json_depends}) + endforeach() + list(REMOVE_DUPLICATES _jse_embed_depends) + + file(MAKE_DIRECTORY "${_jse_embed_output_dir}") + + add_custom_command( + OUTPUT + "${_jse_embed_header}" + "${_jse_embed_source}" + COMMAND + jse_embed_spec_tool + --input "${_jse_input_abs}" + --output-header "${_jse_embed_header}" + --output-source "${_jse_embed_source}" + --namespace "${_jse_embed_namespace}" + --function "${_jse_embed_function}" + ${_jse_embed_include_args} + DEPENDS + jse_embed_spec_tool + ${_jse_embed_depends} + COMMENT + "Generating embedded JSON spec ${_jse_embed_output_stem}" + VERBATIM + ) + + set_source_files_properties( + "${_jse_embed_header}" + "${_jse_embed_source}" + PROPERTIES GENERATED TRUE + ) + + target_sources(${_jse_spec_target} PRIVATE + "${_jse_embed_source}" + "${_jse_embed_header}" + ) + target_include_directories(${_jse_spec_target} ${_jse_usage_scope} + "$" + ) + target_link_libraries(${_jse_spec_target} ${_jse_usage_scope} nlohmann_json::nlohmann_json) + target_compile_features(${_jse_spec_target} ${_jse_usage_scope} cxx_std_17) + + source_group("Generated" FILES "${_jse_embed_source}" "${_jse_embed_header}") + + set(JSE_SPEC_HEADER "${_jse_embed_header}" PARENT_SCOPE) + set(JSE_SPEC_SOURCE "${_jse_embed_source}" PARENT_SCOPE) +endfunction() diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 94dc3f4..d208d37 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -8,12 +8,19 @@ set(test_sources ) add_executable(unit_tests ${test_sources}) -jse_add_embedded_spec( +jse_add_spec(test_dependency_specs + ALIAS jse::test_dependency_specs + INCLUDE_DIRS "${CMAKE_SOURCE_DIR}/data" +) + +jse_add_spec(test_embedded_specs + ALIAS jse::test_embedded_specs INPUT "${CMAKE_SOURCE_DIR}/data/rules_04.json" OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/embedded_specs/rules_04.hpp" + LINK_SPECS jse::test_dependency_specs ) -jse_add_embedded_spec( +jse_add_spec(test_embedded_specs INPUT "${CMAKE_SOURCE_DIR}/data/rules_01.json" OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/embedded_specs/rules_01.hpp" ) @@ -26,7 +33,7 @@ include(catch2) target_link_libraries(unit_tests PUBLIC Catch2::Catch2) target_link_libraries(unit_tests PUBLIC jse::jse) -target_link_libraries(unit_tests PUBLIC jse::embed) +target_link_libraries(unit_tests PUBLIC jse::test_embedded_specs) include(jse_warnings) target_link_libraries(unit_tests PRIVATE jse::warnings) diff --git a/tests/test_validator.cpp b/tests/test_validator.cpp index 44beae9..2129714 100644 --- a/tests/test_validator.cpp +++ b/tests/test_validator.cpp @@ -257,7 +257,7 @@ TEST_CASE("embedded_include_rule", "[validator]") std::ifstream ifs(root_path + "/rules_03.json"); json matching = json::parse(ifs); - REQUIRE(jse::embed::rules_04::spec() == matching); + REQUIRE(jse::embed::test_embedded_specs::rules_04::spec() == matching); } TEST_CASE("embedded_multiple_specs", "[validator]") @@ -265,7 +265,7 @@ TEST_CASE("embedded_multiple_specs", "[validator]") std::ifstream ifs(root_path + "/rules_01.json"); json matching = json::parse(ifs); - REQUIRE(jse::embed::rules_01::spec() == matching); + REQUIRE(jse::embed::test_embedded_specs::rules_01::spec() == matching); } TEST_CASE("null_number_rules_are_opt_in", "[validator]") From 7c8330309d36746d8eedae763f492351a266c6d5 Mon Sep 17 00:00:00 2001 From: teseo Date: Sat, 16 May 2026 20:02:46 -0700 Subject: [PATCH 7/7] windows --- tools/jse_embed_spec.cpp | 46 ++++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/tools/jse_embed_spec.cpp b/tools/jse_embed_spec.cpp index 703ed28..53e1d2b 100644 --- a/tools/jse_embed_spec.cpp +++ b/tools/jse_embed_spec.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include #include @@ -142,16 +143,13 @@ namespace return engine.inject_include(rules); } - std::string raw_string_literal(const std::string &value) + std::string byte_literal(const unsigned char value) { - for (int i = 0; i < 10000; ++i) - { - const std::string delimiter = i == 0 ? "JSE_JSON" : "JSE_JSON_" + std::to_string(i); - if (value.find(")" + delimiter + "\"") == std::string::npos) - return "R\"" + delimiter + "(" + value + ")" + delimiter + "\""; - } - - throw std::runtime_error("Failed to find a valid raw string delimiter."); + constexpr char hex[] = "0123456789ABCDEF"; + std::string result = "0x"; + result += hex[value >> 4]; + result += hex[value & 0x0F]; + return result; } std::vector split_string(const std::string &value, const std::size_t chunk_size) @@ -165,6 +163,28 @@ namespace return chunks; } + void write_byte_array_chunk(std::ostream &os, const std::string &chunk, const std::size_t index) + { + const std::string name = "chunk_" + std::to_string(index); + + os << " static constexpr unsigned char " << name << "[] = {\n"; + for (std::size_t i = 0; i < chunk.size(); ++i) + { + if (i % 16 == 0) + os << " "; + + os << byte_literal(static_cast(chunk[i])); + + if (i + 1 != chunk.size()) + os << ", "; + + if (i % 16 == 15 || i + 1 == chunk.size()) + os << "\n"; + } + os << " };\n"; + os << " text.append(reinterpret_cast(" << name << "), sizeof(" << name << "));\n"; + } + void ensure_parent_directory(const std::filesystem::path &path) { const auto parent = path.parent_path(); @@ -217,12 +237,12 @@ namespace std::string source_content(const Options &options, const jse::json &rules) { - constexpr std::size_t max_string_literal_chunk_size = 8000; + constexpr std::size_t max_byte_array_chunk_size = 4096; const auto namespaces = split_namespace(options.namespace_name); const auto header_name = options.output_header.filename().string(); const std::string rules_text = "\n" + rules.dump(4, ' ', true) + "\n"; - const auto rules_text_chunks = split_string(rules_text, max_string_literal_chunk_size); + const auto rules_text_chunks = split_string(rules_text, max_byte_array_chunk_size); std::ostringstream os; os << "#include \"" << header_name << "\"\n\n"; @@ -233,8 +253,8 @@ namespace os << " static const nlohmann::json value = []() {\n"; os << " std::string text;\n"; os << " text.reserve(" << rules_text.size() << ");\n"; - for (const auto &chunk : rules_text_chunks) - os << " text += " << raw_string_literal(chunk) << ";\n"; + for (std::size_t i = 0; i < rules_text_chunks.size(); ++i) + write_byte_array_chunk(os, rules_text_chunks[i], i); os << " return nlohmann::json::parse(text);\n"; os << " }();\n"; os << " return value;\n";