diff --git a/DEPENDENCIES b/DEPENDENCIES index 4ca0cd80..cc24c052 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -1,5 +1,5 @@ vendorpull https://github.com/sourcemeta/vendorpull 1dcbac42809cf87cb5b045106b863e17ad84ba02 -core https://github.com/sourcemeta/core 9690689b154bbb77c4f1b56a72ce20aa5dbb702b +core https://github.com/sourcemeta/core 367f6ef45c8e8f05269d65d98dab155505f39ccb jsonbinpack https://github.com/sourcemeta/jsonbinpack 183f333e0e032ebf0a397b5d66ed504c44a0a59f -blaze https://github.com/sourcemeta/blaze 22d0e8a03d1de54c44599426d8537386fcf2a617 +blaze https://github.com/sourcemeta/blaze a2870f3a3de5fe8cd0008eed5cd679c48ebe126a ctrf https://github.com/ctrf-io/ctrf 93ea827d951390190171d37443bff169cf47c808 diff --git a/README.markdown b/README.markdown index ba6a4934..61297308 100644 --- a/README.markdown +++ b/README.markdown @@ -95,6 +95,7 @@ documentation: - [`jsonschema bundle`](./docs/bundle.markdown) (for inlining remote references in a schema) - [`jsonschema upgrade`](./docs/upgrade.markdown) (for upgrading schemas to a newer JSON Schema dialect) - [`jsonschema inspect`](./docs/inspect.markdown) (for debugging references) +- [`jsonschema rdf`](./docs/rdf.markdown) (for turning instances into Linked Data / JSON-LD) - [`jsonschema codegen`](./docs/codegen.markdown) (for generating code from schemas) - [`jsonschema encode`](./docs/encode.markdown) (for binary compression) - [`jsonschema decode`](./docs/decode.markdown) diff --git a/completion/jsonschema.bash b/completion/jsonschema.bash index 25a8fbe9..6aa230fb 100644 --- a/completion/jsonschema.bash +++ b/completion/jsonschema.bash @@ -11,7 +11,7 @@ _jsonschema() { previous="" fi - commands="validate metaschema compile test fmt lint bundle inspect encode decode codegen install upgrade version help" + commands="validate metaschema compile test fmt lint bundle inspect encode decode codegen install upgrade rdf version help" global_options="--verbose -v --resolve -r --default-dialect -d --json -j --http -h --debug -g --header -H" @@ -65,6 +65,17 @@ _jsonschema() { COMPREPLY=( $(compgen -f -X '!*.json' -- "${current}") ) return 0 ;; + --compact) + COMPREPLY=( $(compgen -f -X '!*.json' -X '!*.yaml' -X '!*.yml' -- "${current}") ) + return 0 + ;; + -c) + if [ "${command}" = "rdf" ] + then + COMPREPLY=( $(compgen -f -X '!*.json' -X '!*.yaml' -X '!*.yml' -- "${current}") ) + return 0 + fi + ;; --target) COMPREPLY=( $(compgen -W "typescript" -- "${current}") ) return 0 @@ -199,6 +210,15 @@ _jsonschema() { COMPREPLY=( $(compgen -f -X '!*.json' -X '!*.yaml' -X '!*.yml' -- "${current}") ) fi ;; + rdf) + local options="--flatten -l --compact -c --fast -f --format-assertion -F --extension -e --ignore -i" + if [[ ${current} == -* ]] + then + COMPREPLY=( $(compgen -W "${options} ${global_options}" -- "${current}") ) + else + COMPREPLY=( $(compgen -f -X '!*.json' -X '!*.yaml' -X '!*.yml' -- "${current}") ) + fi + ;; version|help) COMPREPLY=() ;; diff --git a/completion/jsonschema.zsh b/completion/jsonschema.zsh index 9b41d0f2..d44305d6 100644 --- a/completion/jsonschema.zsh +++ b/completion/jsonschema.zsh @@ -20,6 +20,7 @@ _jsonschema() { 'codegen:Generate code from a JSON Schema' 'install:Fetch and install external schema dependencies' 'upgrade:Upgrade a schema to a newer JSON Schema dialect' + 'rdf:Turn an instance into Linked Data / JSON-LD' 'version:Print version information' 'help:Print help information' ) @@ -152,6 +153,18 @@ _jsonschema() { '(--to -t)'{--to,-t}'[Target JSON Schema dialect]:dialect:(draft4 draft6 draft7 2019-09 2020-12)' \ '1:schema file:_files -g "*.json *.yaml *.yml"' ;; + rdf) + _arguments \ + ${global_options[@]} \ + '(--flatten -l)'{--flatten,-l}'[Flatten the JSON-LD output]' \ + '(--compact -c)'{--compact,-c}'[Compact the JSON-LD output against a context]:context file:_files -g "*.json *.yaml *.yml"' \ + '(--fast -f)'{--fast,-f}'[Optimise for speed]' \ + '(--format-assertion -F)'{--format-assertion,-F}'[Compile format as an assertion]' \ + '(--extension -e)'{--extension,-e}'[Specify file extension]:extension:_jsonschema_extensions' \ + '(--ignore -i)'{--ignore,-i}'[Ignore schemas or directories]:path:_files' \ + '1:schema file:_files -g "*.json *.yaml *.yml"' \ + '2:instance file:_files -g "*.json *.yaml *.yml"' + ;; version|help) ;; esac diff --git a/docs/rdf.markdown b/docs/rdf.markdown new file mode 100644 index 00000000..797760f6 --- /dev/null +++ b/docs/rdf.markdown @@ -0,0 +1,284 @@ +Linked Data (RDF) +================= + +```sh +jsonschema rdf + [--flatten/-l] [--compact/-c ] + [--fast/-f] [--format-assertion/-F] + [--http/-h] [--verbose/-v] [--debug/-g] + [--header/-H ": "] + [--resolve/-r ...] + [--extension/-e ] [--ignore/-i ] + [--default-dialect/-d ] [--json/-j] +``` + +The same JSON Schema that validates your data can also declare what that data +means. The `rdf` command evaluates an instance against a schema annotated +with `x-jsonld-*` keywords and, if the instance is valid, prints the instance +promoted to [JSON-LD](https://www.w3.org/TR/json-ld11/), the JSON-based +serialization of [RDF](https://www.w3.org/RDF/). The result is a +knowledge-graph-ready document produced in a single evaluation pass, without +maintaining a separate JSON-LD `@context` that can silently drift from the +schema. + +**This command is experimental and we are actively seeking feedback to make +this better. Please open an issue at +https://github.com/sourcemeta/jsonschema/issues with any problems you find.** + +By default, the output is in [expanded +form](https://www.w3.org/TR/json-ld11/#expanded-document-form), which is +canonical and context-free. Pass `--flatten/-l` to +[flatten](https://www.w3.org/TR/json-ld11/#flattened-document-form) the +output, labelling every node, and `--compact/-c` with a context file to +[compact](https://www.w3.org/TR/json-ld11/#compacted-document-form) it into +idiomatic, human-friendly JSON-LD. Both options may be combined. + +As with [`validate`](./validate.markdown), schemas are compiled in exhaustive +mode by default for better validation error messages. Pass `--fast/-f` to +optimise for speed at the expense of error message quality. + +> [!NOTE] +> Annotation collection is a JSON Schema 2019-09 and 2020-12 feature, so this +> command requires the schema to have one of those dialects as its base +> dialect, and reports an error otherwise. Custom meta-schemas that build on +> those dialects work as expected. Referenced schemas may use older dialects +> and validate as usual, but their `x-jsonld-*` keywords do not emit +> annotations and are ignored. If your schemas use an older dialect, consider +> moving them to a newer one with the [`upgrade`](./upgrade.markdown) +> command. + +The Annotation Vocabulary +------------------------- + +The command resolves the following annotation keywords, which may be placed +on any subschema. Unknown `x-` prefixed keywords are standard-compliant JSON +Schema, so annotated schemas remain valid for every other tool. + +| Keyword | Values | Applies To | Meaning | +|---------|--------|------------|---------| +| `x-jsonld-id` | An absolute IRI | property subschema | The predicate IRI the property maps to | +| `x-jsonld-type` | An absolute IRI or array of absolute IRIs | object or reference subschema | The node `@type` | +| `x-jsonld-reverse` | An absolute IRI | property subschema | A reverse predicate IRI, emitting `@reverse` edges | +| `x-jsonld-datatype` | An absolute IRI | scalar subschema | A typed literal datatype IRI, such as `http://www.w3.org/2001/XMLSchema#date` | +| `x-jsonld-language` | A [BCP 47](https://www.rfc-editor.org/info/bcp47) language tag | string subschema | The language of language-tagged literals | +| `x-jsonld-direction` | `ltr` or `rtl` | string subschema | The base direction for internationalised literals | +| `x-jsonld-json` | A boolean | any subschema | Treat the value as an opaque `@json` literal | +| `x-jsonld-graph` | A boolean | object subschema | Wrap the node's edges in a named `@graph` | +| `x-jsonld-container` | `@list`, `@set`, `@language`, or `@index` | array or object property subschema | The container semantics of the property | +| `x-jsonld-self` | An [RFC 6570](https://www.rfc-editor.org/rfc/rfc6570) URI template | scalar or object subschema | Mint the node `@id` from instance values, such as `https://www.iso.org/iso-4217/{this}` | + +For example, consider the following product catalog schema, which validates +products and maps them to [schema.org](https://schema.org) at the same time: + +```json +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "x-jsonld-type": "https://schema.org/Product", + "properties": { + "sku": { + "type": "string", + "x-jsonld-id": "https://schema.org/sku" + }, + "name": { + "type": "string", + "x-jsonld-id": "https://schema.org/name" + }, + "releaseDate": { + "type": "string", + "format": "date", + "x-jsonld-id": "https://schema.org/releaseDate", + "x-jsonld-datatype": "http://www.w3.org/2001/XMLSchema#date" + }, + "keywords": { + "type": "array", + "x-jsonld-id": "https://schema.org/keywords", + "x-jsonld-container": "@list", + "items": { "type": "string" } + }, + "manufacturer": { + "type": "object", + "x-jsonld-id": "https://schema.org/manufacturer", + "x-jsonld-type": "https://schema.org/Organization", + "properties": { + "name": { + "type": "string", + "x-jsonld-id": "https://schema.org/name" + }, + "url": { + "type": "string", + "x-jsonld-id": "https://schema.org/sameAs", + "x-jsonld-self": "{+this}" + } + } + } + } +} +``` + +Also consider a JSON instance called `instance.json` that looks like this: + +```json +{ + "sku": "ABC-123", + "name": "Vacuum Robot", + "releaseDate": "2026-01-15", + "keywords": [ "vacuum", "robot" ], + "manufacturer": { + "name": "ACME", + "url": "https://acme.example.com" + } +} +``` + +We can promote the instance to Linked Data as follows: + +```sh +jsonschema rdf schema.json instance.json +``` + +The resulting document, which will be printed to standard output in expanded +JSON-LD form, would look like this: + +```json +[ + { + "@type": [ "https://schema.org/Product" ], + "https://schema.org/keywords": [ + { "@list": [ { "@value": "vacuum" }, { "@value": "robot" } ] } + ], + "https://schema.org/manufacturer": [ + { + "@type": [ "https://schema.org/Organization" ], + "https://schema.org/name": [ { "@value": "ACME" } ], + "https://schema.org/sameAs": [ { "@id": "https://acme.example.com" } ] + } + ], + "https://schema.org/name": [ { "@value": "Vacuum Robot" } ], + "https://schema.org/releaseDate": [ + { "@value": "2026-01-15", "@type": "http://www.w3.org/2001/XMLSchema#date" } + ], + "https://schema.org/sku": [ { "@value": "ABC-123" } ] + } +] +``` + +To instead produce the idiomatic compacted JSON-LD consumed by search engines +and JSON-LD-aware clients, provide a context file such as this `context.json`: + +```json +{ + "@context": { + "@vocab": "https://schema.org/", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "keywords": { "@id": "https://schema.org/keywords", "@container": "@list" }, + "releaseDate": { "@id": "https://schema.org/releaseDate", "@type": "xsd:date" }, + "sameAs": { "@id": "https://schema.org/sameAs", "@type": "@id" } + } +} +``` + +We can then compact the output against that context as follows: + +```sh +jsonschema rdf schema.json instance.json --compact context.json +``` + +The resulting document would look like this: + +```json +{ + "@type": "Product", + "keywords": [ "vacuum", "robot" ], + "manufacturer": { + "@type": "Organization", + "name": "ACME", + "sameAs": "https://acme.example.com" + }, + "name": "Vacuum Robot", + "releaseDate": "2026-01-15", + "sku": "ABC-123", + "@context": { + "@vocab": "https://schema.org/", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "keywords": { "@id": "https://schema.org/keywords", "@container": "@list" }, + "releaseDate": { "@id": "https://schema.org/releaseDate", "@type": "xsd:date" }, + "sameAs": { "@id": "https://schema.org/sameAs", "@type": "@id" } + } +} +``` + +Examples +-------- + +### Turn a JSON instance into expanded JSON-LD + +```sh +jsonschema rdf path/to/schema.json path/to/instance.json +``` + +### Turn a YAML instance into expanded JSON-LD + +```sh +jsonschema rdf path/to/schema.json path/to/instance.yaml +``` + +### Turn a JSON instance from standard input into expanded JSON-LD + +```sh +cat path/to/instance.json | jsonschema rdf path/to/schema.json - +``` + +### Flatten the JSON-LD output + +```sh +jsonschema rdf path/to/schema.json path/to/instance.json --flatten +``` + +### Compact the JSON-LD output against a context + +```sh +jsonschema rdf path/to/schema.json path/to/instance.json --compact path/to/context.json +``` + +### Flatten and compact the JSON-LD output at once + +```sh +jsonschema rdf path/to/schema.json path/to/instance.json --flatten --compact path/to/context.json +``` + +### Turn a JSON instance into JSON-LD with a schema that imports a local schema + +```sh +jsonschema rdf path/to/schema.json path/to/instance.json --resolve path/to/external.json +``` + +### Turn a JSON instance into JSON-LD with a schema that imports a directory of schemas + +```sh +jsonschema rdf path/to/schema.json path/to/instance.json --resolve path/to/schemas/ +``` + +### Turn a JSON instance into JSON-LD while optimising for speed + +```sh +jsonschema rdf path/to/schema.json path/to/instance.json --fast +``` + +### Turn a JSON instance into JSON-LD treating `format` as an assertion + +```sh +jsonschema rdf path/to/schema.json path/to/instance.json --format-assertion +``` + +### Turn a JSON instance into JSON-LD while enabling HTTP resolution + +```sh +jsonschema rdf path/to/schema.json path/to/instance.json --http +``` + +### Turn a JSON instance into JSON-LD with a custom HTTP header + +```sh +jsonschema rdf path/to/schema.json path/to/instance.json --http --header "Authorization: Bearer TOKEN" +``` diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index de9beb66..6be26265 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -16,7 +16,8 @@ sourcemeta_executable( command_compile.cc command_codegen.cc command_install.cc - command_upgrade.cc) + command_upgrade.cc + command_rdf.cc) set_target_properties(jsonschema_cli PROPERTIES OUTPUT_NAME jsonschema) target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::error) @@ -27,6 +28,7 @@ target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::jsonl) target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::gzip) target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::http) target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::jsonpointer) +target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::jsonld) 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::options) diff --git a/src/command.h b/src/command.h index 448a7bd4..21ad6b1c 100644 --- a/src/command.h +++ b/src/command.h @@ -17,6 +17,7 @@ auto decode(const sourcemeta::core::Options &options) -> void; auto codegen(const sourcemeta::core::Options &options) -> void; auto install(const sourcemeta::core::Options &options) -> void; auto upgrade(const sourcemeta::core::Options &options) -> void; +auto rdf(const sourcemeta::core::Options &options) -> void; } // namespace sourcemeta::jsonschema #endif diff --git a/src/command_rdf.cc b/src/command_rdf.cc new file mode 100644 index 00000000..ca719e2f --- /dev/null +++ b/src/command_rdf.cc @@ -0,0 +1,249 @@ +#include + +#include +#include +#include + +#include +#include +#include + +#include // std::filesystem +#include // std::cout, std::cerr +#include // std::string +#include // std::string_view +#include // std::unordered_set +#include // std::unreachable, std::move +#include // std::get, std::holds_alternative + +#include "command.h" +#include "configuration.h" +#include "error.h" +#include "input.h" +#include "logger.h" +#include "resolver.h" +#include "utils.h" + +namespace { + +auto facet_name(const sourcemeta::blaze::JSONLDFacet facet) + -> std::string_view { + switch (facet) { + case sourcemeta::blaze::JSONLDFacet::Type: + return "type"; + case sourcemeta::blaze::JSONLDFacet::Predicate: + return "predicate"; + case sourcemeta::blaze::JSONLDFacet::Datatype: + return "datatype"; + case sourcemeta::blaze::JSONLDFacet::Language: + return "language"; + case sourcemeta::blaze::JSONLDFacet::Direction: + return "direction"; + case sourcemeta::blaze::JSONLDFacet::Graph: + return "graph"; + case sourcemeta::blaze::JSONLDFacet::JSON: + return "json"; + case sourcemeta::blaze::JSONLDFacet::Container: + return "container"; + case sourcemeta::blaze::JSONLDFacet::Self: + return "self"; + default: + std::unreachable(); + } +} + +auto assert_annotations_support( + const sourcemeta::blaze::SchemaFrame &frame, + const std::filesystem::path &schema_resolution_base) -> void { + const auto &root_location{frame.locations().at( + {sourcemeta::blaze::SchemaReferenceType::Static, frame.root()})}; + switch (root_location.base_dialect) { + case sourcemeta::blaze::SchemaBaseDialect::JSON_Schema_2020_12: + case sourcemeta::blaze::SchemaBaseDialect::JSON_Schema_2020_12_Hyper: + case sourcemeta::blaze::SchemaBaseDialect::JSON_Schema_2019_09: + case sourcemeta::blaze::SchemaBaseDialect::JSON_Schema_2019_09_Hyper: + return; + default: + throw sourcemeta::jsonschema::UnsupportedDialectRdfError{ + schema_resolution_base, std::string{root_location.dialect}}; + } +} + +} // namespace + +auto sourcemeta::jsonschema::rdf(const sourcemeta::core::Options &options) + -> void { + if (options.positional().size() != 2) { + throw PositionalArgumentError{ + "This command expects a path to a schema and a path to an instance " + "to promote to JSON-LD", + "jsonschema rdf path/to/schema.json path/to/instance.json"}; + } + + validate_http_headers(options); + + const auto &schema_path{options.positional().at(0)}; + const auto &instance_path_view{options.positional().at(1)}; + const std::filesystem::path instance_path{instance_path_view}; + const bool schema_from_stdin{schema_path == "-"}; + const bool instance_from_stdin{instance_path_view == "-"}; + + check_no_duplicate_stdin(options.positional()); + + if (!schema_from_stdin && std::filesystem::is_directory(schema_path)) { + throw sourcemeta::core::IOIsADirectoryError{schema_path}; + } + + if (!instance_from_stdin && std::filesystem::is_directory(instance_path)) { + throw sourcemeta::core::IOIsADirectoryError{instance_path}; + } + + const auto schema_config_base{schema_from_stdin + ? std::filesystem::current_path() + : std::filesystem::path(schema_path)}; + const auto schema_resolution_base{ + schema_from_stdin ? stdin_path() : std::filesystem::path(schema_path)}; + + const auto configuration_path{find_configuration(schema_config_base)}; + const auto &configuration{ + read_configuration(options, configuration_path, schema_config_base)}; + const auto dialect{default_dialect(options, configuration)}; + + auto parsed_schema{schema_from_stdin ? read_from_stdin() + : read_file(schema_path)}; + + if (!sourcemeta::blaze::is_schema(parsed_schema.document)) { + throw NotSchemaError{schema_from_stdin ? stdin_path() + : schema_resolution_base}; + } + + const auto &schema{parsed_schema.document}; + const auto &custom_resolver{ + resolver(options, options.contains("http"), dialect, configuration)}; + const auto fast_mode{options.contains("fast")}; + const auto schema_default_id{ + sourcemeta::jsonschema::default_id(schema_resolution_base)}; + + const auto bundled{ + bundle_for_evaluation(schema, custom_resolver, dialect, schema_default_id, + schema_resolution_base, parsed_schema.positions)}; + + sourcemeta::blaze::SchemaFrame frame{ + sourcemeta::blaze::SchemaFrame::Mode::References}; + frame_for_evaluation(frame, bundled, custom_resolver, dialect, + schema_default_id, schema_resolution_base, + parsed_schema.positions); + + assert_annotations_support(frame, schema_resolution_base); + + auto tweaks{ + format_assertion_tweaks(options).value_or(sourcemeta::blaze::Tweaks{})}; + tweaks.annotations = std::unordered_set( + sourcemeta::blaze::JSONLD_KEYWORDS.begin(), + sourcemeta::blaze::JSONLD_KEYWORDS.end()); + + const auto schema_template{compile_for_evaluation( + bundled, custom_resolver, frame, std::string{frame.root()}, + fast_mode ? sourcemeta::blaze::Mode::FastValidation + : sourcemeta::blaze::Mode::Exhaustive, + tweaks, schema_resolution_base, parsed_schema.positions)}; + + const auto parsed_instance{instance_from_stdin ? read_from_stdin() + : read_file(instance_path)}; + const auto &instance{parsed_instance.document}; + const auto instance_display_path{ + instance_from_stdin + ? stdin_path().string() + : sourcemeta::core::weakly_canonical(instance_path).string()}; + + sourcemeta::blaze::Evaluator evaluator; + auto outcome{sourcemeta::blaze::jsonld(evaluator, schema_template, instance)}; + const auto json_output{options.contains("json")}; + + if (std::holds_alternative(outcome)) { + if (json_output) { + const auto suboutput{sourcemeta::blaze::standard( + evaluator, schema_template, instance, + fast_mode ? sourcemeta::blaze::StandardOutput::Flag + : sourcemeta::blaze::StandardOutput::Basic, + parsed_instance.positions)}; + sourcemeta::core::prettify(suboutput, std::cout); + std::cout << "\n"; + } else { + std::cerr << "fail: " << instance_display_path << "\n"; + print(std::get(outcome), + parsed_instance.positions, std::cerr); + } + + throw Fail{EXIT_EXPECTED_FAILURE}; + } + + if (std::holds_alternative( + outcome)) { + auto &error{std::get(outcome)}; + const auto position{parsed_instance.positions.get(error.instance_location)}; + if (position.has_value()) { + throw PositionError{ + std::get<0>(position.value()), + std::get<1>(position.value()), + std::move(error.message), + std::string{facet_name(error.facet)}, + std::move(error.instance_location), + instance_from_stdin ? stdin_path() : instance_path}; + } + + throw RdfResolutionError{ + std::move(error.message), std::string{facet_name(error.facet)}, + std::move(error.instance_location), + instance_from_stdin ? stdin_path() : instance_path}; + } + + auto document{std::get(std::move(outcome))}; + const auto flatten{options.contains("flatten")}; + const auto compact{options.contains("compact") && + !options.at("compact").empty()}; + + if (compact) { + const std::filesystem::path context_path{options.at("compact").front()}; + const auto parsed_context{read_file(context_path)}; + + // Compacting an empty document exercises context processing on its own, + // so context errors are attributed to the context file while errors on + // the real run below are attributed to the instance that produced the + // offending document + try { + [[maybe_unused]] const auto probe{sourcemeta::core::jsonld_compact( + sourcemeta::core::JSON::make_array(), parsed_context.document)}; + } catch (const sourcemeta::core::JSONLDError &error) { + throw sourcemeta::core::FileError( + context_path, error); + } + + try { + document = flatten ? sourcemeta::core::jsonld_flatten( + document, parsed_context.document) + : sourcemeta::core::jsonld_compact( + document, parsed_context.document); + } catch (const sourcemeta::core::JSONLDError &error) { + throw sourcemeta::core::FileError( + instance_from_stdin ? stdin_path() : instance_path, error); + } + } else if (flatten) { + try { + document = sourcemeta::core::jsonld_flatten(document); + } catch (const sourcemeta::core::JSONLDError &error) { + throw sourcemeta::core::FileError( + instance_from_stdin ? stdin_path() : instance_path, error); + } + } + + LOG_VERBOSE(options) << "ok: " << instance_display_path << "\n matches " + << (schema_from_stdin + ? stdin_path().string() + : sourcemeta::core::weakly_canonical( + schema_resolution_base) + .string()) + << "\n"; + sourcemeta::core::prettify(document, std::cout); + std::cout << "\n"; +} diff --git a/src/command_validate.cc b/src/command_validate.cc index aec3781a..ff77334f 100644 --- a/src/command_validate.cc +++ b/src/command_validate.cc @@ -35,12 +35,14 @@ auto get_precompiled_schema_template_path( } } -auto get_schema_template(const sourcemeta::core::JSON &bundled, - const sourcemeta::blaze::SchemaResolver &resolver, - const sourcemeta::blaze::SchemaFrame &frame, - const std::string &entrypoint_uri, - const bool fast_mode, - const sourcemeta::core::Options &options) +auto get_schema_template( + const sourcemeta::core::JSON &bundled, + const sourcemeta::blaze::SchemaResolver &resolver, + const sourcemeta::blaze::SchemaFrame &frame, + const std::string &entrypoint_uri, const bool fast_mode, + const sourcemeta::core::Options &options, + const std::filesystem::path &resolution_base, + const sourcemeta::core::PointerPositionTracker &positions) -> sourcemeta::blaze::Template { const auto precompiled{get_precompiled_schema_template_path(options)}; if (precompiled.has_value()) { @@ -61,12 +63,12 @@ auto get_schema_template(const sourcemeta::core::JSON &bundled, } } - return sourcemeta::blaze::compile( - bundled, sourcemeta::blaze::schema_walker, resolver, - sourcemeta::blaze::default_schema_compiler, frame, entrypoint_uri, + return sourcemeta::jsonschema::compile_for_evaluation( + bundled, resolver, frame, entrypoint_uri, fast_mode ? sourcemeta::blaze::Mode::FastValidation : sourcemeta::blaze::Mode::Exhaustive, - sourcemeta::jsonschema::format_assertion_tweaks(options)); + sourcemeta::jsonschema::format_assertion_tweaks(options), resolution_base, + positions); } auto parse_loop(const sourcemeta::core::Options &options) -> std::uint64_t { @@ -307,105 +309,15 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) const auto schema_default_id{ sourcemeta::jsonschema::default_id(schema_resolution_base)}; - const sourcemeta::core::JSON bundled{[&]() { - try { - return sourcemeta::blaze::bundle( - schema, sourcemeta::blaze::schema_walker, custom_resolver, - sourcemeta::blaze::BundleMode::References, dialect, - schema_default_id); - } catch (const sourcemeta::blaze::SchemaKeywordError &error) { - throw sourcemeta::core::FileError( - schema_resolution_base, error); - } catch (const sourcemeta::blaze::SchemaFrameError &error) { - throw sourcemeta::core::FileError( - schema_resolution_base, error); - } catch (const sourcemeta::blaze::SchemaAnchorCollisionError &error) { - const auto position{parsed_schema.positions.get(error.location())}; - if (position.has_value()) { - throw PositionError>( - std::get<0>(position.value()), std::get<1>(position.value()), - schema_resolution_base, error); - } - - throw sourcemeta::core::FileError< - sourcemeta::blaze::SchemaAnchorCollisionError>(schema_resolution_base, - error); - } catch (const sourcemeta::blaze::SchemaReferenceError &error) { - throw sourcemeta::core::FileError< - sourcemeta::blaze::SchemaReferenceError>( - schema_resolution_base, error.identifier(), error.location(), - error.what()); - } catch (const sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError - &error) { - throw sourcemeta::core::FileError< - sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError>( - schema_resolution_base, error); - } catch (const sourcemeta::blaze::SchemaResolutionError &error) { - throw sourcemeta::core::FileError< - sourcemeta::blaze::SchemaResolutionError>(schema_resolution_base, - error); - } catch (const sourcemeta::blaze::SchemaUnknownBaseDialectError &) { - throw sourcemeta::core::FileError< - sourcemeta::blaze::SchemaUnknownBaseDialectError>( - schema_resolution_base); - } catch (const sourcemeta::blaze::SchemaUnknownDialectError &) { - throw sourcemeta::core::FileError< - sourcemeta::blaze::SchemaUnknownDialectError>(schema_resolution_base); - } catch (const sourcemeta::blaze::SchemaError &error) { - throw sourcemeta::core::FileError( - schema_resolution_base, error.what()); - } catch ( - const sourcemeta::blaze::SchemaReferenceObjectResourceError &error) { - throw sourcemeta::core::FileError< - sourcemeta::blaze::SchemaReferenceObjectResourceError>( - schema_resolution_base, error.identifier()); - } - }()}; + const auto bundled{ + bundle_for_evaluation(schema, custom_resolver, dialect, schema_default_id, + schema_resolution_base, parsed_schema.positions)}; sourcemeta::blaze::SchemaFrame frame{ sourcemeta::blaze::SchemaFrame::Mode::References}; - - try { - frame.analyse(bundled, sourcemeta::blaze::schema_walker, custom_resolver, - dialect, schema_default_id); - } catch (const sourcemeta::blaze::SchemaKeywordError &error) { - throw sourcemeta::core::FileError( - schema_resolution_base, error); - } catch (const sourcemeta::blaze::SchemaFrameError &error) { - throw sourcemeta::core::FileError( - schema_resolution_base, error); - } catch (const sourcemeta::blaze::SchemaAnchorCollisionError &error) { - const auto position{parsed_schema.positions.get(error.location())}; - if (position.has_value()) { - throw PositionError>( - std::get<0>(position.value()), std::get<1>(position.value()), - schema_resolution_base, error); - } - - throw sourcemeta::core::FileError< - sourcemeta::blaze::SchemaAnchorCollisionError>(schema_resolution_base, - error); - } catch ( - const sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError &error) { - throw sourcemeta::core::FileError< - sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError>( - schema_resolution_base, error); - } catch (const sourcemeta::blaze::SchemaResolutionError &error) { - throw sourcemeta::core::FileError( - schema_resolution_base, error); - } catch (const sourcemeta::blaze::SchemaUnknownBaseDialectError &) { - throw sourcemeta::core::FileError< - sourcemeta::blaze::SchemaUnknownBaseDialectError>( - schema_resolution_base); - } catch (const sourcemeta::blaze::SchemaUnknownDialectError &) { - throw sourcemeta::core::FileError< - sourcemeta::blaze::SchemaUnknownDialectError>(schema_resolution_base); - } catch (const sourcemeta::blaze::SchemaError &error) { - throw sourcemeta::core::FileError( - schema_resolution_base, error.what()); - } + frame_for_evaluation(frame, bundled, custom_resolver, dialect, + schema_default_id, schema_resolution_base, + parsed_schema.positions); std::string entrypoint_uri{frame.root()}; if (options.contains("entrypoint") && !options.at("entrypoint").empty()) { @@ -419,71 +331,9 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) } } - const auto schema_template{[&]() { - try { - return get_schema_template(bundled, custom_resolver, frame, - entrypoint_uri, fast_mode, options); - } catch (const sourcemeta::blaze::CompilerInvalidEntryPoint &error) { - throw sourcemeta::core::FileError< - sourcemeta::blaze::CompilerInvalidEntryPoint>(schema_resolution_base, - error); - } catch (const sourcemeta::blaze::CompilerInvalidRegexError &error) { - throw sourcemeta::core::FileError< - sourcemeta::blaze::CompilerInvalidRegexError>(schema_resolution_base, - error); - } catch ( - const sourcemeta::blaze::CompilerReferenceTargetNotSchemaError &error) { - throw sourcemeta::core::FileError< - sourcemeta::blaze::CompilerReferenceTargetNotSchemaError>( - schema_resolution_base, error); - } catch (const sourcemeta::blaze::SchemaKeywordError &error) { - throw sourcemeta::core::FileError( - schema_resolution_base, error); - } catch (const sourcemeta::blaze::SchemaFrameError &error) { - throw sourcemeta::core::FileError( - schema_resolution_base, error); - } catch (const sourcemeta::blaze::SchemaAnchorCollisionError &error) { - const auto position{parsed_schema.positions.get(error.location())}; - if (position.has_value()) { - throw PositionError>( - std::get<0>(position.value()), std::get<1>(position.value()), - schema_resolution_base, error); - } - - throw sourcemeta::core::FileError< - sourcemeta::blaze::SchemaAnchorCollisionError>(schema_resolution_base, - error); - } catch (const sourcemeta::blaze::SchemaReferenceError &error) { - throw sourcemeta::core::FileError< - sourcemeta::blaze::SchemaReferenceError>( - schema_resolution_base, error.identifier(), error.location(), - error.what()); - } catch (const sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError - &error) { - throw sourcemeta::core::FileError< - sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError>( - schema_resolution_base, error); - } catch (const sourcemeta::blaze::SchemaResolutionError &error) { - throw sourcemeta::core::FileError< - sourcemeta::blaze::SchemaResolutionError>(schema_resolution_base, - error); - } catch (const sourcemeta::blaze::SchemaUnknownBaseDialectError &) { - throw sourcemeta::core::FileError< - sourcemeta::blaze::SchemaUnknownBaseDialectError>( - schema_resolution_base); - } catch (const sourcemeta::blaze::SchemaUnknownDialectError &) { - throw sourcemeta::core::FileError< - sourcemeta::blaze::SchemaUnknownDialectError>(schema_resolution_base); - } catch (const sourcemeta::blaze::SchemaVocabularyError &error) { - throw sourcemeta::core::FileError< - sourcemeta::blaze::SchemaVocabularyError>(schema_resolution_base, - error.uri(), error.what()); - } catch (const sourcemeta::blaze::SchemaError &error) { - throw sourcemeta::core::FileError( - schema_resolution_base, error.what()); - } - }()}; + const auto schema_template{get_schema_template( + bundled, custom_resolver, frame, entrypoint_uri, fast_mode, options, + schema_resolution_base, parsed_schema.positions)}; sourcemeta::blaze::Evaluator evaluator; diff --git a/src/configuration.h b/src/configuration.h index 3547661a..8629bfee 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -73,6 +73,9 @@ inline auto load_configuration( throw sourcemeta::core::FileError< sourcemeta::blaze::ConfigurationParseError>( configuration_path.value(), error); + } catch (const sourcemeta::core::JSONParseError &error) { + throw sourcemeta::core::JSONFileParseError(configuration_path.value(), + error); } assert(result.has_value()); diff --git a/src/error.h b/src/error.h index b313a866..3559763a 100644 --- a/src/error.h +++ b/src/error.h @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -25,7 +26,7 @@ #include // std::runtime_error #include // std::string #include // std::is_base_of_v, std::is_same_v -#include // std::forward +#include // std::forward, std::move #include // std::vector #include "exit_code.h" @@ -233,6 +234,54 @@ class CustomMetaschemaUpgradeError : public std::runtime_error { std::string dialect_; }; +class RdfResolutionError : public std::runtime_error { +public: + RdfResolutionError(std::string message, std::string facet, + sourcemeta::core::Pointer instance_location, + std::filesystem::path path) + : std::runtime_error{std::move(message)}, facet_{std::move(facet)}, + instance_location_{std::move(instance_location)}, + path_{std::move(path)} {} + + [[nodiscard]] auto facet() const noexcept -> const std::string & { + return this->facet_; + } + + [[nodiscard]] auto instance_location() const noexcept + -> const sourcemeta::core::Pointer & { + return this->instance_location_; + } + + [[nodiscard]] auto path() const noexcept -> const std::filesystem::path & { + return this->path_; + } + +private: + std::string facet_; + sourcemeta::core::Pointer instance_location_; + std::filesystem::path path_; +}; + +class UnsupportedDialectRdfError : public std::runtime_error { +public: + UnsupportedDialectRdfError(std::filesystem::path path, std::string dialect) + : std::runtime_error{"This command requires the schema to declare JSON " + "Schema 2019-09 or newer"}, + path_{std::move(path)}, dialect_{std::move(dialect)} {} + + [[nodiscard]] auto path() const noexcept -> const std::filesystem::path & { + return this->path_; + } + + [[nodiscard]] auto identifier() const noexcept -> const std::string & { + return this->dialect_; + } + +private: + std::filesystem::path path_; + std::string dialect_; +}; + class UnsupportedDialectUpgradeError : public std::runtime_error { public: UnsupportedDialectUpgradeError(std::filesystem::path path, @@ -513,6 +562,34 @@ inline auto print_exception(const bool is_json, const Exception &exception) } } + if constexpr (requires(const Exception ¤t) { + { + current.instance_location() + } -> std::convertible_to; + }) { + if (is_json) { + error_json.assign("instanceLocation", + sourcemeta::core::JSON{sourcemeta::core::to_string( + exception.instance_location())}); + } else { + std::cerr << " at instance location \"" + << sourcemeta::core::to_string(exception.instance_location()) + << "\"\n"; + } + } + + if constexpr (requires(const Exception ¤t) { + { + current.facet() + } -> std::convertible_to; + }) { + if (is_json) { + error_json.assign("facet", sourcemeta::core::JSON{exception.facet()}); + } else { + std::cerr << " at facet \"" << exception.facet() << "\"\n"; + } + } + if constexpr (requires(const Exception ¤t) { { current.path() @@ -554,6 +631,21 @@ inline auto print_exception(const bool is_json, const Exception &exception) } } + if constexpr (requires(const Exception ¤t) { + { + current.pointer() + } -> std::convertible_to; + }) { + if (is_json) { + error_json.assign("location", + sourcemeta::core::JSON{ + sourcemeta::core::to_string(exception.pointer())}); + } else { + std::cerr << " at document location \"" + << sourcemeta::core::to_string(exception.pointer()) << "\"\n"; + } + } + if constexpr (requires(const Exception ¤t) { { current.other() @@ -768,6 +860,26 @@ inline auto try_catch(const sourcemeta::core::Options &options, const auto is_json{options.contains("json")}; print_exception(is_json, error); return EXIT_NOT_SUPPORTED; + } catch (const PositionError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_SCHEMA_INPUT_ERROR; + } catch (const RdfResolutionError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_SCHEMA_INPUT_ERROR; + } catch (const UnsupportedDialectRdfError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + if (!is_json) { + std::cerr << "\nThe x-jsonld-* keywords rely on annotation collection, " + "which JSON Schema\n"; + std::cerr << "only introduced in the 2019-09 dialect. Consider running " + "the `upgrade`\n"; + std::cerr << "command to move your schema to a newer dialect\n"; + } + + return EXIT_SCHEMA_INPUT_ERROR; } catch (const InvalidLintRuleError &error) { const auto is_json{options.contains("json")}; print_exception(is_json, error); @@ -903,6 +1015,11 @@ inline auto try_catch(const sourcemeta::core::Options &options, const auto is_json{options.contains("json")}; print_exception(is_json, error); return EXIT_OTHER_INPUT_ERROR; + } catch ( + const sourcemeta::core::FileError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_OTHER_INPUT_ERROR; } catch (const sourcemeta::core::FileError< sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError> &error) { const auto is_json{options.contains("json")}; diff --git a/src/main.cc b/src/main.cc index bbab2381..9a016de7 100644 --- a/src/main.cc +++ b/src/main.cc @@ -131,6 +131,17 @@ Global Options: Decode a JSON document or JSONL dataset using JSON BinPack. + rdf [--flatten/-l] + [--compact/-c ] [--fast/-f] [--format-assertion/-F] + [--extension/-e ] [--ignore/-i ] + + Validate an instance against a schema annotated with x-jsonld-* + keywords and, on success, print the instance promoted to expanded + JSON-LD (Linked Data). Use --flatten/-l and/or --compact/-c to + post-process the output. As in `validate`, schemas compile in + exhaustive mode by default. Pass --fast/-f to optimise for speed at + the expense of validation error quality. + install [ ] [--force/-f] [--frozen/-z] Fetch and install external schema dependencies declared in @@ -245,6 +256,16 @@ auto jsonschema_main(const std::string &program, const std::string &command, app.parse(argc, argv, {.skip = 1}); sourcemeta::jsonschema::upgrade(app); return EXIT_SUCCESS; + } else if (command == "rdf") { + app.flag("fast", {"f"}); + app.flag("flatten", {"l"}); + app.flag("format-assertion", {"F"}); + app.option("compact", {"c"}); + app.option("extension", {"e"}); + app.option("ignore", {"i"}); + app.parse(argc, argv, {.skip = 1}); + sourcemeta::jsonschema::rdf(app); + return EXIT_SUCCESS; } else if (command == "help" || command == "--help" || command == "-h") { std::println("JSON Schema CLI - v{}", sourcemeta::jsonschema::PROJECT_VERSION); diff --git a/src/utils.h b/src/utils.h index ea1b4709..d9c6094c 100644 --- a/src/utils.h +++ b/src/utils.h @@ -10,6 +10,7 @@ #include #include +#include #include #include @@ -162,7 +163,182 @@ inline auto format_assertion_tweaks(const sourcemeta::core::Options &options) return std::nullopt; } -inline auto print(const sourcemeta::blaze::SimpleOutput &output, +inline auto +bundle_for_evaluation(const sourcemeta::core::JSON &schema, + const sourcemeta::blaze::SchemaResolver &resolver, + const std::string &dialect, const std::string &default_id, + const std::filesystem::path &resolution_base, + const sourcemeta::core::PointerPositionTracker &positions) + -> sourcemeta::core::JSON { + try { + return sourcemeta::blaze::bundle( + schema, sourcemeta::blaze::schema_walker, resolver, + sourcemeta::blaze::BundleMode::References, dialect, default_id); + } catch (const sourcemeta::blaze::SchemaKeywordError &error) { + throw sourcemeta::core::FileError( + resolution_base, error); + } catch (const sourcemeta::blaze::SchemaFrameError &error) { + throw sourcemeta::core::FileError( + resolution_base, error); + } catch (const sourcemeta::blaze::SchemaAnchorCollisionError &error) { + const auto position{positions.get(error.location())}; + if (position.has_value()) { + throw PositionError>( + std::get<0>(position.value()), std::get<1>(position.value()), + resolution_base, error); + } + + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaAnchorCollisionError>(resolution_base, error); + } catch (const sourcemeta::blaze::SchemaReferenceError &error) { + throw sourcemeta::core::FileError( + resolution_base, error.identifier(), error.location(), error.what()); + } catch ( + const sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError>( + resolution_base, error); + } catch (const sourcemeta::blaze::SchemaResolutionError &error) { + throw sourcemeta::core::FileError( + resolution_base, error); + } catch (const sourcemeta::blaze::SchemaUnknownBaseDialectError &) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownBaseDialectError>(resolution_base); + } catch (const sourcemeta::blaze::SchemaUnknownDialectError &) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownDialectError>(resolution_base); + } catch (const sourcemeta::blaze::SchemaError &error) { + throw sourcemeta::core::FileError( + resolution_base, error.what()); + } catch (const sourcemeta::blaze::SchemaReferenceObjectResourceError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaReferenceObjectResourceError>( + resolution_base, error.identifier()); + } +} + +inline auto +frame_for_evaluation(sourcemeta::blaze::SchemaFrame &frame, + const sourcemeta::core::JSON &bundled, + const sourcemeta::blaze::SchemaResolver &resolver, + const std::string &dialect, const std::string &default_id, + const std::filesystem::path &resolution_base, + const sourcemeta::core::PointerPositionTracker &positions) + -> void { + try { + frame.analyse(bundled, sourcemeta::blaze::schema_walker, resolver, dialect, + default_id); + } catch (const sourcemeta::blaze::SchemaKeywordError &error) { + throw sourcemeta::core::FileError( + resolution_base, error); + } catch (const sourcemeta::blaze::SchemaFrameError &error) { + throw sourcemeta::core::FileError( + resolution_base, error); + } catch (const sourcemeta::blaze::SchemaAnchorCollisionError &error) { + const auto position{positions.get(error.location())}; + if (position.has_value()) { + throw PositionError>( + std::get<0>(position.value()), std::get<1>(position.value()), + resolution_base, error); + } + + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaAnchorCollisionError>(resolution_base, error); + } catch (const sourcemeta::blaze::SchemaReferenceError &error) { + throw sourcemeta::core::FileError( + resolution_base, error.identifier(), error.location(), error.what()); + } catch ( + const sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError>( + resolution_base, error); + } catch (const sourcemeta::blaze::SchemaResolutionError &error) { + throw sourcemeta::core::FileError( + resolution_base, error); + } catch (const sourcemeta::blaze::SchemaUnknownBaseDialectError &) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownBaseDialectError>(resolution_base); + } catch (const sourcemeta::blaze::SchemaUnknownDialectError &) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownDialectError>(resolution_base); + } catch (const sourcemeta::blaze::SchemaError &error) { + throw sourcemeta::core::FileError( + resolution_base, error.what()); + } +} + +inline auto compile_for_evaluation( + const sourcemeta::core::JSON &bundled, + const sourcemeta::blaze::SchemaResolver &resolver, + const sourcemeta::blaze::SchemaFrame &frame, + const std::string &entrypoint_uri, const sourcemeta::blaze::Mode mode, + const std::optional &tweaks, + const std::filesystem::path &resolution_base, + const sourcemeta::core::PointerPositionTracker &positions) + -> sourcemeta::blaze::Template { + try { + return sourcemeta::blaze::compile( + bundled, sourcemeta::blaze::schema_walker, resolver, + sourcemeta::blaze::default_schema_compiler, frame, entrypoint_uri, mode, + tweaks); + } catch (const sourcemeta::blaze::CompilerInvalidEntryPoint &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::CompilerInvalidEntryPoint>(resolution_base, error); + } catch (const sourcemeta::blaze::CompilerInvalidRegexError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::CompilerInvalidRegexError>(resolution_base, error); + } catch ( + const sourcemeta::blaze::CompilerReferenceTargetNotSchemaError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::CompilerReferenceTargetNotSchemaError>( + resolution_base, error); + } catch (const sourcemeta::blaze::SchemaKeywordError &error) { + throw sourcemeta::core::FileError( + resolution_base, error); + } catch (const sourcemeta::blaze::SchemaFrameError &error) { + throw sourcemeta::core::FileError( + resolution_base, error); + } catch (const sourcemeta::blaze::SchemaAnchorCollisionError &error) { + const auto position{positions.get(error.location())}; + if (position.has_value()) { + throw PositionError>( + std::get<0>(position.value()), std::get<1>(position.value()), + resolution_base, error); + } + + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaAnchorCollisionError>(resolution_base, error); + } catch (const sourcemeta::blaze::SchemaReferenceError &error) { + throw sourcemeta::core::FileError( + resolution_base, error.identifier(), error.location(), error.what()); + } catch ( + const sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError>( + resolution_base, error); + } catch (const sourcemeta::blaze::SchemaResolutionError &error) { + throw sourcemeta::core::FileError( + resolution_base, error); + } catch (const sourcemeta::blaze::SchemaUnknownBaseDialectError &) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownBaseDialectError>(resolution_base); + } catch (const sourcemeta::blaze::SchemaUnknownDialectError &) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownDialectError>(resolution_base); + } catch (const sourcemeta::blaze::SchemaVocabularyError &error) { + throw sourcemeta::core::FileError( + resolution_base, error.uri(), error.what()); + } catch (const sourcemeta::blaze::SchemaError &error) { + throw sourcemeta::core::FileError( + resolution_base, error.what()); + } +} + +template +inline auto print(const Entries &output, const sourcemeta::core::PointerPositionTracker &tracker, std::ostream &stream) -> void { stream << "error: Schema validation failure\n"; @@ -193,24 +369,22 @@ print_annotations(const sourcemeta::blaze::SimpleOutput &output, std::ostream &stream) -> void { if (options.contains("verbose")) { for (const auto &annotation : output.annotations()) { - for (const auto &value : annotation.second) { - stream << "annotation: "; - sourcemeta::core::stringify(value, stream); - stream << "\n at instance location \""; - sourcemeta::core::stringify(annotation.first.instance_location, stream); - stream << "\""; - - const auto position{tracker.get( - sourcemeta::core::to_pointer(annotation.first.instance_location))}; - if (position.has_value()) { - const auto [line, column, end_line, end_column] = position.value(); - stream << " (line " << line << ", column " << column << ")"; - } - - stream << "\n at evaluate path \""; - sourcemeta::core::stringify(annotation.first.evaluate_path, stream); - stream << "\"\n"; + stream << "annotation: "; + sourcemeta::core::stringify(annotation.value, stream); + stream << "\n at instance location \""; + sourcemeta::core::stringify(annotation.instance_location, stream); + stream << "\""; + + const auto position{tracker.get( + sourcemeta::core::to_pointer(annotation.instance_location))}; + if (position.has_value()) { + const auto [line, column, end_line, end_column] = position.value(); + stream << " (line " << line << ", column " << column << ")"; } + + stream << "\n at evaluate path \""; + sourcemeta::core::stringify(annotation.evaluate_path, stream); + stream << "\"\n"; } } } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 6cf18a2a..b85ac843 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -946,6 +946,77 @@ add_jsonschema_test_unix(upgrade/fail_invalid_path) add_jsonschema_test_unix(upgrade/fail_schema_invalid_json) add_jsonschema_test_unix(upgrade/fail_draft2) +# RDF +add_jsonschema_test_unix(rdf/pass_2019_09) +add_jsonschema_test_unix(rdf/pass_custom_metaschema) +add_jsonschema_test_unix(rdf/pass_compact) +add_jsonschema_test_unix(rdf/pass_composition_allof) +add_jsonschema_test_unix(rdf/pass_composition_oneof) +add_jsonschema_test_unix(rdf/pass_default_dialect_cli) +add_jsonschema_test_unix(rdf/pass_default_dialect_config) +add_jsonschema_test_unix(rdf/pass_empty_instance) +add_jsonschema_test_unix(rdf/pass_expanded) +add_jsonschema_test_unix(rdf/pass_expanded_verbose) +add_jsonschema_test_unix(rdf/pass_fast) +add_jsonschema_test_unix(rdf/pass_flatten) +add_jsonschema_test_unix(rdf/pass_flatten_compact) +add_jsonschema_test_unix(rdf/pass_keyword_container_index) +add_jsonschema_test_unix(rdf/pass_keyword_container_language) +add_jsonschema_test_unix(rdf/pass_keyword_container_list) +add_jsonschema_test_unix(rdf/pass_keyword_container_set) +add_jsonschema_test_unix(rdf/pass_keyword_datatype) +add_jsonschema_test_unix(rdf/pass_keyword_direction) +add_jsonschema_test_unix(rdf/pass_keyword_graph) +add_jsonschema_test_unix(rdf/pass_keyword_id) +add_jsonschema_test_unix(rdf/pass_keyword_json) +add_jsonschema_test_unix(rdf/pass_keyword_language) +add_jsonschema_test_unix(rdf/pass_keyword_reverse) +add_jsonschema_test_unix(rdf/pass_keyword_self) +add_jsonschema_test_unix(rdf/pass_keyword_type) +add_jsonschema_test_unix(rdf/pass_no_annotations) +add_jsonschema_test_unix(rdf/pass_relative_ref) +add_jsonschema_test_unix(rdf/pass_resolve) +add_jsonschema_test_unix(rdf/pass_resolve_directory) +add_jsonschema_test_unix(rdf/pass_resolve_remap) +add_jsonschema_test_unix(rdf/pass_stdin_instance) +add_jsonschema_test_unix(rdf/pass_stdin_schema) +add_jsonschema_test_unix(rdf/pass_yaml_context) +add_jsonschema_test_unix(rdf/pass_yaml_instance) +add_jsonschema_test_unix(rdf/pass_yaml_schema) +add_jsonschema_test_unix(rdf/fail_compact_context_enoent) +add_jsonschema_test_unix(rdf/fail_compact_context_invalid_json) +add_jsonschema_test_unix(rdf/fail_compact_jsonld_error) +add_jsonschema_test_unix(rdf/fail_compact_missing_value) +add_jsonschema_test_unix(rdf/fail_draft7) +add_jsonschema_test_unix(rdf/pass_draft7_ref_annotations_ignored) +add_jsonschema_test_unix(rdf/pass_draft7_ref_no_annotations) +add_jsonschema_test_unix(rdf/fail_draft4) +add_jsonschema_test_unix(rdf/fail_format_assertion) +add_jsonschema_test_unix(rdf/fail_instance_enoent) +add_jsonschema_test_unix(rdf/fail_instance_invalid_json) +add_jsonschema_test_unix(rdf/fail_invalid_config) +add_jsonschema_test_unix(rdf/fail_invalid_instance) +add_jsonschema_test_unix(rdf/fail_invalid_instance_fast) +add_jsonschema_test_unix(rdf/fail_invalid_instance_stdin) +add_jsonschema_test_unix(rdf/fail_no_arguments) +add_jsonschema_test_unix(rdf/fail_no_instance) +add_jsonschema_test_unix(rdf/fail_resolution_container_conflict) +add_jsonschema_test_unix(rdf/fail_resolution_datatype_conflict) +add_jsonschema_test_unix(rdf/fail_resolution_invalid_language) +add_jsonschema_test_unix(rdf/fail_resolution_invalid_predicate_iri) +add_jsonschema_test_unix(rdf/fail_resolution_self_unbound) +add_jsonschema_test_unix(rdf/fail_resolution_type_on_literal) +add_jsonschema_test_unix(rdf/fail_resolve_invalid_json) +add_jsonschema_test_unix(rdf/fail_schema_enoent) +add_jsonschema_test_unix(rdf/fail_schema_invalid_json) +add_jsonschema_test_unix(rdf/fail_schema_not_a_schema) +add_jsonschema_test_unix(rdf/fail_schema_unknown_dialect) +add_jsonschema_test_unix(rdf/fail_stdin_both) +add_jsonschema_test_unix(rdf/fail_stdin_empty) +add_jsonschema_test_unix(rdf/fail_too_many_arguments) +add_jsonschema_test_unix(rdf/fail_unresolvable_ref) +add_jsonschema_test_unix(rdf/fail_yaml_instance_syntax) + # Encode add_jsonschema_test_unix(encode/pass_schema_less) add_jsonschema_test_unix(encode/pass_schema_less_jsonl) diff --git a/test/autocomplete_bash.sh b/test/autocomplete_bash.sh index b9d5eb3e..b913a17d 100755 --- a/test/autocomplete_bash.sh +++ b/test/autocomplete_bash.sh @@ -102,6 +102,8 @@ test_completion "jsonschema " "encode" "Command completion includes encode" test_completion "jsonschema " "decode" "Command completion includes decode" test_completion "jsonschema " "codegen" "Command completion includes codegen" test_completion "jsonschema " "install" "Command completion includes install" +test_completion "jsonschema " "upgrade" "Command completion includes upgrade" +test_completion "jsonschema " "rdf" "Command completion includes rdf" test_completion "jsonschema " "version" "Command completion includes version" test_completion "jsonschema " "help" "Command completion includes help" @@ -129,6 +131,12 @@ test_completion "jsonschema install --" "--frozen" "Install includes --frozen" test_completion "jsonschema install --" "--verbose" "Install includes global option --verbose" test_completion "jsonschema install --" "--debug" "Install includes global option --debug" +test_completion "jsonschema rdf --" "--flatten" "Rdf includes --flatten" +test_completion "jsonschema rdf --" "--compact" "Rdf includes --compact" +test_completion "jsonschema rdf --" "--fast" "Rdf includes --fast" +test_completion "jsonschema rdf --" "--format-assertion" "Rdf includes --format-assertion" +test_completion "jsonschema rdf --" "--verbose" "Rdf includes global option --verbose" + test_completion "jsonschema validate --" "--header" "Validate includes global option --header" test_no_completion "jsonschema validate --header " "After --header no completion is offered" test_no_completion "jsonschema validate -H " "After -H no completion is offered" diff --git a/test/codegen/fail_unsupported_keyword.sh b/test/codegen/fail_unsupported_keyword.sh index cd4e0ac2..18ff95d8 100755 --- a/test/codegen/fail_unsupported_keyword.sh +++ b/test/codegen/fail_unsupported_keyword.sh @@ -28,6 +28,7 @@ cat << EOF > "$TMP/expected.txt" error: Unsupported keyword in subschema at keyword not at file path $(realpath "$TMP")/schema.json + at document location "/properties/foo" EOF diff "$TMP/stderr.txt" "$TMP/expected.txt" @@ -42,7 +43,8 @@ cat << EOF > "$TMP/expected.txt" { "error": "Unsupported keyword in subschema", "keyword": "not", - "filePath": "$(realpath "$TMP")/schema.json" + "filePath": "$(realpath "$TMP")/schema.json", + "location": "/properties/foo" } EOF diff --git a/test/codegen/fail_unsupported_keyword_value.sh b/test/codegen/fail_unsupported_keyword_value.sh index 47bbda04..5e35d554 100755 --- a/test/codegen/fail_unsupported_keyword_value.sh +++ b/test/codegen/fail_unsupported_keyword_value.sh @@ -25,6 +25,7 @@ cat << EOF > "$TMP/expected.txt" error: Unsupported type value at keyword type at file path $(realpath "$TMP")/schema.json + at document location "" EOF diff "$TMP/stderr.txt" "$TMP/expected.txt" @@ -39,7 +40,8 @@ cat << EOF > "$TMP/expected.txt" { "error": "Unsupported type value", "keyword": "type", - "filePath": "$(realpath "$TMP")/schema.json" + "filePath": "$(realpath "$TMP")/schema.json", + "location": "" } EOF diff --git a/test/codegen/fail_unsupported_schema.sh b/test/codegen/fail_unsupported_schema.sh index e96fd0c3..665b2797 100755 --- a/test/codegen/fail_unsupported_schema.sh +++ b/test/codegen/fail_unsupported_schema.sh @@ -23,6 +23,7 @@ cat << EOF > "$TMP/expected.txt" error: Unsupported keyword in subschema at keyword not at file path $(realpath "$TMP")/schema.json + at document location "" EOF diff "$TMP/stderr.txt" "$TMP/expected.txt" @@ -37,7 +38,8 @@ cat << EOF > "$TMP/expected.txt" { "error": "Unsupported keyword in subschema", "keyword": "not", - "filePath": "$(realpath "$TMP")/schema.json" + "filePath": "$(realpath "$TMP")/schema.json", + "location": "" } EOF diff --git a/test/rdf/fail_compact_context_enoent.sh b/test/rdf/fail_compact_context_enoent.sh new file mode 100755 index 00000000..54faeb58 --- /dev/null +++ b/test/rdf/fail_compact_context_enoent.sh @@ -0,0 +1,49 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "x-jsonld-type": "https://schema.org/Person" +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{} +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" \ + --compact "$TMP/no-such-context.json" 2> "$TMP/stderr.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Other input error +test "$EXIT_CODE" = "6" + +cat << EOF > "$TMP/expected.txt" +error: No such file or directory + at file path $(realpath "$TMP")/no-such-context.json +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" \ + --compact "$TMP/no-such-context.json" --json > "$TMP/stdout.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Other input error +test "$EXIT_CODE" = "6" + +cat << EOF > "$TMP/expected.txt" +{ + "error": "No such file or directory", + "filePath": "$(realpath "$TMP")/no-such-context.json" +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.txt" diff --git a/test/rdf/fail_compact_context_invalid_json.sh b/test/rdf/fail_compact_context_invalid_json.sh new file mode 100755 index 00000000..19523d5e --- /dev/null +++ b/test/rdf/fail_compact_context_invalid_json.sh @@ -0,0 +1,57 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "x-jsonld-type": "https://schema.org/Person" +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{} +EOF + +cat << 'EOF' > "$TMP/context.json" +{ "name" } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" \ + --compact "$TMP/context.json" 2> "$TMP/stderr.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Other input error +test "$EXIT_CODE" = "6" + +cat << EOF > "$TMP/expected.txt" +error: Failed to parse the JSON document + at line 1 + at column 10 + at file path $(realpath "$TMP")/context.json +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" \ + --compact "$TMP/context.json" --json > "$TMP/stdout.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Other input error +test "$EXIT_CODE" = "6" + +cat << EOF > "$TMP/expected.txt" +{ + "error": "Failed to parse the JSON document", + "line": 1, + "column": 10, + "filePath": "$(realpath "$TMP")/context.json" +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.txt" diff --git a/test/rdf/fail_compact_jsonld_error.sh b/test/rdf/fail_compact_jsonld_error.sh new file mode 100755 index 00000000..070078bd --- /dev/null +++ b/test/rdf/fail_compact_jsonld_error.sh @@ -0,0 +1,58 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "x-jsonld-type": "https://schema.org/Person", + "properties": { + "name": { "type": "string", "x-jsonld-id": "https://schema.org/name" } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "name": "Ada" } +EOF + +cat << 'EOF' > "$TMP/context.json" +{ "@context": { "name": true } } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" \ + --compact "$TMP/context.json" 2> "$TMP/stderr.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Other input error +test "$EXIT_CODE" = "6" + +cat << EOF > "$TMP/expected.txt" +error: Invalid term definition + at file path $(realpath "$TMP")/context.json + at document location "/name" +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" \ + --compact "$TMP/context.json" --json > "$TMP/stdout.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Other input error +test "$EXIT_CODE" = "6" + +cat << EOF > "$TMP/expected.txt" +{ + "error": "Invalid term definition", + "filePath": "$(realpath "$TMP")/context.json", + "location": "/name" +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.txt" diff --git a/test/rdf/fail_compact_missing_value.sh b/test/rdf/fail_compact_missing_value.sh new file mode 100755 index 00000000..a8898187 --- /dev/null +++ b/test/rdf/fail_compact_missing_value.sh @@ -0,0 +1,49 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "x-jsonld-type": "https://schema.org/Person" +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{} +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" --compact \ + 2> "$TMP/stderr.txt" && EXIT_CODE="$?" || EXIT_CODE="$?" +# Invalid CLI arguments +test "$EXIT_CODE" = "5" + +cat << 'EOF' > "$TMP/expected.txt" +error: This option must take a value + at option compact + +Run the `help` command for usage information +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" --json --compact \ + > "$TMP/stdout.txt" && EXIT_CODE="$?" || EXIT_CODE="$?" +# Invalid CLI arguments +test "$EXIT_CODE" = "5" + +cat << 'EOF' > "$TMP/expected.txt" +{ + "error": "This option must take a value", + "option": "compact" +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.txt" diff --git a/test/rdf/fail_draft4.sh b/test/rdf/fail_draft4.sh new file mode 100755 index 00000000..4ae55695 --- /dev/null +++ b/test/rdf/fail_draft4.sh @@ -0,0 +1,56 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "x-jsonld-type": "https://schema.org/Person", + "properties": { + "name": { "type": "string", "x-jsonld-id": "https://schema.org/name" } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "name": "Ada" } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" 2> "$TMP/stderr.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Schema input error +test "$EXIT_CODE" = "4" + +cat << EOF > "$TMP/expected.txt" +error: This command requires the schema to declare JSON Schema 2019-09 or newer + at identifier http://json-schema.org/draft-04/schema# + at file path $(realpath "$TMP")/schema.json + +The x-jsonld-* keywords rely on annotation collection, which JSON Schema +only introduced in the 2019-09 dialect. Consider running the \`upgrade\` +command to move your schema to a newer dialect +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" --json > "$TMP/stdout.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Schema input error +test "$EXIT_CODE" = "4" + +cat << EOF > "$TMP/expected.txt" +{ + "error": "This command requires the schema to declare JSON Schema 2019-09 or newer", + "identifier": "http://json-schema.org/draft-04/schema#", + "filePath": "$(realpath "$TMP")/schema.json" +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.txt" diff --git a/test/rdf/fail_draft7.sh b/test/rdf/fail_draft7.sh new file mode 100755 index 00000000..b052fc6c --- /dev/null +++ b/test/rdf/fail_draft7.sh @@ -0,0 +1,56 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "x-jsonld-type": "https://schema.org/Person", + "properties": { + "name": { "type": "string", "x-jsonld-id": "https://schema.org/name" } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "name": "Ada" } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" 2> "$TMP/stderr.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Schema input error +test "$EXIT_CODE" = "4" + +cat << EOF > "$TMP/expected.txt" +error: This command requires the schema to declare JSON Schema 2019-09 or newer + at identifier http://json-schema.org/draft-07/schema# + at file path $(realpath "$TMP")/schema.json + +The x-jsonld-* keywords rely on annotation collection, which JSON Schema +only introduced in the 2019-09 dialect. Consider running the \`upgrade\` +command to move your schema to a newer dialect +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" --json > "$TMP/stdout.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Schema input error +test "$EXIT_CODE" = "4" + +cat << EOF > "$TMP/expected.txt" +{ + "error": "This command requires the schema to declare JSON Schema 2019-09 or newer", + "identifier": "http://json-schema.org/draft-07/schema#", + "filePath": "$(realpath "$TMP")/schema.json" +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.txt" diff --git a/test/rdf/fail_format_assertion.sh b/test/rdf/fail_format_assertion.sh new file mode 100755 index 00000000..4b6f2838 --- /dev/null +++ b/test/rdf/fail_format_assertion.sh @@ -0,0 +1,90 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "x-jsonld-id": "https://schema.org/email" + } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "email": "not an email" } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" > "$TMP/output.json" + +cat << 'EOF' > "$TMP/expected.json" +[ + { + "https://schema.org/email": [ + { + "@value": "not an email" + } + ] + } +] +EOF + +diff "$TMP/output.json" "$TMP/expected.json" + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" --format-assertion \ + 2> "$TMP/stderr.txt" && EXIT_CODE="$?" || EXIT_CODE="$?" +# Test assertion failure +test "$EXIT_CODE" = "2" + +cat << EOF > "$TMP/expected.txt" +fail: $(realpath "$TMP")/instance.json +error: Schema validation failure + The string value "not an email" was expected to represent a valid email address + at instance location "/email" (line 1, column 3) + at evaluate path "/properties/email/format" + The object value was expected to validate against the single defined property subschema + at instance location "" (line 1, column 1) + at evaluate path "/properties" +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" --format-assertion --json \ + > "$TMP/stdout.txt" && EXIT_CODE="$?" || EXIT_CODE="$?" +# Test assertion failure +test "$EXIT_CODE" = "2" + +cat << EOF > "$TMP/expected.txt" +{ + "valid": false, + "errors": [ + { + "keywordLocation": "/properties/email/format", + "absoluteKeywordLocation": "file://$(realpath "$TMP")/schema.json#/properties/email/format", + "instanceLocation": "/email", + "instancePosition": [ 1, 3, 1, 25 ], + "error": "The string value \"not an email\" was expected to represent a valid email address" + }, + { + "keywordLocation": "/properties", + "absoluteKeywordLocation": "file://$(realpath "$TMP")/schema.json#/properties", + "instanceLocation": "", + "instancePosition": [ 1, 1, 1, 27 ], + "error": "The object value was expected to validate against the single defined property subschema" + } + ] +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.txt" diff --git a/test/rdf/fail_instance_enoent.sh b/test/rdf/fail_instance_enoent.sh new file mode 100755 index 00000000..5359e2ea --- /dev/null +++ b/test/rdf/fail_instance_enoent.sh @@ -0,0 +1,46 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "x-jsonld-type": "https://schema.org/Person", + "properties": { + "name": { "type": "string", "x-jsonld-id": "https://schema.org/name" } + } +} +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/no-such-instance.json" 2> "$TMP/stderr.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Other input error +test "$EXIT_CODE" = "6" + +cat << EOF > "$TMP/expected.txt" +error: No such file or directory + at file path $(realpath "$TMP")/no-such-instance.json +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +"$1" rdf "$TMP/schema.json" "$TMP/no-such-instance.json" --json > "$TMP/stdout.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Other input error +test "$EXIT_CODE" = "6" + +cat << EOF > "$TMP/expected.txt" +{ + "error": "No such file or directory", + "filePath": "$(realpath "$TMP")/no-such-instance.json" +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.txt" diff --git a/test/rdf/fail_instance_invalid_json.sh b/test/rdf/fail_instance_invalid_json.sh new file mode 100755 index 00000000..638f84bf --- /dev/null +++ b/test/rdf/fail_instance_invalid_json.sh @@ -0,0 +1,54 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "x-jsonld-type": "https://schema.org/Person", + "properties": { + "name": { "type": "string", "x-jsonld-id": "https://schema.org/name" } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "name" } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" 2> "$TMP/stderr.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Other input error +test "$EXIT_CODE" = "6" + +cat << EOF > "$TMP/expected.txt" +error: Failed to parse the JSON document + at line 1 + at column 10 + at file path $(realpath "$TMP")/instance.json +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" --json > "$TMP/stdout.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Other input error +test "$EXIT_CODE" = "6" + +cat << EOF > "$TMP/expected.txt" +{ + "error": "Failed to parse the JSON document", + "line": 1, + "column": 10, + "filePath": "$(realpath "$TMP")/instance.json" +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.txt" diff --git a/test/rdf/fail_invalid_config.sh b/test/rdf/fail_invalid_config.sh new file mode 100755 index 00000000..ee6aff56 --- /dev/null +++ b/test/rdf/fail_invalid_config.sh @@ -0,0 +1,53 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ "$schema": "https://json-schema.org/draft/2020-12/schema" } +EOF + +cat << 'EOF' > "$TMP/instance.json" +{} +EOF + +cat << 'EOF' > "$TMP/jsonschema.json" +{ invalid +EOF + +cd "$TMP" + +"$1" rdf schema.json instance.json 2> "$TMP/stderr.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Other input error +test "$EXIT_CODE" = "6" + +cat << EOF > "$TMP/expected.txt" +error: Failed to parse the JSON document + at line 1 + at column 3 + at file path $(realpath "$TMP")/jsonschema.json +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +"$1" rdf schema.json instance.json --json > "$TMP/stdout.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Other input error +test "$EXIT_CODE" = "6" + +cat << EOF > "$TMP/expected.txt" +{ + "error": "Failed to parse the JSON document", + "line": 1, + "column": 3, + "filePath": "$(realpath "$TMP")/jsonschema.json" +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.txt" diff --git a/test/rdf/fail_invalid_instance.sh b/test/rdf/fail_invalid_instance.sh new file mode 100755 index 00000000..036af8ad --- /dev/null +++ b/test/rdf/fail_invalid_instance.sh @@ -0,0 +1,72 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "x-jsonld-type": "https://schema.org/Person", + "properties": { + "name": { "type": "string", "x-jsonld-id": "https://schema.org/name" }, + "email": { "type": "string", "x-jsonld-id": "https://schema.org/email" } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "name": 1 } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" 2> "$TMP/stderr.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Test assertion failure +test "$EXIT_CODE" = "2" + +cat << EOF > "$TMP/expected.txt" +fail: $(realpath "$TMP")/instance.json +error: Schema validation failure + The value was expected to be of type string but it was of type integer + at instance location "/name" (line 1, column 3) + at evaluate path "/properties/name/type" + The object value was expected to validate against the defined properties subschemas + at instance location "" (line 1, column 1) + at evaluate path "/properties" +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" --json > "$TMP/stdout.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Test assertion failure +test "$EXIT_CODE" = "2" + +cat << EOF > "$TMP/expected.txt" +{ + "valid": false, + "errors": [ + { + "keywordLocation": "/properties/name/type", + "absoluteKeywordLocation": "file://$(realpath "$TMP")/schema.json#/properties/name/type", + "instanceLocation": "/name", + "instancePosition": [ 1, 3, 1, 11 ], + "error": "The value was expected to be of type string but it was of type integer" + }, + { + "keywordLocation": "/properties", + "absoluteKeywordLocation": "file://$(realpath "$TMP")/schema.json#/properties", + "instanceLocation": "", + "instancePosition": [ 1, 1, 1, 13 ], + "error": "The object value was expected to validate against the defined properties subschemas" + } + ] +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.txt" diff --git a/test/rdf/fail_invalid_instance_fast.sh b/test/rdf/fail_invalid_instance_fast.sh new file mode 100755 index 00000000..87daa9f7 --- /dev/null +++ b/test/rdf/fail_invalid_instance_fast.sh @@ -0,0 +1,53 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "x-jsonld-type": "https://schema.org/Person", + "properties": { + "name": { "type": "string", "x-jsonld-id": "https://schema.org/name" }, + "email": { "type": "string", "x-jsonld-id": "https://schema.org/email" } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "name": 1 } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" --fast 2> "$TMP/stderr.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Test assertion failure +test "$EXIT_CODE" = "2" + +cat << EOF > "$TMP/expected.txt" +fail: $(realpath "$TMP")/instance.json +error: Schema validation failure + The value was expected to be of type string but it was of type integer + at instance location "/name" (line 1, column 3) + at evaluate path "/properties/name/type" +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" --fast --json > "$TMP/stdout.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Test assertion failure +test "$EXIT_CODE" = "2" + +cat << 'EOF' > "$TMP/expected.txt" +{ + "valid": false +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.txt" diff --git a/test/rdf/fail_invalid_instance_stdin.sh b/test/rdf/fail_invalid_instance_stdin.sh new file mode 100755 index 00000000..d7fa321e --- /dev/null +++ b/test/rdf/fail_invalid_instance_stdin.sh @@ -0,0 +1,54 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "x-jsonld-type": "https://schema.org/Thing" +} +EOF + +echo '1' | "$1" rdf "$TMP/schema.json" - 2> "$TMP/stderr.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Test assertion failure +test "$EXIT_CODE" = "2" + +cat << 'EOF' > "$TMP/expected.txt" +fail: /dev/stdin +error: Schema validation failure + The value was expected to be of type string but it was of type integer + at instance location "" (line 1, column 1) + at evaluate path "/type" +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +echo '1' | "$1" rdf "$TMP/schema.json" - --json > "$TMP/stdout.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Test assertion failure +test "$EXIT_CODE" = "2" + +cat << EOF > "$TMP/expected.txt" +{ + "valid": false, + "errors": [ + { + "keywordLocation": "/type", + "absoluteKeywordLocation": "file://$(realpath "$TMP")/schema.json#/type", + "instanceLocation": "", + "instancePosition": [ 1, 1, 1, 1 ], + "error": "The value was expected to be of type string but it was of type integer" + } + ] +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.txt" diff --git a/test/rdf/fail_no_arguments.sh b/test/rdf/fail_no_arguments.sh new file mode 100755 index 00000000..1a2beb35 --- /dev/null +++ b/test/rdf/fail_no_arguments.sh @@ -0,0 +1,33 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +"$1" rdf 2> "$TMP/stderr.txt" && EXIT_CODE="$?" || EXIT_CODE="$?" +# Invalid CLI arguments +test "$EXIT_CODE" = "5" + +cat << 'EOF' > "$TMP/expected.txt" +error: This command expects a path to a schema and a path to an instance to promote to JSON-LD + +For example: jsonschema rdf path/to/schema.json path/to/instance.json +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +"$1" rdf --json > "$TMP/stdout.txt" 2>&1 && EXIT_CODE="$?" || EXIT_CODE="$?" +# Invalid CLI arguments +test "$EXIT_CODE" = "5" + +cat << 'EOF' > "$TMP/expected.txt" +{ + "error": "This command expects a path to a schema and a path to an instance to promote to JSON-LD" +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.txt" diff --git a/test/rdf/fail_no_instance.sh b/test/rdf/fail_no_instance.sh new file mode 100755 index 00000000..568074f8 --- /dev/null +++ b/test/rdf/fail_no_instance.sh @@ -0,0 +1,37 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ "$schema": "https://json-schema.org/draft/2020-12/schema" } +EOF + +"$1" rdf "$TMP/schema.json" 2> "$TMP/stderr.txt" && EXIT_CODE="$?" || EXIT_CODE="$?" +# Invalid CLI arguments +test "$EXIT_CODE" = "5" + +cat << 'EOF' > "$TMP/expected.txt" +error: This command expects a path to a schema and a path to an instance to promote to JSON-LD + +For example: jsonschema rdf path/to/schema.json path/to/instance.json +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +"$1" rdf "$TMP/schema.json" --json > "$TMP/stdout.txt" 2>&1 && EXIT_CODE="$?" || EXIT_CODE="$?" +# Invalid CLI arguments +test "$EXIT_CODE" = "5" + +cat << 'EOF' > "$TMP/expected.txt" +{ + "error": "This command expects a path to a schema and a path to an instance to promote to JSON-LD" +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.txt" diff --git a/test/rdf/fail_resolution_container_conflict.sh b/test/rdf/fail_resolution_container_conflict.sh new file mode 100755 index 00000000..5f75f6b5 --- /dev/null +++ b/test/rdf/fail_resolution_container_conflict.sh @@ -0,0 +1,65 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "tags": { + "type": "array", + "x-jsonld-id": "https://schema.org/keywords", + "items": { "type": "string" }, + "allOf": [ + { "x-jsonld-container": "@list" }, + { "x-jsonld-container": "@set" } + ] + } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "tags": [ "a" ] } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" 2> "$TMP/stderr.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Schema input error +test "$EXIT_CODE" = "4" + +cat << EOF > "$TMP/expected.txt" +error: A JSON-LD container cannot be assigned more than one value + at line 1 + at column 3 + at instance location "/tags" + at facet "container" + at file path $(realpath "$TMP")/instance.json +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" --json > "$TMP/stdout.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Schema input error +test "$EXIT_CODE" = "4" + +cat << EOF > "$TMP/expected.txt" +{ + "error": "A JSON-LD container cannot be assigned more than one value", + "line": 1, + "column": 3, + "instanceLocation": "/tags", + "facet": "container", + "filePath": "$(realpath "$TMP")/instance.json" +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.txt" diff --git a/test/rdf/fail_resolution_datatype_conflict.sh b/test/rdf/fail_resolution_datatype_conflict.sh new file mode 100755 index 00000000..a6b5474d --- /dev/null +++ b/test/rdf/fail_resolution_datatype_conflict.sh @@ -0,0 +1,64 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "price": { + "type": "number", + "x-jsonld-id": "https://schema.org/price", + "allOf": [ + { "x-jsonld-datatype": "http://www.w3.org/2001/XMLSchema#decimal" }, + { "x-jsonld-datatype": "http://www.w3.org/2001/XMLSchema#double" } + ] + } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "price": 1 } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" 2> "$TMP/stderr.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Schema input error +test "$EXIT_CODE" = "4" + +cat << EOF > "$TMP/expected.txt" +error: A JSON-LD datatype cannot be assigned more than one value + at line 1 + at column 3 + at instance location "/price" + at facet "datatype" + at file path $(realpath "$TMP")/instance.json +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" --json > "$TMP/stdout.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Schema input error +test "$EXIT_CODE" = "4" + +cat << EOF > "$TMP/expected.txt" +{ + "error": "A JSON-LD datatype cannot be assigned more than one value", + "line": 1, + "column": 3, + "instanceLocation": "/price", + "facet": "datatype", + "filePath": "$(realpath "$TMP")/instance.json" +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.txt" diff --git a/test/rdf/fail_resolution_invalid_language.sh b/test/rdf/fail_resolution_invalid_language.sh new file mode 100755 index 00000000..63cd3c5e --- /dev/null +++ b/test/rdf/fail_resolution_invalid_language.sh @@ -0,0 +1,61 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { + "type": "string", + "x-jsonld-id": "https://schema.org/name", + "x-jsonld-language": "not a tag!" + } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "name": "Ada" } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" 2> "$TMP/stderr.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Schema input error +test "$EXIT_CODE" = "4" + +cat << EOF > "$TMP/expected.txt" +error: The value of x-jsonld-language must be a BCP 47 language tag + at line 1 + at column 3 + at instance location "/name" + at facet "language" + at file path $(realpath "$TMP")/instance.json +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" --json > "$TMP/stdout.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Schema input error +test "$EXIT_CODE" = "4" + +cat << EOF > "$TMP/expected.txt" +{ + "error": "The value of x-jsonld-language must be a BCP 47 language tag", + "line": 1, + "column": 3, + "instanceLocation": "/name", + "facet": "language", + "filePath": "$(realpath "$TMP")/instance.json" +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.txt" diff --git a/test/rdf/fail_resolution_invalid_predicate_iri.sh b/test/rdf/fail_resolution_invalid_predicate_iri.sh new file mode 100755 index 00000000..1233c3d4 --- /dev/null +++ b/test/rdf/fail_resolution_invalid_predicate_iri.sh @@ -0,0 +1,57 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { "type": "string", "x-jsonld-id": "name" } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "name": "Ada" } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" 2> "$TMP/stderr.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Schema input error +test "$EXIT_CODE" = "4" + +cat << EOF > "$TMP/expected.txt" +error: The value of x-jsonld-id must be an absolute IRI + at line 1 + at column 3 + at instance location "/name" + at facet "predicate" + at file path $(realpath "$TMP")/instance.json +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" --json > "$TMP/stdout.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Schema input error +test "$EXIT_CODE" = "4" + +cat << EOF > "$TMP/expected.txt" +{ + "error": "The value of x-jsonld-id must be an absolute IRI", + "line": 1, + "column": 3, + "instanceLocation": "/name", + "facet": "predicate", + "filePath": "$(realpath "$TMP")/instance.json" +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.txt" diff --git a/test/rdf/fail_resolution_self_unbound.sh b/test/rdf/fail_resolution_self_unbound.sh new file mode 100755 index 00000000..6d7ce4ed --- /dev/null +++ b/test/rdf/fail_resolution_self_unbound.sh @@ -0,0 +1,59 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "x-jsonld-type": "https://schema.org/Person", + "x-jsonld-self": "https://example.com/people/{missing}", + "properties": { + "name": { "type": "string", "x-jsonld-id": "https://schema.org/name" } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "name": "Ada" } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" 2> "$TMP/stderr.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Schema input error +test "$EXIT_CODE" = "4" + +cat << EOF > "$TMP/expected.txt" +error: A JSON-LD self identity template variable must bind to a string, number, or boolean + at line 1 + at column 1 + at instance location "" + at facet "self" + at file path $(realpath "$TMP")/instance.json +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" --json > "$TMP/stdout.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Schema input error +test "$EXIT_CODE" = "4" + +cat << EOF > "$TMP/expected.txt" +{ + "error": "A JSON-LD self identity template variable must bind to a string, number, or boolean", + "line": 1, + "column": 1, + "instanceLocation": "", + "facet": "self", + "filePath": "$(realpath "$TMP")/instance.json" +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.txt" diff --git a/test/rdf/fail_resolution_type_on_literal.sh b/test/rdf/fail_resolution_type_on_literal.sh new file mode 100755 index 00000000..72996642 --- /dev/null +++ b/test/rdf/fail_resolution_type_on_literal.sh @@ -0,0 +1,61 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { + "type": "string", + "x-jsonld-id": "https://schema.org/name", + "x-jsonld-type": "https://schema.org/Text" + } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "name": "Ada" } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" 2> "$TMP/stderr.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Schema input error +test "$EXIT_CODE" = "4" + +cat << EOF > "$TMP/expected.txt" +error: A JSON-LD type can only be assigned to an object value + at line 1 + at column 3 + at instance location "/name" + at facet "type" + at file path $(realpath "$TMP")/instance.json +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" --json > "$TMP/stdout.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Schema input error +test "$EXIT_CODE" = "4" + +cat << EOF > "$TMP/expected.txt" +{ + "error": "A JSON-LD type can only be assigned to an object value", + "line": 1, + "column": 3, + "instanceLocation": "/name", + "facet": "type", + "filePath": "$(realpath "$TMP")/instance.json" +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.txt" diff --git a/test/rdf/fail_resolve_invalid_json.sh b/test/rdf/fail_resolve_invalid_json.sh new file mode 100755 index 00000000..7f64f4d0 --- /dev/null +++ b/test/rdf/fail_resolve_invalid_json.sh @@ -0,0 +1,61 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "x-jsonld-type": "https://schema.org/Person" +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{} +EOF + +cat << 'EOF' > "$TMP/invalid.json" + + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com", + "type": "string" +} +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" \ + --resolve "$TMP/invalid.json" 2> "$TMP/stderr.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Other input error +test "$EXIT_CODE" = "6" + +cat << EOF > "$TMP/expected.txt" +error: Failed to parse the JSON document + at line 2 + at column 12 + at file path $(realpath "$TMP")/invalid.json +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" \ + --resolve "$TMP/invalid.json" --json > "$TMP/stdout.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Other input error +test "$EXIT_CODE" = "6" + +cat << EOF > "$TMP/expected.txt" +{ + "error": "Failed to parse the JSON document", + "line": 2, + "column": 12, + "filePath": "$(realpath "$TMP")/invalid.json" +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.txt" diff --git a/test/rdf/fail_schema_enoent.sh b/test/rdf/fail_schema_enoent.sh new file mode 100755 index 00000000..cf871d29 --- /dev/null +++ b/test/rdf/fail_schema_enoent.sh @@ -0,0 +1,39 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/instance.json" +{} +EOF + +"$1" rdf "$TMP/no-such-schema.json" "$TMP/instance.json" 2> "$TMP/stderr.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Other input error +test "$EXIT_CODE" = "6" + +cat << EOF > "$TMP/expected.txt" +error: No such file or directory + at file path $(realpath "$TMP")/no-such-schema.json +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +"$1" rdf "$TMP/no-such-schema.json" "$TMP/instance.json" --json > "$TMP/stdout.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Other input error +test "$EXIT_CODE" = "6" + +cat << EOF > "$TMP/expected.txt" +{ + "error": "No such file or directory", + "filePath": "$(realpath "$TMP")/no-such-schema.json" +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.txt" diff --git a/test/rdf/fail_schema_invalid_json.sh b/test/rdf/fail_schema_invalid_json.sh new file mode 100755 index 00000000..37f913b3 --- /dev/null +++ b/test/rdf/fail_schema_invalid_json.sh @@ -0,0 +1,49 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "type" string +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{} +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" 2> "$TMP/stderr.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Other input error +test "$EXIT_CODE" = "6" + +cat << EOF > "$TMP/expected.txt" +error: Failed to parse the JSON document + at line 2 + at column 10 + at file path $(realpath "$TMP")/schema.json +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" --json > "$TMP/stdout.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Other input error +test "$EXIT_CODE" = "6" + +cat << EOF > "$TMP/expected.txt" +{ + "error": "Failed to parse the JSON document", + "line": 2, + "column": 10, + "filePath": "$(realpath "$TMP")/schema.json" +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.txt" diff --git a/test/rdf/fail_schema_not_a_schema.sh b/test/rdf/fail_schema_not_a_schema.sh new file mode 100755 index 00000000..1bf7648a --- /dev/null +++ b/test/rdf/fail_schema_not_a_schema.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" +"just a string" +EOF + +cat << 'EOF' > "$TMP/instance.json" +{} +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" 2> "$TMP/stderr.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Schema input error +test "$EXIT_CODE" = "4" + +cat << EOF > "$TMP/expected.txt" +error: The schema file you provided does not represent a valid JSON Schema + at file path $(realpath "$TMP")/schema.json +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" --json > "$TMP/stdout.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Schema input error +test "$EXIT_CODE" = "4" + +cat << EOF > "$TMP/expected.txt" +{ + "error": "The schema file you provided does not represent a valid JSON Schema", + "filePath": "$(realpath "$TMP")/schema.json" +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.txt" diff --git a/test/rdf/fail_schema_unknown_dialect.sh b/test/rdf/fail_schema_unknown_dialect.sh new file mode 100755 index 00000000..1c1fdbb5 --- /dev/null +++ b/test/rdf/fail_schema_unknown_dialect.sh @@ -0,0 +1,47 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ "$schema": "https://example.com/unknown-dialect" } +EOF + +cat << 'EOF' > "$TMP/instance.json" +{} +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" 2> "$TMP/stderr.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Schema input error +test "$EXIT_CODE" = "4" + +cat << EOF > "$TMP/expected.txt" +error: Could not resolve the metaschema of the schema + at identifier https://example.com/unknown-dialect + at file path $(realpath "$TMP")/schema.json + +This is likely because you forgot to import such schema using \`--resolve/-r\` +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" --json > "$TMP/stdout.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Schema input error +test "$EXIT_CODE" = "4" + +cat << EOF > "$TMP/expected.txt" +{ + "error": "Could not resolve the metaschema of the schema", + "identifier": "https://example.com/unknown-dialect", + "filePath": "$(realpath "$TMP")/schema.json" +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.txt" diff --git a/test/rdf/fail_stdin_both.sh b/test/rdf/fail_stdin_both.sh new file mode 100755 index 00000000..4243c655 --- /dev/null +++ b/test/rdf/fail_stdin_both.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +"$1" rdf - - 2> "$TMP/stderr.txt" && EXIT_CODE="$?" || EXIT_CODE="$?" +# Invalid CLI arguments +test "$EXIT_CODE" = "5" + +cat << 'EOF' > "$TMP/expected.txt" +error: Cannot read from standard input more than once +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +"$1" rdf - - --json > "$TMP/stdout.txt" 2>&1 && EXIT_CODE="$?" || EXIT_CODE="$?" +# Invalid CLI arguments +test "$EXIT_CODE" = "5" + +cat << 'EOF' > "$TMP/expected.txt" +{ + "error": "Cannot read from standard input more than once" +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.txt" diff --git a/test/rdf/fail_stdin_empty.sh b/test/rdf/fail_stdin_empty.sh new file mode 100755 index 00000000..53e18435 --- /dev/null +++ b/test/rdf/fail_stdin_empty.sh @@ -0,0 +1,50 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "x-jsonld-type": "https://schema.org/Person", + "properties": { + "name": { "type": "string", "x-jsonld-id": "https://schema.org/name" } + } +} +EOF + +printf '' | "$1" rdf "$TMP/schema.json" - 2> "$TMP/stderr.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Other input error +test "$EXIT_CODE" = "6" + +cat << 'EOF' > "$TMP/expected.txt" +error: Failed to parse the JSON document + at line 1 + at column 1 + at file path /dev/stdin +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +printf '' | "$1" rdf "$TMP/schema.json" - --json > "$TMP/stdout.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Other input error +test "$EXIT_CODE" = "6" + +cat << 'EOF' > "$TMP/expected.txt" +{ + "error": "Failed to parse the JSON document", + "line": 1, + "column": 1, + "filePath": "/dev/stdin" +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.txt" diff --git a/test/rdf/fail_too_many_arguments.sh b/test/rdf/fail_too_many_arguments.sh new file mode 100755 index 00000000..61820661 --- /dev/null +++ b/test/rdf/fail_too_many_arguments.sh @@ -0,0 +1,45 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ "$schema": "https://json-schema.org/draft/2020-12/schema" } +EOF + +cat << 'EOF' > "$TMP/instance.json" +{} +EOF + +cat << 'EOF' > "$TMP/extra.json" +{} +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" "$TMP/extra.json" 2> "$TMP/stderr.txt" && EXIT_CODE="$?" || EXIT_CODE="$?" +# Invalid CLI arguments +test "$EXIT_CODE" = "5" + +cat << 'EOF' > "$TMP/expected.txt" +error: This command expects a path to a schema and a path to an instance to promote to JSON-LD + +For example: jsonschema rdf path/to/schema.json path/to/instance.json +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" "$TMP/extra.json" --json > "$TMP/stdout.txt" 2>&1 && EXIT_CODE="$?" || EXIT_CODE="$?" +# Invalid CLI arguments +test "$EXIT_CODE" = "5" + +cat << 'EOF' > "$TMP/expected.txt" +{ + "error": "This command expects a path to a schema and a path to an instance to promote to JSON-LD" +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.txt" diff --git a/test/rdf/fail_unresolvable_ref.sh b/test/rdf/fail_unresolvable_ref.sh new file mode 100755 index 00000000..4bf68708 --- /dev/null +++ b/test/rdf/fail_unresolvable_ref.sh @@ -0,0 +1,53 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "foo": { "$ref": "https://example.com/nope" } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "foo": 1 } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" 2> "$TMP/stderr.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Schema input error +test "$EXIT_CODE" = "4" + +cat << EOF > "$TMP/expected.txt" +error: Could not resolve the reference to an external schema + at identifier https://example.com/nope + at file path $(realpath "$TMP")/schema.json + +This is likely because you forgot to import such schema using \`--resolve/-r\` +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" --json > "$TMP/stdout.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Schema input error +test "$EXIT_CODE" = "4" + +cat << EOF > "$TMP/expected.txt" +{ + "error": "Could not resolve the reference to an external schema", + "identifier": "https://example.com/nope", + "filePath": "$(realpath "$TMP")/schema.json" +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.txt" diff --git a/test/rdf/fail_yaml_instance_syntax.sh b/test/rdf/fail_yaml_instance_syntax.sh new file mode 100755 index 00000000..0562de59 --- /dev/null +++ b/test/rdf/fail_yaml_instance_syntax.sh @@ -0,0 +1,54 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "x-jsonld-type": "https://schema.org/Person", + "properties": { + "name": { "type": "string", "x-jsonld-id": "https://schema.org/name" } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.yaml" +foo: [unclosed +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.yaml" 2> "$TMP/stderr.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Other input error +test "$EXIT_CODE" = "6" + +cat << EOF > "$TMP/expected.txt" +error: Missing comma in flow sequence + at line 3 + at column 0 + at file path $(realpath "$TMP")/instance.yaml +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +"$1" rdf "$TMP/schema.json" "$TMP/instance.yaml" --json > "$TMP/stdout.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Other input error +test "$EXIT_CODE" = "6" + +cat << EOF > "$TMP/expected.txt" +{ + "error": "Missing comma in flow sequence", + "line": 3, + "column": 0, + "filePath": "$(realpath "$TMP")/instance.yaml" +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.txt" diff --git a/test/rdf/pass_2019_09.sh b/test/rdf/pass_2019_09.sh new file mode 100755 index 00000000..e9adb604 --- /dev/null +++ b/test/rdf/pass_2019_09.sh @@ -0,0 +1,40 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "type": "object", + "x-jsonld-type": "https://schema.org/Person", + "properties": { + "name": { "type": "string", "x-jsonld-id": "https://schema.org/name" } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "name": "Ada" } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" > "$TMP/output.json" + +cat << 'EOF' > "$TMP/expected.json" +[ + { + "@type": [ "https://schema.org/Person" ], + "https://schema.org/name": [ + { + "@value": "Ada" + } + ] + } +] +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_compact.sh b/test/rdf/pass_compact.sh new file mode 100755 index 00000000..74f0517d --- /dev/null +++ b/test/rdf/pass_compact.sh @@ -0,0 +1,47 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "x-jsonld-type": "https://schema.org/Person", + "properties": { + "name": { "type": "string", "x-jsonld-id": "https://schema.org/name" } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "name": "Ada" } +EOF + +cat << 'EOF' > "$TMP/context.json" +{ "name": "https://schema.org/name" } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" \ + --compact "$TMP/context.json" > "$TMP/output.json" + +cat << 'EOF' > "$TMP/expected.json" +{ + "@type": "https://schema.org/Person", + "name": "Ada", + "@context": { + "name": "https://schema.org/name" + } +} +EOF + +diff "$TMP/output.json" "$TMP/expected.json" + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" \ + -c "$TMP/context.json" > "$TMP/output.json" + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_composition_allof.sh b/test/rdf/pass_composition_allof.sh new file mode 100755 index 00000000..3981be9f --- /dev/null +++ b/test/rdf/pass_composition_allof.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" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "allOf": [ + { "x-jsonld-type": "https://schema.org/Person" }, + { "x-jsonld-type": "https://schema.org/Agent" } + ], + "properties": { + "name": { "type": "string", "x-jsonld-id": "https://schema.org/name" } + } + } +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "name": "Ada" } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" > "$TMP/output.json" + +cat << 'EOF' > "$TMP/expected.json" +[ + { + "@type": [ "https://schema.org/Agent", "https://schema.org/Person" ], + "https://schema.org/name": [ + { + "@value": "Ada" + } + ] + } +] +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_composition_oneof.sh b/test/rdf/pass_composition_oneof.sh new file mode 100755 index 00000000..5d8a2f33 --- /dev/null +++ b/test/rdf/pass_composition_oneof.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" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "value": { + "oneOf": [ + { "type": "integer", "x-jsonld-id": "https://schema.org/age" }, + { "type": "string", "x-jsonld-id": "https://schema.org/name" } + ] + } + } + } +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "value": "Ada" } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" > "$TMP/output.json" + +cat << 'EOF' > "$TMP/expected.json" +[ + { + "https://schema.org/name": [ + { + "@value": "Ada" + } + ] + } +] +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_custom_metaschema.sh b/test/rdf/pass_custom_metaschema.sh new file mode 100755 index 00000000..ec04c655 --- /dev/null +++ b/test/rdf/pass_custom_metaschema.sh @@ -0,0 +1,59 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/meta.json" +{ + "$id": "https://example.com/custom-meta", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/applicator": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true, + "https://json-schema.org/draft/2020-12/vocab/meta-data": true, + "https://json-schema.org/draft/2020-12/vocab/format-annotation": true, + "https://json-schema.org/draft/2020-12/vocab/content": true, + "https://json-schema.org/draft/2020-12/vocab/unevaluated": true + }, + "$dynamicAnchor": "meta", + "$ref": "https://json-schema.org/draft/2020-12/schema" +} +EOF + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://example.com/custom-meta", + "type": "object", + "x-jsonld-type": "https://schema.org/Person", + "properties": { + "name": { "type": "string", "x-jsonld-id": "https://schema.org/name" } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "name": "Ada" } +EOF + +"$1" rdf "$TMP/schema.json" --resolve "$TMP/meta.json" "$TMP/instance.json" \ + > "$TMP/output.json" + +cat << 'EOF' > "$TMP/expected.json" +[ + { + "@type": [ "https://schema.org/Person" ], + "https://schema.org/name": [ + { + "@value": "Ada" + } + ] + } +] +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_default_dialect_cli.sh b/test/rdf/pass_default_dialect_cli.sh new file mode 100755 index 00000000..0d0da9c5 --- /dev/null +++ b/test/rdf/pass_default_dialect_cli.sh @@ -0,0 +1,41 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "type": "object", + "x-jsonld-type": "https://schema.org/Person", + "properties": { + "name": { "type": "string", "x-jsonld-id": "https://schema.org/name" } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "name": "Ada" } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" \ + --default-dialect "https://json-schema.org/draft/2020-12/schema" \ + > "$TMP/output.json" + +cat << 'EOF' > "$TMP/expected.json" +[ + { + "@type": [ "https://schema.org/Person" ], + "https://schema.org/name": [ + { + "@value": "Ada" + } + ] + } +] +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_default_dialect_config.sh b/test/rdf/pass_default_dialect_config.sh new file mode 100755 index 00000000..ce95cbf2 --- /dev/null +++ b/test/rdf/pass_default_dialect_config.sh @@ -0,0 +1,47 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "type": "object", + "x-jsonld-type": "https://schema.org/Person", + "properties": { + "name": { "type": "string", "x-jsonld-id": "https://schema.org/name" } + } +} +EOF + +cat << 'EOF' > "$TMP/jsonschema.json" +{ + "defaultDialect": "https://json-schema.org/draft/2020-12/schema" +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "name": "Ada" } +EOF + +cd "$TMP" + +"$1" rdf schema.json instance.json > "$TMP/output.json" + +cat << 'EOF' > "$TMP/expected.json" +[ + { + "@type": [ "https://schema.org/Person" ], + "https://schema.org/name": [ + { + "@value": "Ada" + } + ] + } +] +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_draft7_ref_annotations_ignored.sh b/test/rdf/pass_draft7_ref_annotations_ignored.sh new file mode 100755 index 00000000..578f5bb2 --- /dev/null +++ b/test/rdf/pass_draft7_ref_annotations_ignored.sh @@ -0,0 +1,46 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "x-jsonld-type": "https://schema.org/Person", + "properties": { + "name": { "$ref": "./schemas/name.json" } + } +} +EOF + +mkdir "$TMP/schemas" + +cat << 'EOF' > "$TMP/schemas/name.json" +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "string", + "x-jsonld-id": "https://schema.org/name" +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "name": "Ada" } +EOF + +"$1" rdf "$TMP/schema.json" --resolve "$TMP/schemas" "$TMP/instance.json" \ + > "$TMP/output.json" + +cat << 'EOF' > "$TMP/expected.json" +[ + { + "@type": [ "https://schema.org/Person" ] + } +] +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_draft7_ref_no_annotations.sh b/test/rdf/pass_draft7_ref_no_annotations.sh new file mode 100755 index 00000000..b86eaced --- /dev/null +++ b/test/rdf/pass_draft7_ref_no_annotations.sh @@ -0,0 +1,45 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "x-jsonld-type": "https://schema.org/Person", + "properties": { + "name": { "$ref": "./schemas/name.json" } + } +} +EOF + +mkdir "$TMP/schemas" + +cat << 'EOF' > "$TMP/schemas/name.json" +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "string" +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "name": "Ada" } +EOF + +"$1" rdf "$TMP/schema.json" --resolve "$TMP/schemas" "$TMP/instance.json" \ + > "$TMP/output.json" + +cat << 'EOF' > "$TMP/expected.json" +[ + { + "@type": [ "https://schema.org/Person" ] + } +] +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_empty_instance.sh b/test/rdf/pass_empty_instance.sh new file mode 100755 index 00000000..14a3e4e5 --- /dev/null +++ b/test/rdf/pass_empty_instance.sh @@ -0,0 +1,32 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "x-jsonld-type": "https://schema.org/Thing" +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{} +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" > "$TMP/output.json" + +cat << 'EOF' > "$TMP/expected.json" +[ + { + "@type": [ "https://schema.org/Thing" ] + } +] +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_expanded.sh b/test/rdf/pass_expanded.sh new file mode 100755 index 00000000..f1bef1d0 --- /dev/null +++ b/test/rdf/pass_expanded.sh @@ -0,0 +1,50 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "x-jsonld-type": "https://schema.org/Person", + "properties": { + "name": { "type": "string", "x-jsonld-id": "https://schema.org/name" }, + "email": { "type": "string", "x-jsonld-id": "https://schema.org/email" } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "name": "Ada", "email": "ada@example.com" } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" > "$TMP/output.json" + +cat << 'EOF' > "$TMP/expected.json" +[ + { + "@type": [ "https://schema.org/Person" ], + "https://schema.org/email": [ + { + "@value": "ada@example.com" + } + ], + "https://schema.org/name": [ + { + "@value": "Ada" + } + ] + } +] +EOF + +diff "$TMP/output.json" "$TMP/expected.json" + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" --json > "$TMP/output.json" + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_expanded_verbose.sh b/test/rdf/pass_expanded_verbose.sh new file mode 100755 index 00000000..fadb9ec6 --- /dev/null +++ b/test/rdf/pass_expanded_verbose.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" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "x-jsonld-type": "https://schema.org/Person", + "properties": { + "name": { "type": "string", "x-jsonld-id": "https://schema.org/name" } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "name": "Ada" } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" --verbose > "$TMP/output.txt" 2>&1 + +cat << EOF > "$TMP/expected.txt" +ok: $(realpath "$TMP")/instance.json + matches $(realpath "$TMP")/schema.json +[ + { + "@type": [ "https://schema.org/Person" ], + "https://schema.org/name": [ + { + "@value": "Ada" + } + ] + } +] +EOF + +diff "$TMP/output.txt" "$TMP/expected.txt" diff --git a/test/rdf/pass_fast.sh b/test/rdf/pass_fast.sh new file mode 100755 index 00000000..23b77984 --- /dev/null +++ b/test/rdf/pass_fast.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" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "x-jsonld-type": "https://schema.org/Person", + "properties": { + "name": { "type": "string", "x-jsonld-id": "https://schema.org/name" } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "name": "Ada" } +EOF + +cat << 'EOF' > "$TMP/expected.json" +[ + { + "@type": [ "https://schema.org/Person" ], + "https://schema.org/name": [ + { + "@value": "Ada" + } + ] + } +] +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" > "$TMP/output.json" + +diff "$TMP/output.json" "$TMP/expected.json" + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" --fast > "$TMP/output.json" + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_flatten.sh b/test/rdf/pass_flatten.sh new file mode 100755 index 00000000..c2070334 --- /dev/null +++ b/test/rdf/pass_flatten.sh @@ -0,0 +1,45 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "x-jsonld-type": "https://schema.org/Person", + "properties": { + "name": { "type": "string", "x-jsonld-id": "https://schema.org/name" } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "name": "Ada" } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" --flatten > "$TMP/output.json" + +cat << 'EOF' > "$TMP/expected.json" +[ + { + "@id": "_:b0", + "@type": [ "https://schema.org/Person" ], + "https://schema.org/name": [ + { + "@value": "Ada" + } + ] + } +] +EOF + +diff "$TMP/output.json" "$TMP/expected.json" + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" -l > "$TMP/output.json" + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_flatten_compact.sh b/test/rdf/pass_flatten_compact.sh new file mode 100755 index 00000000..e85e52ec --- /dev/null +++ b/test/rdf/pass_flatten_compact.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" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "x-jsonld-type": "https://schema.org/Person", + "properties": { + "name": { "type": "string", "x-jsonld-id": "https://schema.org/name" } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "name": "Ada" } +EOF + +cat << 'EOF' > "$TMP/context.json" +{ "name": "https://schema.org/name" } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" --flatten \ + --compact "$TMP/context.json" > "$TMP/output.json" + +cat << 'EOF' > "$TMP/expected.json" +{ + "@id": "_:b0", + "@type": "https://schema.org/Person", + "name": "Ada", + "@context": { + "name": "https://schema.org/name" + } +} +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_keyword_container_index.sh b/test/rdf/pass_keyword_container_index.sh new file mode 100755 index 00000000..79a13b52 --- /dev/null +++ b/test/rdf/pass_keyword_container_index.sh @@ -0,0 +1,45 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "data": { + "type": "object", + "x-jsonld-id": "https://schema.org/data", + "x-jsonld-container": "@index" + } + } + } +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "data": { "a": "foo", "b": "bar" } } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" > "$TMP/output.json" + +cat << 'EOF' > "$TMP/expected.json" +[ + { + "https://schema.org/data": [ + { + "@value": "foo" + }, + { + "@value": "bar" + } + ] + } +] +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_keyword_container_language.sh b/test/rdf/pass_keyword_container_language.sh new file mode 100755 index 00000000..c5024e91 --- /dev/null +++ b/test/rdf/pass_keyword_container_language.sh @@ -0,0 +1,47 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "label": { + "type": "object", + "x-jsonld-id": "https://schema.org/name", + "x-jsonld-container": "@language" + } + } + } +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "label": { "en": "Hello", "fr": "Bonjour" } } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" > "$TMP/output.json" + +cat << 'EOF' > "$TMP/expected.json" +[ + { + "https://schema.org/name": [ + { + "@value": "Hello", + "@language": "en" + }, + { + "@value": "Bonjour", + "@language": "fr" + } + ] + } +] +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_keyword_container_list.sh b/test/rdf/pass_keyword_container_list.sh new file mode 100755 index 00000000..c94f585f --- /dev/null +++ b/test/rdf/pass_keyword_container_list.sh @@ -0,0 +1,52 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "steps": { + "type": "array", + "x-jsonld-id": "https://schema.org/step", + "x-jsonld-container": "@list" + } + } + } +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "steps": [ 1, 2, 3 ] } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" > "$TMP/output.json" + +cat << 'EOF' > "$TMP/expected.json" +[ + { + "https://schema.org/step": [ + { + "@list": [ + { + "@value": 1 + }, + { + "@value": 2 + }, + { + "@value": 3 + } + ] + } + ] + } +] +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_keyword_container_set.sh b/test/rdf/pass_keyword_container_set.sh new file mode 100755 index 00000000..1a464bc0 --- /dev/null +++ b/test/rdf/pass_keyword_container_set.sh @@ -0,0 +1,45 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "tags": { + "type": "array", + "x-jsonld-id": "https://schema.org/keywords", + "x-jsonld-container": "@set" + } + } + } +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "tags": [ "a", "b" ] } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" > "$TMP/output.json" + +cat << 'EOF' > "$TMP/expected.json" +[ + { + "https://schema.org/keywords": [ + { + "@value": "a" + }, + { + "@value": "b" + } + ] + } +] +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_keyword_datatype.sh b/test/rdf/pass_keyword_datatype.sh new file mode 100755 index 00000000..25085d6a --- /dev/null +++ b/test/rdf/pass_keyword_datatype.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" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "dob": { + "type": "string", + "x-jsonld-id": "https://schema.org/birthDate", + "x-jsonld-datatype": "http://www.w3.org/2001/XMLSchema#date" + } + } + } +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "dob": "1990-05-15" } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" > "$TMP/output.json" + +cat << 'EOF' > "$TMP/expected.json" +[ + { + "https://schema.org/birthDate": [ + { + "@value": "1990-05-15", + "@type": "http://www.w3.org/2001/XMLSchema#date" + } + ] + } +] +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_keyword_direction.sh b/test/rdf/pass_keyword_direction.sh new file mode 100755 index 00000000..f71f975c --- /dev/null +++ b/test/rdf/pass_keyword_direction.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" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "text": { + "type": "string", + "x-jsonld-id": "https://schema.org/text", + "x-jsonld-direction": "rtl" + } + } + } +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "text": "abc" } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" > "$TMP/output.json" + +cat << 'EOF' > "$TMP/expected.json" +[ + { + "https://schema.org/text": [ + { + "@value": "abc", + "@direction": "rtl" + } + ] + } +] +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_keyword_graph.sh b/test/rdf/pass_keyword_graph.sh new file mode 100755 index 00000000..3ae20201 --- /dev/null +++ b/test/rdf/pass_keyword_graph.sh @@ -0,0 +1,47 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "x-jsonld-graph": true, + "properties": { + "member": { + "type": "object", + "x-jsonld-id": "https://schema.org/member", + "x-jsonld-type": "https://schema.org/Person" + } + } + } +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "member": {} } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" > "$TMP/output.json" + +cat << 'EOF' > "$TMP/expected.json" +[ + { + "@graph": [ + { + "https://schema.org/member": [ + { + "@type": [ "https://schema.org/Person" ] + } + ] + } + ] + } +] +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_keyword_id.sh b/test/rdf/pass_keyword_id.sh new file mode 100755 index 00000000..fc138c72 --- /dev/null +++ b/test/rdf/pass_keyword_id.sh @@ -0,0 +1,46 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "x-jsonld-type": "https://schema.org/Person", + "properties": { + "name": { "type": "string", "x-jsonld-id": "https://schema.org/name" }, + "email": { "type": "string", "x-jsonld-id": "https://schema.org/email" } + } + } +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "name": "Ada", "email": "ada@example.com" } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" > "$TMP/output.json" + +cat << 'EOF' > "$TMP/expected.json" +[ + { + "@type": [ "https://schema.org/Person" ], + "https://schema.org/email": [ + { + "@value": "ada@example.com" + } + ], + "https://schema.org/name": [ + { + "@value": "Ada" + } + ] + } +] +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_keyword_json.sh b/test/rdf/pass_keyword_json.sh new file mode 100755 index 00000000..f680a66f --- /dev/null +++ b/test/rdf/pass_keyword_json.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" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "blob": { + "type": "string", + "x-jsonld-id": "https://schema.org/data", + "x-jsonld-json": true + } + } + } +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "blob": "raw" } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" > "$TMP/output.json" + +cat << 'EOF' > "$TMP/expected.json" +[ + { + "https://schema.org/data": [ + { + "@value": "raw", + "@type": "@json" + } + ] + } +] +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_keyword_language.sh b/test/rdf/pass_keyword_language.sh new file mode 100755 index 00000000..bf368cc8 --- /dev/null +++ b/test/rdf/pass_keyword_language.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" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "greeting": { + "type": "string", + "x-jsonld-id": "https://schema.org/name", + "x-jsonld-language": "en" + } + } + } +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "greeting": "hello" } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" > "$TMP/output.json" + +cat << 'EOF' > "$TMP/expected.json" +[ + { + "https://schema.org/name": [ + { + "@value": "hello", + "@language": "en" + } + ] + } +] +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_keyword_reverse.sh b/test/rdf/pass_keyword_reverse.sh new file mode 100755 index 00000000..112ee0e0 --- /dev/null +++ b/test/rdf/pass_keyword_reverse.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" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "parent": { + "type": "object", + "x-jsonld-reverse": "https://schema.org/children", + "x-jsonld-type": "https://schema.org/Person" + } + } + } +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "parent": {} } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" > "$TMP/output.json" + +cat << 'EOF' > "$TMP/expected.json" +[ + { + "@reverse": { + "https://schema.org/children": [ + { + "@type": [ "https://schema.org/Person" ] + } + ] + } + } +] +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_keyword_self.sh b/test/rdf/pass_keyword_self.sh new file mode 100755 index 00000000..e95e94d4 --- /dev/null +++ b/test/rdf/pass_keyword_self.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" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "currency": { + "type": "string", + "x-jsonld-id": "https://schema.org/priceCurrency", + "x-jsonld-type": "https://schema.org/Currency", + "x-jsonld-self": "https://www.iso.org/iso-4217/{this}" + } + } + } +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "currency": "USD" } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" > "$TMP/output.json" + +cat << 'EOF' > "$TMP/expected.json" +[ + { + "https://schema.org/priceCurrency": [ + { + "@id": "https://www.iso.org/iso-4217/USD", + "@type": [ "https://schema.org/Currency" ] + } + ] + } +] +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_keyword_type.sh b/test/rdf/pass_keyword_type.sh new file mode 100755 index 00000000..93f1f78c --- /dev/null +++ b/test/rdf/pass_keyword_type.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" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "x-jsonld-type": [ + "https://schema.org/Person", + "https://schema.org/Agent" + ], + "properties": { + "name": { "type": "string", "x-jsonld-id": "https://schema.org/name" } + } + } +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "name": "Ada" } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" > "$TMP/output.json" + +cat << 'EOF' > "$TMP/expected.json" +[ + { + "@type": [ "https://schema.org/Agent", "https://schema.org/Person" ], + "https://schema.org/name": [ + { + "@value": "Ada" + } + ] + } +] +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_no_annotations.sh b/test/rdf/pass_no_annotations.sh new file mode 100755 index 00000000..2bd6d491 --- /dev/null +++ b/test/rdf/pass_no_annotations.sh @@ -0,0 +1,27 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object" +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "foo": 1 } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" > "$TMP/output.json" + +cat << 'EOF' > "$TMP/expected.json" +[] +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_relative_ref.sh b/test/rdf/pass_relative_ref.sh new file mode 100755 index 00000000..51f8d903 --- /dev/null +++ b/test/rdf/pass_relative_ref.sh @@ -0,0 +1,53 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "person": { "$ref": "./schemas/person.json" } + } +} +EOF + +mkdir -p "$TMP/schemas" + +cat << 'EOF' > "$TMP/schemas/person.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "x-jsonld-type": "https://schema.org/Person", + "properties": { + "name": { "type": "string", "x-jsonld-id": "https://schema.org/name" } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "person": { "name": "Ada" } } +EOF + +"$1" rdf "$TMP/schema.json" --resolve "$TMP/schemas" "$TMP/instance.json" \ + > "$TMP/output.json" + +cat << 'EOF' > "$TMP/expected.json" +[ + { + "@type": [ "https://schema.org/Person" ], + "https://schema.org/name": [ + { + "@value": "Ada" + } + ] + } +] +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_resolve.sh b/test/rdf/pass_resolve.sh new file mode 100755 index 00000000..d4690e8b --- /dev/null +++ b/test/rdf/pass_resolve.sh @@ -0,0 +1,57 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "person": { "$ref": "https://example.com/person" } + } +} +EOF + +cat << 'EOF' > "$TMP/remote.json" +{ + "$id": "https://example.com/person", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "x-jsonld-id": "https://schema.org/author", + "x-jsonld-type": "https://schema.org/Person", + "properties": { + "name": { "type": "string", "x-jsonld-id": "https://schema.org/name" } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "person": { "name": "Ada" } } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" \ + --resolve "$TMP/remote.json" > "$TMP/output.json" + +cat << 'EOF' > "$TMP/expected.json" +[ + { + "https://schema.org/author": [ + { + "@type": [ "https://schema.org/Person" ], + "https://schema.org/name": [ + { + "@value": "Ada" + } + ] + } + ] + } +] +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_resolve_directory.sh b/test/rdf/pass_resolve_directory.sh new file mode 100755 index 00000000..5659b7d1 --- /dev/null +++ b/test/rdf/pass_resolve_directory.sh @@ -0,0 +1,59 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "person": { "$ref": "https://example.com/person" } + } +} +EOF + +mkdir "$TMP/schemas" + +cat << 'EOF' > "$TMP/schemas/remote.json" +{ + "$id": "https://example.com/person", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "x-jsonld-id": "https://schema.org/author", + "x-jsonld-type": "https://schema.org/Person", + "properties": { + "name": { "type": "string", "x-jsonld-id": "https://schema.org/name" } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "person": { "name": "Ada" } } +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" \ + --resolve "$TMP/schemas" > "$TMP/output.json" + +cat << 'EOF' > "$TMP/expected.json" +[ + { + "https://schema.org/author": [ + { + "@type": [ "https://schema.org/Person" ], + "https://schema.org/name": [ + { + "@value": "Ada" + } + ] + } + ] + } +] +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_resolve_remap.sh b/test/rdf/pass_resolve_remap.sh new file mode 100755 index 00000000..fbf885e4 --- /dev/null +++ b/test/rdf/pass_resolve_remap.sh @@ -0,0 +1,59 @@ +#!/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": "object", + "properties": { + "person": { "$ref": "other" } + } +} +EOF + +cat << 'EOF' > "$TMP/remote.json" +{ + "$id": "https://example.com/nested", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "x-jsonld-id": "https://schema.org/author", + "x-jsonld-type": "https://schema.org/Person" +} +EOF + +cat << 'EOF' > "$TMP/jsonschema.json" +{ + "resolve": { + "https://example.com/other": "https://example.com/nested" + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "person": {} } +EOF + +cd "$TMP" + +"$1" rdf schema.json instance.json --resolve remote.json > "$TMP/output.json" + +cat << 'EOF' > "$TMP/expected.json" +[ + { + "https://schema.org/author": [ + { + "@type": [ "https://schema.org/Person" ] + } + ] + } +] +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_stdin_instance.sh b/test/rdf/pass_stdin_instance.sh new file mode 100755 index 00000000..edb9717e --- /dev/null +++ b/test/rdf/pass_stdin_instance.sh @@ -0,0 +1,36 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "x-jsonld-type": "https://schema.org/Person", + "properties": { + "name": { "type": "string", "x-jsonld-id": "https://schema.org/name" } + } +} +EOF + +cat << 'EOF' > "$TMP/expected.json" +[ + { + "@type": [ "https://schema.org/Person" ], + "https://schema.org/name": [ + { + "@value": "Ada" + } + ] + } +] +EOF + +echo '{ "name": "Ada" }' | "$1" rdf "$TMP/schema.json" - > "$TMP/output.json" + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_stdin_schema.sh b/test/rdf/pass_stdin_schema.sh new file mode 100755 index 00000000..6dd6c186 --- /dev/null +++ b/test/rdf/pass_stdin_schema.sh @@ -0,0 +1,40 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "x-jsonld-type": "https://schema.org/Person", + "properties": { + "name": { "type": "string", "x-jsonld-id": "https://schema.org/name" } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "name": "Ada" } +EOF + +cat << 'EOF' > "$TMP/expected.json" +[ + { + "@type": [ "https://schema.org/Person" ], + "https://schema.org/name": [ + { + "@value": "Ada" + } + ] + } +] +EOF + +"$1" rdf - "$TMP/instance.json" < "$TMP/schema.json" > "$TMP/output.json" + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_yaml_context.sh b/test/rdf/pass_yaml_context.sh new file mode 100755 index 00000000..bcf195aa --- /dev/null +++ b/test/rdf/pass_yaml_context.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" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "x-jsonld-type": "https://schema.org/Person", + "properties": { + "name": { "type": "string", "x-jsonld-id": "https://schema.org/name" } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "name": "Ada" } +EOF + +cat << 'EOF' > "$TMP/context.yaml" +name: https://schema.org/name +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.json" \ + --compact "$TMP/context.yaml" > "$TMP/output.json" + +cat << 'EOF' > "$TMP/expected.json" +{ + "@type": "https://schema.org/Person", + "name": "Ada", + "@context": { + "name": "https://schema.org/name" + } +} +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_yaml_instance.sh b/test/rdf/pass_yaml_instance.sh new file mode 100755 index 00000000..47e0b6a7 --- /dev/null +++ b/test/rdf/pass_yaml_instance.sh @@ -0,0 +1,40 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "x-jsonld-type": "https://schema.org/Person", + "properties": { + "name": { "type": "string", "x-jsonld-id": "https://schema.org/name" } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.yaml" +name: Ada +EOF + +cat << 'EOF' > "$TMP/expected.json" +[ + { + "@type": [ "https://schema.org/Person" ], + "https://schema.org/name": [ + { + "@value": "Ada" + } + ] + } +] +EOF + +"$1" rdf "$TMP/schema.json" "$TMP/instance.yaml" > "$TMP/output.json" + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/rdf/pass_yaml_schema.sh b/test/rdf/pass_yaml_schema.sh new file mode 100755 index 00000000..6f93db59 --- /dev/null +++ b/test/rdf/pass_yaml_schema.sh @@ -0,0 +1,39 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.yaml" +$schema: https://json-schema.org/draft/2020-12/schema +type: object +x-jsonld-type: https://schema.org/Person +properties: + name: + type: string + x-jsonld-id: https://schema.org/name +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "name": "Ada" } +EOF + +cat << 'EOF' > "$TMP/expected.json" +[ + { + "@type": [ "https://schema.org/Person" ], + "https://schema.org/name": [ + { + "@value": "Ada" + } + ] + } +] +EOF + +"$1" rdf "$TMP/schema.yaml" "$TMP/instance.json" > "$TMP/output.json" + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/validate/pass_directory_extension_verbose.sh b/test/validate/pass_directory_extension_verbose.sh index 477b6351..ad5b959d 100755 --- a/test/validate/pass_directory_extension_verbose.sh +++ b/test/validate/pass_directory_extension_verbose.sh @@ -43,23 +43,23 @@ ok: $(realpath "$TMP")/instances/instance_1.data.json annotation: "Test schema" at instance location "" (line 1, column 1) at evaluate path "/description" -annotation: "name" - at instance location "" (line 1, column 1) - at evaluate path "/properties" annotation: "Test" at instance location "" (line 1, column 1) at evaluate path "/title" +annotation: "name" + at instance location "" (line 1, column 1) + at evaluate path "/properties" ok: $(realpath "$TMP")/instances/instance_2.data.json matches $(realpath "$TMP")/schema.json annotation: "Test schema" at instance location "" (line 1, column 1) at evaluate path "/description" -annotation: "name" - at instance location "" (line 1, column 1) - at evaluate path "/properties" annotation: "Test" at instance location "" (line 1, column 1) at evaluate path "/title" +annotation: "name" + at instance location "" (line 1, column 1) + at evaluate path "/properties" EOF diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/validate/pass_directory_ignore_verbose.sh b/test/validate/pass_directory_ignore_verbose.sh index 7e21a76b..e30bd0a1 100755 --- a/test/validate/pass_directory_ignore_verbose.sh +++ b/test/validate/pass_directory_ignore_verbose.sh @@ -39,12 +39,12 @@ ok: $(realpath "$TMP")/instances/instance_1.json annotation: "Test schema" at instance location "" (line 1, column 1) at evaluate path "/description" -annotation: "name" - at instance location "" (line 1, column 1) - at evaluate path "/properties" annotation: "Test" at instance location "" (line 1, column 1) at evaluate path "/title" +annotation: "name" + at instance location "" (line 1, column 1) + at evaluate path "/properties" EOF diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/validate/pass_directory_verbose.sh b/test/validate/pass_directory_verbose.sh index b2062fc6..9921d9f6 100755 --- a/test/validate/pass_directory_verbose.sh +++ b/test/validate/pass_directory_verbose.sh @@ -41,29 +41,29 @@ ok: $(realpath "$TMP")/instances/instance_1.json annotation: "Test schema" at instance location "" (line 1, column 1) at evaluate path "/description" +annotation: "Test" + at instance location "" (line 1, column 1) + at evaluate path "/title" annotation: "age" at instance location "" (line 1, column 1) at evaluate path "/properties" annotation: "name" at instance location "" (line 1, column 1) at evaluate path "/properties" -annotation: "Test" - at instance location "" (line 1, column 1) - at evaluate path "/title" ok: $(realpath "$TMP")/instances/instance_2.json matches $(realpath "$TMP")/schema.json annotation: "Test schema" at instance location "" (line 1, column 1) at evaluate path "/description" +annotation: "Test" + at instance location "" (line 1, column 1) + at evaluate path "/title" annotation: "age" at instance location "" (line 1, column 1) at evaluate path "/properties" annotation: "name" at instance location "" (line 1, column 1) at evaluate path "/properties" -annotation: "Test" - at instance location "" (line 1, column 1) - at evaluate path "/title" EOF diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/validate/pass_jsonl_gz_verbose.sh b/test/validate/pass_jsonl_gz_verbose.sh index b0a21387..fd3dca0f 100755 --- a/test/validate/pass_jsonl_gz_verbose.sh +++ b/test/validate/pass_jsonl_gz_verbose.sh @@ -35,34 +35,34 @@ ok: $(realpath "$TMP")/instance.jsonl.gz (entry #1) annotation: "Test schema" at instance location "" at evaluate path "/description" -annotation: "foo" - at instance location "" - at evaluate path "/properties" annotation: "Test" at instance location "" at evaluate path "/title" +annotation: "foo" + at instance location "" + at evaluate path "/properties" ok: $(realpath "$TMP")/instance.jsonl.gz (entry #2) matches $(realpath "$TMP")/schema.json annotation: "Test schema" at instance location "" at evaluate path "/description" -annotation: "foo" - at instance location "" - at evaluate path "/properties" annotation: "Test" at instance location "" at evaluate path "/title" +annotation: "foo" + at instance location "" + at evaluate path "/properties" ok: $(realpath "$TMP")/instance.jsonl.gz (entry #3) matches $(realpath "$TMP")/schema.json annotation: "Test schema" at instance location "" at evaluate path "/description" -annotation: "foo" - at instance location "" - at evaluate path "/properties" annotation: "Test" at instance location "" at evaluate path "/title" +annotation: "foo" + at instance location "" + at evaluate path "/properties" EOF diff "$TMP/output.txt" "$TMP/expected.txt" diff --git a/test/validate/pass_jsonl_verbose.sh b/test/validate/pass_jsonl_verbose.sh index 63145124..ee823921 100755 --- a/test/validate/pass_jsonl_verbose.sh +++ b/test/validate/pass_jsonl_verbose.sh @@ -35,34 +35,34 @@ ok: $(realpath "$TMP")/instance.jsonl (entry #1) annotation: "Test schema" at instance location "" at evaluate path "/description" -annotation: "foo" - at instance location "" - at evaluate path "/properties" annotation: "Test" at instance location "" at evaluate path "/title" +annotation: "foo" + at instance location "" + at evaluate path "/properties" ok: $(realpath "$TMP")/instance.jsonl (entry #2) matches $(realpath "$TMP")/schema.json annotation: "Test schema" at instance location "" at evaluate path "/description" -annotation: "foo" - at instance location "" - at evaluate path "/properties" annotation: "Test" at instance location "" at evaluate path "/title" +annotation: "foo" + at instance location "" + at evaluate path "/properties" ok: $(realpath "$TMP")/instance.jsonl (entry #3) matches $(realpath "$TMP")/schema.json annotation: "Test schema" at instance location "" at evaluate path "/description" -annotation: "foo" - at instance location "" - at evaluate path "/properties" annotation: "Test" at instance location "" at evaluate path "/title" +annotation: "foo" + at instance location "" + at evaluate path "/properties" EOF diff "$TMP/output.txt" "$TMP/expected.txt" diff --git a/test/validate/pass_many_verbose.sh b/test/validate/pass_many_verbose.sh index 2f0ad3f9..765a338f 100755 --- a/test/validate/pass_many_verbose.sh +++ b/test/validate/pass_many_verbose.sh @@ -44,34 +44,34 @@ ok: $(realpath "$TMP")/instance_1.json annotation: "Test schema" at instance location "" (line 1, column 1) at evaluate path "/description" -annotation: "foo" - at instance location "" (line 1, column 1) - at evaluate path "/properties" annotation: "Test" at instance location "" (line 1, column 1) at evaluate path "/title" +annotation: "foo" + at instance location "" (line 1, column 1) + at evaluate path "/properties" ok: $(realpath "$TMP")/instance_2.json matches $(realpath "$TMP")/schema.json annotation: "Test schema" at instance location "" (line 1, column 1) at evaluate path "/description" -annotation: "foo" - at instance location "" (line 1, column 1) - at evaluate path "/properties" annotation: "Test" at instance location "" (line 1, column 1) at evaluate path "/title" +annotation: "foo" + at instance location "" (line 1, column 1) + at evaluate path "/properties" ok: $(realpath "$TMP")/instance_3.json matches $(realpath "$TMP")/schema.json annotation: "Test schema" at instance location "" (line 1, column 1) at evaluate path "/description" -annotation: "foo" - at instance location "" (line 1, column 1) - at evaluate path "/properties" annotation: "Test" at instance location "" (line 1, column 1) at evaluate path "/title" +annotation: "foo" + at instance location "" (line 1, column 1) + at evaluate path "/properties" EOF diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/validate/pass_stdin_instance_verbose.sh b/test/validate/pass_stdin_instance_verbose.sh index 5ce18ea9..5fa31abf 100755 --- a/test/validate/pass_stdin_instance_verbose.sh +++ b/test/validate/pass_stdin_instance_verbose.sh @@ -28,12 +28,12 @@ ok: /dev/stdin annotation: "Test schema" at instance location "" (line 1, column 1) at evaluate path "/description" -annotation: "foo" - at instance location "" (line 1, column 1) - at evaluate path "/properties" annotation: "Test" at instance location "" (line 1, column 1) at evaluate path "/title" +annotation: "foo" + at instance location "" (line 1, column 1) + at evaluate path "/properties" EOF diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/validate/pass_verbose.sh b/test/validate/pass_verbose.sh index 6eef440d..7552ab81 100755 --- a/test/validate/pass_verbose.sh +++ b/test/validate/pass_verbose.sh @@ -32,12 +32,12 @@ ok: $(realpath "$TMP")/instance.json annotation: "Test schema" at instance location "" (line 1, column 1) at evaluate path "/description" -annotation: "foo" - at instance location "" (line 1, column 1) - at evaluate path "/properties" annotation: "Test" at instance location "" (line 1, column 1) at evaluate path "/title" +annotation: "foo" + at instance location "" (line 1, column 1) + at evaluate path "/properties" EOF diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/validate/pass_verbose_with_template.sh b/test/validate/pass_verbose_with_template.sh index 3d2f8e51..2c19fe17 100755 --- a/test/validate/pass_verbose_with_template.sh +++ b/test/validate/pass_verbose_with_template.sh @@ -35,12 +35,12 @@ ok: $(realpath "$TMP")/instance.json annotation: "Test schema" at instance location "" (line 1, column 1) at evaluate path "/description" -annotation: "foo" - at instance location "" (line 1, column 1) - at evaluate path "/properties" annotation: "Test" at instance location "" (line 1, column 1) at evaluate path "/title" +annotation: "foo" + at instance location "" (line 1, column 1) + at evaluate path "/properties" EOF diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/validate/pass_with_invalid_template_verbose.sh b/test/validate/pass_with_invalid_template_verbose.sh index a9c8b6f9..eeb32e1b 100755 --- a/test/validate/pass_with_invalid_template_verbose.sh +++ b/test/validate/pass_with_invalid_template_verbose.sh @@ -39,12 +39,12 @@ ok: $(realpath "$TMP")/instance.json annotation: "Test schema" at instance location "" (line 1, column 1) at evaluate path "/description" -annotation: "foo" - at instance location "" (line 1, column 1) - at evaluate path "/properties" annotation: "Test" at instance location "" (line 1, column 1) at evaluate path "/title" +annotation: "foo" + at instance location "" (line 1, column 1) + at evaluate path "/properties" EOF diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/validate/pass_yaml_multi_verbose.sh b/test/validate/pass_yaml_multi_verbose.sh index 56d2c7ec..187925e6 100755 --- a/test/validate/pass_yaml_multi_verbose.sh +++ b/test/validate/pass_yaml_multi_verbose.sh @@ -38,34 +38,34 @@ ok: $(realpath "$TMP")/instance.yaml (entry #1) annotation: "Test schema" at instance location "" (line 2, column 1) at evaluate path "/description" -annotation: "foo" - at instance location "" (line 2, column 1) - at evaluate path "/properties" annotation: "Test" at instance location "" (line 2, column 1) at evaluate path "/title" +annotation: "foo" + at instance location "" (line 2, column 1) + at evaluate path "/properties" ok: $(realpath "$TMP")/instance.yaml (entry #2) matches $(realpath "$TMP")/schema.json annotation: "Test schema" at instance location "" (line 4, column 1) at evaluate path "/description" -annotation: "foo" - at instance location "" (line 4, column 1) - at evaluate path "/properties" annotation: "Test" at instance location "" (line 4, column 1) at evaluate path "/title" +annotation: "foo" + at instance location "" (line 4, column 1) + at evaluate path "/properties" ok: $(realpath "$TMP")/instance.yaml (entry #3) matches $(realpath "$TMP")/schema.json annotation: "Test schema" at instance location "" (line 6, column 1) at evaluate path "/description" -annotation: "foo" - at instance location "" (line 6, column 1) - at evaluate path "/properties" annotation: "Test" at instance location "" (line 6, column 1) at evaluate path "/title" +annotation: "foo" + at instance location "" (line 6, column 1) + at evaluate path "/properties" EOF diff "$TMP/output.txt" "$TMP/expected.txt" diff --git a/vendor/blaze/DEPENDENCIES b/vendor/blaze/DEPENDENCIES index ca8a0348..ae0e10b4 100644 --- a/vendor/blaze/DEPENDENCIES +++ b/vendor/blaze/DEPENDENCIES @@ -1,5 +1,5 @@ vendorpull https://github.com/sourcemeta/vendorpull 1dcbac42809cf87cb5b045106b863e17ad84ba02 -core https://github.com/sourcemeta/core f8a406692eb8080c84cd016ec4968e391461b1c1 +core https://github.com/sourcemeta/core 367f6ef45c8e8f05269d65d98dab155505f39ccb jsonschema-test-suite https://github.com/json-schema-org/JSON-Schema-Test-Suite 1acd90e53554fa24d2529b49fd7d50bab18f8b7e jsonschema-2020-12 https://github.com/json-schema-org/json-schema-spec 769daad75a9553562333a8937a187741cb708c72 jsonschema-2019-09 https://github.com/json-schema-org/json-schema-spec 41014ea723120ce70b314d72f863c6929d9f3cfd diff --git a/vendor/blaze/src/output/include/sourcemeta/blaze/output_simple.h b/vendor/blaze/src/output/include/sourcemeta/blaze/output_simple.h index f50e3c53..071cef4a 100644 --- a/vendor/blaze/src/output/include/sourcemeta/blaze/output_simple.h +++ b/vendor/blaze/src/output/include/sourcemeta/blaze/output_simple.h @@ -16,7 +16,6 @@ #include // std::map #include // std::ostream #include // std::string -#include // std::tie #include // std::pair #include // std::vector @@ -83,6 +82,14 @@ class SOURCEMETA_BLAZE_OUTPUT_EXPORT SimpleOutput { std::reference_wrapper schema_location; }; + /// A single collected annotation, in evaluation order + struct AnnotationEntry { + sourcemeta::core::WeakPointer instance_location; + sourcemeta::core::WeakPointer evaluate_path; + std::reference_wrapper schema_location; + sourcemeta::core::JSON value; + }; + auto operator()(const EvaluationType type, const bool result, const Instruction &step, const InstructionExtra &step_metadata, @@ -97,12 +104,6 @@ class SOURCEMETA_BLAZE_OUTPUT_EXPORT SimpleOutput { [[nodiscard]] auto cbegin() const -> const_iterator; [[nodiscard]] auto cend() const -> const_iterator; - /// Access annotations that were collected during evaluation, indexed by - /// instance location and evaluation path - [[nodiscard]] auto annotations() const -> const auto & { - return this->annotations_; - } - /// Move out the collected error entries, leaving this output empty. Useful to /// take ownership of the trace without copying when the output is no longer /// needed @@ -114,22 +115,14 @@ class SOURCEMETA_BLAZE_OUTPUT_EXPORT SimpleOutput { return result; } - // NOLINTNEXTLINE(bugprone-exception-escape) - struct Location { - auto operator<(const Location &other) const noexcept -> bool { - // Perform a lexicographical comparison - return std::tie(this->instance_location, this->evaluate_path, - this->schema_location.get()) < - std::tie(other.instance_location, other.evaluate_path, - other.schema_location.get()); - } - - // NOLINTBEGIN(cppcoreguidelines-avoid-const-or-ref-data-members) - const sourcemeta::core::WeakPointer instance_location; - const sourcemeta::core::WeakPointer evaluate_path; - const std::reference_wrapper schema_location; - // NOLINTEND(cppcoreguidelines-avoid-const-or-ref-data-members) - }; + /// Access the annotations collected during evaluation, as a flat log in + /// evaluation order. The log records every emission as-is, so a location may + /// repeat and hold the same value more than once. Consumers that need them + /// grouped by location, or collapsed to distinct values, index this log + /// themselves + [[nodiscard]] auto annotations() const -> const auto & { + return this->annotations_; + } private: // Exporting symbols that depends on the standard C++ library is considered @@ -148,7 +141,7 @@ class SOURCEMETA_BLAZE_OUTPUT_EXPORT SimpleOutput { std::pair, std::vector> masked_traces; - std::map> annotations_; + std::vector annotations_; #if defined(_MSC_VER) #pragma warning(default : 4251) #endif diff --git a/vendor/blaze/src/output/output_jsonld.cc b/vendor/blaze/src/output/output_jsonld.cc index 320a74b2..33f20f00 100644 --- a/vendor/blaze/src/output/output_jsonld.cc +++ b/vendor/blaze/src/output/output_jsonld.cc @@ -9,7 +9,7 @@ #include // std::ranges::sort, std::ranges::any_of #include // std::deque -#include // std::ref +#include // std::ref, std::reference_wrapper #include // std::optional #include // std::ostringstream #include // std::string @@ -38,56 +38,6 @@ struct Facts { using Accumulator = std::unordered_map; -auto fill_collection(sourcemeta::core::JSONLDWeakAnnotationMap &map, - const Accumulator &accumulator, - const sourcemeta::core::WeakPointer &pointer, - const sourcemeta::core::JSON &value) -> void; - -// Give an undescribed collection member a default kind so it still -// materializes, a scalar as a literal and a nested array as an inner collection -auto fill_element(sourcemeta::core::JSONLDWeakAnnotationMap &map, - const Accumulator &accumulator, - sourcemeta::core::WeakPointer element_pointer, - const sourcemeta::core::JSON &element) -> void { - if (accumulator.contains(element_pointer)) { - return; - } - - if (element.is_array()) { - map.emplace( - element_pointer, - sourcemeta::core::JSONLDDescriptor{ - .edges = {}, .value = sourcemeta::core::JSONLDCollection{}}); - fill_collection(map, accumulator, element_pointer, element); - } else if (!element.is_object()) { - map.emplace(std::move(element_pointer), - sourcemeta::core::JSONLDDescriptor{ - .edges = {}, .value = sourcemeta::core::JSONLDLiteral{}}); - } -} - -// Default the undescribed members of a collection, whether it ranges over an -// array or over an object -auto fill_collection(sourcemeta::core::JSONLDWeakAnnotationMap &map, - const Accumulator &accumulator, - const sourcemeta::core::WeakPointer &pointer, - const sourcemeta::core::JSON &value) -> void { - if (value.is_array()) { - for (std::size_t index = 0; index < value.size(); index += 1) { - auto element_pointer{pointer}; - element_pointer.push_back(index); - fill_element(map, accumulator, std::move(element_pointer), - value.at(index)); - } - } else { - for (const auto &entry : value.as_object()) { - auto element_pointer{pointer}; - element_pointer.push_back(std::cref(entry.first)); - fill_element(map, accumulator, std::move(element_pointer), entry.second); - } - } -} - auto add_edge(std::vector &edges, const sourcemeta::core::JSON::String &predicate, const bool reverse) -> void { @@ -340,7 +290,8 @@ auto expand_self(const sourcemeta::core::WeakPointer &pointer, } if (bound->is_string()) { - if (bound->to_string().empty()) { + const auto &text{bound->to_string()}; + if (text.empty()) { if (!failure.has_value()) { failure = facet_error( pointer, sourcemeta::blaze::JSONLDFacet::Self, @@ -351,8 +302,7 @@ auto expand_self(const sourcemeta::core::WeakPointer &pointer, return std::nullopt; } - return std::make_tuple(std::string_view{bound->to_string()}, - std::nullopt, false); + return std::make_tuple(std::string_view{text}, std::nullopt, false); } // A number or boolean has no direct string, so it binds through its @@ -400,180 +350,212 @@ auto array_of_nodes(const Accumulator &accumulator, return true; } -// Turn the JSON-LD annotations into a resolved map, or the first error found. +// The x-jsonld-* keyword names, pre-hashed for the first-pass dispatch +using namespace std::string_view_literals; +const auto HASH_ID{sourcemeta::core::JSON::Object::hash("x-jsonld-id"sv)}; +const auto HASH_REVERSE{ + sourcemeta::core::JSON::Object::hash("x-jsonld-reverse"sv)}; +const auto HASH_TYPE{sourcemeta::core::JSON::Object::hash("x-jsonld-type"sv)}; +const auto HASH_DATATYPE{ + sourcemeta::core::JSON::Object::hash("x-jsonld-datatype"sv)}; +const auto HASH_LANGUAGE{ + sourcemeta::core::JSON::Object::hash("x-jsonld-language"sv)}; +const auto HASH_DIRECTION{ + sourcemeta::core::JSON::Object::hash("x-jsonld-direction"sv)}; +const auto HASH_JSON{sourcemeta::core::JSON::Object::hash("x-jsonld-json"sv)}; +const auto HASH_GRAPH{sourcemeta::core::JSON::Object::hash("x-jsonld-graph"sv)}; +const auto HASH_CONTAINER{ + sourcemeta::core::JSON::Object::hash("x-jsonld-container"sv)}; +const auto HASH_SELF{sourcemeta::core::JSON::Object::hash("x-jsonld-self"sv)}; + +// Turn the JSON-LD annotations into a resolved list, or the first error found. // The first pass groups the facts by location, the second derives each kind // from the value shape and validates the facts against it auto resolve(const sourcemeta::core::JSON &instance, const sourcemeta::blaze::SimpleOutput &output) - -> std::variant std::variant { Accumulator accumulator; + std::vector> + language_containers; - for (const auto &[location, values] : output.annotations()) { - if (location.evaluate_path.empty()) { + for (const auto &entry : output.annotations()) { + if (entry.evaluate_path.empty()) { continue; } - const auto &keyword{location.evaluate_path.back().to_property()}; - const auto &instance_location{location.instance_location}; + const auto &keyword{entry.evaluate_path.back()}; + const auto &instance_location{entry.instance_location}; + const auto &value{entry.value}; - if (keyword == "x-jsonld-id" || keyword == "x-jsonld-reverse") { - const bool reverse{keyword == "x-jsonld-reverse"}; - for (const auto &value : values) { - if (!is_iri_value(value)) { - return facet_error( - instance_location, sourcemeta::blaze::JSONLDFacet::Predicate, - reverse ? "The value of x-jsonld-reverse must be an absolute IRI" - : "The value of x-jsonld-id must be an absolute IRI"); - } - - add_edge(accumulator[instance_location].edges, value.to_string(), - reverse); + if (keyword.property_equals("x-jsonld-id", HASH_ID) || + keyword.property_equals("x-jsonld-reverse", HASH_REVERSE)) { + const bool reverse{ + keyword.property_equals("x-jsonld-reverse", HASH_REVERSE)}; + if (!is_iri_value(value)) { + return facet_error( + instance_location, sourcemeta::blaze::JSONLDFacet::Predicate, + reverse ? "The value of x-jsonld-reverse must be an absolute IRI" + : "The value of x-jsonld-id must be an absolute IRI"); } - } else if (keyword == "x-jsonld-type") { - auto &types{accumulator[instance_location].types}; - for (const auto &value : values) { - if (value.is_array()) { - for (const auto &element : value.as_array()) { - if (!is_iri_value(element)) { - return type_iri_error(instance_location); - } - add_type(types, element.to_string()); - } - } else { - if (!is_iri_value(value)) { + add_edge(accumulator[instance_location].edges, value.to_string(), + reverse); + } else if (keyword.property_equals("x-jsonld-type", HASH_TYPE)) { + auto &types{accumulator[instance_location].types}; + if (value.is_array()) { + for (const auto &element : value.as_array()) { + if (!is_iri_value(element)) { return type_iri_error(instance_location); } - add_type(types, value.to_string()); + add_type(types, element.to_string()); } - } - } else if (keyword == "x-jsonld-datatype") { - for (const auto &value : values) { + } else { if (!is_iri_value(value)) { - return facet_error( - instance_location, sourcemeta::blaze::JSONLDFacet::Datatype, - "The value of x-jsonld-datatype must be an absolute IRI"); + return type_iri_error(instance_location); } - auto &datatype{accumulator[instance_location].datatype}; - if (datatype.has_value() && datatype.value() != value.to_string()) { - return facet_error( - instance_location, sourcemeta::blaze::JSONLDFacet::Datatype, - "A JSON-LD datatype cannot be assigned more than one value"); - } + add_type(types, value.to_string()); + } + } else if (keyword.property_equals("x-jsonld-datatype", HASH_DATATYPE)) { + auto &datatype{accumulator[instance_location].datatype}; + if (!is_iri_value(value)) { + return facet_error( + instance_location, sourcemeta::blaze::JSONLDFacet::Datatype, + "The value of x-jsonld-datatype must be an absolute IRI"); + } - datatype = value.to_string(); + const auto &text{value.to_string()}; + if (datatype.has_value() && datatype.value() != text) { + return facet_error( + instance_location, sourcemeta::blaze::JSONLDFacet::Datatype, + "A JSON-LD datatype cannot be assigned more than one value"); } - } else if (keyword == "x-jsonld-language") { - for (const auto &value : values) { - if (!value.is_string() || - !sourcemeta::core::is_langtag(value.to_string())) { + + datatype = text; + } else if (keyword.property_equals("x-jsonld-language", HASH_LANGUAGE)) { + auto &language{accumulator[instance_location].language}; + if (!value.is_string() || + !sourcemeta::core::is_langtag(value.to_string())) { + return facet_error( + instance_location, sourcemeta::blaze::JSONLDFacet::Language, + "The value of x-jsonld-language must be a BCP 47 language tag"); + } + + const auto &text{value.to_string()}; + if (language.has_value()) { + // Language tags compare case-insensitively, so the first spelling is + // kept and only a genuinely different tag is a conflict + if (!sourcemeta::core::equals_ignore_case(language.value(), text)) { return facet_error( instance_location, sourcemeta::blaze::JSONLDFacet::Language, - "The value of x-jsonld-language must be a BCP 47 language tag"); + "A JSON-LD language cannot be assigned more than one value"); } + } else { + language = text; + } + } else if (keyword.property_equals("x-jsonld-direction", HASH_DIRECTION)) { + auto &direction{accumulator[instance_location].direction}; + const auto parsed{parse_direction(value)}; + if (!parsed.has_value()) { + return facet_error( + instance_location, sourcemeta::blaze::JSONLDFacet::Direction, + R"(The value of x-jsonld-direction must be "ltr" or "rtl")"); + } - auto &language{accumulator[instance_location].language}; - if (language.has_value()) { - // Language tags compare case-insensitively, so the first spelling is - // kept and only a genuinely different tag is a conflict - if (!sourcemeta::core::equals_ignore_case(language.value(), - value.to_string())) { - return facet_error( - instance_location, sourcemeta::blaze::JSONLDFacet::Language, - "A JSON-LD language cannot be assigned more than one value"); - } - } else { - language = value.to_string(); - } + if (direction.has_value() && direction.value() != parsed.value()) { + return facet_error( + instance_location, sourcemeta::blaze::JSONLDFacet::Direction, + "A JSON-LD direction cannot be assigned more than one value"); } - } else if (keyword == "x-jsonld-direction") { - for (const auto &value : values) { - const auto parsed{parse_direction(value)}; - if (!parsed.has_value()) { - return facet_error( - instance_location, sourcemeta::blaze::JSONLDFacet::Direction, - R"(The value of x-jsonld-direction must be "ltr" or "rtl")"); - } - auto &direction{accumulator[instance_location].direction}; - if (direction.has_value() && direction.value() != parsed.value()) { - return facet_error( - instance_location, sourcemeta::blaze::JSONLDFacet::Direction, - "A JSON-LD direction cannot be assigned more than one value"); - } + direction = parsed; + } else if (keyword.property_equals("x-jsonld-json", HASH_JSON)) { + if (!value.is_boolean()) { + return facet_error(instance_location, + sourcemeta::blaze::JSONLDFacet::JSON, + "The value of x-jsonld-json must be a boolean"); + } - direction = parsed; + // A false value leaves the fact unset, so it must stay a no-op and not + // create an entry + if (value.to_boolean()) { + accumulator[instance_location].json = true; + } + } else if (keyword.property_equals("x-jsonld-graph", HASH_GRAPH)) { + if (!value.is_boolean()) { + return facet_error(instance_location, + sourcemeta::blaze::JSONLDFacet::Graph, + "The value of x-jsonld-graph must be a boolean"); } - } else if (keyword == "x-jsonld-json") { - for (const auto &value : values) { - if (!value.is_boolean()) { - return facet_error(instance_location, - sourcemeta::blaze::JSONLDFacet::JSON, - "The value of x-jsonld-json must be a boolean"); - } - // A false value leaves the fact unset, so it must stay a no-op and not - // create an entry - if (value.to_boolean()) { - accumulator[instance_location].json = true; - } + if (value.to_boolean()) { + accumulator[instance_location].graph = true; + } + } else if (keyword.property_equals("x-jsonld-container", HASH_CONTAINER)) { + auto &container{accumulator[instance_location].container}; + const auto parsed{parse_container(value)}; + if (!parsed.has_value()) { + return facet_error( + instance_location, sourcemeta::blaze::JSONLDFacet::Container, + R"(The value of x-jsonld-container must be "@list", "@set", "@language", or "@index")"); } - } else if (keyword == "x-jsonld-graph") { - for (const auto &value : values) { - if (!value.is_boolean()) { - return facet_error(instance_location, - sourcemeta::blaze::JSONLDFacet::Graph, - "The value of x-jsonld-graph must be a boolean"); - } - if (value.to_boolean()) { - accumulator[instance_location].graph = true; - } + if (container.has_value() && container.value() != parsed.value()) { + return facet_error( + instance_location, sourcemeta::blaze::JSONLDFacet::Container, + "A JSON-LD container cannot be assigned more than one value"); } - } else if (keyword == "x-jsonld-container") { - for (const auto &value : values) { - const auto parsed{parse_container(value)}; - if (!parsed.has_value()) { - return facet_error( - instance_location, sourcemeta::blaze::JSONLDFacet::Container, - R"(The value of x-jsonld-container must be "@list", "@set", "@language", or "@index")"); - } - auto &container{accumulator[instance_location].container}; - if (container.has_value() && container.value() != parsed.value()) { - return facet_error( - instance_location, sourcemeta::blaze::JSONLDFacet::Container, - "A JSON-LD container cannot be assigned more than one value"); - } + container = parsed; - container = parsed; + if (container == sourcemeta::core::JSONLDContainer::Language) { + language_containers.push_back(std::cref(instance_location)); + } + } else if (keyword.property_equals("x-jsonld-self", HASH_SELF)) { + auto &self{accumulator[instance_location].self}; + if (!value.is_string() || + !sourcemeta::core::URITemplate::is_uritemplate(value.to_string())) { + return facet_error(instance_location, + sourcemeta::blaze::JSONLDFacet::Self, + "The value of x-jsonld-self must be a URI Template"); } - } else if (keyword == "x-jsonld-self") { - for (const auto &value : values) { - if (!value.is_string() || - !sourcemeta::core::URITemplate::is_uritemplate(value.to_string())) { - return facet_error( - instance_location, sourcemeta::blaze::JSONLDFacet::Self, - "The value of x-jsonld-self must be a URI Template"); - } - auto &self{accumulator[instance_location].self}; - if (self.has_value() && self.value() != value.to_string()) { - return facet_error( - instance_location, sourcemeta::blaze::JSONLDFacet::Self, - "A JSON-LD self identity cannot be assigned more than one value"); - } + const auto &text{value.to_string()}; + if (self.has_value() && self.value() != text) { + return facet_error( + instance_location, sourcemeta::blaze::JSONLDFacet::Self, + "A JSON-LD self identity cannot be assigned more than one value"); + } + + self = text; + } + } + + // A language container materializes its members directly as language-tagged + // strings, never consulting their descriptors, so a member cannot carry a + // JSON-LD annotation of its own + for (const auto container_location : language_containers) { + const auto &members{ + sourcemeta::core::get(instance, container_location.get())}; + if (!members.is_object()) { + continue; + } - self = value.to_string(); + for (const auto &entry : members.as_object()) { + auto member{container_location.get()}; + member.push_back(std::cref(entry.first)); + if (accumulator.contains(member)) { + return facet_error(member, sourcemeta::blaze::JSONLDFacet::Container, + "A JSON-LD language container member cannot carry a " + "JSON-LD annotation"); } } } - sourcemeta::core::JSONLDWeakAnnotationMap map; - map.reserve(accumulator.size()); + sourcemeta::core::JSONLDWeakAnnotationList annotations; + annotations.reserve(accumulator.size()); for (auto &[pointer, facts] : accumulator) { std::ranges::sort(facts.edges, [](const sourcemeta::core::JSONLDEdge &left, @@ -585,22 +567,6 @@ auto resolve(const sourcemeta::core::JSON &instance, const auto &value{sourcemeta::core::get(instance, pointer)}; - // A language container materializes its members directly as language-tagged - // strings, never consulting their descriptors, so a member cannot carry a - // JSON-LD annotation of its own - if (!pointer.empty()) { - auto parent{pointer}; - parent.pop_back(); - const auto parent_facts{accumulator.find(parent)}; - if (parent_facts != accumulator.cend() && - parent_facts->second.container == - sourcemeta::core::JSONLDContainer::Language) { - return facet_error(pointer, sourcemeta::blaze::JSONLDFacet::Container, - "A JSON-LD language container member cannot carry a " - "JSON-LD annotation"); - } - } - // A container sets the collection shape and stands alone, excluding every // other fact and choosing the value shape it ranges over if (facts.container.has_value()) { @@ -738,21 +704,11 @@ auto resolve(const sourcemeta::core::JSON &instance, .direction = facts.direction}; } - map.emplace(pointer, std::move(descriptor)); - - // A language container reads its object members directly, but every other - // collection needs its undescribed members defaulted so they materialize - if (facts.container.has_value()) { - if (facts.container.value() != - sourcemeta::core::JSONLDContainer::Language) { - fill_collection(map, accumulator, pointer, value); - } - } else if (value.is_array() && !facts.json) { - fill_collection(map, accumulator, pointer, value); - } + annotations.push_back( + {.pointer = pointer, .descriptor = std::move(descriptor)}); } - return map; + return annotations; } } // namespace @@ -772,9 +728,9 @@ auto jsonld(Evaluator &evaluator, const Template &schema, return std::get(std::move(resolved)); } - const auto &map{ - std::get(resolved)}; - return sourcemeta::core::jsonld_materialize(instance, map); + const auto &annotations{ + std::get(resolved)}; + return sourcemeta::core::jsonld_materialize(instance, annotations); } } // namespace sourcemeta::blaze diff --git a/vendor/blaze/src/output/output_simple.cc b/vendor/blaze/src/output/output_simple.cc index bc0f0b93..4bf975fc 100644 --- a/vendor/blaze/src/output/output_simple.cc +++ b/vendor/blaze/src/output/output_simple.cc @@ -66,18 +66,11 @@ auto SimpleOutput::operator()( if (is_annotation(step.type)) { if (type == EvaluationType::Post) { - Location location{.instance_location = instance_location, - .evaluate_path = std::move(effective_evaluate_path), - .schema_location = step_metadata.keyword_location}; - const auto match{this->annotations_.find(location)}; - if (match == this->annotations_.cend()) { - this->annotations_[std::move(location)].push_back(annotation); - - // To avoid emitting the exact same annotation more than once - // This is right now mostly because of `unevaluatedItems` - } else if (match->second.back() != annotation) { - match->second.push_back(annotation); - } + this->annotations_.push_back( + {.instance_location = instance_location, + .evaluate_path = std::move(effective_evaluate_path), + .schema_location = step_metadata.keyword_location, + .value = annotation}); } return; @@ -122,15 +115,11 @@ auto SimpleOutput::operator()( } if (type == EvaluationType::Post && !this->annotations_.empty()) { - for (auto iterator = this->annotations_.begin(); - iterator != this->annotations_.end();) { - if (iterator->first.evaluate_path.starts_with_initial(evaluate_path) && - iterator->first.instance_location == instance_location) { - iterator = this->annotations_.erase(iterator); - } else { - ++iterator; - } - } + std::erase_if( + this->annotations_, [&](const AnnotationEntry &entry) -> bool { + return entry.evaluate_path.starts_with_initial(evaluate_path) && + entry.instance_location == instance_location; + }); } if (keyword == "if") { diff --git a/vendor/blaze/src/output/output_standard.cc b/vendor/blaze/src/output/output_standard.cc index c932c351..a27751ef 100644 --- a/vendor/blaze/src/output/output_standard.cc +++ b/vendor/blaze/src/output/output_standard.cc @@ -1,15 +1,49 @@ #include #include +#include #include #include // assert -#include // std::ref +#include // std::ref, std::reference_wrapper +#include // std::map +#include // std::string +#include // std::tie +#include // std::vector namespace sourcemeta::blaze { namespace { +// NOLINTNEXTLINE(bugprone-exception-escape) +struct AnnotationLocation { + auto operator<(const AnnotationLocation &other) const noexcept -> bool { + return std::tie(this->instance_location, this->evaluate_path, + this->schema_location.get()) < + std::tie(other.instance_location, other.evaluate_path, + other.schema_location.get()); + } + + sourcemeta::core::WeakPointer instance_location; + sourcemeta::core::WeakPointer evaluate_path; + std::reference_wrapper schema_location; +}; + +auto group_annotations(const SimpleOutput &output) + -> std::map> { + std::map> result; + for (const auto &entry : output.annotations()) { + auto &values{result[{.instance_location = entry.instance_location, + .evaluate_path = entry.evaluate_path, + .schema_location = entry.schema_location}]}; + if (values.empty() || values.back() != entry.value) { + values.push_back(entry.value); + } + } + + return result; +} + auto handle_standard(Evaluator &evaluator, const Template &schema, const sourcemeta::core::JSON &instance, const StandardOutput format, @@ -30,7 +64,7 @@ auto handle_standard(Evaluator &evaluator, const Template &schema, auto result{sourcemeta::core::JSON::make_object()}; result.assign_assume_new("valid", sourcemeta::core::JSON{valid}); auto annotations{sourcemeta::core::JSON::make_array()}; - for (const auto &annotation : output.annotations()) { + for (const auto &annotation : group_annotations(output)) { auto unit{sourcemeta::core::JSON::make_object()}; unit.assign_assume_new( "keywordLocation", diff --git a/vendor/core/src/core/json/include/sourcemeta/core/json_object.h b/vendor/core/src/core/json/include/sourcemeta/core/json_object.h index 44db7b90..49a8b991 100644 --- a/vendor/core/src/core/json/include/sourcemeta/core/json_object.h +++ b/vendor/core/src/core/json/include/sourcemeta/core/json_object.h @@ -413,6 +413,54 @@ template class JSONObject { std::unreachable(); } + /// Try to access an object entry by its key name + [[nodiscard]] inline auto try_at(const Key &key, const hash_type key_hash) + -> mapped_type * { + assert(this->hash(key) == key_hash); + + // Move the perfect hash condition out of the loop for extra performance + if (this->hasher.is_perfect(key_hash)) { + for (auto &entry : this->data) { + if (entry.hash == key_hash && entry.first.size() == key.size()) { + return &entry.second; + } + } + } else { + for (auto &entry : this->data) { + if (entry.hash == key_hash && entry.first == key) { + return &entry.second; + } + } + } + + return nullptr; + } + + /// Try to access an object entry by its key name + template + requires std::same_as, KeyView> + [[nodiscard]] inline auto try_at(T key, const hash_type key_hash) + -> mapped_type * { + assert(this->hash(key) == key_hash); + + // Move the perfect hash condition out of the loop for extra performance + if (this->hasher.is_perfect(key_hash)) { + for (auto &entry : this->data) { + if (entry.hash == key_hash && entry.first.size() == key.size()) { + return &entry.second; + } + } + } else { + for (auto &entry : this->data) { + if (entry.hash == key_hash && entry.first == key) { + return &entry.second; + } + } + } + + return nullptr; + } + /// Try to access an object entry by its underlying positional index [[nodiscard]] inline auto try_at(const Key &key, const hash_type key_hash) const diff --git a/vendor/core/src/core/json/include/sourcemeta/core/json_value.h b/vendor/core/src/core/json/include/sourcemeta/core/json_value.h index 55cd02c5..f1219176 100644 --- a/vendor/core/src/core/json/include/sourcemeta/core/json_value.h +++ b/vendor/core/src/core/json/include/sourcemeta/core/json_value.h @@ -1419,6 +1419,67 @@ class SOURCEMETA_CORE_JSON_EXPORT JSON { return object.try_at(key, hash); } + /// This method tries to retrieve a mutable object element by key. For + /// example: + /// + /// ```cpp + /// #include + /// #include + /// + /// sourcemeta::core::JSON document = + /// sourcemeta::core::parse_json("{ \"foo\": 1 }"); + /// auto result{document.try_at("foo")}; + /// assert(result); + /// result->into(sourcemeta::core::JSON{2}); + /// assert(document.at("foo").to_integer() == 2); + /// ``` + [[nodiscard]] SOURCEMETA_FORCEINLINE inline auto try_at(const String &key) + -> JSON * { + assert(this->is_object()); + auto &object{this->data_object}; + return object.try_at(key, object.hash(key)); + } + + /// This method tries to retrieve a mutable object element by string view key + template + requires std::same_as, StringView> + [[nodiscard]] SOURCEMETA_FORCEINLINE inline auto try_at(T key) -> JSON * { + assert(this->is_object()); + auto &object{this->data_object}; + return object.try_at(key, object.hash(key)); + } + + /// This method tries to retrieve a mutable object element given a + /// pre-calculated property hash. For example: + /// + /// ```cpp + /// #include + /// #include + /// + /// sourcemeta::core::JSON document = + /// sourcemeta::core::parse_json("{ \"foo\": 1 }"); + /// auto result{document.try_at("foo", + /// document.as_object().hash("foo"))}; + /// assert(result); + /// result->into(sourcemeta::core::JSON{2}); + /// assert(document.at("foo").to_integer() == 2); + /// ``` + [[nodiscard]] SOURCEMETA_FORCEINLINE inline auto + try_at(const String &key, const typename Object::hash_type hash) -> JSON * { + assert(this->is_object()); + return this->data_object.try_at(key, hash); + } + + /// This method tries to retrieve a mutable object element by string view key + /// given a pre-calculated property hash + template + requires std::same_as, StringView> + [[nodiscard]] SOURCEMETA_FORCEINLINE inline auto + try_at(T key, const typename Object::hash_type hash) -> JSON * { + assert(this->is_object()); + return this->data_object.try_at(key, hash); + } + /// Try to get a property, scanning from a caller-provided start offset. /// On hit, advances `start` past the found index. When looking up multiple /// keys in insertion order, each lookup hits on the first probe, making the diff --git a/vendor/core/src/core/jsonld/include/sourcemeta/core/jsonld_materialize.h b/vendor/core/src/core/jsonld/include/sourcemeta/core/jsonld_materialize.h index 4a0383fa..4fcd7320 100644 --- a/vendor/core/src/core/jsonld/include/sourcemeta/core/jsonld_materialize.h +++ b/vendor/core/src/core/jsonld/include/sourcemeta/core/jsonld_materialize.h @@ -8,11 +8,10 @@ #include #include -#include // std::uint8_t -#include // std::optional -#include // std::unordered_map -#include // std::variant -#include // std::vector +#include // std::uint8_t +#include // std::optional +#include // std::variant +#include // std::vector namespace sourcemeta::core { @@ -108,27 +107,36 @@ struct JSONLDDescriptor { }; /// @ingroup jsonld -/// A resolved mapping of instance positions to their JSON-LD semantics, keyed -/// by a JSON Pointer of the given kind +/// An instance position paired with its JSON-LD semantics +template struct JSONLDBasicAnnotation { + /// The instance position the annotation describes. + PointerT pointer{}; + /// The JSON-LD semantics of the position. + JSONLDDescriptor descriptor{}; +}; + +/// @ingroup jsonld +/// A flat collection of annotated instance positions, in any order. When more +/// than one entry describes the same position, the first one wins. template -using JSONLDBasicAnnotationMap = - std::unordered_map; +using JSONLDBasicAnnotationList = std::vector>; /// @ingroup jsonld -/// A resolved annotation map keyed by an owning JSON Pointer -using JSONLDAnnotationMap = JSONLDBasicAnnotationMap; +/// An annotation list whose positions are owning JSON Pointers +using JSONLDAnnotationList = JSONLDBasicAnnotationList; /// @ingroup jsonld -/// A resolved annotation map keyed by a non-owning weak JSON Pointer. The keys -/// reference strings owned elsewhere that must outlive any materialization -/// call. -using JSONLDWeakAnnotationMap = JSONLDBasicAnnotationMap; +/// An annotation list whose positions are non-owning weak JSON Pointers. The +/// positions reference strings owned elsewhere that must outlive any +/// materialization call. +using JSONLDWeakAnnotationList = JSONLDBasicAnnotationList; /// @ingroup jsonld /// -/// Materialize an instance into expanded JSON-LD using an annotation map that -/// assigns JSON-LD semantics to instance positions. The result is always a JSON -/// array. For example: +/// Materialize an instance into expanded JSON-LD using an annotation list that +/// assigns JSON-LD semantics to instance positions. An undescribed member of a +/// collection defaults to a plain literal, or to an unordered collection for a +/// nested array. The result is always a JSON array. For example: /// /// ```cpp /// #include @@ -138,30 +146,34 @@ using JSONLDWeakAnnotationMap = JSONLDBasicAnnotationMap; /// const auto instance{sourcemeta::core::parse_json( /// R"({ "name": "Sourcemeta" })")}; /// -/// sourcemeta::core::JSONLDAnnotationMap map; -/// map.emplace(sourcemeta::core::Pointer{}, -/// sourcemeta::core::JSONLDDescriptor{ -/// {}, sourcemeta::core::JSONLDNode{ -/// "https://example.com/org", {}, false }}); -/// map.emplace(sourcemeta::core::Pointer{"name"}, -/// sourcemeta::core::JSONLDDescriptor{ -/// { { "https://schema.org/name", false } }, -/// sourcemeta::core::JSONLDLiteral{}}); +/// sourcemeta::core::JSONLDAnnotationList annotations; +/// annotations.push_back( +/// {sourcemeta::core::Pointer{}, +/// sourcemeta::core::JSONLDDescriptor{ +/// {}, sourcemeta::core::JSONLDNode{ +/// "https://example.com/org", {}, false }}}); +/// annotations.push_back( +/// {sourcemeta::core::Pointer{"name"}, +/// sourcemeta::core::JSONLDDescriptor{ +/// { { "https://schema.org/name", false } }, +/// sourcemeta::core::JSONLDLiteral{}}}); /// -/// const auto expanded{sourcemeta::core::jsonld_materialize(instance, map)}; +/// const auto expanded{ +/// sourcemeta::core::jsonld_materialize(instance, annotations)}; /// sourcemeta::core::prettify(expanded, std::cout); /// std::cout << std::endl; /// ``` SOURCEMETA_CORE_JSONLD_EXPORT -auto jsonld_materialize(const JSON &instance, const JSONLDAnnotationMap &map) - -> JSON; +auto jsonld_materialize(const JSON &instance, + const JSONLDAnnotationList &annotations) -> JSON; /// @ingroup jsonld /// -/// Materialize an instance into expanded JSON-LD using a weak annotation map -/// whose keys are non-owning views into strings owned elsewhere. The backing -/// strings must outlive the call. The result is always a JSON array. For -/// example: +/// Materialize an instance into expanded JSON-LD using a weak annotation list +/// whose positions are non-owning views into strings owned elsewhere. The +/// backing strings must outlive the call. An undescribed member of a +/// collection defaults to a plain literal, or to an unordered collection for a +/// nested array. The result is always a JSON array. For example: /// /// ```cpp /// #include @@ -174,23 +186,26 @@ auto jsonld_materialize(const JSON &instance, const JSONLDAnnotationMap &map) /// /// const sourcemeta::core::JSON::String name_key{"name"}; /// -/// sourcemeta::core::JSONLDWeakAnnotationMap map; -/// map.emplace(sourcemeta::core::WeakPointer{}, -/// sourcemeta::core::JSONLDDescriptor{ -/// {}, sourcemeta::core::JSONLDNode{ -/// "https://example.com/org", {}, false }}); -/// map.emplace(sourcemeta::core::WeakPointer{std::cref(name_key)}, -/// sourcemeta::core::JSONLDDescriptor{ -/// { { "https://schema.org/name", false } }, -/// sourcemeta::core::JSONLDLiteral{}}); +/// sourcemeta::core::JSONLDWeakAnnotationList annotations; +/// annotations.push_back( +/// {sourcemeta::core::WeakPointer{}, +/// sourcemeta::core::JSONLDDescriptor{ +/// {}, sourcemeta::core::JSONLDNode{ +/// "https://example.com/org", {}, false }}}); +/// annotations.push_back( +/// {sourcemeta::core::WeakPointer{std::cref(name_key)}, +/// sourcemeta::core::JSONLDDescriptor{ +/// { { "https://schema.org/name", false } }, +/// sourcemeta::core::JSONLDLiteral{}}}); /// -/// const auto expanded{sourcemeta::core::jsonld_materialize(instance, map)}; +/// const auto expanded{ +/// sourcemeta::core::jsonld_materialize(instance, annotations)}; /// sourcemeta::core::prettify(expanded, std::cout); /// std::cout << std::endl; /// ``` SOURCEMETA_CORE_JSONLD_EXPORT auto jsonld_materialize(const JSON &instance, - const JSONLDWeakAnnotationMap &map) -> JSON; + const JSONLDWeakAnnotationList &annotations) -> JSON; } // namespace sourcemeta::core diff --git a/vendor/core/src/core/jsonld/jsonld_materialize.cc b/vendor/core/src/core/jsonld/jsonld_materialize.cc index 7ed01d81..c9d73295 100644 --- a/vendor/core/src/core/jsonld/jsonld_materialize.cc +++ b/vendor/core/src/core/jsonld/jsonld_materialize.cc @@ -4,9 +4,9 @@ #include "jsonld_keywords.h" -#include // std::ranges::sort, std::ranges::none_of -#include // assert -#include // std::size_t +#include // std::ranges::sort, std::ranges::stable_sort, std::ranges::unique, std::ranges::none_of +#include // assert +#include // std::size_t #include // std::reference_wrapper, std::cref #include // std::optional, std::nullopt #include // std::is_same_v @@ -18,18 +18,53 @@ namespace sourcemeta::core { namespace { +template +using AnnotationIndex = std::vector *>; + +// A contiguous run of sorted annotations whose positions are all extensions +// of, or equal to, the position currently being visited +template struct AnnotationRange { + typename AnnotationIndex::const_iterator begin; + typename AnnotationIndex::const_iterator end; +}; + +// The sub-run of annotations that belong to the child position the pointer +// currently names, advancing the scan iterator past it. Annotations sorting +// before the child correspond to positions the walk does not visit and are +// skipped for good. +template +auto child_range(typename AnnotationIndex::const_iterator &iterator, + const typename AnnotationIndex::const_iterator end, + const PointerT &pointer) -> AnnotationRange { + const auto depth{pointer.size() - 1}; + const auto &token{pointer.at(depth)}; + while (iterator != end && (*iterator)->pointer.at(depth) < token) { + iterator += 1; + } + const auto begin{iterator}; + while (iterator != end && (*iterator)->pointer.at(depth) == token) { + iterator += 1; + } + return {begin, iterator}; +} + template auto materialize_value(const JSON &value, PointerT &pointer, - const JSONLDBasicAnnotationMap &map, + AnnotationRange range, std::vector &standalone, const std::vector **matched_edges = nullptr) -> std::optional; template auto fill_node(JSON &node, const JSON &instance_object, PointerT &pointer, - const JSONLDBasicAnnotationMap &map, + const AnnotationRange &range, std::vector &standalone) -> void; +template +auto materialize_member(const JSON &value, PointerT &pointer, + const AnnotationRange &range, + std::vector &standalone) -> std::optional; + // Append an object key to the pointer, copying it for an owning pointer and // taking a non-owning view for a weak pointer. template @@ -70,23 +105,24 @@ auto types_to_array(const std::vector &types) -> JSON { // The property array of node under the given predicate, creating it as needed. auto property_target(JSON &node, const JSON::StringView predicate) -> JSON & { - if (!node.defines(predicate)) { - node.assign_assume_new(JSON::String{predicate}, JSON::make_array()); + const auto hash{node.as_object().hash(predicate)}; + const auto existing{node.try_at(predicate, hash)}; + if (existing != nullptr) { + return *existing; } - return node.at(predicate); + return node.assign_assume_new(JSON::String{predicate}, JSON::make_array(), + hash); } // The property array nested under @reverse and the given predicate. auto reverse_target(JSON &node, const JSON::StringView predicate) -> JSON & { - if (!node.defines(KEYWORD_REVERSE, KEYWORD_REVERSE_HASH)) { - node.assign_assume_new(JSON::String{KEYWORD_REVERSE}, JSON::make_object(), - KEYWORD_REVERSE_HASH); - } - auto &reverse{node.at(KEYWORD_REVERSE, KEYWORD_REVERSE_HASH)}; - if (!reverse.defines(predicate)) { - reverse.assign_assume_new(JSON::String{predicate}, JSON::make_array()); - } - return reverse.at(predicate); + const auto existing{node.try_at(KEYWORD_REVERSE, KEYWORD_REVERSE_HASH)}; + auto &reverse{existing != nullptr + ? *existing + : node.assign_assume_new(JSON::String{KEYWORD_REVERSE}, + JSON::make_object(), + KEYWORD_REVERSE_HASH)}; + return property_target(reverse, predicate); } // Attach a value under a single edge. A set, represented as a bare array, @@ -160,13 +196,16 @@ auto materialize_reference(const JSONLDReference &descriptor) -> JSON { template auto build_collection(const JSON &value, PointerT &pointer, - const JSONLDBasicAnnotationMap &map, + const AnnotationRange &range, std::vector &standalone, const bool ordered) -> JSON { auto elements{JSON::make_array()}; + auto iterator{range.begin}; for (std::size_t index = 0; index < value.size(); index += 1) { pointer.push_back(index); - auto element{materialize_value(value.at(index), pointer, map, standalone)}; + auto element{materialize_member(value.at(index), pointer, + child_range(iterator, range.end, pointer), + standalone)}; pointer.pop_back(); if (!element.has_value()) { continue; @@ -246,13 +285,15 @@ auto build_language_collection(const JSON &value) -> JSON { // The index keys carry no RDF and are dropped. template auto build_index_collection(const JSON &value, PointerT &pointer, - const JSONLDBasicAnnotationMap &map, + const AnnotationRange &range, std::vector &standalone) -> JSON { auto elements{JSON::make_array()}; + auto iterator{range.begin}; for (const auto key : sorted_keys(value)) { push_property(pointer, key.get()); - auto element{ - materialize_value(value.at(key.get()), pointer, map, standalone)}; + auto element{materialize_member(value.at(key.get()), pointer, + child_range(iterator, range.end, pointer), + standalone)}; pointer.pop_back(); if (!element.has_value()) { continue; @@ -270,10 +311,34 @@ auto build_index_collection(const JSON &value, PointerT &pointer, return elements; } +// An undescribed collection member still materializes with a default kind, a +// scalar as a plain literal and a nested array as an unordered collection. +// An undescribed object member keeps the anonymous node treatment of any +// other position. +template +auto materialize_member(const JSON &value, PointerT &pointer, + const AnnotationRange &range, + std::vector &standalone) -> std::optional { + const auto described{range.begin != range.end && + (*range.begin)->pointer.size() == pointer.size()}; + if (described || value.is_object()) { + return materialize_value(value, pointer, range, standalone); + } + + if (value.is_null()) { + return std::nullopt; + } + + if (value.is_array()) { + return build_collection(value, pointer, range, standalone, false); + } + + return materialize_literal(JSONLDLiteral{}, value); +} + template auto materialize_node(const JSONLDNode &descriptor, const JSON &value, - PointerT &pointer, - const JSONLDBasicAnnotationMap &map, + PointerT &pointer, const AnnotationRange &range, std::vector &standalone) -> JSON { auto node{JSON::make_object()}; if (descriptor.id.has_value()) { @@ -298,7 +363,7 @@ auto materialize_node(const JSONLDNode &descriptor, const JSON &value, JSON{descriptor.id.value()}, KEYWORD_ID_HASH); } std::vector graph_nodes; - fill_node(inner, value, pointer, map, graph_nodes); + fill_node(inner, value, pointer, range, graph_nodes); auto graph{JSON::make_array()}; if (inner.object_size() > (descriptor.id.has_value() ? 1 : 0)) { graph.push_back(std::move(inner)); @@ -311,13 +376,13 @@ auto materialize_node(const JSONLDNode &descriptor, const JSON &value, return node; } - fill_node(node, value, pointer, map, standalone); + fill_node(node, value, pointer, range, standalone); return node; } template auto materialize_value(const JSON &value, PointerT &pointer, - const JSONLDBasicAnnotationMap &map, + AnnotationRange range, std::vector &standalone, const std::vector **matched_edges) -> std::optional { @@ -329,14 +394,16 @@ auto materialize_value(const JSON &value, PointerT &pointer, return std::nullopt; } - const auto iterator{map.find(pointer)}; - if (iterator == map.cend()) { + // Every annotation in the range extends the current position, so one of + // equal length is the annotation of the position itself and sorts first + if (range.begin == range.end || + (*range.begin)->pointer.size() != pointer.size()) { // An undescribed object with described descendants becomes an anonymous // blank node so its children have a subject. Anything else is not // annotated. - if (value.is_object()) { + if (value.is_object() && range.begin != range.end) { auto node{JSON::make_object()}; - fill_node(node, value, pointer, map, standalone); + fill_node(node, value, pointer, range, standalone); if (node.empty()) { return std::nullopt; } @@ -345,13 +412,14 @@ auto materialize_value(const JSON &value, PointerT &pointer, return std::nullopt; } - const auto &descriptor{iterator->second}; + const auto &descriptor{(*range.begin)->descriptor}; + range.begin += 1; if (matched_edges != nullptr) { *matched_edges = &descriptor.edges; } if (std::holds_alternative(descriptor.value)) { return materialize_node(std::get(descriptor.value), value, - pointer, map, standalone); + pointer, range, standalone); } if (std::holds_alternative(descriptor.value)) { return materialize_literal(std::get(descriptor.value), @@ -368,7 +436,7 @@ auto materialize_value(const JSON &value, PointerT &pointer, if (!value.is_array()) { return std::nullopt; } - return build_collection(value, pointer, map, standalone, + return build_collection(value, pointer, range, standalone, collection.container == JSONLDContainer::List); case JSONLDContainer::Language: assert(value.is_object()); @@ -379,7 +447,7 @@ auto materialize_value(const JSON &value, PointerT &pointer, return build_language_collection(value); case JSONLDContainer::Index: assert(value.is_object()); - return build_index_collection(value, pointer, map, standalone); + return build_index_collection(value, pointer, range, standalone); } std::unreachable(); @@ -387,7 +455,7 @@ auto materialize_value(const JSON &value, PointerT &pointer, template auto fill_node(JSON &node, const JSON &instance_object, PointerT &pointer, - const JSONLDBasicAnnotationMap &map, + const AnnotationRange &range, std::vector &standalone) -> void { std::vector> keys; keys.reserve(instance_object.object_size()); @@ -398,11 +466,13 @@ auto fill_node(JSON &node, const JSON &instance_object, PointerT &pointer, return left.get() < right.get(); }); + auto iterator{range.begin}; for (const auto key : keys) { push_property(pointer, key.get()); const std::vector *edges{nullptr}; - auto child_value{materialize_value(instance_object.at(key.get()), pointer, - map, standalone, &edges)}; + auto child_value{materialize_value( + instance_object.at(key.get()), pointer, + child_range(iterator, range.end, pointer), standalone, &edges)}; pointer.pop_back(); if (!child_value.has_value()) { continue; @@ -424,10 +494,28 @@ auto fill_node(JSON &node, const JSON &instance_object, PointerT &pointer, template auto materialize_root(const JSON &instance, - const JSONLDBasicAnnotationMap &map) -> JSON { + const JSONLDBasicAnnotationList &annotations) + -> JSON { + AnnotationIndex index; + index.reserve(annotations.size()); + for (const auto &annotation : annotations) { + index.push_back(&annotation); + } + + std::ranges::stable_sort(index, + [](const auto *left, const auto *right) -> bool { + return left->pointer < right->pointer; + }); + const auto duplicates{std::ranges::unique( + index, [](const auto *left, const auto *right) -> bool { + return left->pointer == right->pointer; + })}; + index.erase(duplicates.begin(), duplicates.end()); + std::vector standalone; PointerT pointer; - auto root{materialize_value(instance, pointer, map, standalone)}; + auto root{materialize_value(instance, pointer, {index.cbegin(), index.cend()}, + standalone)}; auto result{JSON::make_array()}; if (root.has_value()) { @@ -454,14 +542,14 @@ auto materialize_root(const JSON &instance, } // namespace -auto jsonld_materialize(const JSON &instance, const JSONLDAnnotationMap &map) - -> JSON { - return materialize_root(instance, map); +auto jsonld_materialize(const JSON &instance, + const JSONLDAnnotationList &annotations) -> JSON { + return materialize_root(instance, annotations); } auto jsonld_materialize(const JSON &instance, - const JSONLDWeakAnnotationMap &map) -> JSON { - return materialize_root(instance, map); + const JSONLDWeakAnnotationList &annotations) -> JSON { + return materialize_root(instance, annotations); } } // namespace sourcemeta::core diff --git a/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_token.h b/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_token.h index 6602495e..4ca4b6dc 100644 --- a/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_token.h +++ b/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_token.h @@ -184,6 +184,39 @@ template class GenericToken { return this->hash; } + /// Check whether a JSON Pointer property token equals the given string, + /// comparing the precomputed hashes first and only falling back to a string + /// comparison when the hash is not perfect. For example: + /// + /// ```cpp + /// #include + /// #include + /// #include + /// + /// const sourcemeta::core::Pointer::Token token{"foo"}; + /// assert(token.property_equals( + /// "foo", sourcemeta::core::JSON::Object::hash("foo"))); + /// ``` + [[nodiscard]] auto + property_equals(const JSON::StringView value, + const typename Hash::hash_type value_hash) const noexcept + -> bool { + assert(this->is_property()); + assert(hasher(value.data(), value.size()) == value_hash); + if constexpr (requires { hasher.is_perfect(value_hash); }) { + // A perfect hash captures the property bytes but not its length, so + // two properties that differ only by trailing NUL bytes hash equal. + // Comparing sizes disambiguates them without the cost of a full + // string comparison + return this->hash == value_hash && + (hasher.is_perfect(value_hash) + ? this->to_property().size() == value.size() + : this->to_property() == value); + } else { + return this->hash == value_hash && this->to_property() == value; + } + } + /// Get the underlying value of a JSON Pointer object property token /// (non-`const` overload). For example: /// @@ -251,8 +284,13 @@ template class GenericToken { return false; } else if (this->as_property) { if constexpr (requires { hasher.is_perfect(this->hash); }) { + // A perfect hash captures the property bytes but not its length, so + // two properties that differ only by trailing NUL bytes hash equal. + // Comparing sizes disambiguates them without the cost of a full + // string comparison if (hasher.is_perfect(this->hash) && hasher.is_perfect(other.hash)) { - return this->hash == other.hash; + return this->hash == other.hash && + this->to_property().size() == other.to_property().size(); } }