Skip to content

Commit

Permalink
Implement array loops in the schema compiler (#631)
Browse files Browse the repository at this point in the history
Signed-off-by: Juan Cruz Viotti <jv@jviotti.com>
  • Loading branch information
jviotti committed May 2, 2024
1 parent a7fb1ff commit b1c633e
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 3 deletions.
29 changes: 28 additions & 1 deletion src/jsonschema/compile_evaluate.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#include <cassert> // assert
#include <functional> // std::reference_wrapper
#include <iterator> // std::distance
#include <map> // std::map
#include <set> // std::set
#include <type_traits> // std::is_same_v
Expand Down Expand Up @@ -360,6 +361,32 @@ auto evaluate_step(

context.pop();
}
} else if (std::holds_alternative<SchemaCompilerLoopItems>(step)) {
const auto &loop{std::get<SchemaCompilerLoopItems>(step)};
context.push(loop);
EVALUATE_CONDITION_GUARD(loop.condition, instance);
const auto &target{context.resolve_target<JSON>(loop.target, instance)};
assert(target.is_array());
const auto &array{target.as_array()};
result = true;
for (auto iterator{array.cbegin()}; iterator != array.cend(); ++iterator) {
const auto index{std::distance(array.cbegin(), iterator)};
context.push(empty_pointer, {static_cast<Pointer::Token::Index>(index)});
for (const auto &child : loop.children) {
if (!evaluate_step(child, instance, mode, callback, context)) {
result = false;
if (mode == SchemaCompilerEvaluationMode::Fast) {
context.pop();
// For efficiently breaking from the outer loop too
goto evaluate_step_end;
} else {
break;
}
}

context.pop();
}
}
}

#undef EVALUATE_CONDITION_GUARD
Expand All @@ -368,7 +395,7 @@ auto evaluate_step(
context.value(nullptr));
context.pop();
return result;
} // namespace
}

} // namespace

Expand Down
8 changes: 8 additions & 0 deletions src/jsonschema/compile_json.cc
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,14 @@ struct StepVisitor {
loop.relative_instance_location, loop.keyword_location, loop.children,
loop.condition);
}

auto operator()(const sourcemeta::jsontoolkit::SchemaCompilerLoopItems &loop)
const -> sourcemeta::jsontoolkit::JSON {
return step_applicator_to_json(
"loop", "items", loop.target, loop.relative_schema_location,
loop.relative_instance_location, loop.keyword_location, loop.children,
loop.condition);
}
};

auto step_to_json(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ struct SchemaCompilerLogicalNot;
/// Represents a compiler step that loops over object properties
struct SchemaCompilerLoopProperties;

/// @ingroup jsonschema
/// Represents a compiler step that loops over array items
struct SchemaCompilerLoopItems;

/// @ingroup jsonschema
/// Represents a compiler step that consists of a mark to jump to
struct SchemaCompilerControlLabel;
Expand All @@ -141,8 +145,8 @@ using SchemaCompilerTemplate = std::vector<std::variant<
SchemaCompilerAssertionNotContains, SchemaCompilerAnnotationPublic,
SchemaCompilerAnnotationPrivate, SchemaCompilerLogicalOr,
SchemaCompilerLogicalAnd, SchemaCompilerLogicalNot,
SchemaCompilerLoopProperties, SchemaCompilerControlLabel,
SchemaCompilerControlJump>>;
SchemaCompilerLoopProperties, SchemaCompilerLoopItems,
SchemaCompilerControlLabel, SchemaCompilerControlJump>>;

#if !defined(DOXYGEN)
#define DEFINE_STEP_WITH_VALUE(category, name, type) \
Expand Down Expand Up @@ -185,6 +189,7 @@ DEFINE_STEP_APPLICATOR(Logical, Or)
DEFINE_STEP_APPLICATOR(Logical, And)
DEFINE_STEP_APPLICATOR(Logical, Not)
DEFINE_STEP_APPLICATOR(Loop, Properties)
DEFINE_STEP_APPLICATOR(Loop, Items)
DEFINE_CONTROL(Label)
DEFINE_CONTROL(Jump)

Expand Down
120 changes: 120 additions & 0 deletions test/jsonschema/jsonschema_compile_evaluate_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -715,3 +715,123 @@ TEST(JSONSchema_compile_evaluate, fast_step_not_type_false) {
const auto result{evaluate(steps, instance)};
EXPECT_FALSE(result);
}

TEST(JSONSchema_compile_evaluate, fast_loop_items_empty) {
using namespace sourcemeta::jsontoolkit;

const SchemaCompilerTemplate children{
SchemaCompilerAssertionType{{SchemaCompilerTargetType::Instance, {}},
Pointer{},
Pointer{},
"#/loop/type",
SchemaCompilerValueType{JSON::Type::String},
{}}};

const SchemaCompilerTemplate steps{
SchemaCompilerLoopItems{{SchemaCompilerTargetType::Instance, {}},
{"loop"},
{},
"#/loop",
children,
{}}};

const JSON instance{parse("[]")};
const auto result{evaluate(steps, instance)};
EXPECT_TRUE(result);
}

TEST(JSONSchema_compile_evaluate, fast_loop_items_single_true) {
using namespace sourcemeta::jsontoolkit;

const SchemaCompilerTemplate children{
SchemaCompilerAssertionType{{SchemaCompilerTargetType::Instance, {}},
Pointer{},
Pointer{},
"#/loop/type",
SchemaCompilerValueType{JSON::Type::String},
{}}};

const SchemaCompilerTemplate steps{
SchemaCompilerLoopItems{{SchemaCompilerTargetType::Instance, {}},
{"loop"},
{},
"#/loop",
children,
{}}};

const JSON instance{parse("[ \"foo\" ]")};
const auto result{evaluate(steps, instance)};
EXPECT_TRUE(result);
}

TEST(JSONSchema_compile_evaluate, fast_loop_items_single_false) {
using namespace sourcemeta::jsontoolkit;

const SchemaCompilerTemplate children{
SchemaCompilerAssertionType{{SchemaCompilerTargetType::Instance, {}},
Pointer{},
Pointer{},
"#/loop/type",
SchemaCompilerValueType{JSON::Type::String},
{}}};

const SchemaCompilerTemplate steps{
SchemaCompilerLoopItems{{SchemaCompilerTargetType::Instance, {}},
{"loop"},
{},
"#/loop",
children,
{}}};

const JSON instance{parse("[ 5 ]")};
const auto result{evaluate(steps, instance)};
EXPECT_FALSE(result);
}

TEST(JSONSchema_compile_evaluate, fast_loop_items_multi_true) {
using namespace sourcemeta::jsontoolkit;

const SchemaCompilerTemplate children{
SchemaCompilerAssertionType{{SchemaCompilerTargetType::Instance, {}},
Pointer{},
Pointer{},
"#/loop/type",
SchemaCompilerValueType{JSON::Type::String},
{}}};

const SchemaCompilerTemplate steps{
SchemaCompilerLoopItems{{SchemaCompilerTargetType::Instance, {}},
{"loop"},
{},
"#/loop",
children,
{}}};

const JSON instance{parse("[ \"foo\", \"bar\", \"baz\" ]")};
const auto result{evaluate(steps, instance)};
EXPECT_TRUE(result);
}

TEST(JSONSchema_compile_evaluate, fast_loop_items_multi_false) {
using namespace sourcemeta::jsontoolkit;

const SchemaCompilerTemplate children{
SchemaCompilerAssertionType{{SchemaCompilerTargetType::Instance, {}},
Pointer{},
Pointer{},
"#/loop/type",
SchemaCompilerValueType{JSON::Type::String},
{}}};

const SchemaCompilerTemplate steps{
SchemaCompilerLoopItems{{SchemaCompilerTargetType::Instance, {}},
{"loop"},
{},
"#/loop",
children,
{}}};

const JSON instance{parse("[ \"foo\", 4, \"baz\" ]")};
const auto result{evaluate(steps, instance)};
EXPECT_FALSE(result);
}

0 comments on commit b1c633e

Please sign in to comment.