From 67d898322abe2fb833388bca2a70eaf524761544 Mon Sep 17 00:00:00 2001 From: teseo Date: Wed, 13 May 2026 12:39:47 -0700 Subject: [PATCH 1/5] 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/5] 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/5] 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/5] 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/5] 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())