summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Storch <david.storch@10gen.com>2017-08-28 18:44:05 -0400
committerDavid Storch <david.storch@10gen.com>2017-08-30 13:22:31 -0400
commit292a7016e0896c93a740c8535de5418633c13148 (patch)
treef864fae0d56ea0b97e1b050c70ae0a216816d260
parent4582f2e1c51f5277488c15732374560ee8ade96f (diff)
downloadmongo-292a7016e0896c93a740c8535de5418633c13148.tar.gz
SERVER-30177 Implement JSON Schema 'dependencies' keyword.
-rw-r--r--jstests/core/json_schema/dependencies.js119
-rw-r--r--src/mongo/db/matcher/schema/json_schema_parser.cpp142
-rw-r--r--src/mongo/db/matcher/schema/json_schema_parser_test.cpp161
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