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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions src/output/include/sourcemeta/blaze/output_standard.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#endif

#include <sourcemeta/core/json.h>
#include <sourcemeta/core/jsonpointer.h>

#include <sourcemeta/blaze/evaluator.h>

Expand Down Expand Up @@ -77,6 +78,58 @@ standard(Evaluator &evaluator, const Template &schema,
const sourcemeta::core::JSON &instance, const StandardOutput format)
-> sourcemeta::core::JSON;

/// @ingroup output
/// Perform JSON Schema evaluation using Standard Output formats, augmenting
/// error and annotation unit objects with an `instancePosition` array property
/// (`[ lineStart, columnStart, lineEnd, columnEnd ]`) based on JSON pointer
/// positions. For example:
///
/// ```cpp
/// #include <sourcemeta/blaze/compiler.h>
/// #include <sourcemeta/blaze/evaluator.h>
/// #include <sourcemeta/blaze/output.h>
///
/// #include <sourcemeta/core/json.h>
/// #include <sourcemeta/core/jsonpointer.h>
/// #include <sourcemeta/core/jsonschema.h>
///
/// #include <cassert>
/// #include <sstream>
/// #include <functional>
///
/// const sourcemeta::core::JSON schema =
/// sourcemeta::core::parse_json(R"JSON({
/// "$schema": "https://json-schema.org/draft/2020-12/schema",
/// "type": "string"
/// })JSON");
///
/// const auto schema_template{sourcemeta::blaze::compile(
/// schema, sourcemeta::core::schema_official_walker,
/// sourcemeta::core::schema_official_resolver,
/// sourcemeta::core::default_schema_compiler)};
///
/// const std::string input{"\"foo bar\""};
/// sourcemeta::core::PointerPositionTracker tracker;
/// const auto instance{
/// sourcemeta::core::parse_json(input, std::ref(tracker))};
///
/// sourcemeta::blaze::Evaluator evaluator;
///
/// const auto result{sourcemeta::blaze::standard(
/// evaluator, schema_template, instance,
/// sourcemeta::blaze::StandardOutput::Basic, tracker)};
///
/// assert(result.is_object());
/// assert(result.defines("valid"));
/// assert(result.at("valid").is_boolean());
/// assert(result.at("valid").to_boolean());
/// ```
auto SOURCEMETA_BLAZE_OUTPUT_EXPORT
standard(Evaluator &evaluator, const Template &schema,
const sourcemeta::core::JSON &instance, const StandardOutput format,
const sourcemeta::core::PointerPositionTracker &positions)
-> sourcemeta::core::JSON;

} // namespace sourcemeta::blaze

#endif
77 changes: 77 additions & 0 deletions src/output/output_standard.cc
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,81 @@ auto standard(Evaluator &evaluator, const Template &schema,
}
}

auto standard(Evaluator &evaluator, const Template &schema,
const sourcemeta::core::JSON &instance,
const StandardOutput format,
const sourcemeta::core::PointerPositionTracker &positions)
-> sourcemeta::core::JSON {
// We avoid a callback for this specific case for performance reasons
if (format == StandardOutput::Flag) {
auto result{sourcemeta::core::JSON::make_object()};
const auto valid{evaluator.validate(schema, instance)};
result.assign("valid", sourcemeta::core::JSON{valid});
return result;
} else {
assert(format == StandardOutput::Basic);
SimpleOutput output{instance};
const auto valid{evaluator.validate(schema, instance, std::ref(output))};

if (valid) {
auto result{sourcemeta::core::JSON::make_object()};
result.assign("valid", sourcemeta::core::JSON{valid});
auto annotations{sourcemeta::core::JSON::make_array()};
for (const auto &annotation : output.annotations()) {
auto unit{sourcemeta::core::JSON::make_object()};
unit.assign("keywordLocation",
sourcemeta::core::to_json(annotation.first.evaluate_path));
unit.assign("absoluteKeywordLocation",
sourcemeta::core::JSON{annotation.first.schema_location});
unit.assign(
"instanceLocation",
sourcemeta::core::to_json(annotation.first.instance_location));
unit.assign("annotation", sourcemeta::core::to_json(annotation.second));

const auto position{positions.get(
sourcemeta::core::to_pointer(annotation.first.instance_location))};
if (position.has_value()) {
unit.assign("instancePosition",
sourcemeta::core::to_json(position.value()));
}

annotations.push_back(std::move(unit));
}

if (!annotations.empty()) {
result.assign("annotations", std::move(annotations));
}

return result;
} else {
auto result{sourcemeta::core::JSON::make_object()};
result.assign("valid", sourcemeta::core::JSON{valid});
auto errors{sourcemeta::core::JSON::make_array()};
for (const auto &entry : output) {
auto unit{sourcemeta::core::JSON::make_object()};
unit.assign("keywordLocation",
sourcemeta::core::to_json(entry.evaluate_path));
unit.assign("absoluteKeywordLocation",
sourcemeta::core::JSON{entry.schema_location});
unit.assign("instanceLocation",
sourcemeta::core::to_json(entry.instance_location));
unit.assign("error", sourcemeta::core::JSON{entry.message});

const auto position{positions.get(
sourcemeta::core::to_pointer(entry.instance_location))};
if (position.has_value()) {
unit.assign("instancePosition",
sourcemeta::core::to_json(position.value()));
}

errors.push_back(std::move(unit));
}

assert(!errors.empty());
result.assign("errors", std::move(errors));
return result;
}
}
}

} // namespace sourcemeta::blaze
145 changes: 145 additions & 0 deletions test/output/output_standard_basic_test.cc
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
#include <gtest/gtest.h>

#include <functional>
#include <sstream>

#include <sourcemeta/blaze/compiler.h>
#include <sourcemeta/blaze/evaluator.h>
#include <sourcemeta/blaze/output.h>

#include <sourcemeta/core/jsonpointer.h>

TEST(Output_standard_basic, prettify_annotations) {
const auto schema{sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
Expand Down Expand Up @@ -317,3 +320,145 @@ TEST(Output_standard_basic, failure_1) {

EXPECT_EQ(result, expected);
}

TEST(Output_standard_basic, positions_annotations) {
const auto schema{sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"foo": { "type": "string" }
}
})JSON")};

const auto schema_template{sourcemeta::blaze::compile(
schema, sourcemeta::core::schema_official_walker,
sourcemeta::core::schema_official_resolver,
sourcemeta::blaze::default_schema_compiler,
sourcemeta::blaze::Mode::Exhaustive)};

const std::string input{R"JSON({
"foo": "bar"
})JSON"};

sourcemeta::core::PointerPositionTracker tracker;
const auto instance{sourcemeta::core::parse_json(input, std::ref(tracker))};

sourcemeta::blaze::Evaluator evaluator;
const auto result{sourcemeta::blaze::standard(
evaluator, schema_template, instance,
sourcemeta::blaze::StandardOutput::Basic, tracker)};

const auto expected{sourcemeta::core::parse_json(R"JSON({
"valid": true,
"annotations": [
{
"keywordLocation": "/properties",
"absoluteKeywordLocation": "#/properties",
"instanceLocation": "",
"annotation": [ "foo" ],
"instancePosition": [ 1, 1, 3, 1 ]
}
]
})JSON")};

EXPECT_EQ(result, expected);
}

TEST(Output_standard_basic, positions_errors) {
const auto schema{sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"foo": { "type": "string" }
}
})JSON")};

const auto schema_template{sourcemeta::blaze::compile(
schema, sourcemeta::core::schema_official_walker,
sourcemeta::core::schema_official_resolver,
sourcemeta::blaze::default_schema_compiler,
sourcemeta::blaze::Mode::FastValidation)};

const std::string input{R"JSON({
"foo": 1
})JSON"};

sourcemeta::core::PointerPositionTracker tracker;
const auto instance{sourcemeta::core::parse_json(input, std::ref(tracker))};

sourcemeta::blaze::Evaluator evaluator;
const auto result{sourcemeta::blaze::standard(
evaluator, schema_template, instance,
sourcemeta::blaze::StandardOutput::Basic, tracker)};

const auto expected{sourcemeta::core::parse_json(R"JSON({
"valid": false,
"errors": [
{
"keywordLocation": "/properties/foo/type",
"absoluteKeywordLocation": "#/properties/foo/type",
"instanceLocation": "/foo",
"error": "The value was expected to be of type string but it was of type integer",
"instancePosition": [ 2, 3, 2, 10 ]
}
]
})JSON")};

EXPECT_EQ(result, expected);
}

TEST(Output_standard_basic, positions_flag_success) {
const auto schema{sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "string"
})JSON")};

const auto schema_template{sourcemeta::blaze::compile(
schema, sourcemeta::core::schema_official_walker,
sourcemeta::core::schema_official_resolver,
sourcemeta::blaze::default_schema_compiler,
sourcemeta::blaze::Mode::FastValidation)};

const std::string input{"\"hello\""};

sourcemeta::core::PointerPositionTracker tracker;
const auto instance{sourcemeta::core::parse_json(input, std::ref(tracker))};

sourcemeta::blaze::Evaluator evaluator;
const auto result{sourcemeta::blaze::standard(
evaluator, schema_template, instance,
sourcemeta::blaze::StandardOutput::Flag, tracker)};

const auto expected{sourcemeta::core::parse_json(R"JSON({
"valid": true
})JSON")};

EXPECT_EQ(result, expected);
}

TEST(Output_standard_basic, positions_flag_failure) {
const auto schema{sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "string"
})JSON")};

const auto schema_template{sourcemeta::blaze::compile(
schema, sourcemeta::core::schema_official_walker,
sourcemeta::core::schema_official_resolver,
sourcemeta::blaze::default_schema_compiler,
sourcemeta::blaze::Mode::FastValidation)};

const std::string input{"42"};

sourcemeta::core::PointerPositionTracker tracker;
const auto instance{sourcemeta::core::parse_json(input, std::ref(tracker))};

sourcemeta::blaze::Evaluator evaluator;
const auto result{sourcemeta::blaze::standard(
evaluator, schema_template, instance,
sourcemeta::blaze::StandardOutput::Flag, tracker)};

const auto expected{sourcemeta::core::parse_json(R"JSON({
"valid": false
})JSON")};

EXPECT_EQ(result, expected);
}