Skip to content

Commit

Permalink
Implement encode and decode schema-less commands (#174)
Browse files Browse the repository at this point in the history
Signed-off-by: Juan Cruz Viotti <[email protected]>
  • Loading branch information
jviotti authored Oct 9, 2024
1 parent c121e97 commit 48f3f42
Show file tree
Hide file tree
Showing 16 changed files with 347 additions and 2 deletions.
2 changes: 2 additions & 0 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ documentation:
- [`jsonschema compile`](./docs/compile.markdown) (for internal debugging)
- [`jsonschema identify`](./docs/identify.markdown)
- [`jsonschema canonicalize`](./docs/canonicalize.markdown) (for static analysis)
- [`jsonschema encode`](./docs/encode.markdown) (for binary compression)
- [`jsonschema decode`](./docs/decode.markdown)

Installation
------------
Expand Down
1 change: 0 additions & 1 deletion cmake/FindJSONBinPack.cmake
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
if(NOT JSONBinPack_FOUND)
set(JSONBINPACK_INSTALL OFF CACHE BOOL "disable installation")
set(JSONBINPACK_RUNTIME OFF CACHE BOOL "disable the JSON BinPack runtime module")
add_subdirectory("${PROJECT_SOURCE_DIR}/vendor/jsonbinpack")
set(JSONBinPack_FOUND ON)
endif()
35 changes: 35 additions & 0 deletions docs/decode.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
Decode
======

```sh
jsonschema decode <output.binpack> <output.json>
```

This command decodes a JSON document using [JSON
BinPack](https://jsonbinpack.sourcemeta.com) schema-less mode. **Note this
command is considered experimental and might not decode binary files produced
by other versions of this CLI**.

Examples
--------

For example, consider the following encoded file:

```
$ xxd output.binpack
00000000: 1308 7665 7273 696f 6e37 02 ..version7.
```

Decoding this file using JSON BinPack will result in the following document:

```json
{
"version": 2.0
}
```

### Decode a binary file

```sh
jsonschema decode path/to/output.binpack path/to/my/output.json
```
35 changes: 35 additions & 0 deletions docs/encode.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
Encode
======

```sh
jsonschema encode <document.json> <output.binpack>
```

This command encodes a JSON document using [JSON
BinPack](https://jsonbinpack.sourcemeta.com) schema-less mode. **Note this
command is considered experimental and its output might not be decodable across
versions of this CLI**.

Examples
--------

For example, consider the following simple document:

```json
{
"version": 2.0
}
```

The JSON BinPack schema-less encoding will result in something like this:

```
$ xxd output.binpack
00000000: 1308 7665 7273 696f 6e37 02 ..version7.
```

### Encode a JSON document

```sh
jsonschema encode path/to/my/document.json path/to/output.binpack
```
5 changes: 4 additions & 1 deletion src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ add_executable(jsonschema_cli
command_metaschema.cc
command_validate.cc
command_identify.cc
command_canonicalize.cc)
command_canonicalize.cc
command_encode.cc
command_decode.cc)

noa_add_default_options(PRIVATE jsonschema_cli)
set_target_properties(jsonschema_cli PROPERTIES OUTPUT_NAME jsonschema)
Expand All @@ -22,6 +24,7 @@ target_link_libraries(jsonschema_cli PRIVATE sourcemeta::alterschema::engine)
target_link_libraries(jsonschema_cli PRIVATE sourcemeta::alterschema::linter)
target_link_libraries(jsonschema_cli PRIVATE sourcemeta::hydra::httpclient)
target_link_libraries(jsonschema_cli PRIVATE sourcemeta::jsonbinpack::compiler)
target_link_libraries(jsonschema_cli PRIVATE sourcemeta::jsonbinpack::runtime)

configure_file(configure.h.in configure.h @ONLY)
target_include_directories(jsonschema_cli PRIVATE "${CMAKE_CURRENT_BINARY_DIR}")
Expand Down
2 changes: 2 additions & 0 deletions src/command.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ auto validate(const std::span<const std::string> &arguments) -> int;
auto metaschema(const std::span<const std::string> &arguments) -> int;
auto identify(const std::span<const std::string> &arguments) -> int;
auto canonicalize(const std::span<const std::string> &arguments) -> int;
auto encode(const std::span<const std::string> &arguments) -> int;
auto decode(const std::span<const std::string> &arguments) -> int;
} // namespace sourcemeta::jsonschema::cli

#endif
55 changes: 55 additions & 0 deletions src/command_decode.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#include <sourcemeta/jsonbinpack/compiler.h>
#include <sourcemeta/jsonbinpack/runtime.h>
#include <sourcemeta/jsontoolkit/json.h>
#include <sourcemeta/jsontoolkit/jsonschema.h>

#include <cassert> // assert
#include <cstdlib> // EXIT_SUCCESS
#include <filesystem> // std::filesystem
#include <fstream> // std::ifstream
#include <iostream> // std::cout, std::endl

#include "command.h"
#include "utils.h"

auto sourcemeta::jsonschema::cli::decode(
const std::span<const std::string> &arguments) -> int {
const auto options{parse_options(arguments, {})};

if (options.at("").size() < 2) {
std::cerr
<< "error: This command expects a path to a binary file and an "
"output path. For example:\n\n"
<< " jsonschema decode path/to/output.binpack path/to/document.json\n";
return EXIT_FAILURE;
}

// TODO: Take a real schema as argument
auto schema{sourcemeta::jsontoolkit::parse(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema"
})JSON")};

sourcemeta::jsonbinpack::compile(
schema, sourcemeta::jsontoolkit::default_schema_walker,
resolver(options, options.contains("h") || options.contains("http")));
const auto encoding{sourcemeta::jsonbinpack::load(schema)};

std::ifstream input_stream{std::filesystem::canonical(options.at("").front()),
std::ios::binary};
input_stream.exceptions(std::ifstream::failbit | std::ifstream::badbit);
assert(!input_stream.fail());
assert(input_stream.is_open());
sourcemeta::jsonbinpack::Decoder decoder{input_stream};
const auto document{decoder.read(encoding)};

std::ofstream output_stream(
std::filesystem::weakly_canonical(options.at("").at(1)),
std::ios::binary);
output_stream.exceptions(std::ios_base::badbit);
sourcemeta::jsontoolkit::prettify(
document, output_stream, sourcemeta::jsontoolkit::schema_format_compare);
output_stream << "\n";
output_stream.flush();
output_stream.close();
return EXIT_SUCCESS;
}
50 changes: 50 additions & 0 deletions src/command_encode.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#include <sourcemeta/jsonbinpack/compiler.h>
#include <sourcemeta/jsonbinpack/runtime.h>
#include <sourcemeta/jsontoolkit/json.h>
#include <sourcemeta/jsontoolkit/jsonschema.h>

#include <cstdlib> // EXIT_SUCCESS
#include <filesystem> // std::filesystem
#include <fstream> // std::ofstream
#include <iostream> // std::cout, std::endl

#include "command.h"
#include "utils.h"

auto sourcemeta::jsonschema::cli::encode(
const std::span<const std::string> &arguments) -> int {
const auto options{parse_options(arguments, {})};

if (options.at("").size() < 2) {
std::cerr
<< "error: This command expects a path to a JSON document and an "
"output path. For example:\n\n"
<< " jsonschema encode path/to/document.json path/to/output.binpack\n";
return EXIT_FAILURE;
}

// TODO: Take a real schema as argument
auto schema{sourcemeta::jsontoolkit::parse(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema"
})JSON")};

sourcemeta::jsonbinpack::compile(
schema, sourcemeta::jsontoolkit::default_schema_walker,
resolver(options, options.contains("h") || options.contains("http")));
const auto encoding{sourcemeta::jsonbinpack::load(schema)};

const auto document{
sourcemeta::jsontoolkit::from_file(options.at("").front())};

std::ofstream output_stream(
std::filesystem::weakly_canonical(options.at("").at(1)),
std::ios::binary);
output_stream.exceptions(std::ios_base::badbit);
sourcemeta::jsonbinpack::Encoder encoder{output_stream};
encoder.write(document, encoding);
output_stream.flush();
const auto size{output_stream.tellp()};
output_stream.close();
std::cerr << "size: " << size << " bytes\n";
return EXIT_SUCCESS;
}
12 changes: 12 additions & 0 deletions src/main.cc
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ Global Options:
Pre-process a JSON Schema into JSON BinPack's canonical form
for static analysis.
encode <document.json> <output.binpack>
Encode a JSON document or JSONL dataset using JSON BinPack.
decode <output.binpack> <output.json>
Decode a JSON document or JSONL dataset using JSON BinPack.
For more documentation, visit https://github.com/sourcemeta/jsonschema
)EOF"};

Expand All @@ -99,6 +107,10 @@ auto jsonschema_main(const std::string &program, const std::string &command,
return sourcemeta::jsonschema::cli::identify(arguments);
} else if (command == "canonicalize") {
return sourcemeta::jsonschema::cli::canonicalize(arguments);
} else if (command == "encode") {
return sourcemeta::jsonschema::cli::encode(arguments);
} else if (command == "decode") {
return sourcemeta::jsonschema::cli::decode(arguments);
} else {
std::cout << "JSON Schema CLI - v"
<< sourcemeta::jsonschema::cli::PROJECT_VERSION << "\n";
Expand Down
10 changes: 10 additions & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,16 @@ add_jsonschema_test_unix(canonicalize/fail_no_schema)
add_jsonschema_test_unix(canonicalize/fail_schema_invalid_json)
add_jsonschema_test_unix(canonicalize/fail_unknown_metaschema)

# Encode
add_jsonschema_test_unix(encode/pass_schema_less)
add_jsonschema_test_unix(encode/fail_no_document)
add_jsonschema_test_unix(encode/fail_no_output)

# Decode
add_jsonschema_test_unix(decode/pass_schema_less)
add_jsonschema_test_unix(decode/fail_no_document)
add_jsonschema_test_unix(decode/fail_no_output)

# CI specific tests
add_jsonschema_test_unix_ci(pass_bundle_http)
add_jsonschema_test_unix_ci(fail_bundle_http_non_200)
Expand Down
23 changes: 23 additions & 0 deletions test/decode/fail_no_document.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/sh

set -o errexit
set -o nounset

TMP="$(mktemp -d)"
clean() { rm -rf "$TMP"; }
trap clean EXIT

cat << 'EOF' > "$TMP/document.json"
{ "version": 2.0 }
EOF

"$1" decode 2>"$TMP/stderr.txt" && CODE="$?" || CODE="$?"
test "$CODE" = "1" || exit 1

cat << 'EOF' > "$TMP/expected.txt"
error: This command expects a path to a binary file and an output path. For example:
jsonschema decode path/to/output.binpack path/to/document.json
EOF

diff "$TMP/stderr.txt" "$TMP/expected.txt"
24 changes: 24 additions & 0 deletions test/decode/fail_no_output.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/bin/sh

set -o errexit
set -o nounset

TMP="$(mktemp -d)"
clean() { rm -rf "$TMP"; }
trap clean EXIT

cat << 'EOF' > "$TMP/document.json"
{ "version": 2.0 }
EOF

"$1" encode "$TMP/document.json" "$TMP/output.binpack"
"$1" decode "$TMP/output.binpack" 2>"$TMP/stderr.txt" && CODE="$?" || CODE="$?"
test "$CODE" = "1" || exit 1

cat << 'EOF' > "$TMP/expected.txt"
error: This command expects a path to a binary file and an output path. For example:
jsonschema decode path/to/output.binpack path/to/document.json
EOF

diff "$TMP/stderr.txt" "$TMP/expected.txt"
23 changes: 23 additions & 0 deletions test/decode/pass_schema_less.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/sh

set -o errexit
set -o nounset

TMP="$(mktemp -d)"
clean() { rm -rf "$TMP"; }
trap clean EXIT

cat << 'EOF' > "$TMP/document.json"
{ "version": 2.0 }
EOF

"$1" encode "$TMP/document.json" "$TMP/output.binpack"
"$1" decode "$TMP/output.binpack" "$TMP/decode.json"

cat << 'EOF' > "$TMP/expected.json"
{
"version": 2.0
}
EOF

diff "$TMP/decode.json" "$TMP/expected.json"
23 changes: 23 additions & 0 deletions test/encode/fail_no_document.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/sh

set -o errexit
set -o nounset

TMP="$(mktemp -d)"
clean() { rm -rf "$TMP"; }
trap clean EXIT

cat << 'EOF' > "$TMP/document.json"
{ "version": 2.0 }
EOF

"$1" encode 2> "$TMP/stderr.txt" && CODE="$?" || CODE="$?"
test "$CODE" = "1" || exit 1

cat << 'EOF' > "$TMP/expected.txt"
error: This command expects a path to a JSON document and an output path. For example:
jsonschema encode path/to/document.json path/to/output.binpack
EOF

diff "$TMP/stderr.txt" "$TMP/expected.txt"
23 changes: 23 additions & 0 deletions test/encode/fail_no_output.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/sh

set -o errexit
set -o nounset

TMP="$(mktemp -d)"
clean() { rm -rf "$TMP"; }
trap clean EXIT

cat << 'EOF' > "$TMP/document.json"
{ "version": 2.0 }
EOF

"$1" encode "$TMP/document.json" 2>"$TMP/stderr.txt" && CODE="$?" || CODE="$?"
test "$CODE" = "1" || exit 1

cat << 'EOF' > "$TMP/expected.txt"
error: This command expects a path to a JSON document and an output path. For example:
jsonschema encode path/to/document.json path/to/output.binpack
EOF

diff "$TMP/stderr.txt" "$TMP/expected.txt"
Loading

0 comments on commit 48f3f42

Please sign in to comment.