diff --git a/CMakeLists.txt b/CMakeLists.txt index 8b368dd..ff42f8c 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_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_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/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; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d010fa1..d208d37 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -8,6 +8,23 @@ set(test_sources ) add_executable(unit_tests ${test_sources}) +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_spec(test_embedded_specs + INPUT "${CMAKE_SOURCE_DIR}/data/rules_01.json" + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/embedded_specs/rules_01.hpp" +) + ################################################################################ # Required Libraries ################################################################################ @@ -16,6 +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::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 beb7ed6..2129714 100644 --- a/tests/test_validator.cpp +++ b/tests/test_validator.cpp @@ -1,5 +1,7 @@ ////////////////////////////////////////////////////////////////////////// #include +#include +#include #include #include #include @@ -250,6 +252,64 @@ 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::test_embedded_specs::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::test_embedded_specs::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]") { std::ifstream ifs1(root_path + "/input_01.json"); @@ -642,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); @@ -655,4 +716,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..53e1d2b --- /dev/null +++ b/tools/jse_embed_spec.cpp @@ -0,0 +1,287 @@ +#include + +#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 byte_literal(const unsigned char value) + { + 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) + { + 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 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(); + 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) + { + 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_byte_array_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 = []() {\n"; + os << " std::string text;\n"; + os << " text.reserve(" << rules_text.size() << ");\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"; + 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; + } +}