summaryrefslogtreecommitdiff
path: root/src/mongo/db/matcher
diff options
context:
space:
mode:
authorKyle Suarez <kyle.suarez@mongodb.com>2017-09-18 13:53:54 -0400
committerKyle Suarez <kyle.suarez@mongodb.com>2017-09-18 14:02:04 -0400
commite7c299f90d2f6b76b5ffe20efd1e80d91662c676 (patch)
tree3ef1cadad47172394b894f44f8ff93e9e41f448b /src/mongo/db/matcher
parente49cbf575a9f8a867280d0a21c3da622700dc3f5 (diff)
downloadmongo-e7c299f90d2f6b76b5ffe20efd1e80d91662c676.tar.gz
SERVER-30178 extend JSON Schema parser to handle "items" and "additionalItems"
Diffstat (limited to 'src/mongo/db/matcher')
-rw-r--r--src/mongo/db/matcher/schema/json_schema_parser.cpp215
-rw-r--r--src/mongo/db/matcher/schema/json_schema_parser_test.cpp336
2 files changed, 530 insertions, 21 deletions
diff --git a/src/mongo/db/matcher/schema/json_schema_parser.cpp b/src/mongo/db/matcher/schema/json_schema_parser.cpp
index eb6ced22a0d..e0b4d78a168 100644
--- a/src/mongo/db/matcher/schema/json_schema_parser.cpp
+++ b/src/mongo/db/matcher/schema/json_schema_parser.cpp
@@ -26,6 +26,8 @@
* then also delete it in the license file.
*/
+#define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kQuery
+
#include "mongo/platform/basic.h"
#include "mongo/db/matcher/schema/json_schema_parser.h"
@@ -40,6 +42,7 @@
#include "mongo/db/matcher/schema/expression_internal_schema_allowed_properties.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_match_array_index.h"
#include "mongo/db/matcher/schema/expression_internal_schema_max_items.h"
#include "mongo/db/matcher/schema/expression_internal_schema_max_length.h"
#include "mongo/db/matcher/schema/expression_internal_schema_max_properties.h"
@@ -49,7 +52,9 @@
#include "mongo/db/matcher/schema/expression_internal_schema_object_match.h"
#include "mongo/db/matcher/schema/expression_internal_schema_unique_items.h"
#include "mongo/db/matcher/schema/expression_internal_schema_xor.h"
+#include "mongo/logger/log_component_settings.h"
#include "mongo/stdx/memory.h"
+#include "mongo/util/log.h"
#include "mongo/util/string_map.h"
namespace mongo {
@@ -837,6 +842,180 @@ StatusWithMatchExpression parseDependencies(StringData path,
return {std::move(andExpr)};
}
+StatusWithMatchExpression parseUniqueItems(BSONElement uniqueItemsElt,
+ StringData path,
+ InternalSchemaTypeExpression* typeExpr) {
+ if (!uniqueItemsElt.isBoolean()) {
+ return {ErrorCodes::TypeMismatch,
+ str::stream() << "$jsonSchema keyword '" << kSchemaUniqueItemsKeyword
+ << "' must be a boolean"};
+ } else if (path.empty()) {
+ return {stdx::make_unique<AlwaysTrueMatchExpression>()};
+ } else if (uniqueItemsElt.boolean()) {
+ auto uniqueItemsExpr = stdx::make_unique<InternalSchemaUniqueItemsMatchExpression>();
+ auto status = uniqueItemsExpr->init(path);
+ if (!status.isOK()) {
+ return status;
+ }
+ return makeRestriction(BSONType::Array, path, std::move(uniqueItemsExpr), typeExpr);
+ }
+
+ return {stdx::make_unique<AlwaysTrueMatchExpression>()};
+}
+
+/**
+ * Parses 'itemsElt' into a match expression and adds it to 'andExpr'. On success, returns the index
+ * from which the "additionalItems" schema should be enforced, if needed.
+ */
+StatusWith<boost::optional<long long>> parseItems(StringData path,
+ BSONElement itemsElt,
+ bool ignoreUnknownKeywords,
+ InternalSchemaTypeExpression* typeExpr,
+ AndMatchExpression* andExpr) {
+ boost::optional<long long> startIndexForAdditionalItems;
+ if (itemsElt.type() == BSONType::Array) {
+ // When "items" is an array, generate match expressions for each subschema for each position
+ // in the array, which are bundled together in an AndMatchExpression.
+ auto andExprForSubschemas = stdx::make_unique<AndMatchExpression>();
+ auto index = 0LL;
+ for (auto subschema : itemsElt.embeddedObject()) {
+ if (subschema.type() != BSONType::Object) {
+ return {ErrorCodes::TypeMismatch,
+ str::stream() << "$jsonSchema keyword '" << kSchemaItemsKeyword
+ << "' requires that each element of the array is an "
+ "object, but found a "
+ << subschema.type()};
+ }
+
+ // We want to make an ExpressionWithPlaceholder for $_internalSchemaMatchArrayIndex,
+ // so we use our default placeholder as the path.
+ auto parsedSubschema =
+ _parse(kNamePlaceholder, subschema.embeddedObject(), ignoreUnknownKeywords);
+ if (!parsedSubschema.isOK()) {
+ return parsedSubschema.getStatus();
+ }
+ auto exprWithPlaceholder = stdx::make_unique<ExpressionWithPlaceholder>(
+ kNamePlaceholder.toString(), std::move(parsedSubschema.getValue()));
+ auto matchArrayIndex =
+ stdx::make_unique<InternalSchemaMatchArrayIndexMatchExpression>();
+ invariantOK(matchArrayIndex->init(path, index, std::move(exprWithPlaceholder)));
+ andExprForSubschemas->add(matchArrayIndex.release());
+ ++index;
+ }
+ startIndexForAdditionalItems = index;
+
+ if (path.empty()) {
+ andExpr->add(stdx::make_unique<AlwaysTrueMatchExpression>().release());
+ } else {
+ andExpr->add(
+ makeRestriction(BSONType::Array, path, std::move(andExprForSubschemas), typeExpr)
+ .release());
+ }
+ } else if (itemsElt.type() == BSONType::Object) {
+ // When "items" is an object, generate a single AllElemMatchFromIndex that applies to every
+ // element in the array to match. The parsed expression is intended for an
+ // ExpressionWithPlaceholder, so we use the default placeholder as the path.
+ auto nestedItemsSchema =
+ _parse(kNamePlaceholder, itemsElt.embeddedObject(), ignoreUnknownKeywords);
+ if (!nestedItemsSchema.isOK()) {
+ return nestedItemsSchema.getStatus();
+ }
+ auto exprWithPlaceholder = stdx::make_unique<ExpressionWithPlaceholder>(
+ kNamePlaceholder.toString(), std::move(nestedItemsSchema.getValue()));
+
+ if (path.empty()) {
+ andExpr->add(stdx::make_unique<AlwaysTrueMatchExpression>().release());
+ } else {
+ auto allElemMatch =
+ stdx::make_unique<InternalSchemaAllElemMatchFromIndexMatchExpression>();
+ constexpr auto startIndexForItems = 0LL;
+ invariantOK(
+ allElemMatch->init(path, startIndexForItems, std::move(exprWithPlaceholder)));
+ andExpr->add(makeRestriction(BSONType::Array, path, std::move(allElemMatch), typeExpr)
+ .release());
+ }
+ } else {
+ return {ErrorCodes::TypeMismatch,
+ str::stream() << "$jsonSchema keyword '" << kSchemaItemsKeyword
+ << "' must be an array or an object, not "
+ << itemsElt.type()};
+ }
+
+ return startIndexForAdditionalItems;
+}
+
+Status parseAdditionalItems(StringData path,
+ BSONElement additionalItemsElt,
+ boost::optional<long long> startIndexForAdditionalItems,
+ bool ignoreUnknownKeywords,
+ InternalSchemaTypeExpression* typeExpr,
+ AndMatchExpression* andExpr) {
+ std::unique_ptr<ExpressionWithPlaceholder> otherwiseExpr;
+ if (additionalItemsElt.type() == BSONType::Bool) {
+ const auto emptyPlaceholder = boost::none;
+ if (additionalItemsElt.boolean()) {
+ otherwiseExpr = stdx::make_unique<ExpressionWithPlaceholder>(
+ emptyPlaceholder, stdx::make_unique<AlwaysTrueMatchExpression>());
+ } else {
+ otherwiseExpr = stdx::make_unique<ExpressionWithPlaceholder>(
+ emptyPlaceholder, stdx::make_unique<AlwaysFalseMatchExpression>());
+ }
+ } else if (additionalItemsElt.type() == BSONType::Object) {
+ auto parsedOtherwiseExpr =
+ _parse(kNamePlaceholder, additionalItemsElt.embeddedObject(), ignoreUnknownKeywords);
+ if (!parsedOtherwiseExpr.isOK()) {
+ return parsedOtherwiseExpr.getStatus();
+ }
+ otherwiseExpr = stdx::make_unique<ExpressionWithPlaceholder>(
+ kNamePlaceholder.toString(), std::move(parsedOtherwiseExpr.getValue()));
+ } else {
+ return {ErrorCodes::TypeMismatch,
+ str::stream() << "$jsonSchema keyword '" << kSchemaAdditionalItemsKeyword
+ << "' must be either an object or a boolean, but got a "
+ << additionalItemsElt.type()};
+ }
+
+ // Only generate a match expression if needed.
+ if (startIndexForAdditionalItems) {
+ if (path.empty()) {
+ andExpr->add(stdx::make_unique<AlwaysTrueMatchExpression>().release());
+ } else {
+ auto allElemMatch =
+ stdx::make_unique<InternalSchemaAllElemMatchFromIndexMatchExpression>();
+ invariantOK(
+ allElemMatch->init(path, *startIndexForAdditionalItems, std::move(otherwiseExpr)));
+ andExpr->add(makeRestriction(BSONType::Array, path, std::move(allElemMatch), typeExpr)
+ .release());
+ }
+ }
+ return Status::OK();
+}
+
+Status parseItemsAndAdditionalItems(StringMap<BSONElement>* keywordMap,
+ StringData path,
+ bool ignoreUnknownKeywords,
+ InternalSchemaTypeExpression* typeExpr,
+ AndMatchExpression* andExpr) {
+ boost::optional<long long> startIndexForAdditionalItems;
+ if (auto itemsElt = keywordMap->get(kSchemaItemsKeyword)) {
+ auto index = parseItems(path, itemsElt, ignoreUnknownKeywords, typeExpr, andExpr);
+ if (!index.isOK()) {
+ return index.getStatus();
+ }
+ startIndexForAdditionalItems = index.getValue();
+ }
+
+ if (auto additionalItemsElt = keywordMap->get(kSchemaAdditionalItemsKeyword)) {
+ return parseAdditionalItems(path,
+ additionalItemsElt,
+ startIndexForAdditionalItems,
+ ignoreUnknownKeywords,
+ typeExpr,
+ andExpr);
+ }
+ return Status::OK();
+}
+
/**
* Parses the logical keywords in 'keywordMap' to their equivalent match expressions
* and, on success, adds the results to 'andExpr'.
@@ -916,6 +1095,7 @@ Status translateLogicalKeywords(StringMap<BSONElement>* keywordMap,
*/
Status translateArrayKeywords(StringMap<BSONElement>* keywordMap,
StringData path,
+ bool ignoreUnknownKeywords,
InternalSchemaTypeExpression* typeExpr,
AndMatchExpression* andExpr) {
if (auto minItemsElt = keywordMap->get(kSchemaMinItemsKeyword)) {
@@ -937,25 +1117,14 @@ Status translateArrayKeywords(StringMap<BSONElement>* keywordMap,
}
if (auto uniqueItemsElt = keywordMap->get(kSchemaUniqueItemsKeyword)) {
- if (!uniqueItemsElt.isBoolean()) {
- return {ErrorCodes::TypeMismatch,
- str::stream() << "$jsonSchema keyword '" << kSchemaUniqueItemsKeyword
- << "' must be a boolean"};
- } else if (path.empty()) {
- andExpr->add(stdx::make_unique<AlwaysTrueMatchExpression>().release());
- } else if (uniqueItemsElt.boolean()) {
- auto uniqueItemsExpr = stdx::make_unique<InternalSchemaUniqueItemsMatchExpression>();
- auto status = uniqueItemsExpr->init(path);
- if (!status.isOK()) {
- return status;
- }
- andExpr->add(
- makeRestriction(BSONType::Array, path, std::move(uniqueItemsExpr), typeExpr)
- .release());
+ auto uniqueItemsExpr = parseUniqueItems(uniqueItemsElt, path, typeExpr);
+ if (!uniqueItemsExpr.isOK()) {
+ return uniqueItemsExpr.getStatus();
}
+ andExpr->add(uniqueItemsExpr.getValue().release());
}
- return Status::OK();
+ return parseItemsAndAdditionalItems(keywordMap, path, ignoreUnknownKeywords, typeExpr, andExpr);
}
/**
@@ -1185,6 +1354,7 @@ StatusWithMatchExpression _parse(StringData path, BSONObj schema, bool ignoreUnk
// 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{
+ {kSchemaAdditionalItemsKeyword, {}},
{kSchemaAdditionalPropertiesKeyword, {}},
{kSchemaAllOfKeyword, {}},
{kSchemaAnyOfKeyword, {}},
@@ -1193,6 +1363,7 @@ StatusWithMatchExpression _parse(StringData path, BSONObj schema, bool ignoreUnk
{kSchemaDescriptionKeyword, {}},
{kSchemaExclusiveMaximumKeyword, {}},
{kSchemaExclusiveMinimumKeyword, {}},
+ {kSchemaItemsKeyword, {}},
{kSchemaMaxItemsKeyword, {}},
{kSchemaMaxLengthKeyword, {}},
{kSchemaMaxPropertiesKeyword, {}},
@@ -1278,7 +1449,8 @@ StatusWithMatchExpression _parse(StringData path, BSONObj schema, bool ignoreUnk
return translationStatus;
}
- translationStatus = translateArrayKeywords(&keywordMap, path, typeExpr.get(), andExpr.get());
+ translationStatus = translateArrayKeywords(
+ &keywordMap, path, ignoreUnknownKeywords, typeExpr.get(), andExpr.get());
if (!translationStatus.isOK()) {
return translationStatus;
}
@@ -1316,7 +1488,12 @@ constexpr StringData JSONSchemaParser::kSchemaTypeObject;
constexpr StringData JSONSchemaParser::kSchemaTypeString;
StatusWithMatchExpression JSONSchemaParser::parse(BSONObj schema, bool ignoreUnknownKeywords) {
- return _parse(StringData{}, schema, ignoreUnknownKeywords);
-}
+ LOG(5) << "Parsing JSON Schema: " << schema.jsonString();
+ auto translation = _parse(""_sd, schema, ignoreUnknownKeywords);
+ if (shouldLog(logger::LogSeverity::Debug(5)) && translation.isOK()) {
+ LOG(5) << "Translated schema match expression: " << translation.getValue()->toString();
+ }
+ return translation;
+}
} // 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 106adb596b0..c43f9c5c1b4 100644
--- a/src/mongo/db/matcher/schema/json_schema_parser_test.cpp
+++ b/src/mongo/db/matcher/schema/json_schema_parser_test.cpp
@@ -1847,7 +1847,7 @@ TEST(JSONSchemaParserTest, FailsToParseIfUniqueItemsIsNotABoolean) {
ASSERT_EQ(JSONSchemaParser::parse(schema).getStatus(), ErrorCodes::TypeMismatch);
}
-TEST(JSONSchemaParserTest, NoMatchExpressionGeneratedIfUniqueItemsFalse) {
+TEST(JSONSchemaParserTest, UniqueItemsFalseGeneratesAlwaysTrueExpression) {
auto schema = fromjson("{properties: {a: {uniqueItems: false}}}");
auto result = JSONSchemaParser::parse(schema);
ASSERT_OK(result.getStatus());
@@ -1856,7 +1856,7 @@ TEST(JSONSchemaParserTest, NoMatchExpressionGeneratedIfUniqueItemsFalse) {
{$and: [
{$or: [
{$nor: [{a: {$exists: true}}]},
- {}
+ {$and: [{$alwaysTrue: 1}]}
]}
]}
]})"));
@@ -1960,5 +1960,337 @@ TEST(JSONSchemaParserTest, FailsToParseUnsupportedKeywordsWhenIgnoreUnknownParam
ASSERT_STRING_CONTAINS(result.getStatus().reason(),
"$jsonSchema keyword '$schema' is not currently supported");
}
+
+TEST(JSONSchemaParserTest, FailsToParseIfItemsIsNotAnArrayOrObject) {
+ auto schema = BSON("items" << 1);
+ ASSERT_EQ(JSONSchemaParser::parse(schema).getStatus(), ErrorCodes::TypeMismatch);
+}
+
+TEST(JSONSchemaParserTest, FailsToParseIfItemsIsAnArrayWithANonObject) {
+ auto schema = fromjson("{items: [{type: 'string'}, 'blah']}");
+ ASSERT_EQ(JSONSchemaParser::parse(schema).getStatus(), ErrorCodes::TypeMismatch);
+}
+
+TEST(JSONSchemaParserTest, FailsToParseIfItemsIsAnInvalidSchema) {
+ auto schema = BSON("items" << BSON("invalid" << 1));
+ ASSERT_EQ(JSONSchemaParser::parse(schema).getStatus(), ErrorCodes::FailedToParse);
+}
+
+TEST(JSONSchemaParserTest, FailsToParseIfItemsIsAnArrayThatContainsAnInvalidSchema) {
+ auto schema = fromjson("{items: [{type: 'string'}, {invalid: 1}]}");
+ ASSERT_EQ(JSONSchemaParser::parse(schema).getStatus(), ErrorCodes::FailedToParse);
+}
+
+TEST(JSONSchemaParserTest, ItemsParsesSuccessfullyAsArrayAtTopLevel) {
+ auto schema = fromjson("{items: [{type: 'string'}]}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_OK(result.getStatus());
+ ASSERT_SERIALIZES_TO(result.getValue(), fromjson("{$and: [{$alwaysTrue: 1}]}"));
+}
+
+TEST(JSONSchemaParserTest, ItemsParsesSuccessfullyAsObjectAtTopLevel) {
+ auto schema = fromjson("{items: {type: 'string'}}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_OK(result.getStatus());
+ ASSERT_SERIALIZES_TO(result.getValue(), fromjson("{$and: [{$alwaysTrue: 1}]}"));
+}
+
+TEST(JSONSchemaParserTest, ItemsParsesSuccessfullyAsArrayInNestedSchema) {
+ auto schema = fromjson("{properties: {a: {items: [{maxLength: 4}, {minimum: 0}]}}}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_OK(result.getStatus());
+ ASSERT_SERIALIZES_TO(result.getValue(), fromjson(R"({
+ $and: [{
+ $and: [{
+ $or: [
+ {$nor: [{a: {$exists: true}}]},
+ {
+ $and: [{
+ $or: [
+ {$nor: [{a: {$_internalSchemaType: [4]}}]},
+ {
+ $and: [
+ {
+ a: {
+ $_internalSchemaMatchArrayIndex: {
+ index: 0,
+ namePlaceholder: "i",
+ expression: {
+ $and: [{
+ $or: [
+ {$nor: [{i: {$_internalSchemaType: [2]}}]},
+ {i: {$_internalSchemaMaxLength: 4}}
+ ]
+ }]
+ }
+ }
+ }
+ },
+ {
+ a: {
+ $_internalSchemaMatchArrayIndex: {
+ index: 1,
+ namePlaceholder: "i",
+ expression: {
+ $and: [{
+ $or: [
+ {
+ $nor: [{
+ i: {
+ $_internalSchemaType: ["number"]
+ }
+ }]
+ },
+ {i: {$gte: 0}}
+ ]
+ }]
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }]
+ }
+ ]
+ }]
+ }]})"));
+}
+
+TEST(JSONSchemaParserTest, ItemsParsesSuccessfullyAsObjectInNestedSchema) {
+ auto schema = fromjson("{properties: {a: {items: {type: 'string'}}}}");
+ auto result = JSONSchemaParser::parse(schema);
+ ASSERT_OK(result.getStatus());
+ ASSERT_SERIALIZES_TO(result.getValue(), fromjson(R"( {
+ $and: [{
+ $and: [{
+ $or: [
+ {$nor: [{a: {$exists: true}}]},
+ {
+ $and: [{
+ $or: [
+ {$nor: [{a: {$_internalSchemaType: [4]}}]},
+ {
+ a: {
+ $_internalSchemaAllElemMatchFromIndex:
+ [0, {$and: [{i: {$_internalSchemaType: [2]}}]}]
+ }
+ }
+ ]
+ }]
+ }
+ ]
+ }]
+ }]})"));
+}
+
+TEST(JSONSchemaParserTest, FailsToParseIfAdditionalItemsIsNotAnObjectOrBoolean) {
+ auto schema = BSON("items" << BSONObj() << "additionalItems" << 1);
+ ASSERT_EQ(JSONSchemaParser::parse(schema).getStatus(), ErrorCodes::TypeMismatch);
+
+ schema = BSON("additionalItems" << 1);
+ ASSERT_EQ(JSONSchemaParser::parse(schema).getStatus(), ErrorCodes::TypeMismatch);
+}
+
+TEST(JSONSchemaParserTest, FailsToParseIfAdditionalItemsIsAnInvalidSchema) {
+ auto schema = BSON("items" << BSONObj() << "additionalItems" << BSON("invalid" << 1));
+ ASSERT_EQ(JSONSchemaParser::parse(schema).getStatus(), ErrorCodes::FailedToParse);
+
+ schema = BSON("additionalItems" << BSON("invalid" << 1));
+ ASSERT_EQ(JSONSchemaParser::parse(schema).getStatus(), ErrorCodes::FailedToParse);
+}
+
+TEST(JSONSchemaParserTest, AdditionalItemsTranslatesSucessfullyAsBooleanAtTopLevel) {
+ auto schema = fromjson("{items: [], additionalItems: true}");
+ auto expr = JSONSchemaParser::parse(schema);
+ ASSERT_OK(expr.getStatus());
+ ASSERT_SERIALIZES_TO(expr.getValue(), fromjson("{$and: [{$alwaysTrue: 1}, {$alwaysTrue: 1}]}"));
+
+ schema = fromjson("{items: [], additionalItems: false}");
+ expr = JSONSchemaParser::parse(schema);
+ ASSERT_OK(expr.getStatus());
+ ASSERT_SERIALIZES_TO(expr.getValue(), fromjson("{$and: [{$alwaysTrue: 1}, {$alwaysTrue: 1}]}"));
+}
+
+TEST(JSONSchemaParserTest, AdditionalItemsTranslatesSucessfullyAsObjectAtTopLevel) {
+ auto schema = fromjson("{items: [], additionalItems: {multipleOf: 7}}");
+ auto expr = JSONSchemaParser::parse(schema);
+ ASSERT_OK(expr.getStatus());
+ ASSERT_SERIALIZES_TO(expr.getValue(), fromjson("{$and: [{$alwaysTrue: 1}, {$alwaysTrue: 1}]}"));
+}
+
+TEST(JSONSchemaParserTest, AdditionalItemsTranslatesSucessfullyAsBooleanInNestedSchema) {
+ auto schema = fromjson("{properties: {a: {items: [], additionalItems: true}}}");
+ auto expr = JSONSchemaParser::parse(schema);
+ ASSERT_OK(expr.getStatus());
+ ASSERT_SERIALIZES_TO(expr.getValue(), fromjson(R"({
+ $and: [{
+ $and: [{
+ $or: [
+ {$nor: [{a: {$exists: true}}]},
+ {
+ $and: [
+ {$or: [{$nor: [{a: {$_internalSchemaType: [4]}}]}, {}]},
+ {
+ $or: [
+ {$nor: [{a: {$_internalSchemaType: [4]}}]},
+ {
+ a: {
+ $_internalSchemaAllElemMatchFromIndex:
+ [0, {$alwaysTrue: 1}]
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }]
+ }]})"));
+
+ schema = fromjson("{properties: {a: {items: [], additionalItems: false}}}");
+ expr = JSONSchemaParser::parse(schema);
+ ASSERT_OK(expr.getStatus());
+ ASSERT_SERIALIZES_TO(expr.getValue(), fromjson(R"({
+ $and: [{
+ $and: [{
+ $or: [
+ {$nor: [{a: {$exists: true}}]},
+ {
+ $and: [
+ {$or: [{$nor: [{a: {$_internalSchemaType: [4]}}]}, {}]},
+ {
+ $or: [
+ {$nor: [{a: {$_internalSchemaType: [4]}}]},
+ {
+ a: {
+ $_internalSchemaAllElemMatchFromIndex:
+ [0, {$alwaysFalse: 1}]
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }]
+ }]})"));
+}
+
+TEST(JSONSchemaParserTest, AdditionalItemsGeneratesEmptyExpressionAtTopLevelIfItemsNotPresent) {
+ auto schema = BSON("additionalItems" << true);
+ auto expr = JSONSchemaParser::parse(schema);
+ ASSERT_OK(expr.getStatus());
+ ASSERT_SERIALIZES_TO(expr.getValue(), BSONObj());
+
+ schema = BSON("additionalItems" << false);
+ expr = JSONSchemaParser::parse(schema);
+ ASSERT_OK(expr.getStatus());
+ ASSERT_SERIALIZES_TO(expr.getValue(), BSONObj());
+
+ schema = BSON("additionalItems" << BSON("minLength" << 1));
+ expr = JSONSchemaParser::parse(schema);
+ ASSERT_OK(expr.getStatus());
+ ASSERT_SERIALIZES_TO(expr.getValue(), BSONObj());
+}
+
+TEST(JSONSchemaParserTest, AdditionalItemsGeneratesEmptyExpressionInNestedSchemaIfItemsNotPresent) {
+ auto schema = fromjson("{properties: {foo: {additionalItems: true}}}");
+ auto expr = JSONSchemaParser::parse(schema);
+ ASSERT_OK(expr.getStatus());
+ ASSERT_SERIALIZES_TO(expr.getValue(), fromjson(R"({
+ $and: [
+ {$and: [
+ {$or: [
+ {$nor: [{foo: {$exists: true}}]},
+ {}
+ ]}
+ ]}
+ ]})"));
+
+ schema = fromjson("{properties: {foo: {additionalItems: false}}}");
+ expr = JSONSchemaParser::parse(schema);
+ ASSERT_OK(expr.getStatus());
+ ASSERT_SERIALIZES_TO(expr.getValue(), fromjson(R"(
+ {$and: [
+ {$and: [
+ {$or: [
+ {$nor: [{foo: {$exists: true}}]},
+ {}
+ ]}
+ ]}
+ ]})"));
+}
+
+TEST(JSONSchemaParserTest, AdditionalItemsGeneratesEmptyExpressionIfItemsAnObject) {
+ auto schema = fromjson("{properties: {a: {items: {minimum: 7}, additionalItems: false}}}");
+ auto expr = JSONSchemaParser::parse(schema);
+ ASSERT_OK(expr.getStatus());
+ ASSERT_SERIALIZES_TO(expr.getValue(), fromjson(R"({
+ $and: [{
+ $and: [{
+ $or: [
+ {$nor: [{a: {$exists: true}}]},
+ {
+ $and: [{
+ $or: [
+ {$nor: [{a: {$_internalSchemaType: [4]}}]},
+ {
+ a: {
+ $_internalSchemaAllElemMatchFromIndex: [
+ 0,
+ {
+ $and: [{
+ $or: [
+ {$nor: [{i: {$_internalSchemaType: ["number"]}}]},
+ {i: {$gte: 7}}
+ ]
+ }]
+ }
+ ]
+ }
+ }
+ ]
+ }]
+ }
+ ]
+ }]
+ }]})"));
+
+ schema = fromjson("{properties: {a: {items: {minimum: 7}, additionalItems: {minLength: 7}}}}");
+ expr = JSONSchemaParser::parse(schema);
+ ASSERT_OK(expr.getStatus());
+ ASSERT_SERIALIZES_TO(expr.getValue(), fromjson(R"({
+ $and: [{
+ $and: [{
+ $or: [
+ {$nor: [{a: {$exists: true}}]},
+ {
+ $and: [{
+ $or: [
+ {$nor: [{a: {$_internalSchemaType: [4]}}]},
+ {
+ a: {
+ $_internalSchemaAllElemMatchFromIndex: [
+ 0,
+ {
+ $and: [{
+ $or: [
+ {$nor: [{i: {$_internalSchemaType: ["number"]}}]},
+ {i: {$gte: 7}}
+ ]
+ }]
+ }
+ ]
+ }
+ }
+ ]
+ }]
+ }
+ ]
+ }]
+ }]})"));
+}
} // namespace
} // namespace mongo