Skip to content

Commit

Permalink
Introduce a new SchemaCompilerAssertionTypeObjectBounded (#1223)
Browse files Browse the repository at this point in the history
Signed-off-by: Juan Cruz Viotti <[email protected]>
  • Loading branch information
jviotti authored Sep 24, 2024
1 parent 0fd8c92 commit b252e8b
Show file tree
Hide file tree
Showing 8 changed files with 309 additions and 5 deletions.
22 changes: 22 additions & 0 deletions src/jsonschema/compile_describe.cc
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,28 @@ struct DescribeVisitor {
return message.str();
}

auto operator()(const SchemaCompilerAssertionTypeObjectBounded &step) const
-> std::string {
std::ostringstream message;

const auto minimum{std::get<0>(step.value)};
const auto maximum{std::get<1>(step.value)};
if (minimum == 0 && maximum.has_value()) {
message << "The value was expected to consist of an object of at most "
<< maximum.value()
<< (maximum.value() == 1 ? " property" : " properties");
} else if (maximum.has_value()) {
message << "The value was expected to consist of an object of " << minimum
<< " to " << maximum.value()
<< (maximum.value() == 1 ? " property" : " properties");
} else {
message << "The value was expected to consist of an object of at least "
<< minimum << (minimum == 1 ? " property" : " properties");
}

return message.str();
}

auto operator()(const SchemaCompilerAssertionRegex &step) const
-> std::string {
assert(this->target.is_string());
Expand Down
12 changes: 12 additions & 0 deletions src/jsonschema/compile_evaluate.cc
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,18 @@ auto evaluate_step(
result = target.type() == JSON::Type::Array && target.size() >= minimum &&
(!maximum.has_value() || target.size() <= maximum.value());
EVALUATE_END(assertion, SchemaCompilerAssertionTypeArrayBounded);
} else if (IS_STEP(SchemaCompilerAssertionTypeObjectBounded)) {
EVALUATE_BEGIN_NO_PRECONDITION(assertion,
SchemaCompilerAssertionTypeObjectBounded);
const auto &target{context.resolve_target()};
const auto minimum{std::get<0>(assertion.value)};
const auto maximum{std::get<1>(assertion.value)};
assert(!maximum.has_value() || maximum.value() >= minimum);
// Require early breaking
assert(!std::get<2>(assertion.value));
result = target.type() == JSON::Type::Object && target.size() >= minimum &&
(!maximum.has_value() || target.size() <= maximum.value());
EVALUATE_END(assertion, SchemaCompilerAssertionTypeObjectBounded);
} else if (IS_STEP(SchemaCompilerAssertionRegex)) {
EVALUATE_BEGIN(assertion, SchemaCompilerAssertionRegex, target.is_string());
result = std::regex_search(target.to_string(), assertion.value.first);
Expand Down
2 changes: 2 additions & 0 deletions src/jsonschema/compile_json.cc
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,8 @@ struct StepVisitor {
SchemaCompilerAssertionTypeStringBounded)
HANDLE_STEP("assertion", "type-array-bounded",
SchemaCompilerAssertionTypeArrayBounded)
HANDLE_STEP("assertion", "type-object-bounded",
SchemaCompilerAssertionTypeObjectBounded)
HANDLE_STEP("assertion", "regex", SchemaCompilerAssertionRegex)
HANDLE_STEP("assertion", "string-size-less",
SchemaCompilerAssertionStringSizeLess)
Expand Down
28 changes: 24 additions & 4 deletions src/jsonschema/default_compiler_draft4.h
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,16 @@ auto compiler_draft4_validation_type(
return {make<SchemaCompilerAssertionTypeStrict>(
true, context, schema_context, dynamic_context, JSON::Type::Boolean)};
} else if (type == "object") {
const auto minimum{
unsigned_integer_property(schema_context.schema, "minProperties", 0)};
const auto maximum{
unsigned_integer_property(schema_context.schema, "maxProperties")};
if (minimum > 0 || maximum.has_value()) {
return {make<SchemaCompilerAssertionTypeObjectBounded>(
true, context, schema_context, dynamic_context,
{minimum, maximum, false})};
}

return {make<SchemaCompilerAssertionTypeStrict>(
true, context, schema_context, dynamic_context, JSON::Type::Object)};
} else if (type == "array") {
Expand Down Expand Up @@ -1164,8 +1174,13 @@ auto compiler_draft4_validation_maxproperties(
return {};
}

// TODO: As an optimization, if `minProperties` is set to the same number, do
// a single size equality assertion
// We'll handle it at the type level as an optimization
if (schema_context.schema.defines("type") &&
schema_context.schema.at("type").is_string() &&
schema_context.schema.at("type").to_string() == "object") {
return {};
}

return {make<SchemaCompilerAssertionObjectSizeLess>(
true, context, schema_context, dynamic_context,
SchemaCompilerValueUnsignedInteger{
Expand All @@ -1189,8 +1204,13 @@ auto compiler_draft4_validation_minproperties(
return {};
}

// TODO: As an optimization, if `maxProperties` is set to the same number, do
// a single size equality assertion
// We'll handle it at the type level as an optimization
if (schema_context.schema.defines("type") &&
schema_context.schema.at("type").is_string() &&
schema_context.schema.at("type").to_string() == "object") {
return {};
}

return {make<SchemaCompilerAssertionObjectSizeGreater>(
true, context, schema_context, dynamic_context,
SchemaCompilerValueUnsignedInteger{
Expand Down
10 changes: 10 additions & 0 deletions src/jsonschema/default_compiler_draft6.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ auto compiler_draft6_validation_type(
return {make<SchemaCompilerAssertionTypeStrict>(
true, context, schema_context, dynamic_context, JSON::Type::Boolean)};
} else if (type == "object") {
const auto minimum{
unsigned_integer_property(schema_context.schema, "minProperties", 0)};
const auto maximum{
unsigned_integer_property(schema_context.schema, "maxProperties")};
if (minimum > 0 || maximum.has_value()) {
return {make<SchemaCompilerAssertionTypeObjectBounded>(
true, context, schema_context, dynamic_context,
{minimum, maximum, false})};
}

return {make<SchemaCompilerAssertionTypeStrict>(
true, context, schema_context, dynamic_context, JSON::Type::Object)};
} else if (type == "array") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ struct SchemaCompilerAssertionTypeStrict;
struct SchemaCompilerAssertionTypeStrictAny;
struct SchemaCompilerAssertionTypeStringBounded;
struct SchemaCompilerAssertionTypeArrayBounded;
struct SchemaCompilerAssertionTypeObjectBounded;
struct SchemaCompilerAssertionRegex;
struct SchemaCompilerAssertionStringSizeLess;
struct SchemaCompilerAssertionStringSizeGreater;
Expand Down Expand Up @@ -195,7 +196,8 @@ using SchemaCompilerTemplate = std::vector<std::variant<
SchemaCompilerAssertionTypeAny, SchemaCompilerAssertionTypeStrict,
SchemaCompilerAssertionTypeStrictAny,
SchemaCompilerAssertionTypeStringBounded,
SchemaCompilerAssertionTypeArrayBounded, SchemaCompilerAssertionRegex,
SchemaCompilerAssertionTypeArrayBounded,
SchemaCompilerAssertionTypeObjectBounded, SchemaCompilerAssertionRegex,
SchemaCompilerAssertionStringSizeLess,
SchemaCompilerAssertionStringSizeGreater,
SchemaCompilerAssertionArraySizeLess,
Expand Down Expand Up @@ -308,6 +310,11 @@ DEFINE_STEP_WITH_VALUE(Assertion, TypeStringBounded, SchemaCompilerValueRange)
/// type array and adheres to the given bounds
DEFINE_STEP_WITH_VALUE(Assertion, TypeArrayBounded, SchemaCompilerValueRange)

/// @ingroup jsonschema_compiler_instructions
/// @brief Represents a compiler assertion step that checks if a document is of
/// type object and adheres to the given bounds
DEFINE_STEP_WITH_VALUE(Assertion, TypeObjectBounded, SchemaCompilerValueRange)

/// @ingroup jsonschema_compiler_instructions
/// @brief Represents a compiler assertion step that checks a string against an
/// ECMA regular expression
Expand Down
127 changes: 127 additions & 0 deletions test/jsonschema/jsonschema_compile_draft4_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3744,6 +3744,54 @@ TEST(JSONSchema_compile_draft4, minProperties_3) {
"contained 1 property: \"foo\"");
}

TEST(JSONSchema_compile_draft4, minProperties_4) {
const sourcemeta::jsontoolkit::JSON schema{
sourcemeta::jsontoolkit::parse(R"JSON({
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"minProperties": 1
})JSON")};

const auto compiled_schema{sourcemeta::jsontoolkit::compile(
schema, sourcemeta::jsontoolkit::default_schema_walker,
sourcemeta::jsontoolkit::official_resolver,
sourcemeta::jsontoolkit::default_schema_compiler)};

const sourcemeta::jsontoolkit::JSON instance{
sourcemeta::jsontoolkit::parse("{ \"foo\": 1 }")};
EVALUATE_WITH_TRACE_FAST_SUCCESS(compiled_schema, instance, 1);

EVALUATE_TRACE_PRE(0, AssertionTypeObjectBounded, "/type", "#/type", "");
EVALUATE_TRACE_POST_SUCCESS(0, AssertionTypeObjectBounded, "/type", "#/type",
"");
EVALUATE_TRACE_POST_DESCRIBE(
instance, 0,
"The value was expected to consist of an object of at least 1 property");
}

TEST(JSONSchema_compile_draft4, minProperties_5) {
const sourcemeta::jsontoolkit::JSON schema{
sourcemeta::jsontoolkit::parse(R"JSON({
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"minProperties": 0
})JSON")};

const auto compiled_schema{sourcemeta::jsontoolkit::compile(
schema, sourcemeta::jsontoolkit::default_schema_walker,
sourcemeta::jsontoolkit::official_resolver,
sourcemeta::jsontoolkit::default_schema_compiler)};

const sourcemeta::jsontoolkit::JSON instance{
sourcemeta::jsontoolkit::parse("{ \"foo\": 1 }")};
EVALUATE_WITH_TRACE_FAST_SUCCESS(compiled_schema, instance, 1);

EVALUATE_TRACE_PRE(0, AssertionTypeStrict, "/type", "#/type", "");
EVALUATE_TRACE_POST_SUCCESS(0, AssertionTypeStrict, "/type", "#/type", "");
EVALUATE_TRACE_POST_DESCRIBE(instance, 0,
"The value was expected to be of type object");
}

TEST(JSONSchema_compile_draft4, maxProperties_1) {
const sourcemeta::jsontoolkit::JSON schema{
sourcemeta::jsontoolkit::parse(R"JSON({
Expand Down Expand Up @@ -3814,6 +3862,85 @@ TEST(JSONSchema_compile_draft4, maxProperties_3) {
"contained 3 properties: \"bar\", \"baz\", and \"foo\"");
}

TEST(JSONSchema_compile_draft4, maxProperties_4) {
const sourcemeta::jsontoolkit::JSON schema{
sourcemeta::jsontoolkit::parse(R"JSON({
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"maxProperties": 2
})JSON")};

const auto compiled_schema{sourcemeta::jsontoolkit::compile(
schema, sourcemeta::jsontoolkit::default_schema_walker,
sourcemeta::jsontoolkit::official_resolver,
sourcemeta::jsontoolkit::default_schema_compiler)};

const sourcemeta::jsontoolkit::JSON instance{
sourcemeta::jsontoolkit::parse("{ \"bar\": 2, \"foo\": 1 }")};
EVALUATE_WITH_TRACE_FAST_SUCCESS(compiled_schema, instance, 1);

EVALUATE_TRACE_PRE(0, AssertionTypeObjectBounded, "/type", "#/type", "");
EVALUATE_TRACE_POST_SUCCESS(0, AssertionTypeObjectBounded, "/type", "#/type",
"");

EVALUATE_TRACE_POST_DESCRIBE(
instance, 0,
"The value was expected to consist of an object of at most 2 properties");
}

TEST(JSONSchema_compile_draft4, maxProperties_5) {
const sourcemeta::jsontoolkit::JSON schema{
sourcemeta::jsontoolkit::parse(R"JSON({
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"maxProperties": 2
})JSON")};

const auto compiled_schema{sourcemeta::jsontoolkit::compile(
schema, sourcemeta::jsontoolkit::default_schema_walker,
sourcemeta::jsontoolkit::official_resolver,
sourcemeta::jsontoolkit::default_schema_compiler)};

const sourcemeta::jsontoolkit::JSON instance{
sourcemeta::jsontoolkit::parse("{ \"bar\": 2, \"foo\": 1, \"baz\": 3 }")};
EVALUATE_WITH_TRACE_FAST_FAILURE(compiled_schema, instance, 1);

EVALUATE_TRACE_PRE(0, AssertionTypeObjectBounded, "/type", "#/type", "");
EVALUATE_TRACE_POST_FAILURE(0, AssertionTypeObjectBounded, "/type", "#/type",
"");

EVALUATE_TRACE_POST_DESCRIBE(
instance, 0,
"The value was expected to consist of an object of at most 2 properties");
}

TEST(JSONSchema_compile_draft4, maxProperties_6) {
const sourcemeta::jsontoolkit::JSON schema{
sourcemeta::jsontoolkit::parse(R"JSON({
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"minProperties": 1,
"maxProperties": 2
})JSON")};

const auto compiled_schema{sourcemeta::jsontoolkit::compile(
schema, sourcemeta::jsontoolkit::default_schema_walker,
sourcemeta::jsontoolkit::official_resolver,
sourcemeta::jsontoolkit::default_schema_compiler)};

const sourcemeta::jsontoolkit::JSON instance{
sourcemeta::jsontoolkit::parse("{ \"bar\": 2, \"foo\": 1 }")};
EVALUATE_WITH_TRACE_FAST_SUCCESS(compiled_schema, instance, 1);

EVALUATE_TRACE_PRE(0, AssertionTypeObjectBounded, "/type", "#/type", "");
EVALUATE_TRACE_POST_SUCCESS(0, AssertionTypeObjectBounded, "/type", "#/type",
"");

EVALUATE_TRACE_POST_DESCRIBE(
instance, 0,
"The value was expected to consist of an object of 1 to 2 properties");
}

TEST(JSONSchema_compile_draft4, minimum_1) {
const sourcemeta::jsontoolkit::JSON schema{
sourcemeta::jsontoolkit::parse(R"JSON({
Expand Down
Loading

4 comments on commit b252e8b

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark (macos/llvm)

Benchmark suite Current: b252e8b Previous: 0fd8c92 Ratio
JSONSchema_Validate_Draft4_Meta_1_No_Callback 755.3319557372072 ns/iter 749.1266296028281 ns/iter 1.01
JSONSchema_Validate_Draft4_Required_Properties 884.8643229375305 ns/iter 889.389810961436 ns/iter 0.99
JSONSchema_Validate_Draft4_Many_Optional_Properties_Minimal_Match 154.8908624008867 ns/iter 173.82713846933268 ns/iter 0.89
JSONSchema_Validate_Draft4_Few_Optional_Properties_Minimal_Match 122.0725782501435 ns/iter 138.23017371407224 ns/iter 0.88
JSONSchema_Validate_Draft4_Items_Schema 2741.1067117109337 ns/iter 2775.9666126908173 ns/iter 0.99
JSONSchema_Validate_Draft4_Nested_Object 1404.8000076516737 ns/iter 1367.8669267574398 ns/iter 1.03
JSONSchema_Validate_Draft4_Properties_Triad_Optional 1537.4686511262132 ns/iter 1472.7402831233082 ns/iter 1.04
JSONSchema_Validate_Draft4_Properties_Triad_Closed 1089.2508326874572 ns/iter 1106.1128378610363 ns/iter 0.98
JSONSchema_Validate_Draft4_Properties_Triad_Required 1475.8257014122605 ns/iter 1480.905916569726 ns/iter 1.00
JSONSchema_Validate_Draft4_Non_Recursive_Ref 205.42227507627038 ns/iter 213.94666687146486 ns/iter 0.96
JSONSchema_Validate_Draft4_Pattern_Properties_True 1502.317050122973 ns/iter 1536.9412278417103 ns/iter 0.98
JSONSchema_Validate_Draft4_Ref_To_Single_Property 119.21122851729112 ns/iter 123.51861311199376 ns/iter 0.97
JSONSchema_Validate_Draft4_Additional_Properties_Type 341.20608532275594 ns/iter 357.4999667449632 ns/iter 0.95

This comment was automatically generated by workflow using github-action-benchmark.

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark (linux/llvm)

Benchmark suite Current: b252e8b Previous: 0fd8c92 Ratio
JSONSchema_Validate_Draft4_Meta_1_No_Callback 17903.62954255202 ns/iter 18509.77049094612 ns/iter 0.97
JSONSchema_Validate_Draft4_Required_Properties 6652.145705989608 ns/iter 7284.257429865209 ns/iter 0.91
JSONSchema_Validate_Draft4_Many_Optional_Properties_Minimal_Match 1926.2716972508333 ns/iter 1916.330547294335 ns/iter 1.01
JSONSchema_Validate_Draft4_Few_Optional_Properties_Minimal_Match 986.5946861101723 ns/iter 971.0989601012524 ns/iter 1.02
JSONSchema_Validate_Draft4_Items_Schema 126883.79793590456 ns/iter 125481.25530767707 ns/iter 1.01
JSONSchema_Validate_Draft4_Nested_Object 57555.00969756544 ns/iter 56938.0150443197 ns/iter 1.01
JSONSchema_Validate_Draft4_Properties_Triad_Optional 9005.9852474526 ns/iter 9199.40942554872 ns/iter 0.98
JSONSchema_Validate_Draft4_Properties_Triad_Closed 8487.417441789683 ns/iter 8607.751496783581 ns/iter 0.99
JSONSchema_Validate_Draft4_Properties_Triad_Required 9186.733850706172 ns/iter 9726.168509209003 ns/iter 0.94
JSONSchema_Validate_Draft4_Non_Recursive_Ref 2283.3169950626675 ns/iter 2207.0602924068735 ns/iter 1.03
JSONSchema_Validate_Draft4_Pattern_Properties_True 6280.779433949226 ns/iter 6521.448017019788 ns/iter 0.96
JSONSchema_Validate_Draft4_Ref_To_Single_Property 1011.9653303747596 ns/iter 976.759720210506 ns/iter 1.04
JSONSchema_Validate_Draft4_Additional_Properties_Type 2401.3185859915557 ns/iter 2340.1737916855827 ns/iter 1.03

This comment was automatically generated by workflow using github-action-benchmark.

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark (linux/gcc)

Benchmark suite Current: b252e8b Previous: 0fd8c92 Ratio
JSONSchema_Validate_Draft4_Meta_1_No_Callback 1078.5516817420826 ns/iter 1110.0602148625035 ns/iter 0.97
JSONSchema_Validate_Draft4_Required_Properties 2115.948358997239 ns/iter 2218.2713650934725 ns/iter 0.95
JSONSchema_Validate_Draft4_Many_Optional_Properties_Minimal_Match 182.27853712693434 ns/iter 189.57407917143155 ns/iter 0.96
JSONSchema_Validate_Draft4_Few_Optional_Properties_Minimal_Match 148.80160675143736 ns/iter 156.1978044053039 ns/iter 0.95
JSONSchema_Validate_Draft4_Items_Schema 3696.492124170553 ns/iter 3626.8852220171734 ns/iter 1.02
JSONSchema_Validate_Draft4_Nested_Object 1730.5895011566058 ns/iter 1717.632699720749 ns/iter 1.01
JSONSchema_Validate_Draft4_Properties_Triad_Optional 1707.670617011583 ns/iter 1695.148604182702 ns/iter 1.01
JSONSchema_Validate_Draft4_Properties_Triad_Closed 1384.0817340294413 ns/iter 1397.2887774639264 ns/iter 0.99
JSONSchema_Validate_Draft4_Properties_Triad_Required 1771.6200947474924 ns/iter 1785.9994968972323 ns/iter 0.99
JSONSchema_Validate_Draft4_Non_Recursive_Ref 447.4151276170305 ns/iter 447.239959158991 ns/iter 1.00
JSONSchema_Validate_Draft4_Pattern_Properties_True 2235.2638188051224 ns/iter 2196.5052960358576 ns/iter 1.02
JSONSchema_Validate_Draft4_Ref_To_Single_Property 162.6209190494121 ns/iter 170.14066219086402 ns/iter 0.96
JSONSchema_Validate_Draft4_Additional_Properties_Type 1028.4623142728847 ns/iter 1059.3226163180464 ns/iter 0.97

This comment was automatically generated by workflow using github-action-benchmark.

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark (windows/msvc)

Benchmark suite Current: b252e8b Previous: 0fd8c92 Ratio
JSONSchema_Validate_Draft4_Meta_1_No_Callback 3085.437946428523 ns/iter 3062.019196428472 ns/iter 1.01
JSONSchema_Validate_Draft4_Required_Properties 1705.4263356518952 ns/iter 1657.356613377233 ns/iter 1.03
JSONSchema_Validate_Draft4_Many_Optional_Properties_Minimal_Match 626.5556249999804 ns/iter 610.9516071429002 ns/iter 1.03
JSONSchema_Validate_Draft4_Few_Optional_Properties_Minimal_Match 447.0038088382948 ns/iter 427.7566875001071 ns/iter 1.04
JSONSchema_Validate_Draft4_Items_Schema 12415.457142858648 ns/iter 12251.155357140371 ns/iter 1.01
JSONSchema_Validate_Draft4_Nested_Object 6913.619642856718 ns/iter 6835.630580356776 ns/iter 1.01
JSONSchema_Validate_Draft4_Properties_Triad_Optional 5852.516964286077 ns/iter 5827.261607141817 ns/iter 1.00
JSONSchema_Validate_Draft4_Properties_Triad_Closed 4844.465178572526 ns/iter 4843.494145976943 ns/iter 1.00
JSONSchema_Validate_Draft4_Properties_Triad_Required 5953.700000000188 ns/iter 5934.634000000187 ns/iter 1.00
JSONSchema_Validate_Draft4_Non_Recursive_Ref 671.2578125000083 ns/iter 686.4872767854889 ns/iter 0.98
JSONSchema_Validate_Draft4_Pattern_Properties_True 8092.954049312928 ns/iter 8107.915178572662 ns/iter 1.00
JSONSchema_Validate_Draft4_Ref_To_Single_Property 444.6568161634277 ns/iter 435.604187499905 ns/iter 1.02
JSONSchema_Validate_Draft4_Additional_Properties_Type 961.555151091452 ns/iter 987.8629687499085 ns/iter 0.97

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.