diff options
-rw-r--r-- | buildscripts/idl/idl/generator.py | 149 | ||||
-rw-r--r-- | buildscripts/idl/tests/test_binder.py | 14 | ||||
-rw-r--r-- | src/mongo/idl/idl_parser.cpp | 17 | ||||
-rw-r--r-- | src/mongo/idl/idl_parser.h | 40 | ||||
-rw-r--r-- | src/mongo/idl/idl_test.cpp | 626 | ||||
-rw-r--r-- | src/mongo/idl/unittest.idl | 10 |
6 files changed, 738 insertions, 118 deletions
diff --git a/buildscripts/idl/idl/generator.py b/buildscripts/idl/idl/generator.py index c2d198e50cf..38094c787eb 100644 --- a/buildscripts/idl/idl/generator.py +++ b/buildscripts/idl/idl/generator.py @@ -1275,6 +1275,90 @@ class _CppSourceFileWriter(_CppFileWriterBase): else: self._writer.write_line('%s = std::move(values);' % (_get_field_member_name(field))) + def _gen_variant_deserializer(self, field, bson_element): + # type: (ast.Field, str) -> None + # pylint: disable=too-many-statements + """Generate the C++ deserializer piece for a variant field.""" + self._writer.write_empty_line() + self._writer.write_line('const BSONType variantType = %s.type();' % (bson_element, )) + + array_types = [v for v in field.type.variant_types if v.is_array] + scalar_types = [v for v in field.type.variant_types if not v.is_array] + + self._writer.write_line('switch (variantType) {') + if array_types: + self._writer.write_line('case Array:') + self._writer.indent() + with self._predicate('%s.Obj().isEmpty()' % (bson_element, )): + # Can't determine element type of an empty array, use the first array type. + self._gen_array_deserializer(field, bson_element, array_types[0]) + + with self._block('else {', '}'): + self._writer.write_line( + 'const BSONType elemType = %s.Obj().firstElement().type();' % (bson_element, )) + + # Start inner switch statement, for each type the first element could be. + self._writer.write_line('switch (elemType) {') + for array_type in array_types: + for bson_type in array_type.bson_serialization_type: + self._writer.write_line('case %s:' % (bson.cpp_bson_type_name(bson_type), )) + # Each copy of the array deserialization code gets an anonymous block. + with self._block('{', '}'): + self._gen_array_deserializer(field, bson_element, array_type) + self._writer.write_line('break;') + + self._writer.write_line('default:') + self._writer.indent() + expected_types = ', '.join( + 'BSONType::%s' % bson.cpp_bson_type_name(t.bson_serialization_type[0]) + for t in array_types) + self._writer.write_line( + 'ctxt.throwBadType(%s, {%s});' % (bson_element, expected_types)) + self._writer.write_line('break;') + self._writer.unindent() + # End of inner switch. + self._writer.write_line('}') + + # End of "case Array:". + self._writer.write_line('break;') + self._writer.unindent() + + for scalar_type in scalar_types: + for bson_type in scalar_type.bson_serialization_type: + self._writer.write_line('case %s:' % (bson.cpp_bson_type_name(bson_type), )) + self._writer.indent() + self.gen_field_deserializer(field, scalar_type, "bsonObject", bson_element, None, + check_type=False) + self._writer.write_line('break;') + self._writer.unindent() + + if field.type.variant_struct_type: + self._writer.write_line('case Object:') + self._writer.indent() + object_value = '%s::parse(ctxt, %s.Obj())' % (field.type.variant_struct_type.cpp_type, + bson_element) + + if field.chained_struct_field: + self._writer.write_line( + '%s.%s(%s);' % (_get_field_member_name(field.chained_struct_field), + _get_field_member_setter_name(field), object_value)) + else: + self._writer.write_line('%s = %s;' % (_get_field_member_name(field), object_value)) + self._writer.write_line('break;') + self._writer.unindent() + + self._writer.write_line('default:') + self._writer.indent() + expected_types = ', '.join( + 'BSONType::%s' % bson.cpp_bson_type_name(t.bson_serialization_type[0]) + for t in scalar_types) + self._writer.write_line('ctxt.throwBadType(%s, {%s});' % (bson_element, expected_types)) + self._writer.write_line('break;') + self._writer.unindent() + + # End of outer switch statement. + self._writer.write_line('}') + def _gen_usage_check(self, field, bson_element, field_usage_check): # type: (ast.Field, str, _FieldUsageCheckerBase) -> None """Generate the field usage check and insert the required field check.""" @@ -1284,22 +1368,25 @@ class _CppSourceFileWriter(_CppFileWriterBase): if _is_required_serializer_field(field): self._writer.write_line('%s = true;' % (_get_has_field_member_name(field))) - def gen_field_deserializer(self, field, bson_object, bson_element, field_usage_check, - is_command_field=False): - # type: (ast.Field, str, str, _FieldUsageCheckerBase, bool) -> None - """Generate the C++ deserializer piece for a field.""" + def gen_field_deserializer(self, field, field_type, bson_object, bson_element, + field_usage_check, is_command_field=False, check_type=True): + # type: (ast.Field, ast.Type, str, str, _FieldUsageCheckerBase, bool, bool) -> None + """Generate the C++ deserializer piece for a field. + + If field_type is scalar and check_type is True (the default), generate type-checking code. + Array elements are always type-checked. + """ # pylint: disable=too-many-arguments - if field.type.is_array: + if field_type.is_array: predicate = "MONGO_likely(ctxt.checkAndAssertType(%s, Array))" % (bson_element) with self._predicate(predicate): self._gen_usage_check(field, bson_element, field_usage_check) - - self._gen_array_deserializer(field, bson_element, field.type) + self._gen_array_deserializer(field, bson_element, field_type) return - elif field.type.is_variant: + elif field_type.is_variant: self._gen_usage_check(field, bson_element, field_usage_check) - # TODO (SERVER-51369): implement _gen_variant_deserializer. + self._gen_variant_deserializer(field, bson_element) return def validate_and_assign_or_uassert(field, expression): @@ -1318,28 +1405,31 @@ class _CppSourceFileWriter(_CppFileWriterBase): if field.chained: # Do not generate a predicate check since we always call these deserializers. - if field.type.is_struct: + if field_type.is_struct: # Do not generate a new parser context, reuse the current one since we are not # entering a nested document. - expression = '%s::parse(ctxt, %s)' % (field.type.cpp_type, bson_object) + expression = '%s::parse(ctxt, %s)' % (field_type.cpp_type, bson_object) else: method_name = writer.get_method_name_from_qualified_method_name( - field.type.deserializer) + field_type.deserializer) expression = "%s(%s)" % (method_name, bson_object) self._gen_usage_check(field, bson_element, field_usage_check) validate_and_assign_or_uassert(field, expression) else: - predicate = _get_bson_type_check(bson_element, 'ctxt', field.type) - if predicate: - predicate = "MONGO_likely(%s)" % (predicate) + predicate = None + if check_type: + predicate = _get_bson_type_check(bson_element, 'ctxt', field_type) + if predicate: + predicate = "MONGO_likely(%s)" % (predicate) + with self._predicate(predicate): self._gen_usage_check(field, bson_element, field_usage_check) object_value = self._gen_field_deserializer_expression( - bson_element, field, field.type) + bson_element, field, field_type) if field.chained_struct_field: if field.optional: # We must invoke the boost::optional constructor when setting optional view @@ -1497,8 +1587,9 @@ class _CppSourceFileWriter(_CppFileWriterBase): if isinstance(struct, ast.Command) and struct.command_field: with self._block('{', '}'): - self.gen_field_deserializer(struct.command_field, bson_object, "commandElement", - None, is_command_field=True) + self.gen_field_deserializer(struct.command_field, struct.command_field.type, + bson_object, "commandElement", None, + is_command_field=True) else: struct_type_info = struct_types.get_struct_info(struct) @@ -1549,7 +1640,7 @@ class _CppSourceFileWriter(_CppFileWriterBase): self._writer.write_line('// ignore field') else: - self.gen_field_deserializer(field, bson_object, "element", + self.gen_field_deserializer(field, field.type, bson_object, "element", field_usage_check) if first_field: @@ -1576,7 +1667,7 @@ class _CppSourceFileWriter(_CppFileWriterBase): continue # Simply generate deserializers since these are all 'any' types - self.gen_field_deserializer(field, bson_object, "element", None) + self.gen_field_deserializer(field, field.type, bson_object, "element", None) self._writer.write_empty_line() self._writer.write_empty_line() @@ -1854,6 +1945,21 @@ class _CppSourceFileWriter(_CppFileWriterBase): 'BSONObjBuilder subObjBuilder(builder->subobjStart(${field_name}));') self._writer.write_template('${access_member}.serialize(&subObjBuilder);') + def _gen_serializer_method_variant(self, field): + # type: (ast.Field) -> None + """Generate the serialize method definition for a variant type.""" + template_params = { + 'field_name': _get_field_constant_name(field), + 'access_member': _access_member(field), + } + + with self._with_template(template_params): + # See https://en.cppreference.com/w/cpp/utility/variant/visit + # This lambda is a template instantiated for each alternate type. Use "if constexpr" + # to compile the appropriate serialization code for each. + with self._block('stdx::visit([builder](auto&& arg) {', '}, ${access_member});'): + self._writer.write_template('idlSerialize(builder, ${field_name}, arg);') + def _gen_serializer_method_common(self, field): # type: (ast.Field) -> None # pylint: disable=too-many-locals @@ -1879,8 +1985,7 @@ class _CppSourceFileWriter(_CppFileWriterBase): if needs_custom_serializer: self._gen_serializer_method_custom(field) elif field.type.is_variant: - # TODO (SERVER-51369): implement deserializer. - return + self._gen_serializer_method_variant(field) else: # Generate default serialization using BSONObjBuilder::append # Note: BSONObjBuilder::append has overrides for std::vector also diff --git a/buildscripts/idl/tests/test_binder.py b/buildscripts/idl/tests/test_binder.py index 5e20a27a346..86bd63db773 100644 --- a/buildscripts/idl/tests/test_binder.py +++ b/buildscripts/idl/tests/test_binder.py @@ -756,6 +756,20 @@ class TestBinder(testcase.IDLTestcase): - array<struct1> """), idl.errors.ERROR_ID_VARIANT_DUPLICATE_TYPES) + # At most one array can have BSON serialization type NumberInt. + self.assert_bind_fail( + test_preamble + textwrap.dedent(""" + structs: + foo: + description: foo + fields: + my_variant_field: + type: + variant: + - array<int> + - array<safeInt> + """), idl.errors.ERROR_ID_VARIANT_DUPLICATE_TYPES) + self.assert_bind_fail( test_preamble + textwrap.dedent(""" structs: diff --git a/src/mongo/idl/idl_parser.cpp b/src/mongo/idl/idl_parser.cpp index 89158281e92..9cad14bffca 100644 --- a/src/mongo/idl/idl_parser.cpp +++ b/src/mongo/idl/idl_parser.cpp @@ -110,12 +110,7 @@ bool IDLParserErrorContext::checkAndAssertTypes(const BSONElement& element, return false; } - std::string path = getElementPath(element); - std::string type_str = toCommaDelimitedList(types); - uasserted(ErrorCodes::TypeMismatch, - str::stream() << "BSON field '" << path << "' is the wrong type '" - << typeName(element.type()) << "', expected types '[" << type_str - << "']"); + throwBadType(element, types); } return true; @@ -225,6 +220,16 @@ void IDLParserErrorContext::throwBadEnumValue(StringData enumValue) const { << "' is not a valid value."); } +void IDLParserErrorContext::throwBadType(const BSONElement& element, + const std::vector<BSONType>& types) const { + std::string path = getElementPath(element); + std::string type_str = toCommaDelimitedList(types); + uasserted(ErrorCodes::TypeMismatch, + str::stream() << "BSON field '" << path << "' is the wrong type '" + << typeName(element.type()) << "', expected types '[" << type_str + << "']"); +} + void IDLParserErrorContext::throwAPIStrictErrorIfApplicable(BSONElement field) const { throwAPIStrictErrorIfApplicable(field.fieldNameStringData()); } diff --git a/src/mongo/idl/idl_parser.h b/src/mongo/idl/idl_parser.h index 551c87bf309..47096c0d117 100644 --- a/src/mongo/idl/idl_parser.h +++ b/src/mongo/idl/idl_parser.h @@ -34,11 +34,45 @@ #include "mongo/base/string_data.h" #include "mongo/bson/bsonelement.h" +#include "mongo/bson/bsonobjbuilder.h" #include "mongo/bson/bsontypes.h" #include "mongo/db/namespace_string.h" namespace mongo { +namespace idl { +template <typename T> +using HasBSONSerializeOp = decltype(std::declval<T>().serialize(std::declval<BSONObjBuilder*>())); + +template <typename T> +constexpr bool hasBSONSerialize = stdx::is_detected_v<HasBSONSerializeOp, T>; + +template <typename T> +void idlSerialize(BSONObjBuilder* builder, StringData fieldName, T arg) { + if constexpr (hasBSONSerialize<decltype(arg)>) { + BSONObjBuilder subObj(builder->subobjStart(fieldName)); + arg.serialize(&subObj); + } else { + builder->append(fieldName, arg); + } +} + +template <typename T> +void idlSerialize(BSONObjBuilder* builder, StringData fieldName, std::vector<T> arg) { + BSONArrayBuilder arrayBuilder(builder->subarrayStart(fieldName)); + for (const auto& item : arg) { + if constexpr (hasBSONSerialize<decltype(item)>) { + BSONObjBuilder subObj(arrayBuilder.subobjStart()); + item.serialize(&subObj); + } else { + arrayBuilder.append(item); + } + } +} + + +} // namespace idl + /** * IDLParserErrorContext manages the current parser context for parsing BSON documents. * @@ -154,6 +188,12 @@ public: MONGO_COMPILER_NORETURN void throwBadEnumValue(int enumValue) const; /** + * Throw an error about a field having the wrong type. + */ + MONGO_COMPILER_NORETURN void throwBadType(const BSONElement& element, + const std::vector<BSONType>& types) const; + + /** * Throw an 'APIStrictError' if the user command has 'apiStrict' field as true. */ void throwAPIStrictErrorIfApplicable(StringData fieldName) const; diff --git a/src/mongo/idl/idl_test.cpp b/src/mongo/idl/idl_test.cpp index cdbfeb70210..2215b62745c 100644 --- a/src/mongo/idl/idl_test.cpp +++ b/src/mongo/idl/idl_test.cpp @@ -136,6 +136,12 @@ BSONObj appendDB(const BSONObj& obj, StringData dbName) { return builder.obj(); } +template <typename T> +BSONObj serializeCmd(const T& cmd) { + auto reply = cmd.serialize({}); + return reply.body; +} + // Use a separate function to get better error messages when types do not match. template <typename T1, typename T2> void assert_same_types() { @@ -464,6 +470,468 @@ TEST(IDLOneTypeTests, TestObjectTypeNegative) { } } +// Trait check used in TestLoopbackVariant. +template <typename T> +struct IsVector : std::false_type {}; +template <typename T> +struct IsVector<std::vector<T>> : std::true_type {}; +template <typename T> +constexpr bool isVector = IsVector<T>::value; + +// We don't generate comparison operators like "==" for variants, so test only for BSON equality. +template <typename ParserT, typename TestT, BSONType Test_bson_type> +void TestLoopbackVariant(TestT test_value) { + IDLParserErrorContext ctxt("root"); + + BSONObjBuilder bob; + if constexpr (idl::hasBSONSerialize<TestT>) { + // TestT might be an IDL struct type like One_string. + BSONObjBuilder subObj(bob.subobjStart("value")); + test_value.serialize(&subObj); + } else if constexpr (isVector<TestT>) { + BSONArrayBuilder arrayBuilder(bob.subarrayStart("value")); + for (const auto& item : test_value) { + if constexpr (idl::hasBSONSerialize<decltype(item)>) { + BSONObjBuilder subObjBuilder(arrayBuilder.subobjStart()); + item.serialize(&subObjBuilder); + } else { + arrayBuilder.append(item); + } + } + } else { + bob.append("value", test_value); + } + + auto obj = bob.obj(); + auto element = obj.firstElement(); + ASSERT_EQUALS(element.type(), Test_bson_type); + + auto parsed = ParserT::parse(ctxt, obj); + if constexpr (std::is_same_v<TestT, BSONObj>) { + ASSERT_BSONOBJ_EQ(stdx::get<TestT>(parsed.getValue()), test_value); + } else { + // Use ASSERT instead of ASSERT_EQ to avoid operator<< + ASSERT(stdx::get<TestT>(parsed.getValue()) == test_value); + } + ASSERT_BSONOBJ_EQ(obj, parsed.toBSON()); + + // Test setValue. + ParserT assembled; + assembled.setValue(test_value); + ASSERT_BSONOBJ_EQ(obj, assembled.toBSON()); + + // Test the constructor. + ParserT constructed(test_value); + if constexpr (std::is_same_v<TestT, BSONObj>) { + ASSERT_BSONOBJ_EQ(stdx::get<TestT>(parsed.getValue()), test_value); + } else { + ASSERT(stdx::get<TestT>(parsed.getValue()) == test_value); + } + ASSERT_BSONOBJ_EQ(obj, constructed.toBSON()); +} + +TEST(IDLVariantTests, TestVariantRoundtrip) { + TestLoopbackVariant<One_variant, int, NumberInt>(1); + TestLoopbackVariant<One_variant, std::string, String>("test_value"); + + TestLoopbackVariant<One_variant_compound, std::string, String>("test_value"); + TestLoopbackVariant<One_variant_compound, BSONObj, Object>(BSON("x" << 1)); + TestLoopbackVariant<One_variant_compound, std::vector<std::string>, Array>({}); + TestLoopbackVariant<One_variant_compound, std::vector<std::string>, Array>({"a"}); + TestLoopbackVariant<One_variant_compound, std::vector<std::string>, Array>({"a", "b"}); + + TestLoopbackVariant<One_variant_struct, int, NumberInt>(1); + TestLoopbackVariant<One_variant_struct, One_string, Object>(One_string("test_value")); + + TestLoopbackVariant<One_variant_struct_array, int, NumberInt>(1); + TestLoopbackVariant<One_variant_struct_array, std::vector<One_string>, Array>( + std::vector<One_string>()); + TestLoopbackVariant<One_variant_struct_array, std::vector<One_string>, Array>( + {One_string("a")}); + TestLoopbackVariant<One_variant_struct_array, std::vector<One_string>, Array>( + {One_string("a"), One_string("b")}); +} + +TEST(IDLVariantTests, TestVariantSafeInt) { + TestLoopbackVariant<One_variant_safeInt, std::string, String>("test_value"); + TestLoopbackVariant<One_variant_safeInt, int, NumberInt>(1); + + // safeInt accepts all numbers, but always deserializes and serializes as int32. + IDLParserErrorContext ctxt("root"); + ASSERT_EQ(stdx::get<std::int32_t>( + One_variant_safeInt::parse(ctxt, BSON("value" << Decimal128(1))).getValue()), + 1); + ASSERT_EQ( + stdx::get<std::int32_t>(One_variant_safeInt::parse(ctxt, BSON("value" << 1LL)).getValue()), + 1); + ASSERT_EQ( + stdx::get<std::int32_t>(One_variant_safeInt::parse(ctxt, BSON("value" << 1.0)).getValue()), + 1); +} + +TEST(IDLVariantTests, TestVariantSafeIntArray) { + using int32vec = std::vector<std::int32_t>; + + TestLoopbackVariant<One_variant_safeInt_array, std::string, String>("test_value"); + TestLoopbackVariant<One_variant_safeInt_array, int32vec, Array>({}); + TestLoopbackVariant<One_variant_safeInt_array, int32vec, Array>({1}); + TestLoopbackVariant<One_variant_safeInt_array, int32vec, Array>({1, 2}); + + // Use ASSERT instead of ASSERT_EQ to avoid operator<< + IDLParserErrorContext ctxt("root"); + ASSERT(stdx::get<int32vec>( + One_variant_safeInt_array::parse(ctxt, BSON("value" << BSON_ARRAY(Decimal128(1)))) + .getValue()) == int32vec{1}); + ASSERT( + stdx::get<int32vec>( + One_variant_safeInt_array::parse(ctxt, BSON("value" << BSON_ARRAY(1LL))).getValue()) == + int32vec{1}); + ASSERT( + stdx::get<int32vec>( + One_variant_safeInt_array::parse(ctxt, BSON("value" << BSON_ARRAY(1.0))).getValue()) == + int32vec{1}); + ASSERT( + stdx::get<int32vec>(One_variant_safeInt_array::parse( + ctxt, BSON("value" << BSON_ARRAY(1.0 << 2LL << 3 << Decimal128(4)))) + .getValue()) == (int32vec{1, 2, 3, 4})); +} + +TEST(IDLVariantTests, TestVariantTwoArrays) { + TestLoopbackVariant<One_variant_two_arrays, std::vector<int>, Array>({}); + TestLoopbackVariant<One_variant_two_arrays, std::vector<int>, Array>({1}); + TestLoopbackVariant<One_variant_two_arrays, std::vector<int>, Array>({1, 2}); + TestLoopbackVariant<One_variant_two_arrays, std::vector<std::string>, Array>({"a"}); + TestLoopbackVariant<One_variant_two_arrays, std::vector<std::string>, Array>({"a", "b"}); + + // This variant can be array<int> or array<string>. It assumes an empty array is array<int> + // because that type is declared first in the IDL. + auto obj = BSON("value" << BSONArray()); + auto parsed = One_variant_two_arrays::parse({"root"}, obj); + ASSERT(stdx::get<std::vector<int>>(parsed.getValue()) == std::vector<int>()); + ASSERT_THROWS(stdx::get<std::vector<std::string>>(parsed.getValue()), stdx::bad_variant_access); + + // Corrupt array: its first key isn't "0". + BSONObjBuilder bob; + { + BSONObjBuilder arrayBob(bob.subarrayStart("value")); + arrayBob.append("1", "test_value"); + } + + ASSERT_THROWS_CODE( + One_variant_two_arrays::parse({"root"}, bob.obj()), AssertionException, 40423); +} + +TEST(IDLVariantTests, TestVariantOptional) { + { + auto obj = BSON("value" << 1); + auto parsed = One_variant_optional::parse({"root"}, obj); + ASSERT_BSONOBJ_EQ(obj, parsed.toBSON()); + ASSERT_EQ(stdx::get<int>(*parsed.getValue()), 1); + } + + { + auto obj = BSON("value" + << "test_value"); + auto parsed = One_variant_optional::parse({"root"}, obj); + ASSERT_BSONOBJ_EQ(obj, parsed.toBSON()); + ASSERT_EQ(stdx::get<std::string>(*parsed.getValue()), "test_value"); + } + + // The optional key is absent. + auto parsed = One_variant_optional::parse({"root"}, BSONObj()); + ASSERT_FALSE(parsed.getValue().is_initialized()); + ASSERT_BSONOBJ_EQ(BSONObj(), parsed.toBSON()); +} + +TEST(IDLVariantTests, TestTwoVariants) { + // Combinations of value0 (int or string) and value1 (object or array<string>). For each, test + // parse(), toBSON(), getValue0(), getValue1(), and the constructor. + { + auto obj = BSON("value0" << 1 << "value1" << BSONObj()); + auto parsed = Two_variants::parse({"root"}, obj); + ASSERT_BSONOBJ_EQ(obj, parsed.toBSON()); + ASSERT_EQ(stdx::get<int>(parsed.getValue0()), 1); + ASSERT_BSONOBJ_EQ(stdx::get<BSONObj>(parsed.getValue1()), BSONObj()); + ASSERT_BSONOBJ_EQ(Two_variants(1, BSONObj()).toBSON(), obj); + } + + { + auto obj = BSON("value0" + << "test_value" + << "value1" << BSONObj()); + auto parsed = Two_variants::parse({"root"}, obj); + ASSERT_BSONOBJ_EQ(obj, parsed.toBSON()); + ASSERT_EQ(stdx::get<std::string>(parsed.getValue0()), "test_value"); + ASSERT_BSONOBJ_EQ(stdx::get<BSONObj>(parsed.getValue1()), BSONObj()); + ASSERT_BSONOBJ_EQ(Two_variants("test_value", BSONObj()).toBSON(), obj); + } + + { + auto obj = BSON("value0" << 1 << "value1" + << BSON_ARRAY("x" + << "y")); + auto parsed = Two_variants::parse({"root"}, obj); + ASSERT_BSONOBJ_EQ(obj, parsed.toBSON()); + ASSERT_EQ(stdx::get<int>(parsed.getValue0()), 1); + ASSERT(stdx::get<std::vector<std::string>>(parsed.getValue1()) == + (std::vector<std::string>{"x", "y"})); + ASSERT_BSONOBJ_EQ(Two_variants(1, std::vector<std::string>{"x", "y"}).toBSON(), obj); + } + + { + auto obj = BSON("value0" + << "test_value" + << "value1" + << BSON_ARRAY("x" + << "y")); + auto parsed = Two_variants::parse({"root"}, obj); + ASSERT_BSONOBJ_EQ(obj, parsed.toBSON()); + ASSERT_EQ(stdx::get<std::string>(parsed.getValue0()), "test_value"); + ASSERT(stdx::get<std::vector<std::string>>(parsed.getValue1()) == + (std::vector<std::string>{"x", "y"})); + ASSERT_BSONOBJ_EQ(Two_variants("test_value", std::vector<std::string>{"x", "y"}).toBSON(), + obj); + } +} + +TEST(IDLVariantTests, TestChainedStructVariant) { + IDLParserErrorContext ctxt("root"); + { + auto obj = BSON("value" + << "x" + << "field1" + << "y"); + auto parsed = Chained_struct_variant::parse(ctxt, obj); + ASSERT_EQ(stdx::get<std::string>(parsed.getOne_variant_compound().getValue()), "x"); + ASSERT_EQ(parsed.getField1(), "y"); + ASSERT_BSONOBJ_EQ(obj, parsed.toBSON()); + + Chained_struct_variant assembled; + assembled.setOne_variant_compound(One_variant_compound("x")); + assembled.setField1("y"); + ASSERT_BSONOBJ_EQ(obj, assembled.toBSON()); + + // Test the constructor. + Chained_struct_variant constructed("y"); + constructed.setOne_variant_compound(One_variant_compound("x")); + ASSERT_EQ(stdx::get<std::string>(constructed.getOne_variant_compound().getValue()), "x"); + ASSERT_EQ(constructed.getField1(), "y"); + ASSERT_BSONOBJ_EQ(obj, constructed.toBSON()); + } + { + auto obj = BSON("value" << BSON_ARRAY("x" + << "y") + << "field1" + << "y"); + auto parsed = Chained_struct_variant::parse(ctxt, obj); + ASSERT(stdx::get<std::vector<std::string>>(parsed.getOne_variant_compound().getValue()) == + (std::vector<std::string>{"x", "y"})); + ASSERT_EQ(parsed.getField1(), "y"); + ASSERT_BSONOBJ_EQ(obj, parsed.toBSON()); + + Chained_struct_variant assembled; + assembled.setOne_variant_compound(One_variant_compound(std::vector<std::string>{"x", "y"})); + assembled.setField1("y"); + ASSERT_BSONOBJ_EQ(obj, assembled.toBSON()); + + // Test the constructor. + Chained_struct_variant constructed("y"); + constructed.setOne_variant_compound( + One_variant_compound(std::vector<std::string>{"x", "y"})); + ASSERT( + stdx::get<std::vector<std::string>>(constructed.getOne_variant_compound().getValue()) == + (std::vector<std::string>{"x", "y"})); + ASSERT_EQ(constructed.getField1(), "y"); + ASSERT_BSONOBJ_EQ(obj, constructed.toBSON()); + } + { + auto obj = BSON("value" << BSONObj() << "field1" + << "y"); + auto parsed = Chained_struct_variant::parse(ctxt, obj); + ASSERT_BSONOBJ_EQ(stdx::get<BSONObj>(parsed.getOne_variant_compound().getValue()), + BSONObj()); + ASSERT_EQ(parsed.getField1(), "y"); + ASSERT_BSONOBJ_EQ(obj, parsed.toBSON()); + + Chained_struct_variant assembled; + assembled.setOne_variant_compound(One_variant_compound(BSONObj())); + assembled.setField1("y"); + ASSERT_BSONOBJ_EQ(obj, assembled.toBSON()); + + // Test the constructor. + Chained_struct_variant constructed("y"); + constructed.setOne_variant_compound({BSONObj()}); + ASSERT_BSONOBJ_EQ(stdx::get<BSONObj>(constructed.getOne_variant_compound().getValue()), + BSONObj()); + ASSERT_EQ(constructed.getField1(), "y"); + ASSERT_BSONOBJ_EQ(obj, constructed.toBSON()); + } +} + +TEST(IDLVariantTests, TestChainedStructVariantInline) { + IDLParserErrorContext ctxt("root"); + { + auto obj = BSON("value" + << "x" + << "field1" + << "y"); + auto parsed = Chained_struct_variant_inline::parse(ctxt, obj); + ASSERT_EQ(stdx::get<std::string>(parsed.getValue()), "x"); + ASSERT_EQ(parsed.getField1(), "y"); + ASSERT_BSONOBJ_EQ(obj, parsed.toBSON()); + + Chained_struct_variant_inline assembled; + assembled.setOne_variant_compound(One_variant_compound("x")); + assembled.setField1("y"); + ASSERT_BSONOBJ_EQ(obj, assembled.toBSON()); + + // Test the constructor. + Chained_struct_variant_inline constructed("y"); + constructed.setOne_variant_compound(One_variant_compound("x")); + ASSERT_EQ(stdx::get<std::string>(constructed.getValue()), "x"); + ASSERT_EQ(constructed.getField1(), "y"); + ASSERT_BSONOBJ_EQ(obj, constructed.toBSON()); + } + { + auto obj = BSON("value" << BSON_ARRAY("x" + << "y") + << "field1" + << "y"); + auto parsed = Chained_struct_variant_inline::parse(ctxt, obj); + ASSERT(stdx::get<std::vector<std::string>>(parsed.getValue()) == + (std::vector<std::string>{"x", "y"})); + ASSERT_EQ(parsed.getField1(), "y"); + ASSERT_BSONOBJ_EQ(obj, parsed.toBSON()); + + Chained_struct_variant_inline assembled; + assembled.setOne_variant_compound(One_variant_compound(std::vector<std::string>{"x", "y"})); + assembled.setField1("y"); + ASSERT_BSONOBJ_EQ(obj, assembled.toBSON()); + + // Test the constructor. + Chained_struct_variant_inline constructed("y"); + constructed.setOne_variant_compound( + One_variant_compound(std::vector<std::string>{"x", "y"})); + ASSERT(stdx::get<std::vector<std::string>>(constructed.getValue()) == + (std::vector<std::string>{"x", "y"})); + ASSERT_EQ(constructed.getField1(), "y"); + ASSERT_BSONOBJ_EQ(obj, constructed.toBSON()); + } + { + auto obj = BSON("value" << BSONObj() << "field1" + << "y"); + auto parsed = Chained_struct_variant_inline::parse(ctxt, obj); + ASSERT_BSONOBJ_EQ(stdx::get<BSONObj>(parsed.getValue()), BSONObj()); + ASSERT_EQ(parsed.getField1(), "y"); + ASSERT_BSONOBJ_EQ(obj, parsed.toBSON()); + + Chained_struct_variant_inline assembled; + assembled.setOne_variant_compound(One_variant_compound(BSONObj())); + assembled.setField1("y"); + ASSERT_BSONOBJ_EQ(obj, assembled.toBSON()); + + // Test the constructor. + Chained_struct_variant_inline constructed("y"); + constructed.setOne_variant_compound({BSONObj()}); + ASSERT_BSONOBJ_EQ(stdx::get<BSONObj>(constructed.getValue()), BSONObj()); + ASSERT_EQ(constructed.getField1(), "y"); + ASSERT_BSONOBJ_EQ(obj, constructed.toBSON()); + } +} + +TEST(IDLVariantTests, TestChainedStructVariantStruct) { + IDLParserErrorContext ctxt("root"); + { + auto obj = BSON("value" << 1 << "field1" + << "y"); + auto parsed = Chained_struct_variant_struct::parse(ctxt, obj); + ASSERT_EQ(stdx::get<int>(parsed.getOne_variant_struct().getValue()), 1); + ASSERT_EQ(parsed.getField1(), "y"); + ASSERT_BSONOBJ_EQ(obj, parsed.toBSON()); + + Chained_struct_variant_struct assembled; + assembled.setOne_variant_struct(One_variant_struct(1)); + assembled.setField1("y"); + ASSERT_BSONOBJ_EQ(obj, assembled.toBSON()); + + // Test the constructor. + Chained_struct_variant_struct constructed("y"); + constructed.setOne_variant_struct(One_variant_struct(1)); + ASSERT_EQ(stdx::get<int>(constructed.getOne_variant_struct().getValue()), 1); + ASSERT_EQ(constructed.getField1(), "y"); + ASSERT_BSONOBJ_EQ(obj, constructed.toBSON()); + } + { + auto obj = BSON("value" << BSON("value" + << "x") + << "field1" + << "y"); + auto parsed = Chained_struct_variant_struct::parse(ctxt, obj); + ASSERT_EQ(stdx::get<One_string>(parsed.getOne_variant_struct().getValue()).getValue(), "x"); + ASSERT_EQ(parsed.getField1(), "y"); + ASSERT_BSONOBJ_EQ(obj, parsed.toBSON()); + + Chained_struct_variant_struct assembled; + assembled.setOne_variant_struct(One_variant_struct(One_string("x"))); + assembled.setField1("y"); + ASSERT_BSONOBJ_EQ(obj, assembled.toBSON()); + + // Test the constructor. + Chained_struct_variant_struct constructed("y"); + constructed.setOne_variant_struct(One_variant_struct(One_string("x"))); + ASSERT_EQ(stdx::get<One_string>(constructed.getOne_variant_struct().getValue()).getValue(), + "x"); + ASSERT_EQ(constructed.getField1(), "y"); + ASSERT_BSONOBJ_EQ(obj, constructed.toBSON()); + } +} + +TEST(IDLVariantTests, TestChainedStructVariantStructInline) { + IDLParserErrorContext ctxt("root"); + { + auto obj = BSON("value" << 1 << "field1" + << "y"); + auto parsed = Chained_struct_variant_struct_inline::parse(ctxt, obj); + ASSERT_EQ(stdx::get<int>(parsed.getValue()), 1); + ASSERT_EQ(parsed.getField1(), "y"); + ASSERT_BSONOBJ_EQ(obj, parsed.toBSON()); + + Chained_struct_variant_struct_inline assembled; + assembled.setOne_variant_struct(One_variant_struct(1)); + assembled.setField1("y"); + ASSERT_BSONOBJ_EQ(obj, assembled.toBSON()); + + // Test the constructor. + Chained_struct_variant_struct_inline constructed("y"); + constructed.setOne_variant_struct(One_variant_struct(1)); + ASSERT_EQ(stdx::get<int>(constructed.getValue()), 1); + ASSERT_EQ(constructed.getField1(), "y"); + ASSERT_BSONOBJ_EQ(obj, constructed.toBSON()); + } + { + auto obj = BSON("value" << BSON("value" + << "x") + << "field1" + << "y"); + auto parsed = Chained_struct_variant_struct_inline::parse(ctxt, obj); + ASSERT_EQ(stdx::get<One_string>(parsed.getValue()).getValue(), "x"); + ASSERT_EQ(parsed.getField1(), "y"); + ASSERT_BSONOBJ_EQ(obj, parsed.toBSON()); + + Chained_struct_variant_struct_inline assembled; + assembled.setOne_variant_struct(One_variant_struct(One_string("x"))); + assembled.setField1("y"); + ASSERT_BSONOBJ_EQ(obj, assembled.toBSON()); + + // Test the constructor. + Chained_struct_variant_struct_inline constructed("y"); + constructed.setOne_variant_struct(One_variant_struct(One_string("x"))); + ASSERT_EQ(stdx::get<One_string>(constructed.getValue()).getValue(), "x"); + ASSERT_EQ(constructed.getField1(), "y"); + ASSERT_BSONOBJ_EQ(obj, constructed.toBSON()); + } +} + /// Struct tests: // Positive: strict, 3 required fields // Negative: strict, ensure extra fields fail @@ -1783,12 +2251,7 @@ TEST(IDLCommand, TestConcatentateWithDb) { assert_same_types<decltype(testStruct.getNamespace()), const NamespaceString&>(); // Positive: Test we can roundtrip from the just parsed document - { - BSONObjBuilder builder; - OpMsgRequest reply = testStruct.serialize(BSONObj()); - - ASSERT_BSONOBJ_EQ(testDoc, reply.body); - } + ASSERT_BSONOBJ_EQ(testDoc, serializeCmd(testStruct)); // Positive: Test we can serialize from nothing the same document except for $db { @@ -1809,13 +2272,10 @@ TEST(IDLCommand, TestConcatentateWithDb) { // Positive: Test we can serialize from nothing the same document { - BSONObjBuilder builder; BasicConcatenateWithDbCommand one_new(NamespaceString("db.coll1")); one_new.setField1(3); one_new.setField2("five"); - OpMsgRequest reply = one_new.serialize(BSONObj()); - - ASSERT_BSONOBJ_EQ(testDoc, reply.body); + ASSERT_BSONOBJ_EQ(testDoc, serializeCmd(testStruct)); } } @@ -1901,12 +2361,7 @@ TEST(IDLCommand, TestConcatentateWithDbOrUUID_TestNSS) { assert_same_types<decltype(testStruct.getNamespaceOrUUID()), const NamespaceStringOrUUID&>(); // Positive: Test we can roundtrip from the just parsed document - { - BSONObjBuilder builder; - OpMsgRequest reply = testStruct.serialize(BSONObj()); - - ASSERT_BSONOBJ_EQ(testDoc, reply.body); - } + ASSERT_BSONOBJ_EQ(testDoc, serializeCmd(testStruct)); // Positive: Test we can serialize from nothing the same document except for $db { @@ -1927,13 +2382,10 @@ TEST(IDLCommand, TestConcatentateWithDbOrUUID_TestNSS) { // Positive: Test we can serialize from nothing the same document { - BSONObjBuilder builder; BasicConcatenateWithDbOrUUIDCommand one_new(NamespaceString("db.coll1")); one_new.setField1(3); one_new.setField2("five"); - OpMsgRequest reply = one_new.serialize(BSONObj()); - - ASSERT_BSONOBJ_EQ(testDoc, reply.body); + ASSERT_BSONOBJ_EQ(testDoc, serializeCmd(one_new)); } } @@ -1958,12 +2410,7 @@ TEST(IDLCommand, TestConcatentateWithDbOrUUID_TestUUID) { assert_same_types<decltype(testStruct.getNamespaceOrUUID()), const NamespaceStringOrUUID&>(); // Positive: Test we can roundtrip from the just parsed document - { - BSONObjBuilder builder; - OpMsgRequest reply = testStruct.serialize(BSONObj()); - - ASSERT_BSONOBJ_EQ(testDoc, reply.body); - } + ASSERT_BSONOBJ_EQ(testDoc, serializeCmd(testStruct)); // Positive: Test we can serialize from nothing the same document except for $db { @@ -1983,13 +2430,10 @@ TEST(IDLCommand, TestConcatentateWithDbOrUUID_TestUUID) { // Positive: Test we can serialize from nothing the same document { - BSONObjBuilder builder; BasicConcatenateWithDbOrUUIDCommand one_new(NamespaceStringOrUUID("db", uuid)); one_new.setField1(3); one_new.setField2("five"); - OpMsgRequest reply = one_new.serialize(BSONObj()); - - ASSERT_BSONOBJ_EQ(testDoc, reply.body); + ASSERT_BSONOBJ_EQ(testDoc, serializeCmd(one_new)); } } @@ -2067,14 +2511,11 @@ TEST(IDLCommand, TestIgnore) { // Positive: Test we can serialize from nothing the same document { - BSONObjBuilder builder; BasicIgnoredCommand one_new; one_new.setField1(3); one_new.setField2("five"); one_new.setDbName("admin"); - OpMsgRequest reply = one_new.serialize(BSONObj()); - - ASSERT_BSONOBJ_EQ(testDocWithDB, reply.body); + ASSERT_BSONOBJ_EQ(testDocWithDB, serializeCmd(one_new)); } } @@ -2105,7 +2546,56 @@ TEST(IDLCommand, TestIgnoredNegative) { } } -// Positive: Test a command read and written to OpMsgRequest works +// We don't generate comparison operators like "==" for variants, so test only for BSON equality. +template <typename CommandT, typename TestT, BSONType Test_bson_type> +void TestLoopbackCommandTypeVariant(TestT test_value) { + IDLParserErrorContext ctxt("root"); + + BSONObjBuilder bob; + if constexpr (idl::hasBSONSerialize<TestT>) { + // TestT might be an IDL struct type like One_string. + BSONObjBuilder subObj(bob.subobjStart(CommandT::kCommandParameterFieldName)); + test_value.serialize(&subObj); + } else { + bob.append(CommandT::kCommandParameterFieldName, test_value); + } + + bob.append("$db", "db"); + auto obj = bob.obj(); + auto element = obj.firstElement(); + ASSERT_EQUALS(element.type(), Test_bson_type); + + auto parsed = CommandT::parse(ctxt, obj); + if constexpr (std::is_same_v<TestT, BSONObj>) { + ASSERT_BSONOBJ_EQ(stdx::get<TestT>(parsed.getValue()), test_value); + } else { + // Use ASSERT instead of ASSERT_EQ to avoid operator<< + ASSERT(stdx::get<TestT>(parsed.getCommandParameter()) == test_value); + } + ASSERT_BSONOBJ_EQ(obj, serializeCmd(parsed)); + + // Test the constructor. + CommandT constructed(test_value); + constructed.setDbName("db"); + if constexpr (std::is_same_v<TestT, BSONObj>) { + ASSERT_BSONOBJ_EQ(stdx::get<TestT>(parsed.getValue()), test_value); + } else { + ASSERT(stdx::get<TestT>(parsed.getCommandParameter()) == test_value); + } + ASSERT_BSONOBJ_EQ(obj, serializeCmd(constructed)); +} + +TEST(IDLCommand, TestCommandTypeVariant) { + TestLoopbackCommandTypeVariant<CommandTypeVariantCommand, int, NumberInt>(1); + TestLoopbackCommandTypeVariant<CommandTypeVariantCommand, std::string, String>("test_value"); + TestLoopbackCommandTypeVariant<CommandTypeVariantCommand, std::vector<std::string>, Array>( + {"x", "y"}); + + TestLoopbackCommandTypeVariant<CommandTypeVariantStructCommand, bool, Bool>(true); + TestLoopbackCommandTypeVariant<CommandTypeVariantStructCommand, One_string, Object>( + One_string("test_value")); +} + TEST(IDLDocSequence, TestBasic) { IDLParserErrorContext ctxt("root"); @@ -2828,12 +3318,7 @@ TEST(IDLTypeCommand, TestString) { assert_same_types<decltype(testStruct.getCommandParameter()), const StringData>(); // Positive: Test we can roundtrip from the just parsed document - { - BSONObjBuilder builder; - OpMsgRequest reply = testStruct.serialize(BSONObj()); - - ASSERT_BSONOBJ_EQ(testDoc, reply.body); - } + ASSERT_BSONOBJ_EQ(testDoc, serializeCmd(testStruct)); // Positive: Test we can serialize from nothing the same document except for $db { @@ -2852,13 +3337,11 @@ TEST(IDLTypeCommand, TestString) { // Positive: Test we can serialize from nothing the same document { - BSONObjBuilder builder; CommandTypeStringCommand one_new("foo"); one_new.setField1(3); one_new.setDbName("db"); OpMsgRequest reply = one_new.serialize(BSONObj()); - - ASSERT_BSONOBJ_EQ(testDoc, reply.body); + ASSERT_BSONOBJ_EQ(testDoc, serializeCmd(one_new)); } } @@ -2878,24 +3361,16 @@ TEST(IDLTypeCommand, TestArrayObject) { const std::vector<mongo::BSONObj>&>(); // Positive: Test we can roundtrip from the just parsed document - { - BSONObjBuilder builder; - OpMsgRequest reply = testStruct.serialize(BSONObj()); - - ASSERT_BSONOBJ_EQ(testDoc, reply.body); - } + ASSERT_BSONOBJ_EQ(testDoc, serializeCmd(testStruct)); // Positive: Test we can serialize from nothing the same document { - BSONObjBuilder builder; std::vector<BSONObj> vec; vec.emplace_back(BSON("sample" << "doc")); CommandTypeArrayObjectCommand one_new(vec); one_new.setDbName("db"); - OpMsgRequest reply = one_new.serialize(BSONObj()); - - ASSERT_BSONOBJ_EQ(testDoc, reply.body); + ASSERT_BSONOBJ_EQ(testDoc, serializeCmd(one_new)); } } @@ -2923,23 +3398,15 @@ TEST(IDLTypeCommand, TestStruct) { } // Positive: Test we can roundtrip from the just parsed document - { - BSONObjBuilder builder; - OpMsgRequest reply = testStruct.serialize(BSONObj()); - - ASSERT_BSONOBJ_EQ(testDoc, reply.body); - } + ASSERT_BSONOBJ_EQ(testDoc, serializeCmd(testStruct)); // Positive: Test we can serialize from nothing the same document { - BSONObjBuilder builder; One_string os; os.setValue("sample"); CommandTypeStructCommand one_new(os); one_new.setDbName("db"); - OpMsgRequest reply = one_new.serialize(BSONObj()); - - ASSERT_BSONOBJ_EQ(testDoc, reply.body); + ASSERT_BSONOBJ_EQ(testDoc, serializeCmd(one_new)); } } @@ -2959,25 +3426,17 @@ TEST(IDLTypeCommand, TestStructArray) { const std::vector<mongo::idl::import::One_string>&>(); // Positive: Test we can roundtrip from the just parsed document - { - BSONObjBuilder builder; - OpMsgRequest reply = testStruct.serialize(BSONObj()); - - ASSERT_BSONOBJ_EQ(testDoc, reply.body); - } + ASSERT_BSONOBJ_EQ(testDoc, serializeCmd(testStruct)); // Positive: Test we can serialize from nothing the same document { - BSONObjBuilder builder; std::vector<One_string> vec; One_string os; os.setValue("sample"); vec.push_back(os); CommandTypeArrayStructCommand one_new(vec); one_new.setDbName("db"); - OpMsgRequest reply = one_new.serialize(BSONObj()); - - ASSERT_BSONOBJ_EQ(testDoc, reply.body); + ASSERT_BSONOBJ_EQ(testDoc, serializeCmd(one_new)); } } @@ -2996,12 +3455,7 @@ TEST(IDLTypeCommand, TestUnderscoreCommand) { assert_same_types<decltype(testStruct.getCommandParameter()), const StringData>(); // Positive: Test we can roundtrip from the just parsed document - { - BSONObjBuilder builder; - OpMsgRequest reply = testStruct.serialize(BSONObj()); - - ASSERT_BSONOBJ_EQ(testDoc, reply.body); - } + ASSERT_BSONOBJ_EQ(testDoc, serializeCmd(testStruct)); // Positive: Test we can serialize from nothing the same document except for $db { @@ -3020,13 +3474,10 @@ TEST(IDLTypeCommand, TestUnderscoreCommand) { // Positive: Test we can serialize from nothing the same document { - BSONObjBuilder builder; WellNamedCommand one_new("foo"); one_new.setField1(3); one_new.setDbName("db"); - OpMsgRequest reply = one_new.serialize(BSONObj()); - - ASSERT_BSONOBJ_EQ(testDoc, reply.body); + ASSERT_BSONOBJ_EQ(testDoc, serializeCmd(one_new)); } } @@ -3138,12 +3589,7 @@ TEST(IDLCommand, BasicNamespaceConstGetterCommand_TestNonConstGetterGeneration) const NamespaceStringOrUUID&>(); // Test we can roundtrip from the just parsed document. - { - BSONObjBuilder builder; - OpMsgRequest reply = testStruct.serialize(BSONObj()); - - ASSERT_BSONOBJ_EQ(testDoc, reply.body); - } + ASSERT_BSONOBJ_EQ(testDoc, serializeCmd(testStruct)); // Test mutable getter modifies the command object. { diff --git a/src/mongo/idl/unittest.idl b/src/mongo/idl/unittest.idl index 00723b8e173..1f1af1ceb24 100644 --- a/src/mongo/idl/unittest.idl +++ b/src/mongo/idl/unittest.idl @@ -609,6 +609,16 @@ structs: - int - one_string + one_variant_safeInt_array: + description: UnitTest for a single variant which accepts a string or array of safeInt + strict: false + fields: + value: + type: + variant: + - string + - array<safeInt> + one_variant_struct_array: description: UnitTest for a single variant which accepts an int or array of structs strict: false |