summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNick Zolnierz <nicholas.zolnierz@mongodb.com>2017-08-10 12:24:19 -0400
committerNick Zolnierz <nicholas.zolnierz@mongodb.com>2017-08-18 12:40:55 -0400
commit1e3240bf58087d1e1796e9dd4f26564f99108520 (patch)
tree2f06e2f97a5dea8ca2a394f7cc4f4127b450515e
parent5bdeccdef411b3c8e19c19b2e5190119889eba61 (diff)
downloadmongo-1e3240bf58087d1e1796e9dd4f26564f99108520.tar.gz
SERVER-30176: Extend the JSON Schema parser to handle logical restriction keywords
-rw-r--r--jstests/core/json_schema/json_schema.js (renamed from jstests/core/json_schema.js)0
-rw-r--r--jstests/core/json_schema/logical_keywords.js145
-rw-r--r--src/mongo/db/matcher/schema/json_schema_parser.cpp159
-rw-r--r--src/mongo/db/matcher/schema/json_schema_parser.h15
-rw-r--r--src/mongo/db/matcher/schema/json_schema_parser_test.cpp342
5 files changed, 558 insertions, 103 deletions
diff --git a/jstests/core/json_schema.js b/jstests/core/json_schema/json_schema.js
index 9906f8b8e27..9906f8b8e27 100644
--- a/jstests/core/json_schema.js
+++ b/jstests/core/json_schema/json_schema.js
diff --git a/jstests/core/json_schema/logical_keywords.js b/jstests/core/json_schema/logical_keywords.js
new file mode 100644
index 00000000000..53ac3d9f2ae
--- /dev/null
+++ b/jstests/core/json_schema/logical_keywords.js
@@ -0,0 +1,145 @@
+/**
+ * Tests for the JSON Schema logical keywords, including:
+ *
+ * - allOf
+ * - anyOf
+ * - oneOf
+ * - not
+ * - enum
+ */
+(function() {
+ "use strict";
+
+ const coll = db.jstests_json_schema_logical;
+ coll.drop();
+
+ assert.writeOK(coll.insert({_id: 0, foo: 3}));
+ assert.writeOK(coll.insert({_id: 1, foo: -3}));
+ assert.writeOK(coll.insert({_id: 2, foo: {}}));
+ assert.writeOK(coll.insert({_id: 3, foo: "string"}));
+ assert.writeOK(coll.insert({_id: 4, foo: ["str", 5]}));
+ assert.writeOK(coll.insert({_id: 5}));
+
+ // Test that $jsonSchema fails to parse if the values for the allOf, anyOf, and oneOf
+ // keywords are not arrays of valid schema.
+ assert.throws(function() {
+ coll.find({$jsonSchema: {properties: {foo: {allOf: {maximum: "0"}}}}}).itcount();
+ });
+ assert.throws(function() {
+ coll.find({$jsonSchema: {properties: {foo: {allOf: [0]}}}}).itcount();
+ });
+ assert.throws(function() {
+ coll.find({$jsonSchema: {properties: {foo: {allOf: [{invalid: "0"}]}}}}).itcount();
+ });
+ assert.throws(function() {
+ coll.find({$jsonSchema: {properties: {foo: {anyOf: {maximum: "0"}}}}}).itcount();
+ });
+ assert.throws(function() {
+ coll.find({$jsonSchema: {properties: {foo: {anyOf: [0]}}}}).itcount();
+ });
+ assert.throws(function() {
+ coll.find({$jsonSchema: {properties: {foo: {anyOf: [{invalid: "0"}]}}}}).itcount();
+ });
+ assert.throws(function() {
+ coll.find({$jsonSchema: {properties: {foo: {oneOf: {maximum: "0"}}}}}).itcount();
+ });
+ assert.throws(function() {
+ coll.find({$jsonSchema: {properties: {foo: {oneOf: [0]}}}}).itcount();
+ });
+ assert.throws(function() {
+ coll.find({$jsonSchema: {properties: {foo: {oneOf: [{invalid: "0"}]}}}}).itcount();
+ });
+
+ // Test that $jsonSchema fails to parse if the value for the 'not' keyword is not a
+ // valid schema object.
+ assert.throws(function() {
+ coll.find({$jsonSchema: {properties: {foo: {not: {maximum: "0"}}}}}).itcount();
+ });
+ assert.throws(function() {
+ coll.find({$jsonSchema: {properties: {foo: {not: [0]}}}}).itcount();
+ });
+ assert.throws(function() {
+ coll.find({$jsonSchema: {properties: {foo: {not: [{}]}}}}).itcount();
+ });
+
+ function runFindSortedResults(schemaFilter) {
+ return coll.find({$jsonSchema: schemaFilter}, {_id: 1}).sort({_id: 1}).toArray();
+ }
+
+ // Test that the 'allOf' keyword correctly returns documents that match every schema in
+ // the array.
+ assert.eq([{_id: 0}, {_id: 2}, {_id: 3}, {_id: 4}, {_id: 5}],
+ runFindSortedResults({properties: {foo: {allOf: [{minimum: 0}]}}}));
+ assert.eq([{_id: 0}, {_id: 1}, {_id: 2}, {_id: 3}, {_id: 4}, {_id: 5}],
+ runFindSortedResults({properties: {foo: {allOf: [{}]}}}));
+ assert.eq([{_id: 0}, {_id: 5}],
+ runFindSortedResults({properties: {foo: {allOf: [{type: 'number'}, {minimum: 0}]}}}));
+
+ // Test that a top-level 'allOf' keyword matches the correct documents.
+ assert.eq([{_id: 0}, {_id: 1}, {_id: 2}, {_id: 3}, {_id: 4}, {_id: 5}],
+ runFindSortedResults({allOf: [{}]}));
+ assert.eq([{_id: 3}, {_id: 5}],
+ runFindSortedResults({allOf: [{properties: {foo: {type: 'string'}}}]}));
+
+ // Test that 'allOf' in conjunction with another keyword matches the correct documents.
+ assert.eq([{_id: 0}, {_id: 5}],
+ runFindSortedResults({properties: {foo: {type: "number", allOf: [{minimum: 0}]}}}));
+
+ // Test that the 'anyOf' keyword correctly returns documents that match at least one schema
+ // in the array.
+ assert.eq([{_id: 0}, {_id: 3}, {_id: 5}],
+ runFindSortedResults(
+ {properties: {foo: {anyOf: [{type: 'string'}, {type: 'number', minimum: 0}]}}}));
+ assert.eq(
+ [{_id: 2}, {_id: 3}, {_id: 5}],
+ runFindSortedResults({properties: {foo: {anyOf: [{type: 'string'}, {type: 'object'}]}}}));
+ assert.eq([{_id: 0}, {_id: 1}, {_id: 2}, {_id: 3}, {_id: 4}, {_id: 5}],
+ runFindSortedResults({properties: {foo: {anyOf: [{}]}}}));
+
+ // Test that a top-level 'anyOf' keyword matches the correct documents.
+ assert.eq([{_id: 0}, {_id: 1}, {_id: 2}, {_id: 3}, {_id: 4}, {_id: 5}],
+ runFindSortedResults({anyOf: [{}]}));
+ assert.eq([{_id: 3}, {_id: 5}],
+ runFindSortedResults({anyOf: [{properties: {foo: {type: 'string'}}}]}));
+
+ // Test that 'anyOf' in conjunction with another keyword matches the correct documents.
+ assert.eq([{_id: 5}],
+ runFindSortedResults({properties: {foo: {type: "number", anyOf: [{minimum: 4}]}}}));
+
+ // Test that the 'oneOf' keyword correctly returns documents that match exactly one schema
+ // in the array.
+ assert.eq([{_id: 1}, {_id: 5}],
+ runFindSortedResults({properties: {foo: {oneOf: [{minimum: 0}, {maximum: 3}]}}}));
+ assert.eq(
+ [{_id: 0}, {_id: 1}, {_id: 2}, {_id: 4}, {_id: 5}],
+ runFindSortedResults({properties: {foo: {oneOf: [{type: 'string'}, {pattern: "ing"}]}}}));
+ assert.eq([{_id: 0}, {_id: 1}, {_id: 2}, {_id: 3}, {_id: 4}, {_id: 5}],
+ runFindSortedResults({properties: {foo: {oneOf: [{}]}}}));
+
+ // Test that a top-level 'oneOf' keyword matches the correct documents.
+ assert.eq([{_id: 0}, {_id: 1}, {_id: 2}, {_id: 3}, {_id: 4}, {_id: 5}],
+ runFindSortedResults({oneOf: [{}]}));
+ assert.eq([{_id: 3}, {_id: 5}],
+ runFindSortedResults({oneOf: [{properties: {foo: {type: 'string'}}}]}));
+ assert.eq([], runFindSortedResults({oneOf: [{}, {}]}));
+
+ // Test that 'oneOf' in conjunction with another keyword matches the correct documents.
+ assert.eq([{_id: 5}],
+ runFindSortedResults({properties: {foo: {type: "number", oneOf: [{minimum: 4}]}}}));
+
+ // Test that the 'not' keyword correctly returns documents that do not match any schema
+ // in the array.
+ assert.eq([{_id: 2}, {_id: 3}, {_id: 4}, {_id: 5}],
+ runFindSortedResults({properties: {foo: {not: {type: 'number'}}}}));
+ assert.eq([{_id: 5}], runFindSortedResults({properties: {foo: {not: {}}}}));
+
+ // Test that a top-level 'not' keyword matches the correct documents.
+ assert.eq([], runFindSortedResults({not: {}}));
+ assert.eq([{_id: 0}, {_id: 1}, {_id: 2}, {_id: 4}],
+ runFindSortedResults({not: {properties: {foo: {type: 'string'}}}}));
+
+ // Test that 'not' in conjunction with another keyword matches the correct documents.
+ assert.eq([{_id: 3}, {_id: 5}],
+ runFindSortedResults({properties: {foo: {type: "string", not: {maxLength: 4}}}}));
+
+}());
diff --git a/src/mongo/db/matcher/schema/json_schema_parser.cpp b/src/mongo/db/matcher/schema/json_schema_parser.cpp
index 7f6cc239fc2..7d808da5d0e 100644
--- a/src/mongo/db/matcher/schema/json_schema_parser.cpp
+++ b/src/mongo/db/matcher/schema/json_schema_parser.cpp
@@ -38,6 +38,7 @@
#include "mongo/db/matcher/schema/expression_internal_schema_max_length.h"
#include "mongo/db/matcher/schema/expression_internal_schema_min_length.h"
#include "mongo/db/matcher/schema/expression_internal_schema_object_match.h"
+#include "mongo/db/matcher/schema/expression_internal_schema_xor.h"
#include "mongo/stdx/memory.h"
#include "mongo/util/string_map.h"
@@ -45,18 +46,33 @@ namespace mongo {
namespace {
// JSON Schema keyword constants.
+constexpr StringData kSchemaAllOfKeyword = "allOf"_sd;
+constexpr StringData kSchemaAnyOfKeyword = "anyOf"_sd;
constexpr StringData kSchemaExclusiveMaximumKeyword = "exclusiveMaximum"_sd;
constexpr StringData kSchemaExclusiveMinimumKeyword = "exclusiveMinimum"_sd;
constexpr StringData kSchemaMaximumKeyword = "maximum"_sd;
-constexpr StringData kSchemaMinimumKeyword = "minimum"_sd;
constexpr StringData kSchemaMaxLengthKeyword = "maxLength"_sd;
+constexpr StringData kSchemaMinimumKeyword = "minimum"_sd;
constexpr StringData kSchemaMinLengthKeyword = "minLength"_sd;
-constexpr StringData kSchemaPatternKeyword = "pattern"_sd;
constexpr StringData kSchemaMultipleOfKeyword = "multipleOf"_sd;
+constexpr StringData kSchemaNotKeyword = "not"_sd;
+constexpr StringData kSchemaOneOfKeyword = "oneOf"_sd;
+constexpr StringData kSchemaPatternKeyword = "pattern"_sd;
constexpr StringData kSchemaPropertiesKeyword = "properties"_sd;
constexpr StringData kSchemaTypeKeyword = "type"_sd;
/**
+ * Parses 'schema' to the semantically equivalent match expression. If the schema has an
+ * associated path, e.g. if we are parsing the nested schema for property "myProp" in
+ *
+ * {properties: {myProp: <nested-schema>}}
+ *
+ * then this is passed in 'path'. In this example, the value of 'path' is "myProp". If there is
+ * no path, e.g. for top-level schemas, then 'path' is empty.
+ */
+StatusWithMatchExpression _parse(StringData path, BSONObj schema);
+
+/**
* Constructs and returns a match expression to evaluate a JSON Schema restriction keyword.
*
* This handles semantic differences between the MongoDB query language and JSON Schema. MongoDB
@@ -279,10 +295,106 @@ StatusWithMatchExpression parseMultipleOf(StringData path,
return makeRestriction(restrictionType, std::move(expr), typeExpr);
}
-} // namespace
+template <class T>
+StatusWithMatchExpression parseLogicalKeyword(StringData path, BSONElement logicalElement) {
+ if (logicalElement.type() != BSONType::Array) {
+ return {ErrorCodes::TypeMismatch,
+ str::stream() << "$jsonSchema keyword '" << logicalElement.fieldNameStringData()
+ << "' must be an array"};
+ }
+
+ auto logicalElementObj = logicalElement.embeddedObject();
+ if (logicalElementObj.isEmpty()) {
+ return {ErrorCodes::BadValue,
+ str::stream() << "$jsonSchema keyword '" << logicalElement.fieldNameStringData()
+ << "' must be a non-empty array"};
+ }
+
+ std::unique_ptr<T> listOfExpr = stdx::make_unique<T>();
+ for (const auto& elem : logicalElementObj) {
+ if (elem.type() != BSONType::Object) {
+ return {ErrorCodes::TypeMismatch,
+ str::stream() << "$jsonSchema keyword '" << logicalElement.fieldNameStringData()
+ << "' must be an array of objects, but found an element of type "
+ << elem.type()};
+ }
+
+ auto nestedSchemaMatch = _parse(path, elem.embeddedObject());
+ if (!nestedSchemaMatch.isOK()) {
+ return nestedSchemaMatch.getStatus();
+ }
+
+ listOfExpr->add(nestedSchemaMatch.getValue().release());
+ }
+
+ return {std::move(listOfExpr)};
+}
+
+/**
+ * Parses the logical keywords in 'keywordMap' to their equivalent match expressions
+ * and, on success, adds the results to 'andExpr'.
+ *
+ * This function parses the following keywords:
+ * - allOf
+ * - anyOf
+ * - oneOf
+ * - not
+ * - enum
+ */
+Status parseLogicalKeywords(StringMap<BSONElement>& keywordMap,
+ StringData path,
+ AndMatchExpression* andExpr) {
+ if (auto allOfElt = keywordMap[kSchemaAllOfKeyword]) {
+ auto allOfExpr = parseLogicalKeyword<AndMatchExpression>(path, allOfElt);
+ if (!allOfExpr.isOK()) {
+ return allOfExpr.getStatus();
+ }
+ andExpr->add(allOfExpr.getValue().release());
+ }
+
+ if (auto anyOfElt = keywordMap[kSchemaAnyOfKeyword]) {
+ auto anyOfExpr = parseLogicalKeyword<OrMatchExpression>(path, anyOfElt);
+ if (!anyOfExpr.isOK()) {
+ return anyOfExpr.getStatus();
+ }
+ andExpr->add(anyOfExpr.getValue().release());
+ }
+
+ if (auto oneOfElt = keywordMap[kSchemaOneOfKeyword]) {
+ auto oneOfExpr = parseLogicalKeyword<InternalSchemaXorMatchExpression>(path, oneOfElt);
+ if (!oneOfExpr.isOK()) {
+ return oneOfExpr.getStatus();
+ }
+ andExpr->add(oneOfExpr.getValue().release());
+ }
+
+ if (auto notElt = keywordMap[kSchemaNotKeyword]) {
+ if (notElt.type() != BSONType::Object) {
+ return {ErrorCodes::TypeMismatch,
+ str::stream() << "$jsonSchema keyword '" << kSchemaNotKeyword
+ << "' must be an object, but found an element of type "
+ << notElt.type()};
+ }
+
+ auto parsedExpr = _parse(path, notElt.embeddedObject());
+ if (!parsedExpr.isOK()) {
+ return parsedExpr.getStatus();
+ }
-StatusWithMatchExpression JSONSchemaParser::_parseProperties(
- StringData path, BSONElement propertiesElt, InternalSchemaTypeExpression* typeExpr) {
+ auto notMatchExpr = stdx::make_unique<NotMatchExpression>();
+ auto initStatus = notMatchExpr->init(parsedExpr.getValue().release());
+ if (!initStatus.isOK()) {
+ return initStatus;
+ }
+ andExpr->add(notMatchExpr.release());
+ }
+
+ return Status::OK();
+}
+
+StatusWithMatchExpression parseProperties(StringData path,
+ BSONElement propertiesElt,
+ InternalSchemaTypeExpression* typeExpr) {
if (propertiesElt.type() != BSONType::Object) {
return {Status(ErrorCodes::TypeMismatch,
str::stream() << "$jsonSchema keyword '" << kSchemaPropertiesKeyword
@@ -334,19 +446,25 @@ StatusWithMatchExpression JSONSchemaParser::_parseProperties(
return makeRestriction(BSONType::Object, std::move(objectMatch), typeExpr);
}
-StatusWithMatchExpression JSONSchemaParser::_parse(StringData path, BSONObj schema) {
+StatusWithMatchExpression _parse(StringData path, BSONObj schema) {
// Map from JSON Schema keyword to the corresponding element from 'schema', or to an empty
// BSONElement if the JSON Schema keyword is not specified.
- StringMap<BSONElement> keywordMap{{kSchemaTypeKeyword, {}},
- {kSchemaPropertiesKeyword, {}},
- {kSchemaMaximumKeyword, {}},
- {kSchemaMinimumKeyword, {}},
- {kSchemaExclusiveMaximumKeyword, {}},
- {kSchemaExclusiveMinimumKeyword, {}},
- {kSchemaMaxLengthKeyword, {}},
- {kSchemaMinLengthKeyword, {}},
- {kSchemaPatternKeyword, {}},
- {kSchemaMultipleOfKeyword, {}}};
+ StringMap<BSONElement> keywordMap{
+ {kSchemaAllOfKeyword, {}},
+ {kSchemaAnyOfKeyword, {}},
+ {kSchemaExclusiveMaximumKeyword, {}},
+ {kSchemaExclusiveMinimumKeyword, {}},
+ {kSchemaMaximumKeyword, {}},
+ {kSchemaMaxLengthKeyword, {}},
+ {kSchemaMinimumKeyword, {}},
+ {kSchemaMinLengthKeyword, {}},
+ {kSchemaMultipleOfKeyword, {}},
+ {kSchemaNotKeyword, {}},
+ {kSchemaOneOfKeyword, {}},
+ {kSchemaPatternKeyword, {}},
+ {kSchemaPropertiesKeyword, {}},
+ {kSchemaTypeKeyword, {}},
+ };
for (auto&& elt : schema) {
auto it = keywordMap.find(elt.fieldNameStringData());
@@ -373,7 +491,7 @@ StatusWithMatchExpression JSONSchemaParser::_parse(StringData path, BSONObj sche
auto andExpr = stdx::make_unique<AndMatchExpression>();
if (auto propertiesElt = keywordMap[kSchemaPropertiesKeyword]) {
- auto propertiesExpr = _parseProperties(path, propertiesElt, typeExpr.getValue().get());
+ auto propertiesExpr = parseProperties(path, propertiesElt, typeExpr.getValue().get());
if (!propertiesExpr.isOK()) {
return propertiesExpr;
}
@@ -467,6 +585,11 @@ StatusWithMatchExpression JSONSchemaParser::_parse(StringData path, BSONObj sche
andExpr->add(multipleOfExpr.getValue().release());
}
+ auto parseStatus = parseLogicalKeywords(keywordMap, path, andExpr.get());
+ if (!parseStatus.isOK()) {
+ return parseStatus;
+ }
+
if (path.empty() && typeExpr.getValue() &&
typeExpr.getValue()->getBSONType() != BSONType::Object) {
// This is a top-level schema which requires that the type is something other than
@@ -480,6 +603,8 @@ StatusWithMatchExpression JSONSchemaParser::_parse(StringData path, BSONObj sche
return {std::move(andExpr)};
}
+} // namespace
+
StatusWithMatchExpression JSONSchemaParser::parse(BSONObj schema) {
return _parse(StringData{}, schema);
}
diff --git a/src/mongo/db/matcher/schema/json_schema_parser.h b/src/mongo/db/matcher/schema/json_schema_parser.h
index 90b7f5402aa..28a87e337f4 100644
--- a/src/mongo/db/matcher/schema/json_schema_parser.h
+++ b/src/mongo/db/matcher/schema/json_schema_parser.h
@@ -41,21 +41,6 @@ public:
* tree. Returns a non-OK status if the schema is invalid or cannot be parsed.
*/
static StatusWithMatchExpression parse(BSONObj schema);
-
-private:
- // Parses 'schema' to the semantically equivalent match expression. If the schema has an
- // associated path, e.g. if we are parsing the nested schema for property "myProp" in
- //
- // {properties: {myProp: <nested-schema>}}
- //
- // then this is passed in 'path'. In this example, the value of 'path' is "myProp". If there is
- // no path, e.g. for top-level schemas, then 'path' is empty.
- static StatusWithMatchExpression _parse(StringData path, BSONObj schema);
-
- // Parser for the JSON Schema 'properties' keyword.
- static StatusWithMatchExpression _parseProperties(StringData path,
- BSONElement propertiesElt,
- InternalSchemaTypeExpression* typeExpr);
};
} // namespace mongo
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 d76f718e57f..a3edbc55ddc 100644
--- a/src/mongo/db/matcher/schema/json_schema_parser_test.cpp
+++ b/src/mongo/db/matcher/schema/json_schema_parser_test.cpp
@@ -35,6 +35,13 @@
namespace mongo {
namespace {
+#define ASSERT_SERIALIZES_TO(match, expected) \
+ do { \
+ BSONObjBuilder bob; \
+ match->serialize(&bob); \
+ ASSERT_BSONOBJ_EQ(bob.obj(), expected); \
+ } while (false)
+
TEST(JSONSchemaParserTest, FailsToParseIfTypeIsNotAString) {
BSONObj schema = fromjson("{type: 1}");
auto result = JSONSchemaParser::parse(schema);
@@ -78,18 +85,14 @@ TEST(JSONSchemaParserTest, EmptySchemaTranslatesCorrectly) {
BSONObj schema = fromjson("{}");
auto result = JSONSchemaParser::parse(schema);
ASSERT_OK(result.getStatus());
- BSONObjBuilder builder;
- result.getValue()->serialize(&builder);
- ASSERT_BSONOBJ_EQ(builder.obj(), fromjson("{}"));
+ ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson("{}"));
}
TEST(JSONSchemaParserTest, TypeObjectTranslatesCorrectly) {
BSONObj schema = fromjson("{type: 'object'}");
auto result = JSONSchemaParser::parse(schema);
ASSERT_OK(result.getStatus());
- BSONObjBuilder builder;
- result.getValue()->serialize(&builder);
- ASSERT_BSONOBJ_EQ(builder.obj(), fromjson("{}"));
+ ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson("{}"));
}
TEST(JSONSchemaParserTest, NestedTypeObjectTranslatesCorrectly) {
@@ -97,9 +100,7 @@ TEST(JSONSchemaParserTest, NestedTypeObjectTranslatesCorrectly) {
fromjson("{properties: {a: {type: 'object', properties: {b: {type: 'string'}}}}}");
auto result = JSONSchemaParser::parse(schema);
ASSERT_OK(result.getStatus());
- BSONObjBuilder builder;
- result.getValue()->serialize(&builder);
- ASSERT_BSONOBJ_EQ(builder.obj(), fromjson(R"({
+ ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson(R"({
$and: [{
$and: [{
$or: [
@@ -131,18 +132,14 @@ TEST(JSONSchemaParserTest, TopLevelNonObjectTypeTranslatesCorrectly) {
BSONObj schema = fromjson("{type: 'string'}");
auto result = JSONSchemaParser::parse(schema);
ASSERT_OK(result.getStatus());
- BSONObjBuilder builder;
- result.getValue()->serialize(&builder);
- ASSERT_BSONOBJ_EQ(builder.obj(), fromjson("{$alwaysFalse: 1}"));
+ ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson("{$alwaysFalse: 1}"));
}
TEST(JSONSchemaParserTest, TypeNumberTranslatesCorrectly) {
BSONObj schema = fromjson("{properties: {num: {type: 'number'}}}");
auto result = JSONSchemaParser::parse(schema);
ASSERT_OK(result.getStatus());
- BSONObjBuilder builder;
- result.getValue()->serialize(&builder);
- ASSERT_BSONOBJ_EQ(builder.obj(), fromjson(R"({
+ ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson(R"({
$and: [{
$and: [{
$or: [
@@ -158,9 +155,7 @@ TEST(JSONSchemaParserTest, MaximumTranslatesCorrectlyWithTypeNumber) {
BSONObj schema = fromjson("{properties: {num: {type: 'number', maximum: 0}}, type: 'object'}");
auto result = JSONSchemaParser::parse(schema);
ASSERT_OK(result.getStatus());
- BSONObjBuilder builder;
- result.getValue()->serialize(&builder);
- ASSERT_BSONOBJ_EQ(builder.obj(), fromjson(R"({
+ ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson(R"({
$and: [{
$and: [{
$or: [
@@ -181,9 +176,7 @@ TEST(JSONSchemaParserTest, MaximumTranslatesCorrectlyWithTypeLong) {
BSONObj schema = fromjson("{properties: {num: {type: 'long', maximum: 0}}, type: 'object'}");
auto result = JSONSchemaParser::parse(schema);
ASSERT_OK(result.getStatus());
- BSONObjBuilder builder;
- result.getValue()->serialize(&builder);
- ASSERT_BSONOBJ_EQ(builder.obj(), fromjson(R"({
+ ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson(R"({
$and: [{
$and: [{
$or: [
@@ -204,9 +197,7 @@ TEST(JSONSchemaParserTest, MaximumTranslatesCorrectlyWithTypeString) {
BSONObj schema = fromjson("{properties: {num: {type: 'string', maximum: 0}}, type: 'object'}");
auto result = JSONSchemaParser::parse(schema);
ASSERT_OK(result.getStatus());
- BSONObjBuilder builder;
- result.getValue()->serialize(&builder);
- ASSERT_BSONOBJ_EQ(builder.obj(), fromjson(R"({
+ ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson(R"({
$and: [{
$and: [{
$or: [
@@ -222,9 +213,7 @@ TEST(JSONSchemaParserTest, MaximumTranslatesCorrectlyWithNoType) {
BSONObj schema = fromjson("{properties: {num: {maximum: 0}}}");
auto result = JSONSchemaParser::parse(schema);
ASSERT_OK(result.getStatus());
- BSONObjBuilder builder;
- result.getValue()->serialize(&builder);
- ASSERT_BSONOBJ_EQ(builder.obj(), fromjson(R"({
+ ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson(R"({
$and: [{
$and: [{
$or: [
@@ -265,9 +254,7 @@ TEST(JSONSchemaParserTest, MinimumTranslatesCorrectlyWithTypeNumber) {
BSONObj schema = fromjson("{properties: {num: {type: 'number', minimum: 0}}, type: 'object'}");
auto result = JSONSchemaParser::parse(schema);
ASSERT_OK(result.getStatus());
- BSONObjBuilder builder;
- result.getValue()->serialize(&builder);
- ASSERT_BSONOBJ_EQ(builder.obj(), fromjson(R"({
+ ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson(R"({
$and: [{
$and: [{
$or: [
@@ -296,9 +283,7 @@ TEST(JSONSchemaParserTest, MaxLengthTranslatesCorrectlyWithIntegralDouble) {
fromjson("{properties: {foo: {type: 'string', maxLength: 5.0}}, type: 'object'}");
auto result = JSONSchemaParser::parse(schema);
ASSERT_OK(result.getStatus());
- BSONObjBuilder builder;
- result.getValue()->serialize(&builder);
- ASSERT_BSONOBJ_EQ(builder.obj(), fromjson(R"({
+ ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson(R"({
$and: [{
$and: [{
$or: [
@@ -320,9 +305,7 @@ TEST(JSONSchemaParserTest, MaxLengthTranslatesCorrectlyWithTypeString) {
fromjson("{properties: {foo: {type: 'string', maxLength: 5}}, type: 'object'}");
auto result = JSONSchemaParser::parse(schema);
ASSERT_OK(result.getStatus());
- BSONObjBuilder builder;
- result.getValue()->serialize(&builder);
- ASSERT_BSONOBJ_EQ(builder.obj(), fromjson(R"({
+ ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson(R"({
$and: [{
$and: [{
$or: [
@@ -343,9 +326,7 @@ TEST(JSONSchemaParserTest, MinimumTranslatesCorrectlyWithTypeLong) {
BSONObj schema = fromjson("{properties: {num: {type: 'long', minimum: 0}}, type: 'object'}");
auto result = JSONSchemaParser::parse(schema);
ASSERT_OK(result.getStatus());
- BSONObjBuilder builder;
- result.getValue()->serialize(&builder);
- ASSERT_BSONOBJ_EQ(builder.obj(), fromjson(R"({
+ ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson(R"({
$and: [{
$and: [{
$or: [
@@ -366,9 +347,7 @@ TEST(JSONSchemaParserTest, MinimumTranslatesCorrectlyWithTypeString) {
BSONObj schema = fromjson("{properties: {num: {type: 'string', minimum: 0}}, type: 'object'}");
auto result = JSONSchemaParser::parse(schema);
ASSERT_OK(result.getStatus());
- BSONObjBuilder builder;
- result.getValue()->serialize(&builder);
- ASSERT_BSONOBJ_EQ(builder.obj(), fromjson(R"({
+ ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson(R"({
$and: [{
$and: [{
$or: [
@@ -384,9 +363,7 @@ TEST(JSONSchemaParserTest, MinimumTranslatesCorrectlyWithNoType) {
BSONObj schema = fromjson("{properties: {num: {minimum: 0}}}");
auto result = JSONSchemaParser::parse(schema);
ASSERT_OK(result.getStatus());
- BSONObjBuilder builder;
- result.getValue()->serialize(&builder);
- ASSERT_BSONOBJ_EQ(builder.obj(), fromjson(R"({
+ ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson(R"({
$and: [{
$and: [{
$or: [
@@ -410,9 +387,7 @@ TEST(JSONSchemaParserTest, MaximumTranslatesCorrectlyWithExclusiveMaximumTrue) {
"{properties: {num: {type: 'long', maximum: 0, exclusiveMaximum: true}}, type: 'object'}");
auto result = JSONSchemaParser::parse(schema);
ASSERT_OK(result.getStatus());
- BSONObjBuilder builder;
- result.getValue()->serialize(&builder);
- ASSERT_BSONOBJ_EQ(builder.obj(), fromjson(R"({
+ ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson(R"({
$and: [{
$and: [{
$or: [
@@ -434,9 +409,7 @@ TEST(JSONSchemaParserTest, MaximumTranslatesCorrectlyWithExclusiveMaximumFalse)
"{properties: {num: {type: 'long', maximum: 0, exclusiveMaximum: false}}, type: 'object'}");
auto result = JSONSchemaParser::parse(schema);
ASSERT_OK(result.getStatus());
- BSONObjBuilder builder;
- result.getValue()->serialize(&builder);
- ASSERT_BSONOBJ_EQ(builder.obj(), fromjson(R"({
+ ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson(R"({
$and: [{
$and: [{
$or: [
@@ -470,9 +443,7 @@ TEST(JSONSchemaParserTest, MinimumTranslatesCorrectlyWithExclusiveMinimumTrue) {
"{properties: {num: {type: 'long', minimum: 0, exclusiveMinimum: true}}, type: 'object'}");
auto result = JSONSchemaParser::parse(schema);
ASSERT_OK(result.getStatus());
- BSONObjBuilder builder;
- result.getValue()->serialize(&builder);
- ASSERT_BSONOBJ_EQ(builder.obj(), fromjson(R"({
+ ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson(R"({
$and: [{
$and: [{
$or: [
@@ -494,9 +465,7 @@ TEST(JSONSchemaParserTest, MinimumTranslatesCorrectlyWithExclusiveMinimumFalse)
"{properties: {num: {type: 'long', minimum: 0, exclusiveMinimum: false}}, type: 'object'}");
auto result = JSONSchemaParser::parse(schema);
ASSERT_OK(result.getStatus());
- BSONObjBuilder builder;
- result.getValue()->serialize(&builder);
- ASSERT_BSONOBJ_EQ(builder.obj(), fromjson(R"({
+ ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson(R"({
$and: [{
$and: [{
$or: [
@@ -549,9 +518,7 @@ TEST(JSONSchemaParserTest, MinLengthTranslatesCorrectlyWithTypeString) {
fromjson("{properties: {foo: {type: 'string', minLength: 5}}, type: 'object'}");
auto result = JSONSchemaParser::parse(schema);
ASSERT_OK(result.getStatus());
- BSONObjBuilder builder;
- result.getValue()->serialize(&builder);
- ASSERT_BSONOBJ_EQ(builder.obj(), fromjson(R"({
+ ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson(R"({
$and: [{
$and: [{
$or: [
@@ -573,9 +540,7 @@ TEST(JSONSchemaParserTest, MinLengthTranslatesCorrectlyWithIntegralDouble) {
fromjson("{properties: {foo: {type: 'string', minLength: 5.0}}, type: 'object'}");
auto result = JSONSchemaParser::parse(schema);
ASSERT_OK(result.getStatus());
- BSONObjBuilder builder;
- result.getValue()->serialize(&builder);
- ASSERT_BSONOBJ_EQ(builder.obj(), fromjson(R"({
+ ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson(R"({
$and: [{
$and: [{
$or: [
@@ -609,9 +574,6 @@ TEST(JSONSchemaParserTest, PatternTranslatesCorrectlyWithString) {
fromjson("{properties: {foo: {type: 'string', pattern: 'abc'}}, type: 'object'}");
auto result = JSONSchemaParser::parse(schema);
ASSERT_OK(result.getStatus());
- BSONObjBuilder builder;
- result.getValue()->serialize(&builder);
-
BSONObj expected =
BSON("$and" << BSON_ARRAY(BSON(
"$and" << BSON_ARRAY(BSON(
@@ -621,8 +583,7 @@ TEST(JSONSchemaParserTest, PatternTranslatesCorrectlyWithString) {
BSON("foo" << BSON("$regex"
<< "abc"))
<< BSON("foo" << BSON("$_internalSchemaType" << 2))))))))));
-
- ASSERT_BSONOBJ_EQ(builder.obj(), expected);
+ ASSERT_SERIALIZES_TO(result.getValue().get(), expected);
}
@@ -649,9 +610,7 @@ TEST(JSONSchemaParserTest, MultipleOfTranslatesCorrectlyWithTypeNumber) {
"{properties: {foo: {type: 'number', multipleOf: NumberDecimal('5.3')}}, type: 'object'}");
auto result = JSONSchemaParser::parse(schema);
ASSERT_OK(result.getStatus());
- BSONObjBuilder builder;
- result.getValue()->serialize(&builder);
- ASSERT_BSONOBJ_EQ(builder.obj(), fromjson(R"({
+ ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson(R"({
$and: [{
$and: [{
$or: [
@@ -668,5 +627,246 @@ TEST(JSONSchemaParserTest, MultipleOfTranslatesCorrectlyWithTypeNumber) {
})"));
}
+TEST(JSONSchemaParserTest, FailsToParseIfAllOfIsNotAnArray) {
+ BSONObj schema = fromjson("{properties: {foo: {allOf: 'foo'}}}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch);
+}
+
+TEST(JSONSchemaParserTest, FailsToParseAllOfIfArrayContainsInvalidSchema) {
+ BSONObj schema = fromjson("{properties: {foo: {allOf: [{type: {}}]}}}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch);
+}
+
+TEST(JSONSchemaParserTest, FailsToParseAllOfIfArrayIsEmpty) {
+ BSONObj schema = fromjson("{properties: {foo: {allOf: []}}}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_EQ(result.getStatus(), ErrorCodes::BadValue);
+}
+
+TEST(JSONSchemaParserTest, AllOfTranslatesCorrectly) {
+ BSONObj schema = fromjson("{properties: {foo: {allOf: [{minimum: 0}, {maximum: 10}]}}}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_OK(result.getStatus());
+ ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson(R"({
+ $and: [{
+ $and: [{
+ $or: [
+ {$nor: [{foo: {$exists: true}}]},
+ {$and: [{
+ $and: [
+ {$and: [{
+ $or: [
+ {$nor: [{foo: {$_internalSchemaType: "number"}}]},
+ {foo: {$gte: 0}}
+ ]
+ }]},
+ {$and: [{
+ $or: [
+ {$nor: [{foo: {$_internalSchemaType: "number"}}]},
+ {foo: {$lte: 10}}
+ ]
+ }]}
+ ]
+ }]}
+ ]
+ }]
+ }]})"));
+}
+
+TEST(JSONSchemaParserTest, TopLevelAllOfTranslatesCorrectly) {
+ BSONObj schema = fromjson("{allOf: [{properties: {foo: {type: 'string'}}}]}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_OK(result.getStatus());
+ ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson(R"(
+ {$and: [{
+ $and: [{
+ $and: [{
+ $and: [{
+ $or: [
+ {$nor: [{foo: {$exists: true}}]},
+ {$and: [{foo: {$_internalSchemaType: 2}}]}
+ ]
+ }]
+ }]
+ }]
+ }]})"));
+}
+
+TEST(JSONSchemaParserTest, FailsToParseIfAnyOfIsNotAnArray) {
+ BSONObj schema = fromjson("{properties: {foo: {anyOf: 'foo'}}}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch);
+}
+
+TEST(JSONSchemaParserTest, FailsToParseAnyOfIfArrayContainsInvalidSchema) {
+ BSONObj schema = fromjson("{properties: {foo: {anyOf: [{type: {}}]}}}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch);
+}
+
+TEST(JSONSchemaParserTest, FailsToParseAnyOfIfArrayIsEmpty) {
+ BSONObj schema = fromjson("{properties: {foo: {anyOf: []}}}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_EQ(result.getStatus(), ErrorCodes::BadValue);
+}
+
+TEST(JSONSchemaParserTest, AnyOfTranslatesCorrectly) {
+ BSONObj schema = fromjson("{properties: {foo: {anyOf: [{type: 'number'}, {type: 'string'}]}}}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_OK(result.getStatus());
+ ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson(R"(
+ {$and: [{
+ $and: [{
+ $or: [
+ {$nor: [{foo: {$exists: true}}]},
+ {$and: [{
+ $or: [
+ {$and: [{foo: {$_internalSchemaType: "number"}}]},
+ {$and: [{foo: {$_internalSchemaType: 2}}]}
+ ]
+ }]}
+ ]
+ }]
+ }]})"));
+}
+
+TEST(JSONSchemaParserTest, TopLevelAnyOfTranslatesCorrectly) {
+ BSONObj schema = fromjson("{anyOf: [{properties: {foo: {type: 'string'}}}]}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_OK(result.getStatus());
+ ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson(R"(
+ {$and: [{
+ $or: [{
+ $and: [{
+ $and: [{
+ $or: [
+ {$nor: [{foo: {$exists: true}}]},
+ {$and: [{foo: {$_internalSchemaType: 2}}]}
+ ]
+ }]
+ }]
+ }]
+ }]})"));
+}
+
+TEST(JSONSchemaParserTest, FailsToParseIfOneOfIsNotAnArray) {
+ BSONObj schema = fromjson("{properties: {foo: {oneOf: 'foo'}}}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch);
+}
+
+TEST(JSONSchemaParserTest, FailsToParseOneOfIfArrayContainsInvalidSchema) {
+ BSONObj schema = fromjson("{properties: {foo: {oneOf: [{type: {}}]}}}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch);
+}
+
+TEST(JSONSchemaParserTest, FailsToParseOneOfIfArrayIsEmpty) {
+ BSONObj schema = fromjson("{properties: {foo: {oneOf: []}}}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_EQ(result.getStatus(), ErrorCodes::BadValue);
+}
+
+TEST(JSONSchemaParserTest, OneOfTranslatesCorrectly) {
+ BSONObj schema = fromjson("{properties: {foo: {oneOf: [{minimum: 0}, {maximum: 10}]}}}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_OK(result.getStatus());
+ ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson(R"(
+ {$and: [{
+ $and: [{
+ $or: [
+ {$nor: [{foo: {$exists: true}}]},
+ {$and: [{
+ $_internalSchemaXor: [
+ {$and: [{
+ $or: [
+ {$nor: [{foo: {$_internalSchemaType: "number"}}]},
+ {foo: {$gte: 0}}
+ ]
+ }]},
+ {$and: [{
+ $or: [
+ {$nor: [{foo: {$_internalSchemaType: "number"}}]},
+ {foo: {$lte: 10}}
+ ]
+ }]}
+ ]
+ }]}
+ ]
+ }]
+ }]})"));
+}
+
+TEST(JSONSchemaParserTest, TopLevelOneOfTranslatesCorrectly) {
+ BSONObj schema = fromjson("{oneOf: [{properties: {foo: {type: 'string'}}}]}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_OK(result.getStatus());
+ ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson(R"(
+ {$and: [{
+ $_internalSchemaXor: [{
+ $and: [{
+ $and: [{
+ $or: [
+ {$nor: [{foo: {$exists: true}}]},
+ {$and: [{foo: {$_internalSchemaType: 2}}]}
+ ]
+ }]
+ }]
+ }]
+ }]})"));
+}
+
+TEST(JSONSchemaParserTest, FailsToParseIfNotIsNotAnObject) {
+ BSONObj schema = fromjson("{properties: {foo: {not: 'foo'}}}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch);
+}
+
+TEST(JSONSchemaParserTest, FailsToParseNotIfObjectContainsInvalidSchema) {
+ BSONObj schema = fromjson("{properties: {foo: {not: {type: {}}}}}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch);
+}
+
+TEST(JSONSchemaParserTest, NotTranslatesCorrectly) {
+ BSONObj schema = fromjson("{properties: {foo: {not: {type: 'number'}}}}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_OK(result.getStatus());
+ ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson(R"(
+ {$and: [{
+ $and: [{
+ $or: [
+ {$nor: [{foo: {$exists: true}}]},
+ {$and: [{
+ $nor: [{
+ $and: [{foo: {$_internalSchemaType: "number"}}]
+ }]
+ }]}
+ ]
+ }]
+ }]})"));
+}
+
+TEST(JSONSchemaParserTest, TopLevelNotTranslatesCorrectly) {
+ BSONObj schema = fromjson("{not: {properties: {foo: {type: 'string'}}}}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_OK(result.getStatus());
+ ASSERT_SERIALIZES_TO(result.getValue().get(), fromjson(R"(
+ {$and: [{
+ $nor: [{
+ $and: [{
+ $and: [{
+ $or: [
+ {$nor: [{foo: {$exists: true}}]},
+ {$and: [{foo: {$_internalSchemaType: 2}}]}
+ ]
+ }]
+ }]
+ }]
+ }]})"));
+}
+
} // namespace
} // namespace mongo