Skip to content
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
2 changes: 2 additions & 0 deletions config.cmake.in
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ foreach(component ${SOURCEMETA_CORE_COMPONENTS})
find_dependency(mpdecimal CONFIG)
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_numeric.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_io.cmake")
find_dependency(PCRE2 CONFIG)
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_regex.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_json.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_jsonpointer.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_jsonschema.cmake")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,10 +251,11 @@ class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaTransformer {
const SchemaTransformRule::Result &)>;

/// Apply the bundle of rules to a schema
auto apply(JSON &schema, const SchemaWalker &walker,
const SchemaResolver &resolver, const Callback &callback,
const std::optional<JSON::String> &default_dialect = std::nullopt,
const std::optional<JSON::String> &default_id = std::nullopt) const
[[nodiscard]] auto
apply(JSON &schema, const SchemaWalker &walker,
const SchemaResolver &resolver, const Callback &callback,
const std::optional<JSON::String> &default_dialect = std::nullopt,
const std::optional<JSON::String> &default_id = std::nullopt) const
-> std::pair<bool, std::uint8_t>;

/// Report back the rules from the bundle that need to be applied to a schema
Expand Down
3 changes: 3 additions & 0 deletions src/extension/alterschema/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME alterschema
common/non_applicable_type_specific_keywords.h
common/not_false.h
common/required_properties_in_properties.h
common/simple_properties_identifiers.h
common/single_type_array.h
common/then_empty.h
common/then_without_if.h
Expand Down Expand Up @@ -96,3 +97,5 @@ endif()

target_link_libraries(sourcemeta_core_alterschema PUBLIC
sourcemeta::core::jsonschema)
target_link_libraries(sourcemeta_core_alterschema PRIVATE
sourcemeta::core::regex)
3 changes: 3 additions & 0 deletions src/extension/alterschema/alterschema.cc
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include <sourcemeta/core/alterschema.h>
#include <sourcemeta/core/regex.h>

// For built-in rules
#include <algorithm> // std::sort, std::unique
Expand Down Expand Up @@ -78,6 +79,7 @@ inline auto APPLIES_TO_POINTERS(std::vector<Pointer> &&keywords)
#include "common/not_false.h"
#include "common/orphan_definitions.h"
#include "common/required_properties_in_properties.h"
#include "common/simple_properties_identifiers.h"
#include "common/single_type_array.h"
#include "common/then_empty.h"
#include "common/then_without_if.h"
Expand Down Expand Up @@ -166,6 +168,7 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) -> void {
bundle.add<UnknownKeywordsPrefix>();
bundle.add<UnknownLocalRef>();
bundle.add<RequiredPropertiesInProperties>();
bundle.add<SimplePropertiesIdentifiers>();
bundle.add<OrphanDefinitions>();

if (mode == AlterSchemaMode::Canonicalizer) {
Expand Down
65 changes: 65 additions & 0 deletions src/extension/alterschema/common/simple_properties_identifiers.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
class SimplePropertiesIdentifiers final : public SchemaTransformRule {
public:
SimplePropertiesIdentifiers()
// Inspired by
// https://json-structure.github.io/core/draft-vasters-json-structure-core.html#section-3.6
: SchemaTransformRule{
"simple_properties_identifiers",
"Set `properties` to identifier names that can be easily mapped to "
"programming languages (matching [A-Za-z_][A-Za-z0-9_]*)"} {};

[[nodiscard]] auto
condition(const sourcemeta::core::JSON &schema,
const sourcemeta::core::JSON &root,
const sourcemeta::core::Vocabularies &vocabularies,
const sourcemeta::core::SchemaFrame &frame,
const sourcemeta::core::SchemaFrame::Location &location,
const sourcemeta::core::SchemaWalker &,
const sourcemeta::core::SchemaResolver &) const
-> sourcemeta::core::SchemaTransformRule::Result override {
ONLY_CONTINUE_IF(vocabularies.contains_any(
{Vocabularies::Known::JSON_Schema_2020_12_Applicator,
Vocabularies::Known::JSON_Schema_2019_09_Applicator,
Vocabularies::Known::JSON_Schema_Draft_7,
Vocabularies::Known::JSON_Schema_Draft_6,
Vocabularies::Known::JSON_Schema_Draft_4,
Vocabularies::Known::JSON_Schema_Draft_3,
Vocabularies::Known::JSON_Schema_Draft_2,
Vocabularies::Known::JSON_Schema_Draft_2_Hyper,
Vocabularies::Known::JSON_Schema_Draft_1,
Vocabularies::Known::JSON_Schema_Draft_1_Hyper}));
ONLY_CONTINUE_IF(schema.is_object() && schema.defines("properties") &&
schema.at("properties").is_object() &&
!schema.at("properties").empty());

if (vocabularies.contains_any(
{Vocabularies::Known::JSON_Schema_2020_12_Core,
Vocabularies::Known::JSON_Schema_2019_09_Core})) {
// Skip meta-schemas with `$vocabulary` (2019-09+)
// We check the current schema resource (not root) to handle bundled
// schemas
const auto base_location{frame.traverse(location.base)};
if (base_location.has_value()) {
const auto &resource{get(root, base_location->get().pointer)};
ONLY_CONTINUE_IF(!resource.is_object() ||
!resource.defines("$vocabulary"));
}
} else {
// Skip pre-vocabulary meta-schemas
ONLY_CONTINUE_IF(location.base != location.dialect &&
(location.base + "#") != location.dialect);
}

std::vector<Pointer> offenders;
for (const auto &entry : schema.at("properties").as_object()) {
static const Regex IDENTIFIER_PATTERN{
to_regex("^[A-Za-z_][A-Za-z0-9_]*$").value()};
if (!matches(IDENTIFIER_PATTERN, entry.first)) {
offenders.push_back(Pointer{"properties", entry.first});
}
}

ONLY_CONTINUE_IF(!offenders.empty());
return APPLIES_TO_POINTERS(std::move(offenders));
}
};
45 changes: 45 additions & 0 deletions test/alterschema/alterschema_canonicalize_2020_12_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1322,3 +1322,48 @@ TEST(AlterSchema_canonicalize_2020_12,

EXPECT_EQ(document, expected);
}

TEST(AlterSchema_canonicalize_2020_12,
simple_properties_identifiers_skip_metaschema) {
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/my-metaschema",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/core": true,
"https://json-schema.org/draft/2020-12/vocab/applicator": true,
"https://json-schema.org/draft/2020-12/vocab/validation": true
},
"type": "object",
"minProperties": 0,
"properties": {
"foo-bar": { "type": "string", "minLength": 0 }
}
})JSON");

CANONICALIZE_WITHOUT_FIX(document, result, traces);

EXPECT_TRUE(result.first);
EXPECT_EQ(traces.size(), 0);
}

TEST(AlterSchema_canonicalize_2020_12,
simple_properties_identifiers_applies_non_meta) {
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/my-schema",
"type": "object",
"minProperties": 0,
"properties": {
"foo-bar": { "type": "string", "minLength": 0 }
}
})JSON");

CANONICALIZE_WITHOUT_FIX(document, result, traces);

EXPECT_FALSE(result.first);
EXPECT_EQ(traces.size(), 1);
EXPECT_LINT_TRACE(traces, 0, "", "simple_properties_identifiers",
"Set `properties` to identifier names that can be easily "
"mapped to programming languages (matching "
"[A-Za-z_][A-Za-z0-9_]*)");
}
40 changes: 40 additions & 0 deletions test/alterschema/alterschema_canonicalize_draft7_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -569,3 +569,43 @@ TEST(AlterSchema_canonicalize_draft7, min_properties_implicit_2) {

EXPECT_EQ(document, expected);
}

TEST(AlterSchema_canonicalize_draft7,
simple_properties_identifiers_skip_metaschema) {
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "http://json-schema.org/draft-07/schema#",
"type": "object",
"minProperties": 0,
"properties": {
"foo-bar": { "type": "string", "minLength": 0 }
}
})JSON");

CANONICALIZE_WITHOUT_FIX(document, result, traces);

EXPECT_TRUE(result.first);
EXPECT_EQ(traces.size(), 0);
}

TEST(AlterSchema_canonicalize_draft7,
simple_properties_identifiers_applies_non_meta) {
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://example.com/my-schema",
"type": "object",
"minProperties": 0,
"properties": {
"foo-bar": { "type": "string", "minLength": 0 }
}
})JSON");

CANONICALIZE_WITHOUT_FIX(document, result, traces);

EXPECT_FALSE(result.first);
EXPECT_EQ(traces.size(), 1);
EXPECT_LINT_TRACE(traces, 0, "", "simple_properties_identifiers",
"Set `properties` to identifier names that can be easily "
"mapped to programming languages (matching "
"[A-Za-z_][A-Za-z0-9_]*)");
}
101 changes: 101 additions & 0 deletions test/alterschema/alterschema_lint_2019_09_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3896,3 +3896,104 @@ TEST(AlterSchema_lint_2019_09,

EXPECT_EQ(document, expected);
}

TEST(AlterSchema_lint_2019_09, simple_properties_identifiers_skip_metaschema) {
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://example.com/my-metaschema",
"title": "My meta-schema",
"description": "A custom meta-schema",
"examples": [ {} ],
"$vocabulary": {
"https://json-schema.org/draft/2019-09/vocab/core": true,
"https://json-schema.org/draft/2019-09/vocab/applicator": true
},
"properties": {
"foo-bar": { "type": "string" }
}
})JSON");

LINT_WITHOUT_FIX(document, result, traces);

EXPECT_TRUE(result.first);
EXPECT_EQ(traces.size(), 0);
}

TEST(AlterSchema_lint_2019_09,
simple_properties_identifiers_skip_metaschema_nested) {
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://example.com/my-metaschema",
"title": "My meta-schema",
"description": "A custom meta-schema",
"examples": [ {} ],
"$vocabulary": {
"https://json-schema.org/draft/2019-09/vocab/core": true,
"https://json-schema.org/draft/2019-09/vocab/applicator": true
},
"properties": {
"valid": {
"properties": {
"also-invalid": { "type": "number" }
}
}
}
})JSON");

LINT_WITHOUT_FIX(document, result, traces);

EXPECT_TRUE(result.first);
EXPECT_EQ(traces.size(), 0);
}

TEST(AlterSchema_lint_2019_09, simple_properties_identifiers_applies_non_meta) {
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://example.com/my-schema",
"title": "Test",
"description": "A test schema",
"examples": [ {} ],
"properties": {
"foo-bar": { "type": "string" }
}
})JSON");

LINT_WITHOUT_FIX(document, result, traces);

EXPECT_FALSE(result.first);
EXPECT_EQ(traces.size(), 1);
EXPECT_LINT_TRACE(traces, 0, "", "simple_properties_identifiers",
"Set `properties` to identifier names that can be easily "
"mapped to programming languages (matching "
"[A-Za-z_][A-Za-z0-9_]*)");
}

TEST(AlterSchema_lint_2019_09,
simple_properties_identifiers_skip_bundled_metaschema) {
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://example.com/main",
"title": "Main schema",
"description": "A schema that bundles a meta-schema",
"examples": [ {} ],
"$ref": "https://example.com/my-metaschema",
"$defs": {
"metaschema": {
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://example.com/my-metaschema",
"$vocabulary": {
"https://json-schema.org/draft/2019-09/vocab/core": true,
"https://json-schema.org/draft/2019-09/vocab/applicator": true
},
"properties": {
"foo-bar": { "type": "string" }
}
}
}
})JSON");

LINT_WITHOUT_FIX(document, result, traces);

EXPECT_TRUE(result.first);
EXPECT_EQ(traces.size(), 0);
}
Loading
Loading