diff options
author | David Storch <david.storch@10gen.com> | 2017-08-28 18:44:05 -0400 |
---|---|---|
committer | David Storch <david.storch@10gen.com> | 2017-08-30 13:22:31 -0400 |
commit | 292a7016e0896c93a740c8535de5418633c13148 (patch) | |
tree | f864fae0d56ea0b97e1b050c70ae0a216816d260 | |
parent | 4582f2e1c51f5277488c15732374560ee8ade96f (diff) | |
download | mongo-292a7016e0896c93a740c8535de5418633c13148.tar.gz |
SERVER-30177 Implement JSON Schema 'dependencies' keyword.
-rw-r--r-- | jstests/core/json_schema/dependencies.js | 119 | ||||
-rw-r--r-- | src/mongo/db/matcher/schema/json_schema_parser.cpp | 142 | ||||
-rw-r--r-- | src/mongo/db/matcher/schema/json_schema_parser_test.cpp | 161 |
3 files changed, 422 insertions, 0 deletions
diff --git a/jstests/core/json_schema/dependencies.js b/jstests/core/json_schema/dependencies.js new file mode 100644 index 00000000000..a227169fe3f --- /dev/null +++ b/jstests/core/json_schema/dependencies.js @@ -0,0 +1,119 @@ +/** + * Tests for the JSON Schema 'dependencies' keyword. + */ +(function() { + "use strict"; + + load("jstests/libs/assert_schema_match.js"); + + const coll = db.jstests_schema_dependencies; + + // Top-level schema dependency. + assertSchemaMatch(coll, {dependencies: {foo: {required: ["bar"]}}}, {}, true); + assertSchemaMatch(coll, {dependencies: {foo: {required: ["bar"]}}}, {foo: 1, bar: 1}, true); + assertSchemaMatch(coll, {dependencies: {foo: {required: ["bar"]}}}, {bar: 1}, true); + assertSchemaMatch(coll, {dependencies: {foo: {required: ["bar"]}}}, {foo: 1}, false); + + assertSchemaMatch( + coll, + {dependencies: {foo: {required: ["bar"], properties: {baz: {type: "string"}}}}}, + {}, + true); + assertSchemaMatch( + coll, + {dependencies: {foo: {required: ["bar"], properties: {baz: {type: "string"}}}}}, + {bar: 1}, + true); + assertSchemaMatch( + coll, + {dependencies: {foo: {required: ["bar"], properties: {baz: {type: "string"}}}}}, + {foo: 1, bar: 1}, + true); + assertSchemaMatch( + coll, + {dependencies: {foo: {required: ["bar"], properties: {baz: {type: "string"}}}}}, + {foo: 1, bar: 1, baz: 1}, + false); + assertSchemaMatch( + coll, + {dependencies: {foo: {required: ["bar"], properties: {baz: {type: "string"}}}}}, + {foo: 1, bar: 1, baz: "str"}, + true); + + // Top-level property dependency. + assertSchemaMatch(coll, {dependencies: {foo: ["bar", "baz"]}}, {}, true); + assertSchemaMatch(coll, {dependencies: {foo: ["bar", "baz"]}}, {bar: 1}, true); + assertSchemaMatch(coll, {dependencies: {foo: ["bar", "baz"]}}, {baz: 1}, true); + assertSchemaMatch(coll, {dependencies: {foo: ["bar", "baz"]}}, {bar: 1, baz: 1}, true); + assertSchemaMatch(coll, {dependencies: {foo: ["bar", "baz"]}}, {foo: 1}, false); + assertSchemaMatch(coll, {dependencies: {foo: ["bar", "baz"]}}, {foo: 1, bar: 1}, false); + assertSchemaMatch(coll, {dependencies: {foo: ["bar", "baz"]}}, {foo: 1, baz: 1}, false); + assertSchemaMatch(coll, {dependencies: {foo: ["bar", "baz"]}}, {foo: 1, bar: 1, baz: 1}, true); + + // Nested schema dependency. + assertSchemaMatch( + coll, {properties: {obj: {dependencies: {foo: {required: ["bar"]}}}}}, {}, true); + assertSchemaMatch( + coll, {properties: {obj: {dependencies: {foo: {required: ["bar"]}}}}}, {obj: 1}, true); + assertSchemaMatch( + coll, {properties: {obj: {dependencies: {foo: {required: ["bar"]}}}}}, {obj: {}}, true); + assertSchemaMatch(coll, + {properties: {obj: {dependencies: {foo: {required: ["bar"]}}}}}, + {obj: {bar: 1}}, + true); + assertSchemaMatch(coll, + {properties: {obj: {dependencies: {foo: {required: ["bar"]}}}}}, + {obj: {foo: 1}}, + false); + assertSchemaMatch(coll, + {properties: {obj: {dependencies: {foo: {required: ["bar"]}}}}}, + {obj: {foo: 1, bar: 1}}, + true); + + // Nested property dependency. + assertSchemaMatch(coll, {properties: {obj: {dependencies: {foo: ["bar"]}}}}, {}, true); + assertSchemaMatch(coll, {properties: {obj: {dependencies: {foo: ["bar"]}}}}, {obj: 1}, true); + assertSchemaMatch(coll, {properties: {obj: {dependencies: {foo: ["bar"]}}}}, {obj: {}}, true); + assertSchemaMatch( + coll, {properties: {obj: {dependencies: {foo: ["bar"]}}}}, {obj: {bar: 1}}, true); + assertSchemaMatch( + coll, {properties: {obj: {dependencies: {foo: ["bar"]}}}}, {obj: {foo: 1}}, false); + assertSchemaMatch( + coll, {properties: {obj: {dependencies: {foo: ["bar"]}}}}, {obj: {foo: 1, bar: 1}}, true); + + // Nested property dependency and nested schema dependency. + assertSchemaMatch( + coll, {properties: {obj: {dependencies: {a: ["b"], c: {required: ["d"]}}}}}, {}, true); + assertSchemaMatch(coll, + {properties: {obj: {dependencies: {a: ["b"], c: {required: ["d"]}}}}}, + {obj: 1}, + true); + assertSchemaMatch(coll, + {properties: {obj: {dependencies: {a: ["b"], c: {required: ["d"]}}}}}, + {obj: {}}, + true); + assertSchemaMatch(coll, + {properties: {obj: {dependencies: {a: ["b"], c: {required: ["d"]}}}}}, + {obj: {b: 1, d: 1}}, + true); + assertSchemaMatch(coll, + {properties: {obj: {dependencies: {a: ["b"], c: {required: ["d"]}}}}}, + {obj: {a: 1, b: 1, c: 1}}, + false); + assertSchemaMatch(coll, + {properties: {obj: {dependencies: {a: ["b"], c: {required: ["d"]}}}}}, + {obj: {a: 1, c: 0, d: 1}}, + false); + assertSchemaMatch(coll, + {properties: {obj: {dependencies: {a: ["b"], c: {required: ["d"]}}}}}, + {obj: {b: 1, c: 1, d: 1}}, + true); + assertSchemaMatch(coll, + {properties: {obj: {dependencies: {a: ["b"], c: {required: ["d"]}}}}}, + {obj: {a: 1, b: 1, d: 1}}, + true); + assertSchemaMatch(coll, + {properties: {obj: {dependencies: {a: ["b"], c: {required: ["d"]}}}}}, + {obj: {a: 1, b: 1, c: 1, d: 1}}, + true); +}()); diff --git a/src/mongo/db/matcher/schema/json_schema_parser.cpp b/src/mongo/db/matcher/schema/json_schema_parser.cpp index 0c1f46cc102..1cfd4476761 100644 --- a/src/mongo/db/matcher/schema/json_schema_parser.cpp +++ b/src/mongo/db/matcher/schema/json_schema_parser.cpp @@ -37,6 +37,7 @@ #include "mongo/db/matcher/expression_parser.h" #include "mongo/db/matcher/matcher_type_set.h" #include "mongo/db/matcher/schema/expression_internal_schema_all_elem_match_from_index.h" +#include "mongo/db/matcher/schema/expression_internal_schema_cond.h" #include "mongo/db/matcher/schema/expression_internal_schema_fmod.h" #include "mongo/db/matcher/schema/expression_internal_schema_max_items.h" #include "mongo/db/matcher/schema/expression_internal_schema_max_length.h" @@ -57,6 +58,7 @@ namespace { constexpr StringData kSchemaAdditionalItemsKeyword = "additionalItems"_sd; constexpr StringData kSchemaAllOfKeyword = "allOf"_sd; constexpr StringData kSchemaAnyOfKeyword = "anyOf"_sd; +constexpr StringData kSchemaDependenciesKeyword = "dependencies"_sd; constexpr StringData kSchemaExclusiveMaximumKeyword = "exclusiveMaximum"_sd; constexpr StringData kSchemaExclusiveMinimumKeyword = "exclusiveMinimum"_sd; constexpr StringData kSchemaItemsKeyword = "items"_sd; @@ -546,6 +548,133 @@ StatusWithMatchExpression parseNumProperties(StringData path, return makeRestriction(BSONType::Object, path, std::move(objectMatch), typeExpr); } +StatusWithMatchExpression makeDependencyExistsClause(StringData path, StringData dependencyName) { + auto existsExpr = stdx::make_unique<ExistsMatchExpression>(); + invariantOK(existsExpr->init(dependencyName)); + + if (path.empty()) { + return {std::move(existsExpr)}; + } + + auto objectMatch = stdx::make_unique<InternalSchemaObjectMatchExpression>(); + auto status = objectMatch->init(std::move(existsExpr), path); + if (!status.isOK()) { + return status; + } + + return {std::move(objectMatch)}; +} + +StatusWithMatchExpression translateSchemaDependency(StringData path, BSONElement dependency) { + invariant(dependency.type() == BSONType::Object); + + auto nestedSchemaMatch = _parse(path, dependency.embeddedObject()); + if (!nestedSchemaMatch.isOK()) { + return nestedSchemaMatch.getStatus(); + } + + auto ifClause = makeDependencyExistsClause(path, dependency.fieldNameStringData()); + if (!ifClause.isOK()) { + return ifClause.getStatus(); + } + + auto condExpr = stdx::make_unique<InternalSchemaCondMatchExpression>(); + condExpr->init({std::move(ifClause.getValue()), + std::move(nestedSchemaMatch.getValue()), + stdx::make_unique<AlwaysTrueMatchExpression>()}); + return {std::move(condExpr)}; +} + +StatusWithMatchExpression translatePropertyDependency(StringData path, BSONElement dependency) { + invariant(dependency.type() == BSONType::Array); + + if (dependency.embeddedObject().isEmpty()) { + return {ErrorCodes::FailedToParse, + str::stream() << "property '" << dependency.fieldNameStringData() + << "' in $jsonSchema keyword '" + << kSchemaDependenciesKeyword + << "' cannot be an empty array"}; + } + + auto propertyDependencyExpr = stdx::make_unique<AndMatchExpression>(); + std::set<StringData> propertyDependencyNames; + for (auto&& propertyDependency : dependency.embeddedObject()) { + if (propertyDependency.type() != BSONType::String) { + return {ErrorCodes::TypeMismatch, + str::stream() << "array '" << dependency.fieldNameStringData() + << "' in $jsonSchema keyword '" + << kSchemaDependenciesKeyword + << "' can only contain strings, but found element of type: " + << typeName(propertyDependency.type())}; + } + + auto insertionResult = propertyDependencyNames.insert(propertyDependency.valueStringData()); + if (!insertionResult.second) { + return {ErrorCodes::FailedToParse, + str::stream() << "array '" << dependency.fieldNameStringData() + << "' in $jsonSchema keyword '" + << kSchemaDependenciesKeyword + << "' contains duplicate element: " + << propertyDependency.valueStringData()}; + } + + auto propertyExistsExpr = + makeDependencyExistsClause(path, propertyDependency.valueStringData()); + if (!propertyExistsExpr.isOK()) { + return propertyExistsExpr.getStatus(); + } + + propertyDependencyExpr->add(propertyExistsExpr.getValue().release()); + } + + auto ifClause = makeDependencyExistsClause(path, dependency.fieldNameStringData()); + if (!ifClause.isOK()) { + return ifClause.getStatus(); + } + + auto condExpr = stdx::make_unique<InternalSchemaCondMatchExpression>(); + condExpr->init({std::move(ifClause.getValue()), + std::move(propertyDependencyExpr), + stdx::make_unique<AlwaysTrueMatchExpression>()}); + return {std::move(condExpr)}; +} + +StatusWithMatchExpression parseDependencies(StringData path, BSONElement dependencies) { + if (dependencies.type() != BSONType::Object) { + return {ErrorCodes::TypeMismatch, + str::stream() << "$jsonSchema keyword '" << kSchemaDependenciesKeyword + << "' must be an object"}; + } + + if (dependencies.embeddedObject().isEmpty()) { + return {ErrorCodes::FailedToParse, + str::stream() << "$jsonSchema keyword '" << kSchemaDependenciesKeyword + << "' must be a non-empty object"}; + } + + auto andExpr = stdx::make_unique<AndMatchExpression>(); + for (auto&& dependency : dependencies.embeddedObject()) { + if (dependency.type() != BSONType::Object && dependency.type() != BSONType::Array) { + return {ErrorCodes::TypeMismatch, + str::stream() << "property '" << dependency.fieldNameStringData() + << "' in $jsonSchema keyword '" + << kSchemaDependenciesKeyword + << "' must be either an object or an array"}; + } + + auto dependencyExpr = (dependency.type() == BSONType::Object) + ? translateSchemaDependency(path, dependency) + : translatePropertyDependency(path, dependency); + if (!dependencyExpr.isOK()) { + return dependencyExpr.getStatus(); + } + + andExpr->add(dependencyExpr.getValue().release()); + } + + return {std::move(andExpr)}; +} + /** * Parses the logical keywords in 'keywordMap' to their equivalent match expressions * and, on success, adds the results to 'andExpr'. @@ -649,7 +778,11 @@ Status translateArrayKeywords(StringMap<BSONElement>* keywordMap, * Returns a non-OK status if an error occurs during parsing. * * This function parses the following keywords: + * - dependencies + * - maxProperties + * - minProperties * - properties + * - required */ Status translateObjectKeywords(StringMap<BSONElement>* keywordMap, StringData path, @@ -698,6 +831,14 @@ Status translateObjectKeywords(StringMap<BSONElement>* keywordMap, andExpr->add(maxPropExpr.getValue().release()); } + if (auto dependenciesElt = keywordMap->get(kSchemaDependenciesKeyword)) { + auto dependenciesExpr = parseDependencies(path, dependenciesElt); + if (!dependenciesExpr.isOK()) { + return dependenciesExpr.getStatus(); + } + andExpr->add(dependenciesExpr.getValue().release()); + } + return Status::OK(); } @@ -815,6 +956,7 @@ StatusWithMatchExpression _parse(StringData path, BSONObj schema) { {kSchemaAllOfKeyword, {}}, {kSchemaAnyOfKeyword, {}}, {kSchemaBsonTypeKeyword, {}}, + {kSchemaDependenciesKeyword, {}}, {kSchemaExclusiveMaximumKeyword, {}}, {kSchemaExclusiveMinimumKeyword, {}}, {kSchemaMaxItemsKeyword, {}}, diff --git a/src/mongo/db/matcher/schema/json_schema_parser_test.cpp b/src/mongo/db/matcher/schema/json_schema_parser_test.cpp index 45f3164e9b8..589afd7c58e 100644 --- a/src/mongo/db/matcher/schema/json_schema_parser_test.cpp +++ b/src/mongo/db/matcher/schema/json_schema_parser_test.cpp @@ -1402,5 +1402,166 @@ TEST(JSONSchemaParserTest, CanTranslateNestedBsonTypeArray) { )")); } +TEST(JSONSchemaParserTest, DependenciesFailsToParseIfNotAnObject) { + BSONObj schema = fromjson("{dependencies: []}"); + auto result = JSONSchemaParser::parse(schema); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(JSONSchemaParserTest, DependenciesFailsToParseIfTheEmptyObject) { + BSONObj schema = fromjson("{dependencies: {}}"); + auto result = JSONSchemaParser::parse(schema); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(JSONSchemaParserTest, DependenciesFailsToParseIfDependencyIsNotObjectOrArray) { + BSONObj schema = fromjson("{dependencies: {a: ['b'], bad: 1}}"); + auto result = JSONSchemaParser::parse(schema); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(JSONSchemaParserTest, DependenciesFailsToParseIfNestedSchemaIsInvalid) { + BSONObj schema = fromjson("{dependencies: {a: {invalid: 1}}}"); + auto result = JSONSchemaParser::parse(schema); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(JSONSchemaParserTest, PropertyDependencyFailsToParseIfEmptyArray) { + BSONObj schema = fromjson("{dependencies: {a: []}}"); + auto result = JSONSchemaParser::parse(schema); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(JSONSchemaParserTest, PropertyDependencyFailsToParseIfArrayContainsNonStringElement) { + BSONObj schema = fromjson("{dependencies: {a: ['b', 1]}}"); + auto result = JSONSchemaParser::parse(schema); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(JSONSchemaParserTest, PropertyDependencyFailsToParseIfRepeatedArrayElement) { + BSONObj schema = fromjson("{dependencies: {a: ['b', 'b']}}"); + auto result = JSONSchemaParser::parse(schema); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(JSONSchemaParserTest, TopLevelSchemaDependencyTranslatesCorrectly) { + BSONObj schema = fromjson("{dependencies: {a: {properties: {b: {type: 'string'}}}}}"); + auto result = JSONSchemaParser::parse(schema); + ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson(R"( + { + $and: [{ + $and: [{ + $_internalSchemaCond: [ + {a: {$exists: true}}, + { + $and: [{ + $and: [{ + $or: [ + {$nor: [{b: {$exists: true}}]}, + {$and: [{b: {$_internalSchemaType: [2]}}]} + ] + }] + }] + }, + {$alwaysTrue: 1} + ] + }] + }] + } + )")); +} + +TEST(JSONSchemaParserTest, TopLevelPropertyDependencyTranslatesCorrectly) { + BSONObj schema = fromjson("{dependencies: {a: ['b', 'c']}}"); + auto result = JSONSchemaParser::parse(schema); + ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson(R"( + { + $and: [{ + $and: [{ + $_internalSchemaCond: [ + {a: {$exists: true}}, + {$and: [{b: {$exists: true}}, {c: {$exists: true}}]}, + {$alwaysTrue: 1} + ] + }] + }] + } + )")); +} + +TEST(JSONSchemaParserTest, NestedSchemaDependencyTranslatesCorrectly) { + BSONObj schema = + fromjson("{properties: {a: {dependencies: {b: {properties: {c: {type: 'object'}}}}}}}"); + auto result = JSONSchemaParser::parse(schema); + ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson(R"( + {$and: [{$and: [{ + $or: [ + {$nor: [{a: {$exists: true}}]}, + { + $and: [{$and: [{ + $_internalSchemaCond: [ + {a: {$_internalSchemaObjectMatch: {b: {$exists: true}}}}, + { + $and: [{ + $or: [ + {$nor: [{a: {$_internalSchemaType: [3]}}]}, + { + a: { + $_internalSchemaObjectMatch: { + $and: [{ + $or: [ + {$nor: [{c: {$exists: true}}]}, + { + $and: [{ + c: { + $_internalSchemaType: [3] + } + }] + } + ] + }] + } + } + } + ] + }] + }, + {$alwaysTrue: 1} + ] + }]}] + } + ] + }] + }]})")); +} + +TEST(JSONSchemaParserTest, NestedPropertyDependencyTranslatesCorrectly) { + BSONObj schema = fromjson("{properties: {a: {dependencies: {b: ['c', 'd']}}}}"); + auto result = JSONSchemaParser::parse(schema); + ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson(R"( + {$and: [{$and: [{ + $or: [ + {$nor: [{a: {$exists: true}}]}, + { + $and: [{ + $and: [{ + $_internalSchemaCond: [ + {a: {$_internalSchemaObjectMatch: {b: {$exists: true}}}}, + { + $and: [ + {a: {$_internalSchemaObjectMatch: {c: {$exists: true}}}}, + {a: {$_internalSchemaObjectMatch: {d: {$exists: true}}}} + ] + }, + {$alwaysTrue: 1} + ] + }] + }] + } + ] + }] + }]})")); +} + } // namespace } // namespace mongo |