Skip to content

Commit 0503ee9

Browse files
authored
Generate C/C++ headers in compile using --include (like xxd) (#596)
Signed-off-by: Juan Cruz Viotti <[email protected]>
1 parent fe07eda commit 0503ee9

File tree

10 files changed

+283
-5
lines changed

10 files changed

+283
-5
lines changed

docs/compile.markdown

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Compiling
88
jsonschema compile <schema.json|.yaml> [--http/-h] [--verbose/-v]
99
[--resolve/-r <schemas-or-directories> ...] [--extension/-e <extension>]
1010
[--ignore/-i <schemas-or-directories>] [--fast/-f] [--default-dialect/-d <uri>]
11-
[--minify/-m] [--json/-j]
11+
[--minify/-m] [--json/-j] [--include/-n <name>]
1212
```
1313

1414
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
4848
```sh
4949
jsonschema compile path/to/my/schema.json --resolve other.json > template.json
5050
```
51+
52+
### Compile a JSON Schema to a C/C++ header file
53+
54+
```sh
55+
jsonschema compile path/to/my/schema.json --include MY_SCHEMA > my_schema.h
56+
```

src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::jsonl)
2222
target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::jsonpointer)
2323
target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::jsonschema)
2424
target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::yaml)
25+
target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::regex)
2526
target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::alterschema)
2627
target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::editorschema)
2728
target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::options)

src/command_compile.cc

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
#include <sourcemeta/core/io.h>
22
#include <sourcemeta/core/json.h>
33
#include <sourcemeta/core/jsonschema.h>
4+
#include <sourcemeta/core/regex.h>
45
#include <sourcemeta/core/yaml.h>
56

67
#include <sourcemeta/blaze/compiler.h>
78

8-
#include <iostream> // std::cerr, std::cout
9+
#include <algorithm> // std::transform
10+
#include <cctype> // std::toupper
11+
#include <iomanip> // std::hex, std::setw, std::setfill
12+
#include <iostream> // std::cerr, std::cout
13+
#include <sstream> // std::ostringstream
914

1015
#include "command.h"
1116
#include "configuration.h"
@@ -62,11 +67,67 @@ auto sourcemeta::jsonschema::compile(const sourcemeta::core::Options &options)
6267
}
6368

6469
const auto template_json{sourcemeta::blaze::to_json(schema_template)};
65-
if (options.contains("minify")) {
70+
71+
if (options.contains("include") && !options.at("include").empty()) {
72+
std::string name{options.at("include").front()};
73+
74+
static const auto IDENTIFIER_PATTERN{
75+
sourcemeta::core::to_regex("^[A-Za-z_][A-Za-z0-9_]*$")};
76+
if (!IDENTIFIER_PATTERN.has_value() ||
77+
!sourcemeta::core::matches(IDENTIFIER_PATTERN.value(), name)) {
78+
throw InvalidIncludeIdentifier{name};
79+
}
80+
81+
std::transform(name.begin(), name.end(), name.begin(),
82+
[](unsigned char character) -> unsigned char {
83+
return static_cast<unsigned char>(std::toupper(character));
84+
});
85+
86+
std::ostringstream json_stream;
87+
sourcemeta::core::stringify(template_json, json_stream);
88+
const auto json_data{std::move(json_stream).str()};
89+
90+
constexpr auto BYTES_PER_LINE{16};
91+
92+
std::cout << "#ifndef SOURCEMETA_JSONSCHEMA_INCLUDE_" << name << "_H_\n";
93+
std::cout << "#define SOURCEMETA_JSONSCHEMA_INCLUDE_" << name << "_H_\n";
94+
std::cout << "\n";
95+
std::cout << "#ifdef __cplusplus\n";
96+
std::cout << "#include <string_view>\n";
97+
std::cout << "#endif\n";
98+
std::cout << "\n";
99+
std::cout << "static const char " << name << "_DATA[] =";
100+
101+
for (std::size_t index = 0; index < json_data.size(); ++index) {
102+
if (index % BYTES_PER_LINE == 0) {
103+
std::cout << "\n \"";
104+
}
105+
106+
std::cout << "\\x" << std::hex << std::setw(2) << std::setfill('0')
107+
<< (static_cast<unsigned int>(
108+
static_cast<unsigned char>(json_data[index])));
109+
110+
if ((index + 1) % BYTES_PER_LINE == 0 || index + 1 == json_data.size()) {
111+
std::cout << "\"";
112+
}
113+
}
114+
115+
std::cout << ";\n";
116+
std::cout << std::dec;
117+
std::cout << "static const unsigned int " << name
118+
<< "_LENGTH = " << json_data.size() << ";\n";
119+
std::cout << "\n";
120+
std::cout << "#ifdef __cplusplus\n";
121+
std::cout << "static constexpr std::string_view " << name << "{" << name
122+
<< "_DATA, " << name << "_LENGTH};\n";
123+
std::cout << "#endif\n";
124+
std::cout << "\n";
125+
std::cout << "#endif\n";
126+
} else if (options.contains("minify")) {
66127
sourcemeta::core::stringify(template_json, std::cout);
128+
std::cout << "\n";
67129
} else {
68130
sourcemeta::core::prettify(template_json, std::cout);
131+
std::cout << "\n";
69132
}
70-
71-
std::cout << "\n";
72133
}

src/error.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,21 @@ class InvalidLintRuleError : public std::runtime_error {
8181
std::string rule_;
8282
};
8383

84+
class InvalidIncludeIdentifier : public std::runtime_error {
85+
public:
86+
InvalidIncludeIdentifier(std::string identifier)
87+
: std::runtime_error{"The include identifier is not a valid C/C++ "
88+
"identifier"},
89+
identifier_{std::move(identifier)} {}
90+
91+
[[nodiscard]] auto identifier() const noexcept -> const std::string & {
92+
return this->identifier_;
93+
}
94+
95+
private:
96+
std::string identifier_;
97+
};
98+
8499
class LintAutoFixError : public std::runtime_error {
85100
public:
86101
LintAutoFixError(std::string message, std::filesystem::path path,
@@ -316,6 +331,10 @@ inline auto try_catch(const sourcemeta::core::Options &options,
316331
const auto is_json{options.contains("json")};
317332
print_exception(is_json, error);
318333
return EXIT_FAILURE;
334+
} catch (const InvalidIncludeIdentifier &error) {
335+
const auto is_json{options.contains("json")};
336+
print_exception(is_json, error);
337+
return EXIT_FAILURE;
319338
} catch (const LintAutoFixError &error) {
320339
const auto is_json{options.contains("json")};
321340
print_exception(is_json, error);

src/main.cc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,10 @@ Global Options:
5959
6060
compile <schema.json|.yaml> [--extension/-e <extension>]
6161
[--ignore/-i <schemas-or-directories>] [--fast/-f] [--minify/-m]
62+
[--include/-n <name>]
6263
6364
Compile the given schema into an internal optimised representation.
65+
Use --include/-n to output as a C/C++ header file.
6466
6567
test [schemas-or-directories...] [--extension/-e <extension>]
6668
[--ignore/-i <schemas-or-directories>]
@@ -168,6 +170,7 @@ auto jsonschema_main(const std::string &program, const std::string &command,
168170
} else if (command == "compile") {
169171
app.flag("fast", {"f"});
170172
app.flag("minify", {"m"});
173+
app.option("include", {"n"});
171174
app.parse(argc, argv, {.skip = 1});
172175
sourcemeta::jsonschema::compile(app);
173176
return EXIT_SUCCESS;

test/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,10 @@ add_jsonschema_test_unix(compile/pass_without_extension_json)
359359
add_jsonschema_test_unix(compile/pass_without_extension_yaml)
360360
add_jsonschema_test_unix(compile/pass_custom_extension_json)
361361
add_jsonschema_test_unix(compile/pass_custom_extension_yaml)
362+
add_jsonschema_test_unix(compile/pass_include)
363+
add_jsonschema_test_unix(compile/pass_include_lowercase)
364+
add_jsonschema_test_unix(compile/pass_include_short)
365+
add_jsonschema_test_unix(compile/fail_include_invalid_identifier)
362366

363367
# Canonicalize
364368
add_jsonschema_test_unix(canonicalize/pass_input_unmodified)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#!/bin/sh
2+
3+
set -o errexit
4+
set -o nounset
5+
6+
TMP="$(mktemp -d)"
7+
clean() { rm -rf "$TMP"; }
8+
trap clean EXIT
9+
10+
cat << 'EOF' > "$TMP/schema.json"
11+
{
12+
"$id": "https://example.com",
13+
"$schema": "https://json-schema.org/draft/2020-12/schema",
14+
"type": "string"
15+
}
16+
EOF
17+
18+
# Test with hyphen (invalid)
19+
"$1" compile "$TMP/schema.json" --include my-schema 2>"$TMP/stderr.txt" \
20+
&& CODE="$?" || CODE="$?"
21+
test "$CODE" = "1" || exit 1
22+
23+
cat << 'EOF' > "$TMP/expected.txt"
24+
error: The include identifier is not a valid C/C++ identifier
25+
at identifier my-schema
26+
EOF
27+
28+
diff "$TMP/stderr.txt" "$TMP/expected.txt"
29+
30+
# JSON error
31+
"$1" compile "$TMP/schema.json" --include my-schema --json > "$TMP/stdout.txt" 2>&1 \
32+
&& CODE="$?" || CODE="$?"
33+
test "$CODE" = "1" || exit 1
34+
35+
cat << 'EOF' > "$TMP/expected.txt"
36+
{
37+
"error": "The include identifier is not a valid C/C++ identifier",
38+
"identifier": "my-schema"
39+
}
40+
EOF
41+
42+
diff "$TMP/stdout.txt" "$TMP/expected.txt"

test/compile/pass_include.sh

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#!/bin/sh
2+
3+
set -o errexit
4+
set -o nounset
5+
6+
TMP="$(mktemp -d)"
7+
clean() { rm -rf "$TMP"; }
8+
trap clean EXIT
9+
10+
cat << 'EOF' > "$TMP/schema.json"
11+
{
12+
"$id": "https://example.com",
13+
"$schema": "https://json-schema.org/draft/2020-12/schema",
14+
"type": "string"
15+
}
16+
EOF
17+
18+
"$1" compile "$TMP/schema.json" --include TEST_SCHEMA > "$TMP/output.h"
19+
20+
cat << 'EOF' > "$TMP/expected.h"
21+
#ifndef SOURCEMETA_JSONSCHEMA_INCLUDE_TEST_SCHEMA_H_
22+
#define SOURCEMETA_JSONSCHEMA_INCLUDE_TEST_SCHEMA_H_
23+
24+
#ifdef __cplusplus
25+
#include <string_view>
26+
#endif
27+
28+
static const char TEST_SCHEMA_DATA[] =
29+
"\x5b\x66\x61\x6c\x73\x65\x2c\x74\x72\x75\x65\x2c\x5b\x22\x22\x2c"
30+
"\x22\x68\x74\x74\x70\x73\x3a\x2f\x2f\x65\x78\x61\x6d\x70\x6c\x65"
31+
"\x2e\x63\x6f\x6d\x22\x5d\x2c\x5b\x5b\x31\x31\x2c\x22\x2f\x74\x79"
32+
"\x70\x65\x22\x2c\x22\x22\x2c\x22\x23\x2f\x74\x79\x70\x65\x22\x2c"
33+
"\x32\x2c\x5b\x38\x2c\x34\x5d\x5d\x5d\x5d";
34+
static const unsigned int TEST_SCHEMA_LENGTH = 74;
35+
36+
#ifdef __cplusplus
37+
static constexpr std::string_view TEST_SCHEMA{TEST_SCHEMA_DATA, TEST_SCHEMA_LENGTH};
38+
#endif
39+
40+
#endif
41+
EOF
42+
43+
diff "$TMP/output.h" "$TMP/expected.h"
44+
45+
# Verify the header compiles with a C compiler
46+
cat << 'EOF' > "$TMP/test.c"
47+
#include "output.h"
48+
EOF
49+
cc -c "$TMP/test.c" -o "$TMP/test.o"
50+
51+
# Verify the header compiles with a C++ compiler
52+
cat << 'EOF' > "$TMP/test.cc"
53+
#include "output.h"
54+
EOF
55+
c++ -std=c++17 -c "$TMP/test.cc" -o "$TMP/test_cpp.o"
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/bin/sh
2+
3+
set -o errexit
4+
set -o nounset
5+
6+
TMP="$(mktemp -d)"
7+
clean() { rm -rf "$TMP"; }
8+
trap clean EXIT
9+
10+
cat << 'EOF' > "$TMP/schema.json"
11+
{
12+
"$id": "https://example.com",
13+
"$schema": "https://json-schema.org/draft/2020-12/schema",
14+
"type": "string"
15+
}
16+
EOF
17+
18+
"$1" compile "$TMP/schema.json" --include test_schema > "$TMP/output.h"
19+
20+
cat << 'EOF' > "$TMP/expected.h"
21+
#ifndef SOURCEMETA_JSONSCHEMA_INCLUDE_TEST_SCHEMA_H_
22+
#define SOURCEMETA_JSONSCHEMA_INCLUDE_TEST_SCHEMA_H_
23+
24+
#ifdef __cplusplus
25+
#include <string_view>
26+
#endif
27+
28+
static const char TEST_SCHEMA_DATA[] =
29+
"\x5b\x66\x61\x6c\x73\x65\x2c\x74\x72\x75\x65\x2c\x5b\x22\x22\x2c"
30+
"\x22\x68\x74\x74\x70\x73\x3a\x2f\x2f\x65\x78\x61\x6d\x70\x6c\x65"
31+
"\x2e\x63\x6f\x6d\x22\x5d\x2c\x5b\x5b\x31\x31\x2c\x22\x2f\x74\x79"
32+
"\x70\x65\x22\x2c\x22\x22\x2c\x22\x23\x2f\x74\x79\x70\x65\x22\x2c"
33+
"\x32\x2c\x5b\x38\x2c\x34\x5d\x5d\x5d\x5d";
34+
static const unsigned int TEST_SCHEMA_LENGTH = 74;
35+
36+
#ifdef __cplusplus
37+
static constexpr std::string_view TEST_SCHEMA{TEST_SCHEMA_DATA, TEST_SCHEMA_LENGTH};
38+
#endif
39+
40+
#endif
41+
EOF
42+
43+
diff "$TMP/output.h" "$TMP/expected.h"

test/compile/pass_include_short.sh

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#!/bin/sh
2+
3+
set -o errexit
4+
set -o nounset
5+
6+
TMP="$(mktemp -d)"
7+
clean() { rm -rf "$TMP"; }
8+
trap clean EXIT
9+
10+
cat << 'EOF' > "$TMP/schema.json"
11+
{
12+
"$id": "https://example.com",
13+
"$schema": "https://json-schema.org/draft/2020-12/schema",
14+
"type": "string"
15+
}
16+
EOF
17+
18+
# Use short option -n
19+
"$1" compile "$TMP/schema.json" -n TEST_SCHEMA > "$TMP/output.h"
20+
21+
cat << 'EOF' > "$TMP/expected.h"
22+
#ifndef SOURCEMETA_JSONSCHEMA_INCLUDE_TEST_SCHEMA_H_
23+
#define SOURCEMETA_JSONSCHEMA_INCLUDE_TEST_SCHEMA_H_
24+
25+
#ifdef __cplusplus
26+
#include <string_view>
27+
#endif
28+
29+
static const char TEST_SCHEMA_DATA[] =
30+
"\x5b\x66\x61\x6c\x73\x65\x2c\x74\x72\x75\x65\x2c\x5b\x22\x22\x2c"
31+
"\x22\x68\x74\x74\x70\x73\x3a\x2f\x2f\x65\x78\x61\x6d\x70\x6c\x65"
32+
"\x2e\x63\x6f\x6d\x22\x5d\x2c\x5b\x5b\x31\x31\x2c\x22\x2f\x74\x79"
33+
"\x70\x65\x22\x2c\x22\x22\x2c\x22\x23\x2f\x74\x79\x70\x65\x22\x2c"
34+
"\x32\x2c\x5b\x38\x2c\x34\x5d\x5d\x5d\x5d";
35+
static const unsigned int TEST_SCHEMA_LENGTH = 74;
36+
37+
#ifdef __cplusplus
38+
static constexpr std::string_view TEST_SCHEMA{TEST_SCHEMA_DATA, TEST_SCHEMA_LENGTH};
39+
#endif
40+
41+
#endif
42+
EOF
43+
44+
diff "$TMP/output.h" "$TMP/expected.h"

0 commit comments

Comments
 (0)