Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce a new LoopPropertiesType(Strict) compiler step #1213

Merged
merged 1 commit into from
Sep 23, 2024
Merged
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
16 changes: 16 additions & 0 deletions src/jsonschema/compile_describe.cc
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,22 @@ struct DescribeVisitor {
return message.str();
}

auto operator()(const SchemaCompilerLoopPropertiesType &step) const
-> std::string {
std::ostringstream message;
message << "The object properties were expected to be of type "
<< to_string(step.value);
return message.str();
}

auto operator()(const SchemaCompilerLoopPropertiesTypeStrict &step) const
-> std::string {
std::ostringstream message;
message << "The object properties were expected to be of type "
<< to_string(step.value);
return message.str();
}

auto operator()(const SchemaCompilerLoopKeys &) const -> std::string {
assert(this->keyword == "propertyNames");
assert(this->target.is_object());
Expand Down
36 changes: 36 additions & 0 deletions src/jsonschema/compile_evaluate.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1005,6 +1005,42 @@ auto evaluate_step(

evaluate_loop_properties_except_end:
EVALUATE_END(loop, SchemaCompilerLoopPropertiesExcept);
} else if (IS_STEP(SchemaCompilerLoopPropertiesType)) {
EVALUATE_BEGIN(loop, SchemaCompilerLoopPropertiesType, target.is_object());
result = true;
for (const auto &entry : target.as_object()) {
context.enter(entry.first);
const auto &entry_target{context.resolve_target()};
// In non-strict mode, we consider a real number that represents an
// integer to be an integer
if (entry_target.type() != loop.value &&
(loop.value != JSON::Type::Integer ||
entry_target.is_integer_real())) {
result = false;
context.leave();
break;
}

context.leave();
}

EVALUATE_END(loop, SchemaCompilerLoopPropertiesType);
} else if (IS_STEP(SchemaCompilerLoopPropertiesTypeStrict)) {
EVALUATE_BEGIN(loop, SchemaCompilerLoopPropertiesTypeStrict,
target.is_object());
result = true;
for (const auto &entry : target.as_object()) {
context.enter(entry.first);
if (context.resolve_target().type() != loop.value) {
result = false;
context.leave();
break;
}

context.leave();
}

EVALUATE_END(loop, SchemaCompilerLoopPropertiesTypeStrict);
} else if (IS_STEP(SchemaCompilerLoopKeys)) {
EVALUATE_BEGIN(loop, SchemaCompilerLoopKeys, target.is_object());
result = true;
Expand Down
4 changes: 2 additions & 2 deletions src/jsonschema/compile_helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ auto make(const bool report, const SchemaCompilerContext &context,
const SchemaCompilerSchemaContext &schema_context,
const SchemaCompilerDynamicContext &dynamic_context,
// Take the value type from the "type" property of the step struct
decltype(std::declval<Step>().value) &&value) -> Step {
const decltype(std::declval<Step>().value) &value) -> Step {
return {
dynamic_context.keyword.empty()
? dynamic_context.base_schema_location
Expand All @@ -28,7 +28,7 @@ auto make(const bool report, const SchemaCompilerContext &context,
schema_context.base.recompose(),
context.uses_dynamic_scopes,
report,
std::move(value)};
value};
}

// Instantiate an applicator step
Expand Down
3 changes: 3 additions & 0 deletions src/jsonschema/compile_json.cc
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,9 @@ struct StepVisitor {
HANDLE_STEP("loop", "properties-no-annotation",
SchemaCompilerLoopPropertiesNoAnnotation)
HANDLE_STEP("loop", "properties-except", SchemaCompilerLoopPropertiesExcept)
HANDLE_STEP("loop", "properties-type", SchemaCompilerLoopPropertiesType)
HANDLE_STEP("loop", "properties-type-strict",
SchemaCompilerLoopPropertiesTypeStrict)
HANDLE_STEP("loop", "keys", SchemaCompilerLoopKeys)
HANDLE_STEP("loop", "items", SchemaCompilerLoopItems)
HANDLE_STEP("loop", "items-unmarked", SchemaCompilerLoopItemsUnmarked)
Expand Down
18 changes: 18 additions & 0 deletions src/jsonschema/default_compiler_draft4.h
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,24 @@ auto compiler_draft4_applicator_additionalproperties_conditional_annotation(
true, context, schema_context, dynamic_context, std::move(filter),
std::move(children))};
} else {
if (children.size() == 1) {
// Optimize `additionalProperties` set to just `type`, which is a
// pretty common pattern
if (std::holds_alternative<SchemaCompilerAssertionTypeStrict>(
children.front())) {
const auto &type_step{
std::get<SchemaCompilerAssertionTypeStrict>(children.front())};
return {make<SchemaCompilerLoopPropertiesTypeStrict>(
true, context, schema_context, dynamic_context, type_step.value)};
} else if (std::holds_alternative<SchemaCompilerAssertionType>(
children.front())) {
const auto &type_step{
std::get<SchemaCompilerAssertionType>(children.front())};
return {make<SchemaCompilerLoopPropertiesType>(
true, context, schema_context, dynamic_context, type_step.value)};
}
}

return {make<SchemaCompilerLoopProperties>(
true, context, schema_context, dynamic_context,
SchemaCompilerValueNone{}, std::move(children))};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ struct SchemaCompilerLoopProperties;
struct SchemaCompilerLoopPropertiesRegex;
struct SchemaCompilerLoopPropertiesNoAnnotation;
struct SchemaCompilerLoopPropertiesExcept;
struct SchemaCompilerLoopPropertiesType;
struct SchemaCompilerLoopPropertiesTypeStrict;
struct SchemaCompilerLoopKeys;
struct SchemaCompilerLoopItems;
struct SchemaCompilerLoopItemsUnmarked;
Expand Down Expand Up @@ -214,7 +216,8 @@ using SchemaCompilerTemplate = std::vector<std::variant<
SchemaCompilerLogicalWhenArraySizeEqual, SchemaCompilerLoopPropertiesMatch,
SchemaCompilerLoopProperties, SchemaCompilerLoopPropertiesRegex,
SchemaCompilerLoopPropertiesNoAnnotation,
SchemaCompilerLoopPropertiesExcept, SchemaCompilerLoopKeys,
SchemaCompilerLoopPropertiesExcept, SchemaCompilerLoopPropertiesType,
SchemaCompilerLoopPropertiesTypeStrict, SchemaCompilerLoopKeys,
SchemaCompilerLoopItems, SchemaCompilerLoopItemsUnmarked,
SchemaCompilerLoopItemsUnevaluated, SchemaCompilerLoopContains,
SchemaCompilerControlLabel, SchemaCompilerControlMark,
Expand Down Expand Up @@ -490,6 +493,16 @@ DEFINE_STEP_APPLICATOR(Loop, PropertiesNoAnnotation, SchemaCompilerValueStrings)
DEFINE_STEP_APPLICATOR(Loop, PropertiesExcept,
SchemaCompilerValuePropertyFilter)

/// @ingroup jsonschema_compiler_instructions
/// @brief Represents a compiler step that checks every object property is of a
/// given type
DEFINE_STEP_WITH_VALUE(Loop, PropertiesType, SchemaCompilerValueType)

/// @ingroup jsonschema_compiler_instructions
/// @brief Represents a compiler step that checks every object property is of a
/// given type (strict mode)
DEFINE_STEP_WITH_VALUE(Loop, PropertiesTypeStrict, SchemaCompilerValueType)

/// @ingroup jsonschema_compiler_instructions
/// @brief Represents a compiler step that loops over object property keys
DEFINE_STEP_APPLICATOR(Loop, Keys, SchemaCompilerValueNone)
Expand Down
106 changes: 20 additions & 86 deletions test/jsonschema/jsonschema_compile_draft4_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -834,26 +834,15 @@ TEST(JSONSchema_compile_draft4, ref_9) {
const sourcemeta::jsontoolkit::JSON instance{
sourcemeta::jsontoolkit::parse("{ \"foo\": true }")};

EVALUATE_WITH_TRACE_FAST_SUCCESS(compiled_schema, instance, 2);
EVALUATE_WITH_TRACE_FAST_SUCCESS(compiled_schema, instance, 1);

EVALUATE_TRACE_PRE(0, LoopProperties, "/additionalProperties",
EVALUATE_TRACE_PRE(0, LoopPropertiesTypeStrict, "/additionalProperties",
"#/additionalProperties", "");
EVALUATE_TRACE_PRE(1, AssertionTypeStrict,
"/additionalProperties/$ref/$ref/type",
"#/definitions/two/type", "/foo");

EVALUATE_TRACE_POST_SUCCESS(0, AssertionTypeStrict,
"/additionalProperties/$ref/$ref/type",
"#/definitions/two/type", "/foo");
EVALUATE_TRACE_POST_SUCCESS(1, LoopProperties, "/additionalProperties",
"#/additionalProperties", "");

EVALUATE_TRACE_POST_DESCRIBE(instance, 0,
"The value was expected to be of type boolean");
EVALUATE_TRACE_POST_SUCCESS(0, LoopPropertiesTypeStrict,
"/additionalProperties", "#/additionalProperties",
"");
EVALUATE_TRACE_POST_DESCRIBE(
instance, 1,
"The object properties not covered by other adjacent object keywords "
"were expected to validate against this subschema");
instance, 0, "The object properties were expected to be of type boolean");
}

TEST(JSONSchema_compile_draft4, ref_10) {
Expand All @@ -876,26 +865,15 @@ TEST(JSONSchema_compile_draft4, ref_10) {
const sourcemeta::jsontoolkit::JSON instance{
sourcemeta::jsontoolkit::parse("{ \"foo\": true }")};

EVALUATE_WITH_TRACE_FAST_SUCCESS(compiled_schema, instance, 2);
EVALUATE_WITH_TRACE_FAST_SUCCESS(compiled_schema, instance, 1);

EVALUATE_TRACE_PRE(0, LoopProperties, "/additionalProperties",
EVALUATE_TRACE_PRE(0, LoopPropertiesTypeStrict, "/additionalProperties",
"#/additionalProperties", "");
EVALUATE_TRACE_PRE(1, AssertionTypeStrict,
"/additionalProperties/$ref/$ref/type",
"#/definitions/two/type", "/foo");

EVALUATE_TRACE_POST_SUCCESS(0, AssertionTypeStrict,
"/additionalProperties/$ref/$ref/type",
"#/definitions/two/type", "/foo");
EVALUATE_TRACE_POST_SUCCESS(1, LoopProperties, "/additionalProperties",
"#/additionalProperties", "");

EVALUATE_TRACE_POST_DESCRIBE(instance, 0,
"The value was expected to be of type boolean");
EVALUATE_TRACE_POST_SUCCESS(0, LoopPropertiesTypeStrict,
"/additionalProperties", "#/additionalProperties",
"");
EVALUATE_TRACE_POST_DESCRIBE(
instance, 1,
"The object properties not covered by other adjacent object keywords "
"were expected to validate against this subschema");
instance, 0, "The object properties were expected to be of type boolean");
}

TEST(JSONSchema_compile_draft4, ref_11) {
Expand Down Expand Up @@ -1539,59 +1517,15 @@ TEST(JSONSchema_compile_draft4, additionalProperties_1) {
const sourcemeta::jsontoolkit::JSON instance{
sourcemeta::jsontoolkit::parse("{ \"bar\": 2, \"foo\": 1 }")};

EVALUATE_WITH_TRACE_FAST_SUCCESS(compiled_schema, instance, 3);

if (FIRST_PROPERTY_IS(instance, "foo")) {
EVALUATE_TRACE_PRE(0, LoopProperties, "/additionalProperties",
"#/additionalProperties", "");
EVALUATE_TRACE_PRE(1, AssertionTypeStrict, "/additionalProperties/type",
"#/additionalProperties/type", "/foo");
EVALUATE_TRACE_PRE(2, AssertionTypeStrict, "/additionalProperties/type",
"#/additionalProperties/type", "/bar");

EVALUATE_TRACE_POST_SUCCESS(0, AssertionTypeStrict,
"/additionalProperties/type",
"#/additionalProperties/type", "/foo");
EVALUATE_TRACE_POST_SUCCESS(1, AssertionTypeStrict,
"/additionalProperties/type",
"#/additionalProperties/type", "/bar");
EVALUATE_TRACE_POST_SUCCESS(2, LoopProperties, "/additionalProperties",
"#/additionalProperties", "");

EVALUATE_TRACE_POST_DESCRIBE(
instance, 0, "The value was expected to be of type integer");
EVALUATE_TRACE_POST_DESCRIBE(
instance, 1, "The value was expected to be of type integer");
EVALUATE_TRACE_POST_DESCRIBE(instance, 2,
"The object properties not covered by other "
"adjacent object keywords were "
"expected to validate against this subschema");
} else {
EVALUATE_TRACE_PRE(0, LoopProperties, "/additionalProperties",
"#/additionalProperties", "");
EVALUATE_TRACE_PRE(1, AssertionTypeStrict, "/additionalProperties/type",
"#/additionalProperties/type", "/bar");
EVALUATE_TRACE_PRE(2, AssertionTypeStrict, "/additionalProperties/type",
"#/additionalProperties/type", "/foo");

EVALUATE_TRACE_POST_SUCCESS(0, AssertionTypeStrict,
"/additionalProperties/type",
"#/additionalProperties/type", "/bar");
EVALUATE_TRACE_POST_SUCCESS(1, AssertionTypeStrict,
"/additionalProperties/type",
"#/additionalProperties/type", "/foo");
EVALUATE_TRACE_POST_SUCCESS(2, LoopProperties, "/additionalProperties",
"#/additionalProperties", "");
EVALUATE_WITH_TRACE_FAST_SUCCESS(compiled_schema, instance, 1);

EVALUATE_TRACE_POST_DESCRIBE(
instance, 0, "The value was expected to be of type integer");
EVALUATE_TRACE_POST_DESCRIBE(
instance, 1, "The value was expected to be of type integer");
EVALUATE_TRACE_POST_DESCRIBE(instance, 2,
"The object properties not covered by other "
"adjacent object keywords were "
"expected to validate against this subschema");
}
EVALUATE_TRACE_PRE(0, LoopPropertiesTypeStrict, "/additionalProperties",
"#/additionalProperties", "");
EVALUATE_TRACE_POST_SUCCESS(0, LoopPropertiesTypeStrict,
"/additionalProperties", "#/additionalProperties",
"");
EVALUATE_TRACE_POST_DESCRIBE(
instance, 0, "The object properties were expected to be of type integer");
}

TEST(JSONSchema_compile_draft4, additionalProperties_2) {
Expand Down