diff options
author | Nick Zolnierz <nicholas.zolnierz@mongodb.com> | 2017-08-10 12:24:19 -0400 |
---|---|---|
committer | Nick Zolnierz <nicholas.zolnierz@mongodb.com> | 2017-08-18 12:40:55 -0400 |
commit | 1e3240bf58087d1e1796e9dd4f26564f99108520 (patch) | |
tree | 2f06e2f97a5dea8ca2a394f7cc4f4127b450515e | |
parent | 5bdeccdef411b3c8e19c19b2e5190119889eba61 (diff) | |
download | mongo-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.js | 145 | ||||
-rw-r--r-- | src/mongo/db/matcher/schema/json_schema_parser.cpp | 159 | ||||
-rw-r--r-- | src/mongo/db/matcher/schema/json_schema_parser.h | 15 | ||||
-rw-r--r-- | src/mongo/db/matcher/schema/json_schema_parser_test.cpp | 342 |
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 |