Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion docs/compile.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Compiling
jsonschema compile <schema.json|.yaml> [--http/-h] [--verbose/-v]
[--resolve/-r <schemas-or-directories> ...] [--extension/-e <extension>]
[--ignore/-i <schemas-or-directories>] [--fast/-f] [--default-dialect/-d <uri>]
[--minify/-m] [--json/-j]
[--minify/-m] [--json/-j] [--include/-n <name>]
```

The `validate` command will first compile the schema into an optimised
Expand Down Expand Up @@ -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
```
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
69 changes: 65 additions & 4 deletions src/command_compile.cc
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
#include <sourcemeta/core/io.h>
#include <sourcemeta/core/json.h>
#include <sourcemeta/core/jsonschema.h>
#include <sourcemeta/core/regex.h>
#include <sourcemeta/core/yaml.h>

#include <sourcemeta/blaze/compiler.h>

#include <iostream> // std::cerr, std::cout
#include <algorithm> // std::transform
#include <cctype> // std::toupper
#include <iomanip> // std::hex, std::setw, std::setfill
#include <iostream> // std::cerr, std::cout
#include <sstream> // std::ostringstream

#include "command.h"
#include "configuration.h"
Expand Down Expand Up @@ -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<unsigned char>(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 <string_view>\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<unsigned int>(
static_cast<unsigned char>(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";
}
19 changes: 19 additions & 0 deletions src/error.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions src/main.cc
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,10 @@ Global Options:

compile <schema.json|.yaml> [--extension/-e <extension>]
[--ignore/-i <schemas-or-directories>] [--fast/-f] [--minify/-m]
[--include/-n <name>]

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 <extension>]
[--ignore/-i <schemas-or-directories>]
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
42 changes: 42 additions & 0 deletions test/compile/fail_include_invalid_identifier.sh
Original file line number Diff line number Diff line change
@@ -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"
55 changes: 55 additions & 0 deletions test/compile/pass_include.sh
Original file line number Diff line number Diff line change
@@ -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 <string_view>
#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"
43 changes: 43 additions & 0 deletions test/compile/pass_include_lowercase.sh
Original file line number Diff line number Diff line change
@@ -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 <string_view>
#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"
44 changes: 44 additions & 0 deletions test/compile/pass_include_short.sh
Original file line number Diff line number Diff line change
@@ -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 <string_view>
#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"