diff --git a/docs/compile.markdown b/docs/compile.markdown index ff7f05a3..580079aa 100644 --- a/docs/compile.markdown +++ b/docs/compile.markdown @@ -8,7 +8,7 @@ Compiling jsonschema compile [--http/-h] [--verbose/-v] [--resolve/-r ...] [--extension/-e ] [--ignore/-i ] [--fast/-f] [--default-dialect/-d ] - [--minify/-m] [--json/-j] + [--minify/-m] [--json/-j] [--include/-n ] ``` The `validate` command will first compile the schema into an optimised @@ -48,3 +48,9 @@ jsonschema compile path/to/my/schema.json --fast > template.json ```sh jsonschema compile path/to/my/schema.json --resolve other.json > template.json ``` + +### Compile a JSON Schema to a C/C++ header file + +```sh +jsonschema compile path/to/my/schema.json --include MY_SCHEMA > my_schema.h +``` diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 563c709e..87483387 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -22,6 +22,7 @@ target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::jsonl) target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::jsonpointer) target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::jsonschema) target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::yaml) +target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::regex) target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::alterschema) target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::editorschema) target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::options) diff --git a/src/command_compile.cc b/src/command_compile.cc index 1541c01f..02099c4e 100644 --- a/src/command_compile.cc +++ b/src/command_compile.cc @@ -1,11 +1,16 @@ #include #include #include +#include #include #include -#include // std::cerr, std::cout +#include // std::transform +#include // std::toupper +#include // std::hex, std::setw, std::setfill +#include // std::cerr, std::cout +#include // std::ostringstream #include "command.h" #include "configuration.h" @@ -62,11 +67,67 @@ auto sourcemeta::jsonschema::compile(const sourcemeta::core::Options &options) } const auto template_json{sourcemeta::blaze::to_json(schema_template)}; - if (options.contains("minify")) { + + if (options.contains("include") && !options.at("include").empty()) { + std::string name{options.at("include").front()}; + + static const auto IDENTIFIER_PATTERN{ + sourcemeta::core::to_regex("^[A-Za-z_][A-Za-z0-9_]*$")}; + if (!IDENTIFIER_PATTERN.has_value() || + !sourcemeta::core::matches(IDENTIFIER_PATTERN.value(), name)) { + throw InvalidIncludeIdentifier{name}; + } + + std::transform(name.begin(), name.end(), name.begin(), + [](unsigned char character) -> unsigned char { + return static_cast(std::toupper(character)); + }); + + std::ostringstream json_stream; + sourcemeta::core::stringify(template_json, json_stream); + const auto json_data{std::move(json_stream).str()}; + + constexpr auto BYTES_PER_LINE{16}; + + std::cout << "#ifndef SOURCEMETA_JSONSCHEMA_INCLUDE_" << name << "_H_\n"; + std::cout << "#define SOURCEMETA_JSONSCHEMA_INCLUDE_" << name << "_H_\n"; + std::cout << "\n"; + std::cout << "#ifdef __cplusplus\n"; + std::cout << "#include \n"; + std::cout << "#endif\n"; + std::cout << "\n"; + std::cout << "static const char " << name << "_DATA[] ="; + + for (std::size_t index = 0; index < json_data.size(); ++index) { + if (index % BYTES_PER_LINE == 0) { + std::cout << "\n \""; + } + + std::cout << "\\x" << std::hex << std::setw(2) << std::setfill('0') + << (static_cast( + static_cast(json_data[index]))); + + if ((index + 1) % BYTES_PER_LINE == 0 || index + 1 == json_data.size()) { + std::cout << "\""; + } + } + + std::cout << ";\n"; + std::cout << std::dec; + std::cout << "static const unsigned int " << name + << "_LENGTH = " << json_data.size() << ";\n"; + std::cout << "\n"; + std::cout << "#ifdef __cplusplus\n"; + std::cout << "static constexpr std::string_view " << name << "{" << name + << "_DATA, " << name << "_LENGTH};\n"; + std::cout << "#endif\n"; + std::cout << "\n"; + std::cout << "#endif\n"; + } else if (options.contains("minify")) { sourcemeta::core::stringify(template_json, std::cout); + std::cout << "\n"; } else { sourcemeta::core::prettify(template_json, std::cout); + std::cout << "\n"; } - - std::cout << "\n"; } diff --git a/src/error.h b/src/error.h index 892955a7..bdd7a2c6 100644 --- a/src/error.h +++ b/src/error.h @@ -81,6 +81,21 @@ class InvalidLintRuleError : public std::runtime_error { std::string rule_; }; +class InvalidIncludeIdentifier : public std::runtime_error { +public: + InvalidIncludeIdentifier(std::string identifier) + : std::runtime_error{"The include identifier is not a valid C/C++ " + "identifier"}, + identifier_{std::move(identifier)} {} + + [[nodiscard]] auto identifier() const noexcept -> const std::string & { + return this->identifier_; + } + +private: + std::string identifier_; +}; + class LintAutoFixError : public std::runtime_error { public: LintAutoFixError(std::string message, std::filesystem::path path, @@ -316,6 +331,10 @@ inline auto try_catch(const sourcemeta::core::Options &options, const auto is_json{options.contains("json")}; print_exception(is_json, error); return EXIT_FAILURE; + } catch (const InvalidIncludeIdentifier &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_FAILURE; } catch (const LintAutoFixError &error) { const auto is_json{options.contains("json")}; print_exception(is_json, error); diff --git a/src/main.cc b/src/main.cc index e4319648..bc7bc335 100644 --- a/src/main.cc +++ b/src/main.cc @@ -59,8 +59,10 @@ Global Options: compile [--extension/-e ] [--ignore/-i ] [--fast/-f] [--minify/-m] + [--include/-n ] Compile the given schema into an internal optimised representation. + Use --include/-n to output as a C/C++ header file. test [schemas-or-directories...] [--extension/-e ] [--ignore/-i ] @@ -168,6 +170,7 @@ auto jsonschema_main(const std::string &program, const std::string &command, } else if (command == "compile") { app.flag("fast", {"f"}); app.flag("minify", {"m"}); + app.option("include", {"n"}); app.parse(argc, argv, {.skip = 1}); sourcemeta::jsonschema::compile(app); return EXIT_SUCCESS; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 2a124db3..e7074095 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -359,6 +359,10 @@ add_jsonschema_test_unix(compile/pass_without_extension_json) add_jsonschema_test_unix(compile/pass_without_extension_yaml) add_jsonschema_test_unix(compile/pass_custom_extension_json) add_jsonschema_test_unix(compile/pass_custom_extension_yaml) +add_jsonschema_test_unix(compile/pass_include) +add_jsonschema_test_unix(compile/pass_include_lowercase) +add_jsonschema_test_unix(compile/pass_include_short) +add_jsonschema_test_unix(compile/fail_include_invalid_identifier) # Canonicalize add_jsonschema_test_unix(canonicalize/pass_input_unmodified) diff --git a/test/compile/fail_include_invalid_identifier.sh b/test/compile/fail_include_invalid_identifier.sh new file mode 100755 index 00000000..5f3ca8d9 --- /dev/null +++ b/test/compile/fail_include_invalid_identifier.sh @@ -0,0 +1,42 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$id": "https://example.com", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string" +} +EOF + +# Test with hyphen (invalid) +"$1" compile "$TMP/schema.json" --include my-schema 2>"$TMP/stderr.txt" \ + && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << 'EOF' > "$TMP/expected.txt" +error: The include identifier is not a valid C/C++ identifier + at identifier my-schema +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +"$1" compile "$TMP/schema.json" --include my-schema --json > "$TMP/stdout.txt" 2>&1 \ + && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << 'EOF' > "$TMP/expected.txt" +{ + "error": "The include identifier is not a valid C/C++ identifier", + "identifier": "my-schema" +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.txt" diff --git a/test/compile/pass_include.sh b/test/compile/pass_include.sh new file mode 100755 index 00000000..2d5e548f --- /dev/null +++ b/test/compile/pass_include.sh @@ -0,0 +1,55 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$id": "https://example.com", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string" +} +EOF + +"$1" compile "$TMP/schema.json" --include TEST_SCHEMA > "$TMP/output.h" + +cat << 'EOF' > "$TMP/expected.h" +#ifndef SOURCEMETA_JSONSCHEMA_INCLUDE_TEST_SCHEMA_H_ +#define SOURCEMETA_JSONSCHEMA_INCLUDE_TEST_SCHEMA_H_ + +#ifdef __cplusplus +#include +#endif + +static const char TEST_SCHEMA_DATA[] = + "\x5b\x66\x61\x6c\x73\x65\x2c\x74\x72\x75\x65\x2c\x5b\x22\x22\x2c" + "\x22\x68\x74\x74\x70\x73\x3a\x2f\x2f\x65\x78\x61\x6d\x70\x6c\x65" + "\x2e\x63\x6f\x6d\x22\x5d\x2c\x5b\x5b\x31\x31\x2c\x22\x2f\x74\x79" + "\x70\x65\x22\x2c\x22\x22\x2c\x22\x23\x2f\x74\x79\x70\x65\x22\x2c" + "\x32\x2c\x5b\x38\x2c\x34\x5d\x5d\x5d\x5d"; +static const unsigned int TEST_SCHEMA_LENGTH = 74; + +#ifdef __cplusplus +static constexpr std::string_view TEST_SCHEMA{TEST_SCHEMA_DATA, TEST_SCHEMA_LENGTH}; +#endif + +#endif +EOF + +diff "$TMP/output.h" "$TMP/expected.h" + +# Verify the header compiles with a C compiler +cat << 'EOF' > "$TMP/test.c" +#include "output.h" +EOF +cc -c "$TMP/test.c" -o "$TMP/test.o" + +# Verify the header compiles with a C++ compiler +cat << 'EOF' > "$TMP/test.cc" +#include "output.h" +EOF +c++ -std=c++17 -c "$TMP/test.cc" -o "$TMP/test_cpp.o" diff --git a/test/compile/pass_include_lowercase.sh b/test/compile/pass_include_lowercase.sh new file mode 100755 index 00000000..9fa6024f --- /dev/null +++ b/test/compile/pass_include_lowercase.sh @@ -0,0 +1,43 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$id": "https://example.com", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string" +} +EOF + +"$1" compile "$TMP/schema.json" --include test_schema > "$TMP/output.h" + +cat << 'EOF' > "$TMP/expected.h" +#ifndef SOURCEMETA_JSONSCHEMA_INCLUDE_TEST_SCHEMA_H_ +#define SOURCEMETA_JSONSCHEMA_INCLUDE_TEST_SCHEMA_H_ + +#ifdef __cplusplus +#include +#endif + +static const char TEST_SCHEMA_DATA[] = + "\x5b\x66\x61\x6c\x73\x65\x2c\x74\x72\x75\x65\x2c\x5b\x22\x22\x2c" + "\x22\x68\x74\x74\x70\x73\x3a\x2f\x2f\x65\x78\x61\x6d\x70\x6c\x65" + "\x2e\x63\x6f\x6d\x22\x5d\x2c\x5b\x5b\x31\x31\x2c\x22\x2f\x74\x79" + "\x70\x65\x22\x2c\x22\x22\x2c\x22\x23\x2f\x74\x79\x70\x65\x22\x2c" + "\x32\x2c\x5b\x38\x2c\x34\x5d\x5d\x5d\x5d"; +static const unsigned int TEST_SCHEMA_LENGTH = 74; + +#ifdef __cplusplus +static constexpr std::string_view TEST_SCHEMA{TEST_SCHEMA_DATA, TEST_SCHEMA_LENGTH}; +#endif + +#endif +EOF + +diff "$TMP/output.h" "$TMP/expected.h" diff --git a/test/compile/pass_include_short.sh b/test/compile/pass_include_short.sh new file mode 100755 index 00000000..530dce14 --- /dev/null +++ b/test/compile/pass_include_short.sh @@ -0,0 +1,44 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$id": "https://example.com", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string" +} +EOF + +# Use short option -n +"$1" compile "$TMP/schema.json" -n TEST_SCHEMA > "$TMP/output.h" + +cat << 'EOF' > "$TMP/expected.h" +#ifndef SOURCEMETA_JSONSCHEMA_INCLUDE_TEST_SCHEMA_H_ +#define SOURCEMETA_JSONSCHEMA_INCLUDE_TEST_SCHEMA_H_ + +#ifdef __cplusplus +#include +#endif + +static const char TEST_SCHEMA_DATA[] = + "\x5b\x66\x61\x6c\x73\x65\x2c\x74\x72\x75\x65\x2c\x5b\x22\x22\x2c" + "\x22\x68\x74\x74\x70\x73\x3a\x2f\x2f\x65\x78\x61\x6d\x70\x6c\x65" + "\x2e\x63\x6f\x6d\x22\x5d\x2c\x5b\x5b\x31\x31\x2c\x22\x2f\x74\x79" + "\x70\x65\x22\x2c\x22\x22\x2c\x22\x23\x2f\x74\x79\x70\x65\x22\x2c" + "\x32\x2c\x5b\x38\x2c\x34\x5d\x5d\x5d\x5d"; +static const unsigned int TEST_SCHEMA_LENGTH = 74; + +#ifdef __cplusplus +static constexpr std::string_view TEST_SCHEMA{TEST_SCHEMA_DATA, TEST_SCHEMA_LENGTH}; +#endif + +#endif +EOF + +diff "$TMP/output.h" "$TMP/expected.h"