From 506384e42f210c7b2c5d1818f765ed15d6d4606f Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Mon, 24 Nov 2025 12:35:29 -0400 Subject: [PATCH] [WIP] Add rich error information to every possible exception Signed-off-by: Juan Cruz Viotti --- DEPENDENCIES | 2 +- src/command_bundle.cc | 72 ++++++++---- src/command_compile.cc | 38 ++++-- src/command_fmt.cc | 78 +++++++----- src/command_inspect.cc | 48 +++++--- src/command_test.cc | 81 +++++++++---- src/command_validate.cc | 84 +++++++++++-- src/error.h | 74 ++---------- src/resolver.h | 38 +++--- .../fail_relative_external_ref_missing.sh | 4 +- test/bundle/fail_resolve_duplicate.sh | 8 +- test/bundle/fail_unknown_metaschema.sh | 4 +- test/ci/fail_bundle_http_non_schema.sh | 1 + .../ci/fail_bundle_http_non_schema_verbose.sh | 1 + test/ci/fail_validate_http_non_schema.sh | 1 + .../fail_validate_http_non_schema_verbose.sh | 1 + test/compile/fail_unknown_metaschema.sh | 4 +- test/format/fail_no_dialect.sh | 6 +- test/format/fail_unknown_metaschema.sh | 4 +- .../fail_relative_file_metaschema_ref.sh | 3 +- test/inspect/fail_unknown_metaschema.sh | 4 +- test/test/fail_no_schema.sh | 4 +- test/test/fail_no_tests.sh | 1 + test/test/fail_not_object.sh | 1 + .../test/fail_resolve_directory_non_schema.sh | 1 + test/test/fail_schema_non_string.sh | 1 + .../test/fail_test_case_data_and_data_path.sh | 1 + test/test/fail_test_case_no_data.sh | 1 + test/test/fail_test_case_no_valid.sh | 1 + test/test/fail_test_case_non_boolean_valid.sh | 1 + test/test/fail_test_case_non_object.sh | 1 + .../fail_test_case_non_string_data_path.sh | 1 + .../fail_test_case_non_string_description.sh | 1 + test/test/fail_tests_non_array.sh | 1 + test/test/fail_unresolvable.sh | 1 + test/test/fail_unresolvable_anchor.sh | 1 + test/test/fail_unresolvable_fragment.sh | 1 + test/validate/fail_draft7_top_level_ref.sh | 12 +- test/validate/fail_invalid_ref.sh | 2 + .../fail_no_identifier_ref_without_resolve.sh | 1 + .../fail_relative_external_ref_missing.sh | 4 +- test/validate/fail_schema_unknown_dialect.sh | 4 +- vendor/core/src/core/json/parser.h | 108 +++++++++++++---- .../core/src/core/jsonschema/CMakeLists.txt | 4 +- vendor/core/src/core/jsonschema/frame.cc | 31 +++-- .../include/sourcemeta/core/jsonschema.h | 1 - .../sourcemeta/core/jsonschema_error.h | 111 ++++++++++++++---- .../sourcemeta/core/jsonschema_frame.h | 1 - .../sourcemeta/core/jsonschema_resolver.h | 78 ------------ .../sourcemeta/core/jsonschema_transform.h | 4 +- vendor/core/src/core/jsonschema/jsonschema.cc | 14 +-- vendor/core/src/core/jsonschema/resolver.cc | 71 ----------- vendor/core/src/lang/numeric/decimal.cc | 33 +++++- 53 files changed, 607 insertions(+), 447 deletions(-) delete mode 100644 vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_resolver.h delete mode 100644 vendor/core/src/core/jsonschema/resolver.cc diff --git a/DEPENDENCIES b/DEPENDENCIES index ab4f6116..55d93e1a 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -1,5 +1,5 @@ vendorpull https://github.com/sourcemeta/vendorpull 1dcbac42809cf87cb5b045106b863e17ad84ba02 -core https://github.com/sourcemeta/core 8b34af868820407091f108486798b4c73dec6631 +core https://github.com/sourcemeta/core eababe4f4a967bfa546146c8531b8668913a3880 jsonbinpack https://github.com/sourcemeta/jsonbinpack abd40e41050d14d74af1fddb5c397de5cca3b13c blaze https://github.com/sourcemeta/blaze bfdc479b5ae0c17a356be5f42dcc713ab31f976a hydra https://github.com/sourcemeta/hydra af9f2c54709d620872ead0c3f8f683c15a0fa702 diff --git a/src/command_bundle.cc b/src/command_bundle.cc index 958e3518..e583725f 100644 --- a/src/command_bundle.cc +++ b/src/command_bundle.cc @@ -25,36 +25,56 @@ auto sourcemeta::jsonschema::bundle(const sourcemeta::core::Options &options) const auto configuration_path{find_configuration(schema_path)}; const auto &configuration{read_configuration(options, configuration_path)}; const auto dialect{default_dialect(options, configuration)}; - const auto &custom_resolver{ - resolver(options, options.contains("http"), dialect, configuration)}; auto schema{sourcemeta::core::read_yaml_or_json(schema_path)}; - sourcemeta::core::bundle(schema, sourcemeta::core::schema_official_walker, - custom_resolver, dialect, - sourcemeta::core::URI::from_path( - sourcemeta::core::weakly_canonical(schema_path)) - .recompose()); - - if (options.contains("without-id")) { - sourcemeta::jsonschema::LOG_WARNING() - << "You are opting in to remove schema identifiers in " - "the bundled schema.\n" - << "The only legit use case of this advanced feature we know of " - "is to workaround\n" - << "non-compliant JSON Schema implementations such as Visual " - "Studio Code.\n" - << "Otherwise, this is not needed and may harm other use " - "cases. For example,\n" - << "you will be unable to reference the resulting schema from " - "other schemas\n" - << "using the --resolve/-r option.\n"; - sourcemeta::core::for_editor(schema, - sourcemeta::core::schema_official_walker, - custom_resolver, dialect); + try { + const auto &custom_resolver{ + resolver(options, options.contains("http"), dialect, configuration)}; + sourcemeta::core::bundle( + schema, sourcemeta::core::schema_official_walker, custom_resolver, + dialect, + sourcemeta::core::URI::from_path( + sourcemeta::core::weakly_canonical(schema_path)) + .recompose()); + + if (options.contains("without-id")) { + sourcemeta::jsonschema::LOG_WARNING() + << "You are opting in to remove schema identifiers in " + "the bundled schema.\n" + << "The only legit use case of this advanced feature we know of " + "is to workaround\n" + << "non-compliant JSON Schema implementations such as Visual " + "Studio Code.\n" + << "Otherwise, this is not needed and may harm other use " + "cases. For example,\n" + << "you will be unable to reference the resulting schema from " + "other schemas\n" + << "using the --resolve/-r option.\n"; + sourcemeta::core::for_editor(schema, + sourcemeta::core::schema_official_walker, + custom_resolver, dialect); + } + + sourcemeta::core::format(schema, sourcemeta::core::schema_official_walker, + custom_resolver, dialect); + } catch (const sourcemeta::core::SchemaReferenceError &error) { + throw FileError( + schema_path, std::string{error.identifier()}, error.location(), + error.what()); + } catch ( + const sourcemeta::core::SchemaRelativeMetaschemaResolutionError &error) { + throw FileError( + schema_path, error); + } catch (const sourcemeta::core::SchemaResolutionError &error) { + throw FileError(schema_path, + error); + } catch (const sourcemeta::core::SchemaUnknownBaseDialectError &) { + throw FileError( + schema_path); + } catch (const sourcemeta::core::SchemaError &error) { + throw FileError(schema_path, error.what()); } - sourcemeta::core::format(schema, sourcemeta::core::schema_official_walker, - custom_resolver, dialect); sourcemeta::core::prettify(schema, std::cout); std::cout << "\n"; } diff --git a/src/command_compile.cc b/src/command_compile.cc index 0f5899bd..ad926cde 100644 --- a/src/command_compile.cc +++ b/src/command_compile.cc @@ -24,8 +24,6 @@ auto sourcemeta::jsonschema::compile(const sourcemeta::core::Options &options) const auto configuration_path{find_configuration(schema_path)}; const auto &configuration{read_configuration(options, configuration_path)}; const auto dialect{default_dialect(options, configuration)}; - const auto &custom_resolver{ - resolver(options, options.contains("http"), dialect, configuration)}; const auto schema{sourcemeta::core::read_yaml_or_json(schema_path)}; @@ -34,15 +32,33 @@ auto sourcemeta::jsonschema::compile(const sourcemeta::core::Options &options) } const auto fast_mode{options.contains("fast")}; - const auto schema_template{sourcemeta::blaze::compile( - schema, sourcemeta::core::schema_official_walker, custom_resolver, - sourcemeta::blaze::default_schema_compiler, - fast_mode ? sourcemeta::blaze::Mode::FastValidation - : sourcemeta::blaze::Mode::Exhaustive, - dialect, - sourcemeta::core::URI::from_path( - sourcemeta::core::weakly_canonical(schema_path)) - .recompose())}; + + sourcemeta::blaze::Template schema_template; + try { + const auto &custom_resolver{ + resolver(options, options.contains("http"), dialect, configuration)}; + schema_template = sourcemeta::blaze::compile( + schema, sourcemeta::core::schema_official_walker, custom_resolver, + sourcemeta::blaze::default_schema_compiler, + fast_mode ? sourcemeta::blaze::Mode::FastValidation + : sourcemeta::blaze::Mode::Exhaustive, + dialect, + sourcemeta::core::URI::from_path( + sourcemeta::core::weakly_canonical(schema_path)) + .recompose()); + } catch ( + const sourcemeta::core::SchemaRelativeMetaschemaResolutionError &error) { + throw FileError( + schema_path, error); + } catch (const sourcemeta::core::SchemaResolutionError &error) { + throw FileError(schema_path, + error); + } catch (const sourcemeta::core::SchemaUnknownBaseDialectError &) { + throw FileError( + schema_path); + } catch (const sourcemeta::core::SchemaError &error) { + throw FileError(schema_path, error.what()); + } const auto template_json{sourcemeta::blaze::to_json(schema_template)}; if (options.contains("minify")) { diff --git a/src/command_fmt.cc b/src/command_fmt.cc index 05f6a004..2e505d21 100644 --- a/src/command_fmt.cc +++ b/src/command_fmt.cc @@ -33,42 +33,58 @@ auto sourcemeta::jsonschema::fmt(const sourcemeta::core::Options &options) LOG_VERBOSE(options) << "Formatting: " << entry.first.string() << "\n"; } - const auto configuration_path{find_configuration(entry.first)}; - const auto &configuration{read_configuration(options, configuration_path)}; - const auto dialect{default_dialect(options, configuration)}; - const auto &custom_resolver{ - resolver(options, options.contains("http"), dialect, configuration)}; + try { + const auto configuration_path{find_configuration(entry.first)}; + const auto &configuration{ + read_configuration(options, configuration_path)}; + const auto dialect{default_dialect(options, configuration)}; + const auto &custom_resolver{ + resolver(options, options.contains("http"), dialect, configuration)}; - std::ostringstream expected; - if (options.contains("keep-ordering")) { - sourcemeta::core::prettify(entry.second, expected, indentation); - } else { - auto copy = entry.second; - sourcemeta::core::format(copy, sourcemeta::core::schema_official_walker, - custom_resolver, dialect); - sourcemeta::core::prettify(copy, expected, indentation); - } - expected << "\n"; + std::ostringstream expected; + if (options.contains("keep-ordering")) { + sourcemeta::core::prettify(entry.second, expected, indentation); + } else { + auto copy = entry.second; + sourcemeta::core::format(copy, sourcemeta::core::schema_official_walker, + custom_resolver, dialect); + sourcemeta::core::prettify(copy, expected, indentation); + } + expected << "\n"; - std::ifstream current_stream{entry.first}; - std::ostringstream current; - current << current_stream.rdbuf(); + std::ifstream current_stream{entry.first}; + std::ostringstream current; + current << current_stream.rdbuf(); - if (options.contains("check")) { - if (current.str() == expected.str()) { - LOG_VERBOSE(options) << "ok: " << entry.first.string() << "\n"; - } else if (output_json) { - failed_files.push_back(entry.first.string()); - result = false; + if (options.contains("check")) { + if (current.str() == expected.str()) { + LOG_VERBOSE(options) << "ok: " << entry.first.string() << "\n"; + } else if (output_json) { + failed_files.push_back(entry.first.string()); + result = false; + } else { + std::cerr << "fail: " << entry.first.string() << "\n"; + result = false; + } } else { - std::cerr << "fail: " << entry.first.string() << "\n"; - result = false; - } - } else { - if (current.str() != expected.str()) { - std::ofstream output{entry.first}; - output << expected.str(); + if (current.str() != expected.str()) { + std::ofstream output{entry.first}; + output << expected.str(); + } } + } catch (const sourcemeta::core::SchemaRelativeMetaschemaResolutionError + &error) { + throw FileError< + sourcemeta::core::SchemaRelativeMetaschemaResolutionError>( + entry.first, error); + } catch (const sourcemeta::core::SchemaResolutionError &error) { + throw FileError(entry.first, + error); + } catch (const sourcemeta::core::SchemaUnknownBaseDialectError &) { + throw FileError( + entry.first); + } catch (const sourcemeta::core::SchemaError &error) { + throw FileError(entry.first, error.what()); } } diff --git a/src/command_inspect.cc b/src/command_inspect.cc index 7c0ccd50..085532d3 100644 --- a/src/command_inspect.cc +++ b/src/command_inspect.cc @@ -159,27 +159,41 @@ auto sourcemeta::jsonschema::inspect(const sourcemeta::core::Options &options) const auto configuration_path{find_configuration(schema_path)}; const auto &configuration{read_configuration(options, configuration_path)}; const auto dialect{default_dialect(options, configuration)}; - const auto &custom_resolver{ - resolver(options, options.contains("http"), dialect, configuration)}; - - const auto identifier{ - sourcemeta::core::identify(schema, custom_resolver, dialect)}; sourcemeta::core::SchemaFrame frame{ sourcemeta::core::SchemaFrame::Mode::Instances}; - frame.analyse( - schema, sourcemeta::core::schema_official_walker, custom_resolver, - dialect, - - // Only use the file-based URI if the schema has no identifier, - // as otherwise we make the output unnecessarily hard when it - // comes to debugging schemas - identifier.has_value() - ? std::optional(std::nullopt) - : sourcemeta::core::URI::from_path( - sourcemeta::core::weakly_canonical(schema_path)) - .recompose()); + try { + const auto &custom_resolver{ + resolver(options, options.contains("http"), dialect, configuration)}; + const auto identifier{ + sourcemeta::core::identify(schema, custom_resolver, dialect)}; + + frame.analyse( + schema, sourcemeta::core::schema_official_walker, custom_resolver, + dialect, + + // Only use the file-based URI if the schema has no identifier, + // as otherwise we make the output unnecessarily hard when it + // comes to debugging schemas + identifier.has_value() + ? std::optional(std::nullopt) + : sourcemeta::core::URI::from_path( + sourcemeta::core::weakly_canonical(schema_path)) + .recompose()); + } catch ( + const sourcemeta::core::SchemaRelativeMetaschemaResolutionError &error) { + throw FileError( + schema_path, error); + } catch (const sourcemeta::core::SchemaResolutionError &error) { + throw FileError(schema_path, + error); + } catch (const sourcemeta::core::SchemaUnknownBaseDialectError &) { + throw FileError( + schema_path); + } catch (const sourcemeta::core::SchemaError &error) { + throw FileError(schema_path, error.what()); + } if (options.contains("json")) { sourcemeta::core::prettify(frame.to_json(positions), std::cout); diff --git a/src/command_test.cc b/src/command_test.cc index 98a06c80..a29cfe36 100644 --- a/src/command_test.cc +++ b/src/command_test.cc @@ -59,39 +59,45 @@ auto sourcemeta::jsonschema::test(const sourcemeta::core::Options &options) const auto configuration_path{find_configuration(entry.first)}; const auto &configuration{read_configuration(options, configuration_path)}; const auto dialect{default_dialect(options, configuration)}; - const auto &test_resolver{ - resolver(options, options.contains("http"), dialect, configuration)}; const sourcemeta::core::JSON test{ sourcemeta::core::read_yaml_or_json(entry.first)}; if (!test.is_object()) { std::cout << entry.first.string() << ":\n"; - throw TestError{"The test document must be an object", std::nullopt}; + throw FileError{ + entry.first, "The test document must be an object", std::nullopt}; } + const auto &test_resolver{ + resolver(options, options.contains("http"), dialect, configuration)}; + if (!test.defines("target")) { std::cout << entry.first.string() << ":\n"; - throw TestError{"The test document must contain a `target` property", - std::nullopt}; + throw FileError{ + entry.first, "The test document must contain a `target` property", + std::nullopt}; } if (!test.at("target").is_string()) { std::cout << entry.first.string() << ":\n"; - throw TestError{"The test document `target` property must be a URI", - std::nullopt}; + throw FileError{ + entry.first, "The test document `target` property must be a URI", + std::nullopt}; } if (!test.defines("tests")) { std::cout << entry.first.string() << ":\n"; - throw TestError{"The test document must contain a `tests` property", - std::nullopt}; + throw FileError{ + entry.first, "The test document must contain a `tests` property", + std::nullopt}; } if (!test.at("tests").is_array()) { std::cout << entry.first.string() << ":\n"; - throw TestError{"The test document `tests` property must be an array", - std::nullopt}; + throw FileError{ + entry.first, "The test document `tests` property must be an array", + std::nullopt}; } const auto test_path_uri{sourcemeta::core::URI::from_path(entry.first)}; @@ -123,15 +129,32 @@ auto sourcemeta::jsonschema::test(const sourcemeta::core::Options &options) sourcemeta::blaze::Mode::FastValidation, dialect); } catch (const sourcemeta::core::SchemaReferenceError &error) { if (error.location() == sourcemeta::core::Pointer{"$ref"} && - error.id() == schema_uri.recompose()) { + error.identifier() == schema_uri.recompose()) { std::cout << entry.first.string() << ":"; std::cout << "\n"; - throw sourcemeta::core::SchemaResolutionError( - test.at("target").to_string(), - "Could not resolve schema under test"); + throw FileError{ + entry.first, test.at("target").to_string(), + "Could not resolve schema under test"}; } throw; + } catch (const sourcemeta::core::SchemaRelativeMetaschemaResolutionError + &error) { + std::cout << entry.first.string() << ":"; + std::cout << "\n"; + throw FileError< + sourcemeta::core::SchemaRelativeMetaschemaResolutionError>{ + entry.first, error}; + } catch (const sourcemeta::core::SchemaResolutionError &error) { + std::cout << entry.first.string() << ":"; + std::cout << "\n"; + throw FileError{entry.first, + error}; + } catch (const sourcemeta::core::SchemaUnknownBaseDialectError &) { + std::cout << entry.first.string() << ":"; + std::cout << "\n"; + throw FileError{ + entry.first}; } catch (...) { std::cout << entry.first.string() << ":"; std::cout << "\n"; @@ -148,27 +171,32 @@ auto sourcemeta::jsonschema::test(const sourcemeta::core::Options &options) if (!test_case.is_object()) { std::cout << "\n"; - throw TestError{"Test case documents must be objects", index}; + throw FileError{ + entry.first, "Test case documents must be objects", index}; } if (!test_case.defines("data") && !test_case.defines("dataPath")) { std::cout << "\n"; - throw TestError{ + throw FileError{ + entry.first, "Test case documents must contain a `data` or `dataPath` property", index}; } if (test_case.defines("data") && test_case.defines("dataPath")) { std::cout << "\n"; - throw TestError{"Test case documents must contain either a `data` or " - "`dataPath` property, but not both", - index}; + throw FileError{ + entry.first, + "Test case documents must contain either a `data` or " + "`dataPath` property, but not both", + index}; } if (test_case.defines("dataPath") && !test_case.at("dataPath").is_string()) { std::cout << "\n"; - throw TestError{ + throw FileError{ + entry.first, "Test case documents must set the `dataPath` property to a string", index}; } @@ -176,19 +204,22 @@ auto sourcemeta::jsonschema::test(const sourcemeta::core::Options &options) if (test_case.defines("description") && !test_case.at("description").is_string()) { std::cout << "\n"; - throw TestError{ + throw FileError{ + entry.first, "If you set a test case description, it must be a string", index}; } if (!test_case.defines("valid")) { std::cout << "\n"; - throw TestError{"Test case documents must contain a `valid` property", - index}; + throw FileError{ + entry.first, "Test case documents must contain a `valid` property", + index}; } if (!test_case.at("valid").is_boolean()) { std::cout << "\n"; - throw TestError{ + throw FileError{ + entry.first, "The test case document `valid` property must be a boolean", index}; } diff --git a/src/command_validate.cc b/src/command_validate.cc index 39a3c86e..812254e2 100644 --- a/src/command_validate.cc +++ b/src/command_validate.cc @@ -151,8 +151,6 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) const auto configuration_path{find_configuration(schema_path)}; const auto &configuration{read_configuration(options, configuration_path)}; const auto dialect{default_dialect(options, configuration)}; - const auto &custom_resolver{ - resolver(options, options.contains("http"), dialect, configuration)}; const auto schema{sourcemeta::core::read_yaml_or_json(schema_path)}; @@ -160,6 +158,9 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) throw NotSchemaError{schema_path}; } + const auto &custom_resolver{ + resolver(options, options.contains("http"), dialect, configuration)}; + const auto fast_mode{options.contains("fast")}; const auto benchmark{options.contains("benchmark")}; const auto benchmark_loop{parse_loop(options)}; @@ -173,16 +174,79 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) const auto default_id{sourcemeta::core::URI::from_path( sourcemeta::core::weakly_canonical(schema_path)) .recompose()}; - const sourcemeta::core::JSON bundled{ - sourcemeta::core::bundle(schema, sourcemeta::core::schema_official_walker, - custom_resolver, dialect, default_id)}; + + const sourcemeta::core::JSON bundled{[&]() { + try { + return sourcemeta::core::bundle(schema, + sourcemeta::core::schema_official_walker, + custom_resolver, dialect, default_id); + } catch (const sourcemeta::core::SchemaReferenceError &error) { + throw FileError( + schema_path, std::string{error.identifier()}, error.location(), + error.what()); + } catch (const sourcemeta::core::SchemaRelativeMetaschemaResolutionError + &error) { + throw FileError< + sourcemeta::core::SchemaRelativeMetaschemaResolutionError>( + schema_path, error); + } catch (const sourcemeta::core::SchemaResolutionError &error) { + throw FileError(schema_path, + error); + } catch (const sourcemeta::core::SchemaUnknownBaseDialectError &) { + throw FileError( + schema_path); + } catch (const sourcemeta::core::SchemaError &error) { + throw FileError(schema_path, error.what()); + } catch ( + const sourcemeta::core::SchemaReferenceObjectResourceError &error) { + throw FileError( + schema_path, error.identifier()); + } + }()}; + sourcemeta::core::SchemaFrame frame{ sourcemeta::core::SchemaFrame::Mode::References}; - frame.analyse(bundled, sourcemeta::core::schema_official_walker, - custom_resolver, dialect, default_id); - const auto schema_template{get_schema_template(bundled, custom_resolver, - frame, dialect, default_id, - fast_mode, options)}; + + try { + frame.analyse(bundled, sourcemeta::core::schema_official_walker, + custom_resolver, dialect, default_id); + } catch ( + const sourcemeta::core::SchemaRelativeMetaschemaResolutionError &error) { + throw FileError( + schema_path, error); + } catch (const sourcemeta::core::SchemaResolutionError &error) { + throw FileError(schema_path, + error); + } catch (const sourcemeta::core::SchemaUnknownBaseDialectError &) { + throw FileError( + schema_path); + } catch (const sourcemeta::core::SchemaError &error) { + throw FileError(schema_path, error.what()); + } + + const auto schema_template{[&]() { + try { + return get_schema_template(bundled, custom_resolver, frame, dialect, + default_id, fast_mode, options); + } catch (const sourcemeta::core::SchemaReferenceError &error) { + throw FileError( + schema_path, std::string{error.identifier()}, error.location(), + error.what()); + } catch (const sourcemeta::core::SchemaRelativeMetaschemaResolutionError + &error) { + throw FileError< + sourcemeta::core::SchemaRelativeMetaschemaResolutionError>( + schema_path, error); + } catch (const sourcemeta::core::SchemaResolutionError &error) { + throw FileError(schema_path, + error); + } catch (const sourcemeta::core::SchemaUnknownBaseDialectError &) { + throw FileError( + schema_path); + } catch (const sourcemeta::core::SchemaError &error) { + throw FileError(schema_path, error.what()); + } + }()}; sourcemeta::blaze::Evaluator evaluator; diff --git a/src/error.h b/src/error.h index b2248ba7..fcb4aa1b 100644 --- a/src/error.h +++ b/src/error.h @@ -197,11 +197,12 @@ inline auto print_exception(const bool is_json, const Exception &exception) } } - if constexpr (requires(const Exception ¤t) { current.id(); }) { + if constexpr (requires(const Exception ¤t) { current.identifier(); }) { if (is_json) { - error_json.assign("identifier", sourcemeta::core::JSON{exception.id()}); + error_json.assign("identifier", + sourcemeta::core::JSON{exception.identifier()}); } else { - std::cerr << " at identifier " << exception.id() << "\n"; + std::cerr << " at identifier " << exception.identifier() << "\n"; } } @@ -362,7 +363,7 @@ inline auto try_catch(const sourcemeta::core::Options &options, } return EXIT_FAILURE; - } catch (const TestError &error) { + } catch (const FileError &error) { const auto is_json{options.contains("json")}; print_exception(is_json, error); if (!is_json) { @@ -373,7 +374,7 @@ inline auto try_catch(const sourcemeta::core::Options &options, } return EXIT_FAILURE; - } catch (const sourcemeta::core::SchemaReferenceError &error) { + } catch (const FileError &error) { const auto is_json{options.contains("json")}; print_exception(is_json, error); return EXIT_FAILURE; @@ -387,16 +388,11 @@ inline auto try_catch(const sourcemeta::core::Options &options, const auto is_json{options.contains("json")}; print_exception(is_json, error); return EXIT_FAILURE; - } catch ( - const sourcemeta::core::SchemaRelativeMetaschemaResolutionError &error) { - const auto is_json{options.contains("json")}; - print_exception(is_json, error); - return EXIT_FAILURE; } catch (const FileError &error) { const auto is_json{options.contains("json")}; print_exception(is_json, error); if (!is_json) { - if (error.id().starts_with("file://")) { + if (error.identifier().starts_with("file://")) { std::cerr << "\nThis is likely because the file does not exist\n"; } else { std::cerr @@ -405,30 +401,6 @@ inline auto try_catch(const sourcemeta::core::Options &options, } } - return EXIT_FAILURE; - } catch (const sourcemeta::core::SchemaResolutionError &error) { - const auto is_json{options.contains("json")}; - print_exception(is_json, error); - if (!is_json) { - if (error.id().starts_with("file://")) { - std::cerr << "\nThis is likely because the file does not exist\n"; - } else { - std::cerr - << "\nThis is likely because you forgot to import such schema " - "using `--resolve/-r`\n"; - } - } - - return EXIT_FAILURE; - } catch (const sourcemeta::core::SchemaUnknownDialectError &error) { - const auto is_json{options.contains("json")}; - print_exception(is_json, error); - if (!is_json) { - std::cerr - << "\nThis is likely because you forgot to import such meta-schema " - "using `--resolve/-r`\n"; - } - return EXIT_FAILURE; } catch ( const FileError &error) { @@ -445,21 +417,12 @@ inline auto try_catch(const sourcemeta::core::Options &options, } return EXIT_FAILURE; - } catch (const sourcemeta::core::SchemaUnknownBaseDialectError &error) { + } catch (const FileError &error) { const auto is_json{options.contains("json")}; print_exception(is_json, error); - if (!is_json) { - std::cerr << "\nAre you sure the input is a valid JSON Schema and its " - "base dialect is known?\n"; - std::cerr - << "If the input does not declare the `$schema` keyword, you might " - "want to\n"; - std::cerr << "explicitly declare a default dialect using " - "`--default-dialect/-d`\n"; - } - return EXIT_FAILURE; - } catch (const sourcemeta::core::SchemaReferenceObjectResourceError &error) { + } catch (const FileError + &error) { const auto is_json{options.contains("json")}; print_exception(is_json, error); return EXIT_FAILURE; @@ -467,23 +430,6 @@ inline auto try_catch(const sourcemeta::core::Options &options, const auto is_json{options.contains("json")}; print_exception(is_json, error); return EXIT_FAILURE; - } catch (const sourcemeta::core::SchemaError &error) { - const auto is_json{options.contains("json")}; - print_exception(is_json, error); - return EXIT_FAILURE; - } catch (const sourcemeta::core::SchemaVocabularyError &error) { - const auto is_json{options.contains("json")}; - print_exception(is_json, error); - if (!is_json) { - std::cerr << "\nTo request support for it, please open an issue " - "at\nhttps://github.com/sourcemeta/jsonschema\n"; - } - - return EXIT_FAILURE; - } catch (const sourcemeta::core::URIParseError &error) { - const auto is_json{options.contains("json")}; - print_exception(is_json, error); - return EXIT_FAILURE; } catch (const sourcemeta::core::JSONFileParseError &error) { const auto is_json{options.contains("json")}; print_exception(is_json, error); diff --git a/src/resolver.h b/src/resolver.h index 7302fa75..c9e4f8e2 100644 --- a/src/resolver.h +++ b/src/resolver.h @@ -94,18 +94,27 @@ class CustomResolver { "The file you provided does not represent a valid JSON Schema"); } - const auto result = - this->add(entry.second, default_dialect, - sourcemeta::core::URI::from_path(entry.first).recompose(), - [&options](const auto &identifier) { - LOG_VERBOSE(options) - << "Importing schema into the resolution context: " - << identifier << "\n"; - }); - if (!result) { - LOG_WARNING() << "No schema resources were imported from this file\n" - << " at " << entry.first.string() << "\n" - << "Are you sure this schema sets any identifiers?\n"; + try { + const auto result = this->add( + entry.second, default_dialect, + sourcemeta::core::URI::from_path(entry.first).recompose(), + [&options](const auto &identifier) { + LOG_VERBOSE(options) + << "Importing schema into the resolution context: " + << identifier << "\n"; + }); + if (!result) { + LOG_WARNING() + << "No schema resources were imported from this file\n" + << " at " << entry.first.string() << "\n" + << "Are you sure this schema sets any identifiers?\n"; + } + } catch (const sourcemeta::core::SchemaFrameError &error) { + throw FileError( + entry.first, std::string{error.identifier()}, error.what()); + } catch (const sourcemeta::core::SchemaUnknownBaseDialectError &) { + throw FileError( + entry.first); } } } @@ -142,9 +151,8 @@ class CustomResolver { const auto result{this->schemas.emplace(key.second, subschema)}; if (!result.second && result.first->second != schema) { - std::ostringstream error; - error << "Cannot register the same identifier twice: " << key.second; - throw sourcemeta::core::SchemaError(error.str()); + throw sourcemeta::core::SchemaFrameError( + key.second, "Cannot register the same identifier twice"); } if (callback) { diff --git a/test/bundle/fail_relative_external_ref_missing.sh b/test/bundle/fail_relative_external_ref_missing.sh index 8fb6e3ed..574c4ce0 100755 --- a/test/bundle/fail_relative_external_ref_missing.sh +++ b/test/bundle/fail_relative_external_ref_missing.sh @@ -21,6 +21,7 @@ test "$CODE" = "1" || exit 1 cat << EOF > "$TMP/expected.txt" error: Could not resolve the reference to an external schema at identifier https://example.com/nested + at file path $(realpath "$TMP")/schema.json This is likely because you forgot to import such schema using \`--resolve/-r\` EOF @@ -34,7 +35,8 @@ test "$CODE" = "1" || exit 1 cat << EOF > "$TMP/expected.txt" { "error": "Could not resolve the reference to an external schema", - "identifier": "https://example.com/nested" + "identifier": "https://example.com/nested", + "filePath": "$(realpath "$TMP")/schema.json" } EOF diff --git a/test/bundle/fail_resolve_duplicate.sh b/test/bundle/fail_resolve_duplicate.sh index c6429d64..a8ddc3d3 100755 --- a/test/bundle/fail_resolve_duplicate.sh +++ b/test/bundle/fail_resolve_duplicate.sh @@ -39,7 +39,9 @@ EOF test "$CODE" = "1" || exit 1 cat << EOF > "$TMP/expected.txt" -error: Cannot register the same identifier twice: https://example.com/nested +error: Cannot register the same identifier twice + at identifier https://example.com/nested + at file path $(realpath "$TMP")/schemas/remote.json EOF diff "$TMP/stderr.txt" "$TMP/expected.txt" @@ -52,7 +54,9 @@ test "$CODE" = "1" || exit 1 cat << EOF > "$TMP/expected.txt" { - "error": "Cannot register the same identifier twice: https://example.com/nested" + "error": "Cannot register the same identifier twice", + "identifier": "https://example.com/nested", + "filePath": "$(realpath "$TMP")/schemas/remote.json" } EOF diff --git a/test/bundle/fail_unknown_metaschema.sh b/test/bundle/fail_unknown_metaschema.sh index 584d327b..3aa4b8f7 100755 --- a/test/bundle/fail_unknown_metaschema.sh +++ b/test/bundle/fail_unknown_metaschema.sh @@ -21,6 +21,7 @@ test "$CODE" = "1" || exit 1 cat << EOF > "$TMP/expected.txt" error: Could not resolve the metaschema of the schema at identifier https://example.com/unknown + at file path $(realpath "$TMP")/schema.json This is likely because you forgot to import such schema using \`--resolve/-r\` EOF @@ -34,7 +35,8 @@ test "$CODE" = "1" || exit 1 cat << EOF > "$TMP/expected.txt" { "error": "Could not resolve the metaschema of the schema", - "identifier": "https://example.com/unknown" + "identifier": "https://example.com/unknown", + "filePath": "$(realpath "$TMP")/schema.json" } EOF diff --git a/test/ci/fail_bundle_http_non_schema.sh b/test/ci/fail_bundle_http_non_schema.sh index 1f51c227..866330e1 100755 --- a/test/ci/fail_bundle_http_non_schema.sh +++ b/test/ci/fail_bundle_http_non_schema.sh @@ -21,6 +21,7 @@ test "$CODE" = "1" || exit 1 cat << EOF > "$TMP/expected.txt" error: The JSON document is not a valid JSON Schema at identifier https://schemas.sourcemeta.com/self/api/schemas/stats/jsonschema/2020-12/schema + at file path $(realpath "$TMP")/schema.json at location "/allOf/0/\$ref" EOF diff --git a/test/ci/fail_bundle_http_non_schema_verbose.sh b/test/ci/fail_bundle_http_non_schema_verbose.sh index 2ca450c1..c52c5034 100755 --- a/test/ci/fail_bundle_http_non_schema_verbose.sh +++ b/test/ci/fail_bundle_http_non_schema_verbose.sh @@ -22,6 +22,7 @@ cat << EOF > "$TMP/expected.txt" Resolving over HTTP: https://schemas.sourcemeta.com/self/api/schemas/stats/jsonschema/2020-12/schema error: The JSON document is not a valid JSON Schema at identifier https://schemas.sourcemeta.com/self/api/schemas/stats/jsonschema/2020-12/schema + at file path $(realpath "$TMP")/schema.json at location "/allOf/0/\$ref" EOF diff --git a/test/ci/fail_validate_http_non_schema.sh b/test/ci/fail_validate_http_non_schema.sh index d9922da2..11515cfc 100755 --- a/test/ci/fail_validate_http_non_schema.sh +++ b/test/ci/fail_validate_http_non_schema.sh @@ -25,6 +25,7 @@ test "$CODE" = "1" || exit 1 cat << EOF > "$TMP/expected.txt" error: The JSON document is not a valid JSON Schema at identifier https://schemas.sourcemeta.com/self/api/schemas/stats/jsonschema/2020-12/schema + at file path $(realpath "$TMP")/schema.json at location "/allOf/0/\$ref" EOF diff --git a/test/ci/fail_validate_http_non_schema_verbose.sh b/test/ci/fail_validate_http_non_schema_verbose.sh index 84d4ed2a..2ce997d8 100755 --- a/test/ci/fail_validate_http_non_schema_verbose.sh +++ b/test/ci/fail_validate_http_non_schema_verbose.sh @@ -26,6 +26,7 @@ cat << EOF > "$TMP/expected.txt" Resolving over HTTP: https://schemas.sourcemeta.com/self/api/schemas/stats/jsonschema/2020-12/schema error: The JSON document is not a valid JSON Schema at identifier https://schemas.sourcemeta.com/self/api/schemas/stats/jsonschema/2020-12/schema + at file path $(realpath "$TMP")/schema.json at location "/allOf/0/\$ref" EOF diff --git a/test/compile/fail_unknown_metaschema.sh b/test/compile/fail_unknown_metaschema.sh index 387d7a88..27b8883f 100755 --- a/test/compile/fail_unknown_metaschema.sh +++ b/test/compile/fail_unknown_metaschema.sh @@ -21,6 +21,7 @@ test "$CODE" = "1" || exit 1 cat << EOF > "$TMP/expected.txt" error: Could not resolve the metaschema of the schema at identifier https://example.com/unknown + at file path $(realpath "$TMP")/schema.json This is likely because you forgot to import such schema using \`--resolve/-r\` EOF @@ -34,7 +35,8 @@ test "$CODE" = "1" || exit 1 cat << EOF > "$TMP/expected.txt" { "error": "Could not resolve the metaschema of the schema", - "identifier": "https://example.com/unknown" + "identifier": "https://example.com/unknown", + "filePath": "$(realpath "$TMP")/schema.json" } EOF diff --git a/test/format/fail_no_dialect.sh b/test/format/fail_no_dialect.sh index 150f2508..a9a64684 100755 --- a/test/format/fail_no_dialect.sh +++ b/test/format/fail_no_dialect.sh @@ -22,6 +22,7 @@ test "$CODE" = "1" || exit 1 cat << EOF > "$TMP/expected.txt" error: Could not determine the base dialect of the schema + at file path $(realpath "$TMP")/schema.json Are you sure the input is a valid JSON Schema and its base dialect is known? If the input does not declare the \`\$schema\` keyword, you might want to @@ -34,9 +35,10 @@ diff "$TMP/output.txt" "$TMP/expected.txt" "$1" fmt "$TMP/schema.json" --json >"$TMP/output_json.txt" 2>&1 && CODE="$?" || CODE="$?" test "$CODE" = "1" || exit 1 -cat << 'EOF' > "$TMP/expected_json.txt" +cat << EOF > "$TMP/expected_json.txt" { - "error": "Could not determine the base dialect of the schema" + "error": "Could not determine the base dialect of the schema", + "filePath": "$(realpath "$TMP")/schema.json" } EOF diff --git a/test/format/fail_unknown_metaschema.sh b/test/format/fail_unknown_metaschema.sh index e1b15c7d..2fcaaefa 100755 --- a/test/format/fail_unknown_metaschema.sh +++ b/test/format/fail_unknown_metaschema.sh @@ -24,6 +24,7 @@ test "$CODE" = "1" || exit 1 cat << EOF > "$TMP/expected.txt" error: Could not resolve the metaschema of the schema at identifier https://example.com/unknown + at file path $(realpath "$TMP")/schema.json This is likely because you forgot to import such schema using \`--resolve/-r\` EOF @@ -37,7 +38,8 @@ test "$CODE" = "1" || exit 1 cat << EOF > "$TMP/expected_json.txt" { "error": "Could not resolve the metaschema of the schema", - "identifier": "https://example.com/unknown" + "identifier": "https://example.com/unknown", + "filePath": "$(realpath "$TMP")/schema.json" } EOF diff --git a/test/inspect/fail_relative_file_metaschema_ref.sh b/test/inspect/fail_relative_file_metaschema_ref.sh index 7f9eb2d7..7176adb4 100755 --- a/test/inspect/fail_relative_file_metaschema_ref.sh +++ b/test/inspect/fail_relative_file_metaschema_ref.sh @@ -27,9 +27,10 @@ EOF && CODE="$?" || CODE="$?" test "$CODE" = "1" || exit 1 -cat << 'EOF' > "$TMP/expected.txt" +cat << EOF > "$TMP/expected.txt" error: Relative meta-schema URIs are not valid according to the JSON Schema specification at identifier ../meta.json + at file path $(realpath "$TMP")/schemas/folder/test.json EOF diff "$TMP/result.txt" "$TMP/expected.txt" diff --git a/test/inspect/fail_unknown_metaschema.sh b/test/inspect/fail_unknown_metaschema.sh index d5e14426..598073fa 100755 --- a/test/inspect/fail_unknown_metaschema.sh +++ b/test/inspect/fail_unknown_metaschema.sh @@ -21,6 +21,7 @@ test "$CODE" = "1" || exit 1 cat << EOF > "$TMP/expected.txt" error: Could not resolve the metaschema of the schema at identifier https://example.com/unknown + at file path $(realpath "$TMP")/schema.json This is likely because you forgot to import such schema using \`--resolve/-r\` EOF @@ -34,7 +35,8 @@ test "$CODE" = "1" || exit 1 cat << EOF > "$TMP/expected.txt" { "error": "Could not resolve the metaschema of the schema", - "identifier": "https://example.com/unknown" + "identifier": "https://example.com/unknown", + "filePath": "$(realpath "$TMP")/schema.json" } EOF diff --git a/test/test/fail_no_schema.sh b/test/test/fail_no_schema.sh index ba33a30b..163bc142 100755 --- a/test/test/fail_no_schema.sh +++ b/test/test/fail_no_schema.sh @@ -29,6 +29,7 @@ test "$CODE" = "1" || exit 1 cat << EOF > "$TMP/expected.txt" $(realpath "$TMP")/test.json: error: The test document must contain a \`target\` property + at file path $(realpath "$TMP")/test.json Learn more here: https://github.com/sourcemeta/jsonschema/blob/main/docs/test.markdown EOF @@ -43,7 +44,8 @@ test "$CODE" = "1" || exit 1 cat << EOF > "$TMP/expected.txt" $(realpath "$TMP")/test.json: { - "error": "The test document must contain a \`target\` property" + "error": "The test document must contain a \`target\` property", + "filePath": "$(realpath "$TMP")/test.json" } EOF diff --git a/test/test/fail_no_tests.sh b/test/test/fail_no_tests.sh index 2af5abce..45636fd6 100755 --- a/test/test/fail_no_tests.sh +++ b/test/test/fail_no_tests.sh @@ -20,6 +20,7 @@ test "$CODE" = "1" || exit 1 cat << EOF > "$TMP/expected.txt" $(realpath "$TMP")/test.json: error: The test document must contain a \`tests\` property + at file path $(realpath "$TMP")/test.json Learn more here: https://github.com/sourcemeta/jsonschema/blob/main/docs/test.markdown EOF diff --git a/test/test/fail_not_object.sh b/test/test/fail_not_object.sh index 2246b8b1..00f48618 100755 --- a/test/test/fail_not_object.sh +++ b/test/test/fail_not_object.sh @@ -32,6 +32,7 @@ test "$CODE" = "1" || exit 1 cat << EOF > "$TMP/expected.txt" $(realpath "$TMP")/test.json: error: The test document must be an object + at file path $(realpath "$TMP")/test.json Learn more here: https://github.com/sourcemeta/jsonschema/blob/main/docs/test.markdown EOF diff --git a/test/test/fail_resolve_directory_non_schema.sh b/test/test/fail_resolve_directory_non_schema.sh index 933b0ac8..6758e77d 100755 --- a/test/test/fail_resolve_directory_non_schema.sh +++ b/test/test/fail_resolve_directory_non_schema.sh @@ -43,6 +43,7 @@ Importing schema into the resolution context: file://$(realpath "$TMP")/schemas/ Importing schema into the resolution context: https://example.com Detecting schema resources from file: $(realpath "$TMP")/schemas/test.json error: Could not determine the base dialect of the schema + at file path $(realpath "$TMP")/schemas/test.json Are you sure the input is a valid JSON Schema and its base dialect is known? If the input does not declare the \`\$schema\` keyword, you might want to diff --git a/test/test/fail_schema_non_string.sh b/test/test/fail_schema_non_string.sh index a9f61936..d0d4fc40 100755 --- a/test/test/fail_schema_non_string.sh +++ b/test/test/fail_schema_non_string.sh @@ -30,6 +30,7 @@ test "$CODE" = "1" || exit 1 cat << EOF > "$TMP/expected.txt" $(realpath "$TMP")/test.json: error: The test document \`target\` property must be a URI + at file path $(realpath "$TMP")/test.json Learn more here: https://github.com/sourcemeta/jsonschema/blob/main/docs/test.markdown EOF diff --git a/test/test/fail_test_case_data_and_data_path.sh b/test/test/fail_test_case_data_and_data_path.sh index dde01d4c..2181a3e6 100755 --- a/test/test/fail_test_case_data_and_data_path.sh +++ b/test/test/fail_test_case_data_and_data_path.sh @@ -28,6 +28,7 @@ cat << EOF > "$TMP/expected.txt" $(realpath "$TMP")/test.json: error: Test case documents must contain either a \`data\` or \`dataPath\` property, but not both at test case #1 + at file path $(realpath "$TMP")/test.json Learn more here: https://github.com/sourcemeta/jsonschema/blob/main/docs/test.markdown EOF diff --git a/test/test/fail_test_case_no_data.sh b/test/test/fail_test_case_no_data.sh index 010e7343..d79f28ff 100755 --- a/test/test/fail_test_case_no_data.sh +++ b/test/test/fail_test_case_no_data.sh @@ -34,6 +34,7 @@ cat << EOF > "$TMP/expected.txt" $(realpath "$TMP")/test.json: error: Test case documents must contain a \`data\` or \`dataPath\` property at test case #3 + at file path $(realpath "$TMP")/test.json Learn more here: https://github.com/sourcemeta/jsonschema/blob/main/docs/test.markdown EOF diff --git a/test/test/fail_test_case_no_valid.sh b/test/test/fail_test_case_no_valid.sh index 771de766..72ab19f1 100755 --- a/test/test/fail_test_case_no_valid.sh +++ b/test/test/fail_test_case_no_valid.sh @@ -30,6 +30,7 @@ cat << EOF > "$TMP/expected.txt" $(realpath "$TMP")/test.json: error: Test case documents must contain a \`valid\` property at test case #1 + at file path $(realpath "$TMP")/test.json Learn more here: https://github.com/sourcemeta/jsonschema/blob/main/docs/test.markdown EOF diff --git a/test/test/fail_test_case_non_boolean_valid.sh b/test/test/fail_test_case_non_boolean_valid.sh index aea87240..f7fc7e0c 100755 --- a/test/test/fail_test_case_non_boolean_valid.sh +++ b/test/test/fail_test_case_non_boolean_valid.sh @@ -31,6 +31,7 @@ cat << EOF > "$TMP/expected.txt" $(realpath "$TMP")/test.json: error: The test case document \`valid\` property must be a boolean at test case #1 + at file path $(realpath "$TMP")/test.json Learn more here: https://github.com/sourcemeta/jsonschema/blob/main/docs/test.markdown EOF diff --git a/test/test/fail_test_case_non_object.sh b/test/test/fail_test_case_non_object.sh index 16bd7dce..e838741f 100755 --- a/test/test/fail_test_case_non_object.sh +++ b/test/test/fail_test_case_non_object.sh @@ -28,6 +28,7 @@ cat << EOF > "$TMP/expected.txt" $(realpath "$TMP")/test.json: error: Test case documents must be objects at test case #2 + at file path $(realpath "$TMP")/test.json Learn more here: https://github.com/sourcemeta/jsonschema/blob/main/docs/test.markdown EOF diff --git a/test/test/fail_test_case_non_string_data_path.sh b/test/test/fail_test_case_non_string_data_path.sh index 703aeb6a..7da2b614 100755 --- a/test/test/fail_test_case_non_string_data_path.sh +++ b/test/test/fail_test_case_non_string_data_path.sh @@ -27,6 +27,7 @@ cat << EOF > "$TMP/expected.txt" $(realpath "$TMP")/test.json: error: Test case documents must set the \`dataPath\` property to a string at test case #1 + at file path $(realpath "$TMP")/test.json Learn more here: https://github.com/sourcemeta/jsonschema/blob/main/docs/test.markdown EOF diff --git a/test/test/fail_test_case_non_string_description.sh b/test/test/fail_test_case_non_string_description.sh index 749d81aa..c3984ecf 100755 --- a/test/test/fail_test_case_non_string_description.sh +++ b/test/test/fail_test_case_non_string_description.sh @@ -33,6 +33,7 @@ cat << EOF > "$TMP/expected.txt" $(realpath "$TMP")/test.json: error: If you set a test case description, it must be a string at test case #2 + at file path $(realpath "$TMP")/test.json Learn more here: https://github.com/sourcemeta/jsonschema/blob/main/docs/test.markdown EOF diff --git a/test/test/fail_tests_non_array.sh b/test/test/fail_tests_non_array.sh index c430c5c7..636ea277 100755 --- a/test/test/fail_tests_non_array.sh +++ b/test/test/fail_tests_non_array.sh @@ -24,6 +24,7 @@ test "$CODE" = "1" || exit 1 cat << EOF > "$TMP/expected.txt" $(realpath "$TMP")/test.json: error: The test document \`tests\` property must be an array + at file path $(realpath "$TMP")/test.json Learn more here: https://github.com/sourcemeta/jsonschema/blob/main/docs/test.markdown EOF diff --git a/test/test/fail_unresolvable.sh b/test/test/fail_unresolvable.sh index 51546ca3..9484bc62 100755 --- a/test/test/fail_unresolvable.sh +++ b/test/test/fail_unresolvable.sh @@ -32,6 +32,7 @@ Looking for target: https://example.com/unknown $(realpath "$TMP")/test.json: error: Could not resolve the reference to an external schema at identifier https://example.com/unknown + at file path $(realpath "$TMP")/test.json This is likely because you forgot to import such schema using \`--resolve/-r\` EOF diff --git a/test/test/fail_unresolvable_anchor.sh b/test/test/fail_unresolvable_anchor.sh index 53c493bb..20c8f12d 100755 --- a/test/test/fail_unresolvable_anchor.sh +++ b/test/test/fail_unresolvable_anchor.sh @@ -46,6 +46,7 @@ Looking for target: https://example.com#foo $(realpath "$TMP")/test.json: error: Could not resolve schema under test at identifier https://example.com#foo + at file path $(realpath "$TMP")/test.json This is likely because you forgot to import such schema using \`--resolve/-r\` EOF diff --git a/test/test/fail_unresolvable_fragment.sh b/test/test/fail_unresolvable_fragment.sh index 65d46ea0..db32bb44 100755 --- a/test/test/fail_unresolvable_fragment.sh +++ b/test/test/fail_unresolvable_fragment.sh @@ -46,6 +46,7 @@ Looking for target: https://example.com#/foo $(realpath "$TMP")/test.json: error: Could not resolve schema under test at identifier https://example.com#/foo + at file path $(realpath "$TMP")/test.json This is likely because you forgot to import such schema using \`--resolve/-r\` EOF diff --git a/test/validate/fail_draft7_top_level_ref.sh b/test/validate/fail_draft7_top_level_ref.sh index 38a7143e..a6005dc9 100755 --- a/test/validate/fail_draft7_top_level_ref.sh +++ b/test/validate/fail_draft7_top_level_ref.sh @@ -27,8 +27,10 @@ EOF && EXIT_CODE="$?" || EXIT_CODE="$?" test "$EXIT_CODE" = "1" || exit 1 -cat << 'EOF' > "$TMP/expected.txt" -error: A schema with a top-level `$ref` in JSON Schema Draft 7 and older dialects ignores every sibling keywords (like identifiers and meta-schema declarations) and therefore many operations, like bundling, are not possible without undefined behavior +cat << EOF > "$TMP/expected.txt" +error: A schema with a top-level \`\$ref\` in JSON Schema Draft 7 and older dialects ignores every sibling keywords (like identifiers and meta-schema declarations) and therefore many operations, like bundling, are not possible without undefined behavior + at identifier file://$(realpath "$TMP")/schema.json + at file path $(realpath "$TMP")/schema.json EOF diff "$TMP/stderr.txt" "$TMP/expected.txt" @@ -37,9 +39,11 @@ diff "$TMP/stderr.txt" "$TMP/expected.txt" && EXIT_CODE="$?" || EXIT_CODE="$?" test "$EXIT_CODE" = "1" || exit 1 -cat << 'EOF' > "$TMP/expected.json" +cat << EOF > "$TMP/expected.json" { - "error": "A schema with a top-level `$ref` in JSON Schema Draft 7 and older dialects ignores every sibling keywords (like identifiers and meta-schema declarations) and therefore many operations, like bundling, are not possible without undefined behavior" + "error": "A schema with a top-level \`\$ref\` in JSON Schema Draft 7 and older dialects ignores every sibling keywords (like identifiers and meta-schema declarations) and therefore many operations, like bundling, are not possible without undefined behavior", + "identifier": "file://$(realpath "$TMP")/schema.json", + "filePath": "$(realpath "$TMP")/schema.json" } EOF diff --git a/test/validate/fail_invalid_ref.sh b/test/validate/fail_invalid_ref.sh index 3e376023..4a4adcf3 100755 --- a/test/validate/fail_invalid_ref.sh +++ b/test/validate/fail_invalid_ref.sh @@ -29,6 +29,7 @@ test "$EXIT_CODE" = "1" || exit 1 cat << EOF > "$TMP/expected.txt" error: Could not resolve schema reference at identifier file://$(realpath "$TMP")/schema.json#/definitions/i-dont-exist + at file path $(realpath "$TMP")/schema.json at location "/properties/foo/\$ref" EOF @@ -43,6 +44,7 @@ cat << EOF > "$TMP/expected.txt" { "error": "Could not resolve schema reference", "identifier": "file://$(realpath "$TMP")/schema.json#/definitions/i-dont-exist", + "filePath": "$(realpath "$TMP")/schema.json", "location": "/properties/foo/\$ref" } EOF diff --git a/test/validate/fail_no_identifier_ref_without_resolve.sh b/test/validate/fail_no_identifier_ref_without_resolve.sh index dec295fd..eb526909 100755 --- a/test/validate/fail_no_identifier_ref_without_resolve.sh +++ b/test/validate/fail_no_identifier_ref_without_resolve.sh @@ -26,6 +26,7 @@ cat << EOF > "$TMP/expected.txt" Attempting to read file reference from disk: $(realpath "$TMP")/schemas/other.json error: Could not resolve the reference to an external schema at identifier file://$(realpath "$TMP")/schemas/other.json + at file path $(realpath "$TMP")/schema.json This is likely because the file does not exist EOF diff --git a/test/validate/fail_relative_external_ref_missing.sh b/test/validate/fail_relative_external_ref_missing.sh index a3d268b3..b38ce8d8 100755 --- a/test/validate/fail_relative_external_ref_missing.sh +++ b/test/validate/fail_relative_external_ref_missing.sh @@ -28,6 +28,7 @@ test "$EXIT_CODE" = "1" || exit 1 cat << EOF > "$TMP/expected.txt" error: Could not resolve the reference to an external schema at identifier https://example.com/nested + at file path $(realpath "$TMP")/schema.json This is likely because you forgot to import such schema using \`--resolve/-r\` EOF @@ -42,7 +43,8 @@ test "$EXIT_CODE" = "1" || exit 1 cat << EOF > "$TMP/expected.txt" { "error": "Could not resolve the reference to an external schema", - "identifier": "https://example.com/nested" + "identifier": "https://example.com/nested", + "filePath": "$(realpath "$TMP")/schema.json" } EOF diff --git a/test/validate/fail_schema_unknown_dialect.sh b/test/validate/fail_schema_unknown_dialect.sh index 9f6c386f..bb68f525 100755 --- a/test/validate/fail_schema_unknown_dialect.sh +++ b/test/validate/fail_schema_unknown_dialect.sh @@ -25,6 +25,7 @@ test "$EXIT_CODE" = "1" || exit 1 cat << EOF > "$TMP/expected.txt" error: Could not resolve the metaschema of the schema at identifier https://example.com/unknown + at file path $(realpath "$TMP")/schema.json This is likely because you forgot to import such schema using \`--resolve/-r\` EOF @@ -39,7 +40,8 @@ test "$EXIT_CODE" = "1" || exit 1 cat << EOF > "$TMP/expected.txt" { "error": "Could not resolve the metaschema of the schema", - "identifier": "https://example.com/unknown" + "identifier": "https://example.com/unknown", + "filePath": "$(realpath "$TMP")/schema.json" } EOF diff --git a/vendor/core/src/core/json/parser.h b/vendor/core/src/core/json/parser.h index eef31d07..8546346d 100644 --- a/vendor/core/src/core/json/parser.h +++ b/vendor/core/src/core/json/parser.h @@ -316,7 +316,26 @@ auto parse_number_integer_maybe_decimal( template auto parse_number_real_maybe_decimal( const std::uint64_t line, const std::uint64_t column, - const std::basic_string &string) -> JSON { + const std::basic_string &string, + const std::size_t first_nonzero_position, + const std::size_t decimal_position) -> JSON { + // We are guaranteed to not be dealing with exponential numbers here + assert((string.find('e') == std::basic_string::npos)); + assert((string.find('E') == std::basic_string::npos)); + + // If the number has enough significant digits, then we risk completely losing + // precision of the fractional component, and thus incorrectly interpreting a + // fractional number as an integral value + const auto decimal_after_first_nonzero{ + decimal_position != std::basic_string::npos && + decimal_position > first_nonzero_position}; + const auto significant_digits{string.length() - first_nonzero_position - + (decimal_after_first_nonzero ? 1 : 0)}; + constexpr std::size_t MAX_SAFE_SIGNIFICANT_DIGITS{15}; + if (significant_digits > MAX_SAFE_SIGNIFICANT_DIGITS) { + return parse_number_decimal(line, column, string); + } + const auto result{sourcemeta::core::to_double(string)}; return result.has_value() ? JSON{result.value()} : parse_number_decimal(line, column, string); @@ -433,7 +452,9 @@ auto parse_number_fractional( std::basic_istream &stream, std::basic_ostringstream> - &result) -> JSON { + &result, + std::size_t &first_nonzero_position, const std::size_t decimal_position) + -> JSON { while (!stream.eof()) { const typename JSON::Char character{ static_cast(stream.peek())}; @@ -450,6 +471,10 @@ auto parse_number_fractional( stream, result); case internal::token_number_zero: + result.put(character); + stream.ignore(1); + column += 1; + break; case internal::token_number_one: case internal::token_number_two: case internal::token_number_three: @@ -459,13 +484,19 @@ auto parse_number_fractional( case internal::token_number_seven: case internal::token_number_eight: case internal::token_number_nine: + if (first_nonzero_position == + std::basic_string::npos) { + first_nonzero_position = result.str().size(); + } result.put(character); stream.ignore(1); column += 1; break; default: - return parse_number_real_maybe_decimal(line, original_column, - result.str()); + return parse_number_real_maybe_decimal( + line, original_column, result.str(), first_nonzero_position, + decimal_position); } } @@ -478,7 +509,9 @@ auto parse_number_fractional_first( std::basic_istream &stream, std::basic_ostringstream> - &result) -> JSON { + &result, + std::size_t &first_nonzero_position, const std::size_t decimal_position) + -> JSON { const typename JSON::Char character{ static_cast(stream.peek())}; switch (character) { @@ -490,6 +523,12 @@ auto parse_number_fractional_first( column += 1; throw JSONParseError(line, column); case internal::token_number_zero: + result.put(character); + stream.ignore(1); + column += 1; + return parse_number_fractional(line, column, original_column, stream, + result, first_nonzero_position, + decimal_position); case internal::token_number_one: case internal::token_number_two: case internal::token_number_three: @@ -499,14 +538,21 @@ auto parse_number_fractional_first( case internal::token_number_seven: case internal::token_number_eight: case internal::token_number_nine: + if (first_nonzero_position == + std::basic_string::npos) { + first_nonzero_position = result.str().size(); + } result.put(character); stream.ignore(1); column += 1; return parse_number_fractional(line, column, original_column, stream, - result); + result, first_nonzero_position, + decimal_position); default: - return parse_number_real_maybe_decimal(line, original_column, - result.str()); + return parse_number_real_maybe_decimal( + line, original_column, result.str(), first_nonzero_position, + decimal_position); } } @@ -516,19 +562,23 @@ auto parse_number_maybe_fractional( std::basic_istream &stream, std::basic_ostringstream> - &result) -> JSON { + &result, + std::size_t &first_nonzero_position) -> JSON { const typename JSON::Char character{ static_cast(stream.peek())}; switch (character) { // [A number] may have a fractional part prefixed by a decimal point // (U+002E). See // https://www.ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf - case internal::token_number_decimal_point: + case internal::token_number_decimal_point: { + const std::size_t decimal_position{result.str().size()}; result.put(character); stream.ignore(1); column += 1; - return JSON{parse_number_fractional_first(line, column, original_column, - stream, result)}; + return JSON{parse_number_fractional_first( + line, column, original_column, stream, result, first_nonzero_position, + decimal_position)}; + } case internal::token_number_exponent_uppercase: case internal::token_number_exponent_lowercase: result.put(character); @@ -559,7 +609,8 @@ auto parse_number_any_rest( std::basic_istream &stream, std::basic_ostringstream> - &result) -> JSON { + &result, + std::size_t &first_nonzero_position) -> JSON { while (!stream.eof()) { const typename JSON::Char character{ static_cast(stream.peek())}; @@ -567,12 +618,15 @@ auto parse_number_any_rest( // [A number] may have a fractional part prefixed by a decimal point // (U+002E). See // https://www.ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf - case internal::token_number_decimal_point: + case internal::token_number_decimal_point: { + const std::size_t decimal_position{result.str().size()}; result.put(character); stream.ignore(1); column += 1; - return JSON{parse_number_fractional_first(line, column, original_column, - stream, result)}; + return JSON{parse_number_fractional_first( + line, column, original_column, stream, result, + first_nonzero_position, decimal_position)}; + } case internal::token_number_exponent_uppercase: case internal::token_number_exponent_lowercase: result.put(character); @@ -609,7 +663,8 @@ auto parse_number_any_negative_first( std::basic_istream &stream, std::basic_ostringstream> - &result) -> JSON { + &result, + std::size_t &first_nonzero_position) -> JSON { const typename JSON::Char character{ static_cast(stream.get())}; column += 1; @@ -620,7 +675,8 @@ auto parse_number_any_negative_first( case internal::token_number_zero: result.put(character); return parse_number_maybe_fractional(line, column, original_column, - stream, result); + stream, result, + first_nonzero_position); case internal::token_number_one: case internal::token_number_two: case internal::token_number_three: @@ -630,9 +686,10 @@ auto parse_number_any_negative_first( case internal::token_number_seven: case internal::token_number_eight: case internal::token_number_nine: + first_nonzero_position = result.str().size(); result.put(character); return parse_number_any_rest(line, column, original_column, stream, - result); + result, first_nonzero_position); default: throw JSONParseError(line, column); } @@ -647,19 +704,24 @@ auto parse_number( result; result.put(first); + std::size_t first_nonzero_position{ + std::basic_string::npos}; + // A number is a sequence of decimal digits with no superfluous leading zero. // It may have a preceding minus sign (U+002D). See // https://www.ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf switch (first) { case internal::token_number_minus: return parse_number_any_negative_first(line, column, column, stream, - result); + result, first_nonzero_position); case internal::token_number_zero: - return parse_number_maybe_fractional(line, column, column, stream, - result); + return parse_number_maybe_fractional(line, column, column, stream, result, + first_nonzero_position); // Any other digit default: - return parse_number_any_rest(line, column, column, stream, result); + first_nonzero_position = 0; + return parse_number_any_rest(line, column, column, stream, result, + first_nonzero_position); } } diff --git a/vendor/core/src/core/jsonschema/CMakeLists.txt b/vendor/core/src/core/jsonschema/CMakeLists.txt index 216ba896..7048cb30 100644 --- a/vendor/core/src/core/jsonschema/CMakeLists.txt +++ b/vendor/core/src/core/jsonschema/CMakeLists.txt @@ -3,9 +3,9 @@ set(OFFICIAL_RESOLVER_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/official_resolver.cc") include(./official_resolver.cmake) sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME jsonschema - PRIVATE_HEADERS bundle.h resolver.h walker.h frame.h error.h + PRIVATE_HEADERS bundle.h walker.h frame.h error.h types.h transform.h - SOURCES jsonschema.cc official_walker.cc frame.cc resolver.cc + SOURCES jsonschema.cc official_walker.cc frame.cc walker.cc bundle.cc transformer.cc format.cc "${CMAKE_CURRENT_BINARY_DIR}/official_resolver.cc") diff --git a/vendor/core/src/core/jsonschema/frame.cc b/vendor/core/src/core/jsonschema/frame.cc index 7adc34ca..24a7f306 100644 --- a/vendor/core/src/core/jsonschema/frame.cc +++ b/vendor/core/src/core/jsonschema/frame.cc @@ -213,16 +213,15 @@ auto fragment_string(const sourcemeta::core::URI &uri) [[noreturn]] auto throw_already_exists(const sourcemeta::core::JSON::String &uri) -> void { - std::ostringstream error; - error << "Schema identifier already exists: " << uri; - throw sourcemeta::core::SchemaError(error.str()); + throw sourcemeta::core::SchemaFrameError(uri, + "Schema identifier already exists"); } auto store(sourcemeta::core::SchemaFrame::Locations &frame, sourcemeta::core::SchemaFrame::Instances &instances, const sourcemeta::core::SchemaReferenceType type, const sourcemeta::core::SchemaFrame::LocationType entry_type, - sourcemeta::core::JSON::String uri, + const sourcemeta::core::JSON::String &uri, const std::optional &root_id, const sourcemeta::core::JSON::String &base_id, const sourcemeta::core::Pointer &pointer_from_root, @@ -233,9 +232,8 @@ auto store(sourcemeta::core::SchemaFrame::Locations &frame, const std::optional &parent, const bool ignore_if_present = false, const bool already_canonical = false) -> void { - const auto canonical{already_canonical - ? std::move(uri) - : sourcemeta::core::URI::canonicalize(uri)}; + const auto canonical{ + already_canonical ? uri : sourcemeta::core::URI::canonicalize(uri)}; const auto inserted{frame .insert({{type, canonical}, {.parent = parent, @@ -832,8 +830,8 @@ auto SchemaFrame::analyse(const JSON &root, const SchemaWalker &walker, this->mode_ == SchemaFrame::Mode::Instances) { store(this->locations_, this->instances_, SchemaReferenceType::Static, - SchemaFrame::LocationType::Subschema, std::move(result), - root_id, current_base, pointer, + SchemaFrame::LocationType::Subschema, result, root_id, + current_base, pointer, pointer.resolve_from(nearest_bases.second), dialects.first.front(), current_base_dialect, {subschema->second.instance_location}, @@ -841,8 +839,8 @@ auto SchemaFrame::analyse(const JSON &root, const SchemaWalker &walker, } else { store(this->locations_, this->instances_, SchemaReferenceType::Static, - SchemaFrame::LocationType::Subschema, std::move(result), - root_id, current_base, pointer, + SchemaFrame::LocationType::Subschema, result, root_id, + current_base, pointer, pointer.resolve_from(nearest_bases.second), dialects.first.front(), current_base_dialect, {}, subschema->second.parent, false, true); @@ -850,8 +848,8 @@ auto SchemaFrame::analyse(const JSON &root, const SchemaWalker &walker, } else { store(this->locations_, this->instances_, SchemaReferenceType::Static, - SchemaFrame::LocationType::Pointer, std::move(result), - root_id, current_base, pointer, + SchemaFrame::LocationType::Pointer, result, root_id, + current_base, pointer, pointer.resolve_from(nearest_bases.second), dialects.first.front(), current_base_dialect, {}, dialects.second, false, true); @@ -903,9 +901,10 @@ auto SchemaFrame::analyse(const JSON &root, const SchemaWalker &walker, // See // https://json-schema.org/draft/2019-09/draft-handrews-json-schema-02#rfc.section.8.2.4.2.1 if (ref != "#") { - std::ostringstream error; - error << "Invalid recursive reference: " << ref; - throw sourcemeta::core::SchemaError(error.str()); + throw sourcemeta::core::SchemaReferenceError( + entry.id.value_or(""), + entry.common.pointer.concat({"$recursiveRef"}), + "Invalid recursive reference"); } auto anchor_uri_string{ diff --git a/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema.h b/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema.h index f4a9029a..7c6d5107 100644 --- a/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema.h +++ b/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema.h @@ -12,7 +12,6 @@ #include #include #include -#include #include #include #include diff --git a/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_error.h b/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_error.h index 25e4a16c..af29c9f8 100644 --- a/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_error.h +++ b/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_error.h @@ -25,13 +25,17 @@ namespace sourcemeta::core { /// An error that represents a general schema error event class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaError : public std::exception { public: - SchemaError(std::string message) : message_{std::move(message)} {} + SchemaError(const char *message) : message_{message} {} + SchemaError(std::string message) = delete; + SchemaError(std::string &&message) = delete; + SchemaError(std::string_view message) = delete; + [[nodiscard]] auto what() const noexcept -> const char * override { - return this->message_.c_str(); + return this->message_; } private: - std::string message_; + const char *message_; }; /// @ingroup jsonschema @@ -39,19 +43,24 @@ class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaError : public std::exception { class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaResolutionError : public std::exception { public: - SchemaResolutionError(std::string identifier, std::string message) - : identifier_{std::move(identifier)}, message_{std::move(message)} {} + SchemaResolutionError(std::string identifier, const char *message) + : identifier_{std::move(identifier)}, message_{message} {} + SchemaResolutionError(std::string identifier, std::string message) = delete; + SchemaResolutionError(std::string identifier, std::string &&message) = delete; + SchemaResolutionError(std::string identifier, + std::string_view message) = delete; + [[nodiscard]] auto what() const noexcept -> const char * override { - return this->message_.c_str(); + return this->message_; } - [[nodiscard]] auto id() const noexcept -> std::string_view { + [[nodiscard]] auto identifier() const noexcept -> std::string_view { return this->identifier_; } private: std::string identifier_; - std::string message_; + const char *message_; }; /// @ingroup jsonschema @@ -72,10 +81,14 @@ class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaRelativeMetaschemaResolutionError class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaVocabularyError : public std::exception { public: - SchemaVocabularyError(std::string uri, std::string message) - : uri_{std::move(uri)}, message_{std::move(message)} {} + SchemaVocabularyError(std::string uri, const char *message) + : uri_{std::move(uri)}, message_{message} {} + SchemaVocabularyError(std::string uri, std::string message) = delete; + SchemaVocabularyError(std::string uri, std::string &&message) = delete; + SchemaVocabularyError(std::string uri, std::string_view message) = delete; + [[nodiscard]] auto what() const noexcept -> const char * override { - return this->message_.c_str(); + return this->message_; } [[nodiscard]] auto uri() const noexcept -> std::string_view { @@ -84,7 +97,7 @@ class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaVocabularyError private: std::string uri_; - std::string message_; + const char *message_; }; /// @ingroup jsonschema @@ -93,15 +106,21 @@ class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaReferenceError : public std::exception { public: SchemaReferenceError(std::string identifier, Pointer schema_location, - std::string message) + const char *message) : identifier_{std::move(identifier)}, - schema_location_{std::move(schema_location)}, - message_{std::move(message)} {} + schema_location_{std::move(schema_location)}, message_{message} {} + SchemaReferenceError(std::string identifier, Pointer schema_location, + std::string message) = delete; + SchemaReferenceError(std::string identifier, Pointer schema_location, + std::string &&message) = delete; + SchemaReferenceError(std::string identifier, Pointer schema_location, + std::string_view message) = delete; + [[nodiscard]] auto what() const noexcept -> const char * override { - return this->message_.c_str(); + return this->message_; } - [[nodiscard]] auto id() const noexcept -> std::string_view { + [[nodiscard]] auto identifier() const noexcept -> std::string_view { return this->identifier_; } @@ -112,7 +131,7 @@ class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaReferenceError private: std::string identifier_; Pointer schema_location_; - std::string message_; + const char *message_; }; /// @ingroup jsonschema @@ -127,13 +146,17 @@ class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaBrokenReferenceError class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaAbortError : public std::exception { public: - SchemaAbortError(std::string message) : message_{std::move(message)} {} + SchemaAbortError(const char *message) : message_{message} {} + SchemaAbortError(std::string message) = delete; + SchemaAbortError(std::string &&message) = delete; + SchemaAbortError(std::string_view message) = delete; + [[nodiscard]] auto what() const noexcept -> const char * override { - return this->message_.c_str(); + return this->message_; } private: - std::string message_; + const char *message_; }; /// @ingroup jsonschema @@ -142,6 +165,7 @@ class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaUnknownDialectError : public std::exception { public: SchemaUnknownDialectError() = default; + [[nodiscard]] auto what() const noexcept -> const char * override { return "Could not determine the dialect of the schema"; } @@ -154,6 +178,7 @@ class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaUnknownBaseDialectError : public std::exception { public: SchemaUnknownBaseDialectError() = default; + [[nodiscard]] auto what() const noexcept -> const char * override { return "Could not determine the base dialect of the schema"; } @@ -208,6 +233,50 @@ class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaReferenceObjectResourceError std::string identifier_; }; +/// @ingroup jsonschema +/// An error that represents an unrecognized base dialect +class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaBaseDialectError + : public std::exception { +public: + SchemaBaseDialectError(std::string base_dialect) + : base_dialect_{std::move(base_dialect)} {} + + [[nodiscard]] auto what() const noexcept -> const char * override { + return "Unrecognized base dialect"; + } + + [[nodiscard]] auto base_dialect() const noexcept -> std::string_view { + return this->base_dialect_; + } + +private: + std::string base_dialect_; +}; + +/// @ingroup jsonschema +/// An error that represents a schema frame error +class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaFrameError + : public std::exception { +public: + SchemaFrameError(std::string identifier, const char *message) + : identifier_{std::move(identifier)}, message_{message} {} + SchemaFrameError(std::string identifier, std::string message) = delete; + SchemaFrameError(std::string identifier, std::string &&message) = delete; + SchemaFrameError(std::string identifier, std::string_view message) = delete; + + [[nodiscard]] auto what() const noexcept -> const char * override { + return this->message_; + } + + [[nodiscard]] auto identifier() const noexcept -> std::string_view { + return this->identifier_; + } + +private: + std::string identifier_; + const char *message_; +}; + #if defined(_MSC_VER) #pragma warning(default : 4251 4275) #endif diff --git a/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_frame.h b/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_frame.h index 005432bd..fb5c61fa 100644 --- a/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_frame.h +++ b/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_frame.h @@ -8,7 +8,6 @@ #include #include -#include #include #include diff --git a/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_resolver.h b/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_resolver.h deleted file mode 100644 index 29437431..00000000 --- a/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_resolver.h +++ /dev/null @@ -1,78 +0,0 @@ -#ifndef SOURCEMETA_CORE_JSONSCHEMA_RESOLVER_H_ -#define SOURCEMETA_CORE_JSONSCHEMA_RESOLVER_H_ - -#ifndef SOURCEMETA_CORE_JSONSCHEMA_EXPORT -#include -#endif - -#include -#include - -#include // std::function -#include // std::map -#include // std::optional -#include // std::string_view - -namespace sourcemeta::core { - -/// @ingroup jsonschema -/// This is a convenient helper for constructing schema resolvers at runtime. -/// For example: -/// -/// ```cpp -/// #include -/// #include -/// -/// // (1) Create a map resolver that falls back to the official resolver -/// sourcemeta::core::SchemaMapResolver -/// resolver{sourcemeta::core::schema_official_resolver}; -/// -/// const sourcemeta::core::JSON schema = -/// sourcemeta::core::parse_json(R"JSON({ -/// "$id": "https://www.example.com" -/// "$schema": "https://json-schema.org/draft/2020-12/schema", -/// "type": "string" -/// })JSON"); -/// -/// // (2) Register a schema -/// resolver.add(schema); -/// -/// assert(resolver("https://www.example.com").has_value()); -/// ``` -class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaMapResolver { -public: - /// Construct an empty resolver. If you don't add schemas to it, it will - /// always resolve to nothing - SchemaMapResolver(); - - /// Construct an empty resolver that has another schema resolver as a fallback - SchemaMapResolver(SchemaResolver resolver); - - /// Register a schema to the map resolver. Returns whether at least one - /// schema was imported into the resolver - auto add(const JSON &schema, - const std::optional &default_dialect = std::nullopt, - const std::optional &default_id = std::nullopt, - const std::function &callback = nullptr) - -> bool; - - /// Attempt to resolve a schema - auto operator()(std::string_view identifier) const -> std::optional; - -private: -// Exporting symbols that depends on the standard C++ library is considered -// safe. -// https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-2-c4275?view=msvc-170&redirectedfrom=MSDN -#if defined(_MSC_VER) -#pragma warning(disable : 4251) -#endif - std::map schemas{}; - SchemaResolver default_resolver = nullptr; -#if defined(_MSC_VER) -#pragma warning(default : 4251) -#endif -}; - -} // namespace sourcemeta::core - -#endif diff --git a/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_transform.h b/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_transform.h index 8d9da8ab..7c1fe1e8 100644 --- a/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_transform.h +++ b/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_transform.h @@ -8,8 +8,6 @@ #include #include -#include - #include // assert #include // std::derived_from, std::same_as #include // std::function @@ -85,7 +83,7 @@ class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaTransformRule { /// The result of evaluating a rule struct Result { Result(const bool applies_) : applies{applies_} {} - Result(Pointer pointer) : applies{true}, locations{std::move(pointer)} { + Result(const Pointer &pointer) : applies{true}, locations{pointer} { assert(this->locations.size() == 1); } diff --git a/vendor/core/src/core/jsonschema/jsonschema.cc b/vendor/core/src/core/jsonschema/jsonschema.cc index b7112f29..7bbbaa97 100644 --- a/vendor/core/src/core/jsonschema/jsonschema.cc +++ b/vendor/core/src/core/jsonschema/jsonschema.cc @@ -4,7 +4,6 @@ #include // std::uint64_t #include // std::numeric_limits #include // std::accumulate -#include // std::ostringstream #include // std::remove_reference_t #include // std::unordered_map #include // std::move @@ -45,9 +44,7 @@ static auto id_keyword(const std::string &base_dialect) -> std::string { return "id"; } - std::ostringstream error; - error << "Unrecognized base dialect: " << base_dialect; - throw sourcemeta::core::SchemaError(error.str()); + throw sourcemeta::core::SchemaBaseDialectError(base_dialect); } } // namespace @@ -90,9 +87,8 @@ auto sourcemeta::core::identify(const JSON &schema, const auto &identifier{schema.at(keyword)}; if (!identifier.is_string() || identifier.empty()) { - std::ostringstream error; - error << "The value of the " << keyword << " property is not valid"; - throw sourcemeta::core::SchemaError(error.str()); + throw sourcemeta::core::SchemaError( + "The schema identifier property is invalid"); } // In older drafts, the presence of `$ref` would override any sibling @@ -272,9 +268,7 @@ auto core_vocabulary(std::string_view base_dialect) -> std::string { "https://json-schema.org/draft/2019-09/hyper-schema") { return "https://json-schema.org/draft/2019-09/vocab/core"; } else { - std::ostringstream error; - error << "Unrecognized base dialect: " << base_dialect; - throw sourcemeta::core::SchemaError(error.str()); + throw sourcemeta::core::SchemaBaseDialectError(std::string{base_dialect}); } } } // namespace diff --git a/vendor/core/src/core/jsonschema/resolver.cc b/vendor/core/src/core/jsonschema/resolver.cc deleted file mode 100644 index df6175dc..00000000 --- a/vendor/core/src/core/jsonschema/resolver.cc +++ /dev/null @@ -1,71 +0,0 @@ -#include - -#include // assert -#include // std::ostringstream - -namespace sourcemeta::core { - -SchemaMapResolver::SchemaMapResolver() = default; - -SchemaMapResolver::SchemaMapResolver(SchemaResolver resolver) - : default_resolver{std::move(resolver)} {} - -auto SchemaMapResolver::add( - const JSON &schema, const std::optional &default_dialect, - const std::optional &default_id, - const std::function &callback) -> bool { - assert(sourcemeta::core::is_schema(schema)); - - // Registering the top-level schema is not enough. We need to check - // and register every embedded schema resource too - SchemaFrame frame{SchemaFrame::Mode::References}; - frame.analyse(schema, schema_official_walker, *this, default_dialect, - default_id); - - bool added_any_schema{false}; - for (const auto &[key, entry] : frame.locations()) { - if (entry.type != SchemaFrame::LocationType::Resource) { - continue; - } - - auto subschema{get(schema, entry.pointer)}; - const auto subschema_vocabularies{frame.vocabularies(entry, *this)}; - - // Given we might be resolving embedded resources, we fully - // resolve their dialect and identifiers, otherwise the - // consumer might have no idea what to do with them - subschema.assign("$schema", JSON{entry.dialect}); - reidentify(subschema, key.second, entry.base_dialect); - - const auto result{this->schemas.emplace(key.second, subschema)}; - if (!result.second && result.first->second != schema) { - std::ostringstream error; - error << "Cannot register the same identifier twice: " << key.second; - throw SchemaError(error.str()); - } - - if (callback) { - callback(key.second); - } - - added_any_schema = true; - } - - return added_any_schema; -} - -auto SchemaMapResolver::operator()(std::string_view identifier) const - -> std::optional { - const std::string string_identifier{identifier}; - if (this->schemas.contains(string_identifier)) { - return this->schemas.at(string_identifier); - } - - if (this->default_resolver) { - return this->default_resolver(identifier); - } - - return std::nullopt; -} - -} // namespace sourcemeta::core diff --git a/vendor/core/src/lang/numeric/decimal.cc b/vendor/core/src/lang/numeric/decimal.cc index f1f5328f..d7a40eee 100644 --- a/vendor/core/src/lang/numeric/decimal.cc +++ b/vendor/core/src/lang/numeric/decimal.cc @@ -52,11 +52,24 @@ auto Decimal::data() const -> const Data * { namespace { +// One-time global initialization of MPD_MINALLOC +// This must happen before any thread-local contexts are initialized +[[maybe_unused]] static const bool minalloc_initialized = []() { + constexpr mpd_ssize_t precision{16}; + const mpd_ssize_t ideal_minalloc = + 2 * ((precision + MPD_RDIGITS - 1) / MPD_RDIGITS); + mpd_setminalloc(ideal_minalloc); + return true; +}(); + // Thread-local context for decimal arithmetic operations // Matches the C++ wrapper context_template settings (16 digit precision) +// Note: We use mpd_defaultcontext + mpd_qsetprec instead of mpd_init +// to avoid calling mpd_setminalloc multiple times (once per thread) thread_local mpd_context_t decimal_context = []() { mpd_context_t context; - mpd_init(&context, 16); + mpd_defaultcontext(&context); + mpd_qsetprec(&context, 16); context.emax = 999999; context.emin = -999999; context.round = MPD_ROUND_HALF_EVEN; @@ -292,11 +305,23 @@ Decimal::Decimal(const std::string &string_value) Decimal::Decimal(const std::string_view string_value) : Decimal{std::string{string_value}} {} -auto Decimal::nan() -> Decimal { return Decimal{"NaN"}; } +auto Decimal::nan() -> Decimal { + Decimal result; + mpd_setspecial(&result.data()->value, MPD_POS, MPD_NAN); + return result; +} -auto Decimal::infinity() -> Decimal { return Decimal{"Infinity"}; } +auto Decimal::infinity() -> Decimal { + Decimal result; + mpd_setspecial(&result.data()->value, MPD_POS, MPD_INF); + return result; +} -auto Decimal::negative_infinity() -> Decimal { return Decimal{"-Infinity"}; } +auto Decimal::negative_infinity() -> Decimal { + Decimal result; + mpd_setspecial(&result.data()->value, MPD_NEG, MPD_INF); + return result; +} auto Decimal::to_scientific_string() const -> std::string { // Note that `mpd_to_sci`, contrary to its name, does NOT guarantee