summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--buildscripts/idl/idl/ast.py4
-rw-r--r--buildscripts/idl/idl/binder.py145
-rw-r--r--buildscripts/idl/idl/common.py12
-rw-r--r--buildscripts/idl/idl/errors.py97
-rw-r--r--buildscripts/idl/idl/generator.py48
-rw-r--r--buildscripts/idl/idl/parser.py12
-rw-r--r--buildscripts/idl/idl/syntax.py18
-rw-r--r--buildscripts/idl/tests/test_binder.py390
-rw-r--r--buildscripts/idl/tests/test_import.py2
-rw-r--r--buildscripts/idl/tests/test_parser.py81
-rw-r--r--src/mongo/idl/idl_test.cpp227
-rw-r--r--src/mongo/idl/idl_test_types.h50
-rw-r--r--src/mongo/idl/unittest.idl74
13 files changed, 1112 insertions, 48 deletions
diff --git a/buildscripts/idl/idl/ast.py b/buildscripts/idl/idl/ast.py
index b8fb88a6479..3a9df8340e8 100644
--- a/buildscripts/idl/idl/ast.py
+++ b/buildscripts/idl/idl/ast.py
@@ -79,6 +79,7 @@ class Struct(common.SourceLocation):
self.name = None # type: unicode
self.description = None # type: unicode
self.strict = True # type: bool
+ self.chained_types = [] # type: List[Field]
self.fields = [] # type: List[Field]
super(Struct, self).__init__(file_name, line, column)
@@ -88,7 +89,7 @@ class Field(common.SourceLocation):
An instance of a field in a struct.
Name is always populated.
- A struct will either have a struct_type or a cpp_type, but not both.
+ A field will either have a struct_type or a cpp_type, but not both.
Not all fields are set, it depends on the input document.
"""
@@ -102,6 +103,7 @@ class Field(common.SourceLocation):
self.cpp_name = None # type: unicode
self.optional = False # type: bool
self.ignore = False # type: bool
+ self.chained = False # type: bool
# Properties specific to fields which are types.
self.cpp_type = None # type: unicode
diff --git a/buildscripts/idl/idl/binder.py b/buildscripts/idl/idl/binder.py
index c6f7ee5032c..fbbcaf660ca 100644
--- a/buildscripts/idl/idl/binder.py
+++ b/buildscripts/idl/idl/binder.py
@@ -12,15 +12,17 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
+# pylint: disable=too-many-lines
"""Transform idl.syntax trees from the parser into well-defined idl.ast trees."""
from __future__ import absolute_import, print_function, unicode_literals
import re
-from typing import Union
+from typing import List, Union
from . import ast
from . import bson
+from . import common
from . import cpp_types
from . import errors
from . import syntax
@@ -31,8 +33,8 @@ def _validate_single_bson_type(ctxt, idl_type, syntax_type):
"""Validate bson serialization type is correct for a type."""
bson_type = idl_type.bson_serialization_type[0]
- # Any is only valid if it is the only bson type specified
- if bson_type == "any":
+ # Any and Chain are only valid if they are the only bson types specified
+ if bson_type in ["any", "chain"]:
return True
if not bson.is_valid_bson_type(bson_type):
@@ -64,8 +66,8 @@ def _validate_bson_types_list(ctxt, idl_type, syntax_type):
return _validate_single_bson_type(ctxt, idl_type, syntax_type)
for bson_type in bson_types:
- if bson_type == "any":
- ctxt.add_bad_any_type_use_error(idl_type, syntax_type, idl_type.name)
+ if bson_type in ["any", "chain"]:
+ ctxt.add_bad_any_type_use_error(idl_type, bson_type, syntax_type, idl_type.name)
return False
if not bson.is_valid_bson_type(bson_type):
@@ -141,8 +143,24 @@ def _validate_cpp_type(ctxt, idl_type, syntax_type):
return
+def _validate_chain_type_properties(ctxt, idl_type, syntax_type):
+ # type: (errors.ParserContext, Union[syntax.Type, ast.Field], unicode) -> None
+ """Validate a chained type has both a deserializer and serializer."""
+ assert len(
+ idl_type.bson_serialization_type) == 1 and idl_type.bson_serialization_type[0] == 'chain'
+
+ if idl_type.deserializer is None:
+ ctxt.add_missing_ast_required_field_error(idl_type, syntax_type, idl_type.name,
+ "deserializer")
+
+ if idl_type.serializer is None:
+ ctxt.add_missing_ast_required_field_error(idl_type, syntax_type, idl_type.name,
+ "serializer")
+
+
def _validate_type_properties(ctxt, idl_type, syntax_type):
# type: (errors.ParserContext, Union[syntax.Type, ast.Field], unicode) -> None
+ # pylint: disable=too-many-branches
"""Validate each type or field is correct."""
# Validate bson type restrictions
@@ -153,11 +171,14 @@ def _validate_type_properties(ctxt, idl_type, syntax_type):
bson_type = idl_type.bson_serialization_type[0]
if bson_type == "any":
- # For any, a deserializer is required but the user can try to get away with the default
+ # For 'any', a deserializer is required but the user can try to get away with the default
# serialization for their C++ type.
if idl_type.deserializer is None:
ctxt.add_missing_ast_required_field_error(idl_type, syntax_type, idl_type.name,
"deserializer")
+ elif bson_type == "chain":
+ _validate_chain_type_properties(ctxt, idl_type, syntax_type)
+
elif bson_type == "string":
# Strings support custom serialization unlike other non-object scalar types
if idl_type.deserializer is None:
@@ -197,6 +218,22 @@ def _validate_types(ctxt, parsed_spec):
_validate_type(ctxt, idl_type)
+def _is_duplicate_field(ctxt, field_container, fields, ast_field):
+ # type: (errors.ParserContext, unicode, List[ast.Field], ast.Field) -> bool
+ """Return True if there is a naming conflict for a given field."""
+
+ # This is normally tested in the parser as part of duplicate detection in a map
+ if ast_field.name in [field.name for field in fields]:
+ for field in fields:
+ if field.name == ast_field.name:
+ duplicate_field = field
+
+ ctxt.add_duplicate_field_error(ast_field, field_container, ast_field.name, duplicate_field)
+ return True
+
+ return False
+
+
def _bind_struct(ctxt, parsed_spec, struct):
# type: (errors.ParserContext, syntax.IDLSpec, syntax.Struct) -> ast.Struct
"""
@@ -215,9 +252,26 @@ def _bind_struct(ctxt, parsed_spec, struct):
if ast_struct.name.startswith("array<"):
ctxt.add_array_not_valid_error(ast_struct, "struct", ast_struct.name)
- for field in struct.fields:
+ # Merge chained types as chained fields
+ if struct.chained_types:
+ if ast_struct.strict:
+ ctxt.add_chained_type_no_strict_error(ast_struct, ast_struct.name)
+
+ for chained_type in struct.chained_types:
+ ast_field = _bind_chained_type(ctxt, parsed_spec, ast_struct, chained_type)
+ if ast_field and not _is_duplicate_field(ctxt, chained_type, ast_struct.fields,
+ ast_field):
+ ast_struct.fields.append(ast_field)
+
+ # Merge chained structs as a chained struct and ignored fields
+ for chained_struct in struct.chained_structs or []:
+ _bind_chained_struct(ctxt, parsed_spec, ast_struct, chained_struct)
+
+ # Parse the fields last so that they are serialized after chained stuff.
+ for field in struct.fields or []:
ast_field = _bind_field(ctxt, parsed_spec, field)
- if ast_field:
+ if ast_field and not _is_duplicate_field(ctxt, ast_struct.name, ast_struct.fields,
+ ast_field):
ast_struct.fields.append(ast_field)
return ast_struct
@@ -265,7 +319,7 @@ def _bind_field(ctxt, parsed_spec, field):
_validate_ignored_field(ctxt, field)
return ast_field
- (struct, idltype) = parsed_spec.symbols.resolve_field_type(ctxt, field)
+ (struct, idltype) = parsed_spec.symbols.resolve_field_type(ctxt, field, field.name, field.type)
if not struct and not idltype:
return None
@@ -274,7 +328,7 @@ def _bind_field(ctxt, parsed_spec, field):
ast_field.array = True
if field.default or (idltype and idltype.default):
- ctxt.add_array_no_default(field, field.name)
+ ctxt.add_array_no_default_error(field, field.name)
# Copy over only the needed information if this a struct or a type
if struct:
@@ -301,6 +355,77 @@ def _bind_field(ctxt, parsed_spec, field):
return ast_field
+def _bind_chained_type(ctxt, parsed_spec, location, chained_type):
+ # type: (errors.ParserContext, syntax.IDLSpec, common.SourceLocation, unicode) -> ast.Field
+ """Bind the specified chained type."""
+ (struct, idltype) = parsed_spec.symbols.resolve_field_type(ctxt, location, chained_type,
+ chained_type)
+ if not idltype:
+ if struct:
+ ctxt.add_chained_type_not_found_error(location, chained_type)
+
+ return None
+
+ if len(idltype.bson_serialization_type) != 1 or idltype.bson_serialization_type[0] != 'chain':
+ ctxt.add_chained_type_wrong_type_error(location, chained_type,
+ idltype.bson_serialization_type[0])
+ return None
+
+ ast_field = ast.Field(location.file_name, location.line, location.column)
+ ast_field.name = idltype.name
+ ast_field.cpp_name = idltype.name
+ ast_field.description = idltype.description
+ ast_field.chained = True
+
+ ast_field.cpp_type = idltype.cpp_type
+ ast_field.bson_serialization_type = idltype.bson_serialization_type
+ ast_field.serializer = idltype.serializer
+ ast_field.deserializer = idltype.deserializer
+
+ return ast_field
+
+
+def _bind_chained_struct(ctxt, parsed_spec, ast_struct, chained_struct):
+ # type: (errors.ParserContext, syntax.IDLSpec, ast.Struct, unicode) -> None
+ """Bind the specified chained struct."""
+ (struct, idltype) = parsed_spec.symbols.resolve_field_type(ctxt, ast_struct, chained_struct,
+ chained_struct)
+ if not struct:
+ if idltype:
+ ctxt.add_chained_struct_not_found_error(ast_struct, chained_struct)
+
+ return None
+
+ if struct.strict:
+ ctxt.add_chained_nested_struct_no_strict_error(ast_struct, ast_struct.name, chained_struct)
+
+ if struct.chained_types or struct.chained_structs:
+ ctxt.add_chained_nested_struct_no_nested_error(ast_struct, ast_struct.name, chained_struct)
+
+ # Configure a field for the chained struct.
+ ast_field = ast.Field(ast_struct.file_name, ast_struct.line, ast_struct.column)
+ ast_field.name = struct.name
+ ast_field.cpp_name = struct.name
+ ast_field.description = struct.description
+ ast_field.struct_type = struct.name
+ ast_field.bson_serialization_type = ["object"]
+
+ ast_field.chained = True
+
+ if not _is_duplicate_field(ctxt, chained_struct, ast_struct.fields, ast_field):
+ ast_struct.fields.append(ast_field)
+ else:
+ return
+
+ # Merge all the fields from resolved struct into this ast struct as 'ignored'.
+ for field in struct.fields or []:
+ ast_field = _bind_field(ctxt, parsed_spec, field)
+ if ast_field and not _is_duplicate_field(ctxt, chained_struct, ast_struct.fields,
+ ast_field):
+ ast_field.ignore = True
+ ast_struct.fields.append(ast_field)
+
+
def _bind_globals(parsed_spec):
# type: (syntax.IDLSpec) -> ast.Global
"""Bind the globals object from the idl.syntax tree into the idl.ast tree by doing a deep copy."""
diff --git a/buildscripts/idl/idl/common.py b/buildscripts/idl/idl/common.py
index 15ef4af77dd..920cf3b37bd 100644
--- a/buildscripts/idl/idl/common.py
+++ b/buildscripts/idl/idl/common.py
@@ -20,6 +20,7 @@ Classes which are shared among both the IDL idl.syntax and idl.AST trees.
from __future__ import absolute_import, print_function, unicode_literals
+import os
import string
from typing import Mapping
@@ -63,3 +64,14 @@ class SourceLocation(object):
self.file_name = file_name
self.line = line
self.column = column
+
+ def __str__(self):
+ # type: () -> str
+ """
+ Return a formatted location.
+
+ Example location message:
+ test.idl: (17, 4)
+ """
+ msg = "%s: (%d, %d)" % (os.path.basename(self.file_name), self.line, self.column)
+ return msg # type: ignore
diff --git a/buildscripts/idl/idl/errors.py b/buildscripts/idl/idl/errors.py
index 08d773fc022..3b301000ebe 100644
--- a/buildscripts/idl/idl/errors.py
+++ b/buildscripts/idl/idl/errors.py
@@ -60,6 +60,13 @@ ERROR_ID_BAD_ARRAY_TYPE_NAME = "ID0023"
ERROR_ID_ARRAY_NO_DEFAULT = "ID0024"
ERROR_ID_BAD_IMPORT = "ID0025"
ERROR_ID_BAD_BINDATA_DEFAULT = "ID0026"
+ERROR_ID_CHAINED_TYPE_NOT_FOUND = "ID0027"
+ERROR_ID_CHAINED_TYPE_WRONG_BSON_TYPE = "ID0028"
+ERROR_ID_CHAINED_DUPLICATE_FIELD = "ID0029"
+ERROR_ID_CHAINED_NO_TYPE_STRICT = "ID0030"
+ERROR_ID_CHAINED_STRUCT_NOT_FOUND = "ID0031"
+ERROR_ID_CHAINED_NO_NESTED_STRUCT_STRICT = "ID0032"
+ERROR_ID_CHAINED_NO_NESTED_CHAINED = "ID0033"
class IDLError(Exception):
@@ -228,13 +235,19 @@ class ParserContext(object):
"""Return True if this YAML node is a Scalar."""
return self._is_node_type(node, node_name, "scalar")
- def is_sequence_node(self, node, node_name):
+ def is_scalar_sequence(self, node, node_name):
# type: (Union[yaml.nodes.MappingNode, yaml.nodes.ScalarNode, yaml.nodes.SequenceNode], unicode) -> bool
- """Return True if this YAML node is a Sequence."""
- return self._is_node_type(node, node_name, "sequence")
+ """Return True if this YAML node is a Sequence of Scalars."""
+ if self._is_node_type(node, node_name, "sequence"):
+ for seq_node in node.value:
+ if not self.is_scalar_node(seq_node, node_name):
+ return False
+ return True
+ return False
- def is_sequence_or_scalar_node(self, node, node_name):
+ def is_scalar_sequence_or_scalar_node(self, node, node_name):
# type: (Union[yaml.nodes.MappingNode, yaml.nodes.ScalarNode, yaml.nodes.SequenceNode], unicode) -> bool
+ # pylint: disable=invalid-name
"""Return True if the YAML node is a Scalar or Sequence."""
if not node.id == "scalar" and not node.id == "sequence":
self._add_node_error(
@@ -242,6 +255,10 @@ class ParserContext(object):
"Illegal node type '%s' for '%s', expected either node type 'scalar' or 'sequence'"
% (node.id, node_name))
return False
+
+ if node.id == "sequence":
+ return self.is_scalar_sequence(node, node_name)
+
return True
def is_scalar_bool_node(self, node, node_name):
@@ -270,7 +287,7 @@ class ParserContext(object):
def get_list(self, node):
# type: (Union[yaml.nodes.MappingNode, yaml.nodes.ScalarNode, yaml.nodes.SequenceNode]) -> List[unicode]
"""Get a YAML scalar or sequence node as a list of strings."""
- assert self.is_sequence_or_scalar_node(node, "unknown")
+ assert self.is_scalar_sequence_or_scalar_node(node, "unknown")
if node.id == "scalar":
return [node.value]
else:
@@ -287,8 +304,8 @@ class ParserContext(object):
# type: (yaml.nodes.Node, unicode) -> None
"""Add an error about a struct without fields."""
self._add_node_error(node, ERROR_ID_EMPTY_FIELDS,
- "Struct '%s' must have fields specified but no fields were found" %
- (name))
+ ("Struct '%s' must either have fields, chained_types, or " +
+ " chained_structs specified but neither were found") % (name))
def add_missing_required_field_error(self, node, node_parent, node_name):
# type: (yaml.nodes.Node, unicode, unicode) -> None
@@ -376,13 +393,13 @@ class ParserContext(object):
" use bson type '%s', use a bson_serialization_type of 'any' instead.") %
(ast_type, ast_parent, bson_type_name))
- def add_bad_any_type_use_error(self, location, ast_type, ast_parent):
- # type: (common.SourceLocation, unicode, unicode) -> None
+ def add_bad_any_type_use_error(self, location, bson_type, ast_type, ast_parent):
+ # type: (common.SourceLocation, unicode, unicode, unicode) -> None
# pylint: disable=invalid-name
"""Add an error about any being used in a list of bson types."""
self._add_error(location, ERROR_ID_BAD_ANY_TYPE_USE, (
- "The BSON Type 'any' is not allowed in a list of bson serialization types for" +
- "%s '%s'. It must be only a single bson type.") % (ast_type, ast_parent))
+ "The BSON Type '%s' is not allowed in a list of bson serialization types for" +
+ "%s '%s'. It must be only a single bson type.") % (bson_type, ast_type, ast_parent))
def add_bad_cpp_numeric_type_use_error(self, location, ast_type, ast_parent, cpp_type):
# type: (common.SourceLocation, unicode, unicode, unicode) -> None
@@ -393,14 +410,14 @@ class ParserContext(object):
" 'std::uint32_t', 'std::uint64_t', and 'std::int64_t' are supported.") %
(cpp_type, ast_type, ast_parent))
- def add_bad_array_type_name(self, location, field_name, type_name):
+ def add_bad_array_type_name_error(self, location, field_name, type_name):
# type: (common.SourceLocation, unicode, unicode) -> None
"""Add an error about a field type having a malformed type name."""
self._add_error(location, ERROR_ID_BAD_ARRAY_TYPE_NAME,
("'%s' is not a valid array type for field '%s'. A valid array type" +
" is in the form 'array<type_name>'.") % (type_name, field_name))
- def add_array_no_default(self, location, field_name):
+ def add_array_no_default_error(self, location, field_name):
# type: (common.SourceLocation, unicode) -> None
"""Add an error about an array having a type with a default value."""
self._add_error(
@@ -421,6 +438,60 @@ class ParserContext(object):
self._add_error(location, ERROR_ID_BAD_BINDATA_DEFAULT,
("Default values are not allowed for %s '%s'") % (ast_type, ast_parent))
+ def add_chained_type_not_found_error(self, location, type_name):
+ # type: (common.SourceLocation, unicode) -> None
+ # pylint: disable=invalid-name
+ """Add an error about a chained_type not found."""
+ self._add_error(location, ERROR_ID_CHAINED_TYPE_NOT_FOUND,
+ ("Type '%s' is not a valid chained type") % (type_name))
+
+ def add_chained_type_wrong_type_error(self, location, type_name, bson_type_name):
+ # type: (common.SourceLocation, unicode, unicode) -> None
+ # pylint: disable=invalid-name
+ """Add an error about a chained_type being the wrong type."""
+ self._add_error(location, ERROR_ID_CHAINED_TYPE_WRONG_BSON_TYPE,
+ ("Chained Type '%s' has the wrong bson serialization type '%s', only" +
+ "'chain' is supported for chained types.") % (type_name, bson_type_name))
+
+ def add_duplicate_field_error(self, location, field_container, field_name, duplicate_location):
+ # type: (common.SourceLocation, unicode, unicode, common.SourceLocation) -> None
+ """Add an error about duplicate fields as a result of chained structs/types."""
+ self._add_error(location, ERROR_ID_CHAINED_DUPLICATE_FIELD, (
+ "Chained Struct or Type '%s' duplicates an existing field '%s' at location" + "'%s'.") %
+ (field_container, field_name, duplicate_location))
+
+ def add_chained_type_no_strict_error(self, location, struct_name):
+ # type: (common.SourceLocation, unicode) -> None
+ # pylint: disable=invalid-name
+ """Add an error about strict parser validate and chained types."""
+ self._add_error(location, ERROR_ID_CHAINED_NO_TYPE_STRICT,
+ ("Strict IDL parser validation is not supported with chained types for " +
+ "struct '%s'. Specify 'strict: false' for this struct.") % (struct_name))
+
+ def add_chained_struct_not_found_error(self, location, struct_name):
+ # type: (common.SourceLocation, unicode) -> None
+ # pylint: disable=invalid-name
+ """Add an error about a chained_struct not found."""
+ self._add_error(location, ERROR_ID_CHAINED_STRUCT_NOT_FOUND,
+ ("Type '%s' is not a valid chained struct") % (struct_name))
+
+ def add_chained_nested_struct_no_strict_error(self, location, struct_name, nested_struct_name):
+ # type: (common.SourceLocation, unicode, unicode) -> None
+ # pylint: disable=invalid-name
+ """Add an error about strict parser validate and chained types."""
+ self._add_error(location, ERROR_ID_CHAINED_NO_NESTED_STRUCT_STRICT,
+ ("Strict IDL parser validation is not supported for a chained struct '%s'" +
+ " contained by struct '%s'. Specify 'strict: false' for this struct.") %
+ (nested_struct_name, struct_name))
+
+ def add_chained_nested_struct_no_nested_error(self, location, struct_name, chained_name):
+ # type: (common.SourceLocation, unicode, unicode) -> None
+ # pylint: disable=invalid-name
+ """Add an error about struct's chaining being a struct with nested chaining."""
+ self._add_error(location, ERROR_ID_CHAINED_NO_NESTED_CHAINED,
+ ("Struct '%s' is not allowed to nest struct '%s' since it has chained" +
+ " structs and/or types.") % (struct_name, chained_name))
+
def _assert_unique_error_messages():
# type: () -> None
diff --git a/buildscripts/idl/idl/generator.py b/buildscripts/idl/idl/generator.py
index 320c0f5435d..71c49491897 100644
--- a/buildscripts/idl/idl/generator.py
+++ b/buildscripts/idl/idl/generator.py
@@ -54,8 +54,11 @@ def _get_bson_type_check(bson_element, ctxt_name, field):
"""Get the C++ bson type check for a field."""
bson_types = field.bson_serialization_type
if len(bson_types) == 1:
- if bson_types[0] == 'any':
- # Skip BSON valiation when any
+ if bson_types[0] in ['any', 'chain']:
+ # Skip BSON validation for 'any' types since they are required to validate the
+ # BSONElement.
+ # Skip BSON validation for 'chain' types since they process the raw BSONObject the
+ # encapsulating IDL struct parser is passed.
return None
if not bson_types[0] == 'bindata':
@@ -121,7 +124,7 @@ class _FieldUsageChecker(object):
# type: () -> None
"""Output the code to check for missing fields."""
for field in self.fields:
- if (not field.optional) and (not field.ignore):
+ if (not field.optional) and (not field.ignore) and (not field.chained):
with writer.IndentedScopedBlock(self._writer,
'if (usedFields.find("%s") == usedFields.end()) {' %
(field.name), '}'):
@@ -507,11 +510,23 @@ class _CppSourceFileWriter(_CppFileWriterBase):
self._gen_array_deserializer(field)
return
- # May be an empty block if the type is any
- with self._predicate(_get_bson_type_check('element', 'ctxt', field)):
+ if field.chained:
+ # Do not generate a predicate check since we always call these deserializers.
- object_value = self._gen_field_deserializer_expression('element', field)
- self._writer.write_line('%s = %s;' % (_get_field_member_name(field), object_value))
+ if field.struct_type:
+ # Do not generate a new parser context, reuse the current one since we are not
+ # entering a nested document.
+ expression = '%s::parse(ctxt, bsonObject)' % (common.title_case(field.struct_type))
+ else:
+ method_name = writer.get_method_name_from_qualified_method_name(field.deserializer)
+ expression = "%s(bsonObject)" % (method_name)
+
+ self._writer.write_line('%s = %s;' % (_get_field_member_name(field), expression))
+ else:
+ # May be an empty block if the type is 'any'
+ with self._predicate(_get_bson_type_check('element', 'ctxt', field)):
+ object_value = self._gen_field_deserializer_expression('element', field)
+ self._writer.write_line('%s = %s;' % (_get_field_member_name(field), object_value))
def gen_deserializer_methods(self, struct):
# type: (ast.Struct) -> None
@@ -543,6 +558,10 @@ class _CppSourceFileWriter(_CppFileWriterBase):
first_field = True
for field in struct.fields:
+ # Do not parse chained fields as fields since they are actually chained types.
+ if field.chained:
+ continue
+
field_predicate = 'fieldName == "%s"' % (field.name)
field_usage_check.add(field)
@@ -563,6 +582,15 @@ class _CppSourceFileWriter(_CppFileWriterBase):
self._writer.write_empty_line()
+ # Parse chained types
+ for field in struct.fields:
+ if not field.chained:
+ continue
+
+ # Simply generate deserializers since these are all 'any' types
+ self.gen_field_deserializer(field)
+ self._writer.write_empty_line()
+
# Check for required fields
field_usage_check.add_final_checks()
self._writer.write_empty_line()
@@ -638,7 +666,11 @@ class _CppSourceFileWriter(_CppFileWriterBase):
with self._with_template(template_params):
- if field.array:
+ if field.chained:
+ # Just directly call the serializer for chained structs without opening up a nested
+ # document.
+ self._writer.write_template('${access_member}.serialize(builder);')
+ elif field.array:
self._writer.write_template(
'BSONArrayBuilder arrayBuilder(builder->subarrayStart("${field_name}"));')
with self._block('for (const auto& item : ${access_member}) {', '}'):
diff --git a/buildscripts/idl/idl/parser.py b/buildscripts/idl/idl/parser.py
index 350d04a2805..8a4020f4725 100644
--- a/buildscripts/idl/idl/parser.py
+++ b/buildscripts/idl/idl/parser.py
@@ -85,7 +85,10 @@ def _generic_parser(
if ctxt.is_scalar_bool_node(second_node, first_name):
syntax_node.__dict__[first_name] = ctxt.get_bool(second_node)
elif rule_desc.node_type == "scalar_or_sequence":
- if ctxt.is_sequence_or_scalar_node(second_node, first_name):
+ if ctxt.is_scalar_sequence_or_scalar_node(second_node, first_name):
+ syntax_node.__dict__[first_name] = ctxt.get_list(second_node)
+ elif rule_desc.node_type == "sequence":
+ if ctxt.is_scalar_sequence(second_node, first_name):
syntax_node.__dict__[first_name] = ctxt.get_list(second_node)
elif rule_desc.node_type == "mapping":
if ctxt.is_mapping_node(second_node, first_name):
@@ -138,7 +141,7 @@ def _parse_global(ctxt, spec, node):
def _parse_imports(ctxt, spec, node):
# type: (errors.ParserContext, syntax.IDLSpec, Union[yaml.nodes.MappingNode, yaml.nodes.ScalarNode, yaml.nodes.SequenceNode]) -> None
"""Parse an imports section in the IDL file."""
- if not ctxt.is_sequence_node(node, "imports"):
+ if not ctxt.is_scalar_sequence(node, "imports"):
return
if spec.imports:
@@ -250,10 +253,13 @@ def _parse_struct(ctxt, spec, name, node):
_generic_parser(ctxt, node, "struct", struct, {
"description": _RuleDesc('scalar', _RuleDesc.REQUIRED),
"fields": _RuleDesc('mapping', mapping_parser_func=_parse_fields),
+ "chained_types": _RuleDesc('sequence'),
+ "chained_structs": _RuleDesc('sequence'),
"strict": _RuleDesc("bool_scalar"),
})
- if struct.fields is None:
+ # TODO: SHOULD WE ALLOW STRUCTS ONLY WITH CHAINED STUFF and no fields???
+ if struct.fields is None and struct.chained_types is None and struct.chained_structs is None:
ctxt.add_empty_struct_error(node, struct.name)
spec.symbols.add_struct(ctxt, struct)
diff --git a/buildscripts/idl/idl/syntax.py b/buildscripts/idl/idl/syntax.py
index a7a8a10d249..d1c5b72d45a 100644
--- a/buildscripts/idl/idl/syntax.py
+++ b/buildscripts/idl/idl/syntax.py
@@ -134,13 +134,13 @@ class SymbolTable(object):
for idltype in imported_symbols.types:
self.add_type(ctxt, idltype)
- def resolve_field_type(self, ctxt, field):
- # type: (errors.ParserContext, Field) -> Tuple[Optional[Struct], Optional[Type]]
+ def resolve_field_type(self, ctxt, location, field_name, type_name):
+ # type: (errors.ParserContext, common.SourceLocation, unicode, unicode) -> Tuple[Optional[Struct], Optional[Type]]
"""Find the type or struct a field refers to or log an error."""
- return self._resolve_field_type(ctxt, field, field.type)
+ return self._resolve_field_type(ctxt, location, field_name, type_name)
- def _resolve_field_type(self, ctxt, field, type_name):
- # type: (errors.ParserContext, Field, unicode) -> Tuple[Optional[Struct], Optional[Type]]
+ def _resolve_field_type(self, ctxt, location, field_name, type_name):
+ # type: (errors.ParserContext, common.SourceLocation, unicode, unicode) -> Tuple[Optional[Struct], Optional[Type]]
"""Find the type or struct a field refers to or log an error."""
for idltype in self.types:
if idltype.name == type_name:
@@ -153,12 +153,12 @@ class SymbolTable(object):
if type_name.startswith('array<'):
array_type_name = parse_array_type(type_name)
if not array_type_name:
- ctxt.add_bad_array_type_name(field, field.name, type_name)
+ ctxt.add_bad_array_type_name_error(location, field_name, type_name)
return (None, None)
- return self._resolve_field_type(ctxt, field, array_type_name)
+ return self._resolve_field_type(ctxt, location, field_name, array_type_name)
- ctxt.add_unknown_type_error(field, field.name, type_name)
+ ctxt.add_unknown_type_error(location, field_name, type_name)
return (None, None)
@@ -260,6 +260,8 @@ class Struct(common.SourceLocation):
self.name = None # type: unicode
self.description = None # type: unicode
self.strict = True # type: bool
+ self.chained_types = None # type: List[unicode]
+ self.chained_structs = None # type: List[unicode]
self.fields = None # type: List[Field]
# Internal property that is not represented as syntax. An imported struct is read from an
diff --git a/buildscripts/idl/tests/test_binder.py b/buildscripts/idl/tests/test_binder.py
index 144885cc81e..1b63aff4bf1 100644
--- a/buildscripts/idl/tests/test_binder.py
+++ b/buildscripts/idl/tests/test_binder.py
@@ -13,6 +13,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
+# pylint: disable=too-many-lines
"""Test cases for IDL binder."""
from __future__ import absolute_import, print_function, unicode_literals
@@ -137,7 +138,7 @@ class TestBinder(testcase.IDLTestcase):
default: foo
"""))
- # Test object
+ # Test 'any'
self.assert_bind(
textwrap.dedent("""
types:
@@ -150,6 +151,19 @@ class TestBinder(testcase.IDLTestcase):
default: foo
"""))
+ # Test 'chain'
+ self.assert_bind(
+ textwrap.dedent("""
+ types:
+ foofoo:
+ description: foo
+ cpp_type: foo
+ bson_serialization_type: chain
+ serializer: foo
+ deserializer: foo
+ default: foo
+ """))
+
# Test supported bindata_subtype
for bindata_subtype in ["generic", "function", "uuid", "md5"]:
self.assert_bind(
@@ -342,7 +356,7 @@ class TestBinder(testcase.IDLTestcase):
- string
"""), idl.errors.ERROR_ID_BAD_BSON_TYPE)
- # Test any in list of types
+ # Test 'any' in list of types
self.assert_bind_fail(
textwrap.dedent("""
types:
@@ -378,6 +392,18 @@ class TestBinder(testcase.IDLTestcase):
- fake
"""), idl.errors.ERROR_ID_BAD_BSON_TYPE)
+ # Test 'chain' in list of types
+ self.assert_bind_fail(
+ textwrap.dedent("""
+ types:
+ foofoo:
+ description: foo
+ cpp_type: foo
+ bson_serialization_type:
+ - chain
+ - int
+ """), idl.errors.ERROR_ID_BAD_ANY_TYPE_USE)
+
# Test unsupported serialization
for bson_type in [
"bool", "date", "null", "decimal", "double", "int", "long", "objectid", "regex",
@@ -406,7 +432,7 @@ class TestBinder(testcase.IDLTestcase):
""" % (bson_type)),
idl.errors.ERROR_ID_CUSTOM_SCALAR_SERIALIZATION_NOT_SUPPORTED)
- # Test any serialization needs deserializer
+ # Test 'any' serialization needs deserializer
self.assert_bind_fail(
textwrap.dedent("""
types:
@@ -416,6 +442,28 @@ class TestBinder(testcase.IDLTestcase):
bson_serialization_type: any
"""), idl.errors.ERROR_ID_MISSING_AST_REQUIRED_FIELD)
+ # Test 'chain' serialization needs deserializer
+ self.assert_bind_fail(
+ textwrap.dedent("""
+ types:
+ foofoo:
+ description: foo
+ cpp_type: foo
+ bson_serialization_type: chain
+ serializer: bar
+ """), idl.errors.ERROR_ID_MISSING_AST_REQUIRED_FIELD)
+
+ # Test 'chain' serialization needs serializer
+ self.assert_bind_fail(
+ textwrap.dedent("""
+ types:
+ foofoo:
+ description: foo
+ cpp_type: foo
+ bson_serialization_type: chain
+ deserializer: bar
+ """), idl.errors.ERROR_ID_MISSING_AST_REQUIRED_FIELD)
+
# Test list of bson types needs deserializer
self.assert_bind_fail(
textwrap.dedent("""
@@ -689,6 +737,342 @@ class TestBinder(testcase.IDLTestcase):
%s
""" % (test_value)), idl.errors.ERROR_ID_FIELD_MUST_BE_EMPTY_FOR_IGNORED)
+ def test_chained_type_positive(self):
+ # type: () -> None
+ """Positive parser chaining test cases."""
+ # Setup some common types
+ test_preamble = textwrap.dedent("""
+ types:
+ string:
+ description: foo
+ cpp_type: foo
+ bson_serialization_type: string
+ serializer: foo
+ deserializer: foo
+ default: foo
+
+
+ foo1:
+ description: foo
+ cpp_type: foo
+ bson_serialization_type: chain
+ serializer: foo
+ deserializer: foo
+ default: foo
+
+ """)
+
+ # Chaining only
+ self.assert_bind(test_preamble + textwrap.dedent("""
+ structs:
+ bar1:
+ description: foo
+ strict: false
+ chained_types:
+ - foo1
+ """))
+
+ def test_chained_type_negative(self):
+ # type: () -> None
+ """Negative parser chaining test cases."""
+ # Setup some common types
+ test_preamble = textwrap.dedent("""
+ types:
+ string:
+ description: foo
+ cpp_type: foo
+ bson_serialization_type: string
+ serializer: foo
+ deserializer: foo
+ default: foo
+
+
+ foo1:
+ description: foo
+ cpp_type: foo
+ bson_serialization_type: chain
+ serializer: foo
+ deserializer: foo
+ default: foo
+
+ """)
+
+ # Chaining with strict struct
+ self.assert_bind_fail(test_preamble + textwrap.dedent("""
+ structs:
+ bar1:
+ description: foo
+ strict: true
+ chained_types:
+ - foo1
+ """), idl.errors.ERROR_ID_CHAINED_NO_TYPE_STRICT)
+
+ # Non-'any' type as chained type
+ self.assert_bind_fail(test_preamble + textwrap.dedent("""
+ structs:
+ bar1:
+ description: foo
+ strict: false
+ chained_types:
+ - string
+ """), idl.errors.ERROR_ID_CHAINED_TYPE_WRONG_BSON_TYPE)
+
+ # Duplicate chained types
+ self.assert_bind_fail(test_preamble + textwrap.dedent("""
+ structs:
+ bar1:
+ description: foo
+ strict: false
+ chained_types:
+ - foo1
+ - foo1
+ """), idl.errors.ERROR_ID_CHAINED_DUPLICATE_FIELD)
+
+ # Chaining and fields only with same name
+ self.assert_bind_fail(test_preamble + textwrap.dedent("""
+ structs:
+ bar1:
+ description: foo
+ strict: false
+ chained_types:
+ - foo1
+ fields:
+ foo1: string
+ """), idl.errors.ERROR_ID_CHAINED_DUPLICATE_FIELD)
+
+ def test_chained_struct_positive(self):
+ # type: () -> None
+ """Positive parser chaining test cases."""
+ # Setup some common types
+ test_preamble = textwrap.dedent("""
+ types:
+ string:
+ description: foo
+ cpp_type: foo
+ bson_serialization_type: string
+ serializer: foo
+ deserializer: foo
+ default: foo
+
+
+ foo1:
+ description: foo
+ cpp_type: foo
+ bson_serialization_type: chain
+ serializer: foo
+ deserializer: foo
+ default: foo
+
+ structs:
+ chained:
+ description: foo
+ strict: false
+ chained_types:
+ - foo1
+
+ chained2:
+ description: foo
+ strict: false
+ fields:
+ field1: string
+ """)
+
+ # A struct with only chaining
+ self.assert_bind(test_preamble + indent_text(1,
+ textwrap.dedent("""
+ bar1:
+ description: foo
+ strict: true
+ chained_structs:
+ - chained2
+ """)))
+
+ # Chaining struct's fields and explicit fields
+ self.assert_bind(test_preamble + indent_text(1,
+ textwrap.dedent("""
+ bar1:
+ description: foo
+ strict: true
+ chained_structs:
+ - chained2
+ fields:
+ str1: string
+ """)))
+
+ # Chained types and structs
+ self.assert_bind(test_preamble + indent_text(1,
+ textwrap.dedent("""
+ bar1:
+ description: foo
+ strict: false
+ chained_types:
+ - foo1
+ chained_structs:
+ - chained2
+ fields:
+ str1: string
+ """)))
+
+ # Non-strict chained struct
+ self.assert_bind(test_preamble + indent_text(1,
+ textwrap.dedent("""
+ bar1:
+ description: foo
+ strict: false
+ chained_structs:
+ - chained2
+ fields:
+ foo1: string
+ """)))
+
+ def test_chained_struct_negative(self):
+ # type: () -> None
+ """Negative parser chaining test cases."""
+ # Setup some common types
+ test_preamble = textwrap.dedent("""
+ types:
+ string:
+ description: foo
+ cpp_type: foo
+ bson_serialization_type: string
+ serializer: foo
+ deserializer: foo
+ default: foo
+
+
+ foo1:
+ description: foo
+ cpp_type: foo
+ bson_serialization_type: chain
+ serializer: foo
+ deserializer: foo
+ default: foo
+
+ structs:
+ chained:
+ description: foo
+ strict: false
+ fields:
+ field1: string
+
+ chained2:
+ description: foo
+ strict: false
+ fields:
+ field1: string
+ """)
+
+ # Type as chained struct
+ self.assert_bind_fail(test_preamble + indent_text(1,
+ textwrap.dedent("""
+ bar1:
+ description: foo
+ strict: true
+ chained_structs:
+ - foo1
+ """)), idl.errors.ERROR_ID_CHAINED_STRUCT_NOT_FOUND)
+
+ # Struct as chained type
+ self.assert_bind_fail(test_preamble + indent_text(1,
+ textwrap.dedent("""
+ bar1:
+ description: foo
+ strict: false
+ chained_types:
+ - chained
+ """)), idl.errors.ERROR_ID_CHAINED_TYPE_NOT_FOUND)
+
+ # Duplicated field names across chained struct's fields and fields
+ self.assert_bind_fail(test_preamble + indent_text(1,
+ textwrap.dedent("""
+ bar1:
+ description: foo
+ strict: false
+ chained_structs:
+ - chained
+ fields:
+ field1: string
+ """)), idl.errors.ERROR_ID_CHAINED_DUPLICATE_FIELD)
+
+ # Duplicated field names across chained structs
+ self.assert_bind_fail(test_preamble + indent_text(1,
+ textwrap.dedent("""
+ bar1:
+ description: foo
+ strict: false
+ chained_structs:
+ - chained
+ - chained2
+ """)), idl.errors.ERROR_ID_CHAINED_DUPLICATE_FIELD)
+
+ # Duplicate chained structs
+ self.assert_bind_fail(test_preamble + indent_text(1,
+ textwrap.dedent("""
+ bar1:
+ description: foo
+ strict: true
+ chained_structs:
+ - chained
+ - chained
+ """)), idl.errors.ERROR_ID_CHAINED_DUPLICATE_FIELD)
+
+ # Chained struct with strict true
+ self.assert_bind_fail(test_preamble + indent_text(1,
+ textwrap.dedent("""
+ bar1:
+ description: foo
+ strict: true
+ fields:
+ field1: string
+
+ foobar:
+ description: foo
+ strict: false
+ chained_structs:
+ - bar1
+ fields:
+ f1: string
+
+ """)), idl.errors.ERROR_ID_CHAINED_NO_NESTED_STRUCT_STRICT)
+
+ # Chained struct with nested chained struct
+ self.assert_bind_fail(test_preamble + indent_text(1,
+ textwrap.dedent("""
+ bar1:
+ description: foo
+ strict: false
+ chained_structs:
+ - chained
+
+ foobar:
+ description: foo
+ strict: false
+ chained_structs:
+ - bar1
+ fields:
+ f1: string
+
+ """)), idl.errors.ERROR_ID_CHAINED_NO_NESTED_CHAINED)
+
+ # Chained struct with nested chained type
+ self.assert_bind_fail(test_preamble + indent_text(1,
+ textwrap.dedent("""
+ bar1:
+ description: foo
+ strict: false
+ chained_types:
+ - foo1
+
+ foobar:
+ description: foo
+ strict: false
+ chained_structs:
+ - bar1
+ fields:
+ f1: bar1
+
+ """)), idl.errors.ERROR_ID_CHAINED_NO_NESTED_CHAINED)
+
if __name__ == '__main__':
diff --git a/buildscripts/idl/tests/test_import.py b/buildscripts/idl/tests/test_import.py
index c498f8c1322..d0466d423b1 100644
--- a/buildscripts/idl/tests/test_import.py
+++ b/buildscripts/idl/tests/test_import.py
@@ -85,7 +85,7 @@ class TestImport(testcase.IDLTestcase):
self.assert_parse_fail(
textwrap.dedent("""
- imports:
+ imports:
a: "a.idl"
b: "b.idl"
"""), idl.errors.ERROR_ID_IS_NODE_TYPE)
diff --git a/buildscripts/idl/tests/test_parser.py b/buildscripts/idl/tests/test_parser.py
index cdab68e1457..5013229d92f 100644
--- a/buildscripts/idl/tests/test_parser.py
+++ b/buildscripts/idl/tests/test_parser.py
@@ -125,6 +125,13 @@ class TestParser(testcase.IDLTestcase):
cpp_includes:
inc1: 'foo'"""), idl.errors.ERROR_ID_IS_NODE_TYPE_SCALAR_OR_SEQUENCE)
+ # cpp_includes as a sequence of tuples
+ self.assert_parse_fail(
+ textwrap.dedent("""
+ global:
+ cpp_includes:
+ - inc1: 'foo'"""), idl.errors.ERROR_ID_IS_NODE_TYPE)
+
# Unknown scalar
self.assert_parse_fail(
textwrap.dedent("""
@@ -446,6 +453,80 @@ class TestParser(testcase.IDLTestcase):
default: foo
"""), idl.errors.ERROR_ID_DUPLICATE_SYMBOL)
+ def test_chained_type_positive(self):
+ # type: () -> None
+ """Positive parser chaining test cases."""
+ self.assert_parse(
+ textwrap.dedent("""
+ structs:
+ foo1:
+ description: foo
+ chained_types:
+ - foo1
+ - foo2
+ """))
+
+ def test_chained_type_negative(self):
+ # type: () -> None
+ """Negative parser chaining test cases."""
+ self.assert_parse_fail(
+ textwrap.dedent("""
+ structs:
+ foo1:
+ description: foo
+ chained_types: foo1
+ fields:
+ foo: bar
+ """), idl.errors.ERROR_ID_IS_NODE_TYPE)
+
+ self.assert_parse_fail(
+ textwrap.dedent("""
+ structs:
+ foo1:
+ description: foo
+ chained_types:
+ foo1: bar
+ fields:
+ foo: bar
+ """), idl.errors.ERROR_ID_IS_NODE_TYPE)
+
+ def test_chained_struct_positive(self):
+ # type: () -> None
+ """Positive parser chaining test cases."""
+ self.assert_parse(
+ textwrap.dedent("""
+ structs:
+ foo1:
+ description: foo
+ chained_structs:
+ - foo1
+ - foo2
+ """))
+
+ def test_chained_struct_negative(self):
+ # type: () -> None
+ """Negative parser chaining test cases."""
+ self.assert_parse_fail(
+ textwrap.dedent("""
+ structs:
+ foo1:
+ description: foo
+ chained_structs: foo1
+ fields:
+ foo: bar
+ """), idl.errors.ERROR_ID_IS_NODE_TYPE)
+
+ self.assert_parse_fail(
+ textwrap.dedent("""
+ structs:
+ foo1:
+ description: foo
+ chained_structs:
+ foo1: bar
+ fields:
+ foo: bar
+ """), idl.errors.ERROR_ID_IS_NODE_TYPE)
+
if __name__ == '__main__':
diff --git a/src/mongo/idl/idl_test.cpp b/src/mongo/idl/idl_test.cpp
index 4f26af506c9..00328d11648 100644
--- a/src/mongo/idl/idl_test.cpp
+++ b/src/mongo/idl/idl_test.cpp
@@ -227,6 +227,8 @@ TEST(IDLOneTypeTests, TestNamespaceString) {
ASSERT_EQUALS(element.type(), String);
auto testStruct = One_namespacestring::parse(ctxt, testDoc);
+ assert_same_types<decltype(testStruct.getValue()), const NamespaceString&>();
+
ASSERT_EQUALS(testStruct.getValue(), NamespaceString("foo.bar"));
// Positive: Test we can roundtrip from the just parsed document
@@ -1084,5 +1086,230 @@ TEST(IDLCustomType, TestDerivedParser) {
}
}
+// Chained type testing
+// Check each of types
+// Check for round-tripping of fields and documents
+
+// Positive: demonstrate a class struct chained types
+TEST(IDLChainedType, TestChainedType) {
+ IDLParserErrorContext ctxt("root");
+
+ auto testDoc = BSON("field1"
+ << "abc"
+ << "field2"
+ << 5);
+
+ auto testStruct = Chained_struct_only::parse(ctxt, testDoc);
+
+ assert_same_types<decltype(testStruct.getChainedType()), const mongo::ChainedType&>();
+ assert_same_types<decltype(testStruct.getAnotherChainedType()),
+ const mongo::AnotherChainedType&>();
+
+ ASSERT_EQUALS(testStruct.getChainedType().getField1(), "abc");
+ ASSERT_EQUALS(testStruct.getAnotherChainedType().getField2(), 5);
+
+ // Positive: Test we can roundtrip from the just parsed document
+ {
+ BSONObjBuilder builder;
+ testStruct.serialize(&builder);
+ auto loopbackDoc = builder.obj();
+
+ ASSERT_BSONOBJ_EQ(testDoc, loopbackDoc);
+ }
+
+ // Positive: Test we can serialize from nothing the same document
+ {
+ BSONObjBuilder builder;
+ Chained_struct_only one_new;
+ ChainedType ct;
+ ct.setField1("abc");
+ one_new.setChainedType(ct);
+ AnotherChainedType act;
+ act.setField2(5);
+ one_new.setAnotherChainedType(act);
+ one_new.serialize(&builder);
+
+ auto serializedDoc = builder.obj();
+ ASSERT_BSONOBJ_EQ(testDoc, serializedDoc);
+ }
+}
+
+// Positive: demonstrate a struct with chained types ignoring extra fields
+TEST(IDLChainedType, TestExtraFields) {
+ IDLParserErrorContext ctxt("root");
+
+ auto testDoc = BSON("field1"
+ << "abc"
+ << "field2"
+ << 5
+ << "field3"
+ << 123456);
+
+ auto testStruct = Chained_struct_only::parse(ctxt, testDoc);
+ ASSERT_EQUALS(testStruct.getChainedType().getField1(), "abc");
+ ASSERT_EQUALS(testStruct.getAnotherChainedType().getField2(), 5);
+}
+
+
+// Negative: demonstrate a struct with chained types with duplicate fields
+TEST(IDLChainedType, TestDuplicateFields) {
+ IDLParserErrorContext ctxt("root");
+
+ auto testDoc = BSON("field1"
+ << "abc"
+ << "field2"
+ << 5
+ << "field2"
+ << 123456);
+
+ ASSERT_THROWS(Chained_struct_only::parse(ctxt, testDoc), UserException);
+}
+
+
+// Positive: demonstrate a struct with chained structs
+TEST(IDLChainedType, TestChainedStruct) {
+ IDLParserErrorContext ctxt("root");
+
+ auto testDoc = BSON("anyField" << 123.456 << "objectField" << BSON("random"
+ << "pair")
+ << "field3"
+ << "abc");
+
+ auto testStruct = Chained_struct_mixed::parse(ctxt, testDoc);
+
+ assert_same_types<decltype(testStruct.getChained_any_basic_type()),
+ const Chained_any_basic_type&>();
+ assert_same_types<decltype(testStruct.getChained_object_basic_type()),
+ const Chained_object_basic_type&>();
+
+ ASSERT_EQUALS(testStruct.getField3(), "abc");
+
+ // Positive: Test we can roundtrip from the just parsed document
+ {
+ BSONObjBuilder builder;
+ testStruct.serialize(&builder);
+ auto loopbackDoc = builder.obj();
+
+ ASSERT_BSONOBJ_EQ(testDoc, loopbackDoc);
+ }
+}
+
+// Negative: demonstrate a struct with chained structs and extra fields
+TEST(IDLChainedType, TestChainedStructWithExtraFields) {
+ IDLParserErrorContext ctxt("root");
+
+ // Extra field
+ {
+ auto testDoc = BSON("field3"
+ << "abc"
+ << "anyField"
+ << 123.456
+ << "objectField"
+ << BSON("random"
+ << "pair")
+ << "extraField"
+ << 787);
+ ASSERT_THROWS(Chained_struct_mixed::parse(ctxt, testDoc), UserException);
+ }
+
+
+ // Duplicate any
+ {
+ auto testDoc = BSON("field3"
+ << "abc"
+ << "anyField"
+ << 123.456
+ << "objectField"
+ << BSON("random"
+ << "pair")
+ << "anyField"
+ << 787);
+ ASSERT_THROWS(Chained_struct_mixed::parse(ctxt, testDoc), UserException);
+ }
+
+ // Duplicate object
+ {
+ auto testDoc = BSON("objectField" << BSON("fake"
+ << "thing")
+ << "field3"
+ << "abc"
+ << "anyField"
+ << 123.456
+ << "objectField"
+ << BSON("random"
+ << "pair"));
+ ASSERT_THROWS(Chained_struct_mixed::parse(ctxt, testDoc), UserException);
+ }
+
+ // Duplicate field3
+ {
+ auto testDoc = BSON("field3"
+ << "abc"
+ << "anyField"
+ << 123.456
+ << "objectField"
+ << BSON("random"
+ << "pair")
+ << "field3"
+ << "def");
+ ASSERT_THROWS(Chained_struct_mixed::parse(ctxt, testDoc), UserException);
+ }
+}
+
+
+// Positive: demonstrate a struct with chained structs and types
+TEST(IDLChainedType, TestChainedMixedStruct) {
+ IDLParserErrorContext ctxt("root");
+
+ auto testDoc = BSON("field1"
+ << "abc"
+ << "field2"
+ << 5
+ << "stringField"
+ << "def"
+ << "field3"
+ << 456);
+
+ auto testStruct = Chained_struct_type_mixed::parse(ctxt, testDoc);
+
+ assert_same_types<decltype(testStruct.getChainedType()), const mongo::ChainedType&>();
+ assert_same_types<decltype(testStruct.getAnotherChainedType()),
+ const mongo::AnotherChainedType&>();
+
+ ASSERT_EQUALS(testStruct.getChainedType().getField1(), "abc");
+ ASSERT_EQUALS(testStruct.getAnotherChainedType().getField2(), 5);
+ ASSERT_EQUALS(testStruct.getChained_string_basic_type().getStringField(), "def");
+ ASSERT_EQUALS(testStruct.getField3(), 456);
+
+ // Positive: Test we can roundtrip from the just parsed document
+ {
+ BSONObjBuilder builder;
+ testStruct.serialize(&builder);
+ auto loopbackDoc = builder.obj();
+
+ ASSERT_BSONOBJ_EQ(testDoc, loopbackDoc);
+ }
+
+ // Positive: Test we can serialize from nothing the same document
+ {
+ BSONObjBuilder builder;
+ Chained_struct_type_mixed one_new;
+ ChainedType ct;
+ ct.setField1("abc");
+ one_new.setChainedType(ct);
+ AnotherChainedType act;
+ act.setField2(5);
+ one_new.setAnotherChainedType(act);
+ one_new.setField3(456);
+ Chained_string_basic_type csbt;
+ csbt.setStringField("def");
+ one_new.setChained_string_basic_type(csbt);
+ one_new.serialize(&builder);
+
+ auto serializedDoc = builder.obj();
+ ASSERT_BSONOBJ_EQ(testDoc, serializedDoc);
+ }
+}
+
} // namespace
} // namespace mongo
diff --git a/src/mongo/idl/idl_test_types.h b/src/mongo/idl/idl_test_types.h
index 6e03d48b470..44e26c47912 100644
--- a/src/mongo/idl/idl_test_types.h
+++ b/src/mongo/idl/idl_test_types.h
@@ -113,4 +113,54 @@ private:
std::vector<std::uint8_t> _vec;
};
+/**
+ * Simple class that demonstrates the contract a class must implement to parse an IDL "chain" type
+ * from the IDL parser.
+ */
+class ChainedType {
+public:
+ static ChainedType parse(const BSONObj& obj) {
+ ChainedType object;
+ object._str = obj["field1"].str();
+ return object;
+ }
+
+ void serialize(BSONObjBuilder* builder) const {
+ builder->append("field1", _str);
+ }
+
+ StringData getField1() const {
+ return _str;
+ }
+ void setField1(StringData value) {
+ _str = value.toString();
+ }
+
+private:
+ std::string _str;
+};
+
+class AnotherChainedType {
+public:
+ static AnotherChainedType parse(const BSONObj& obj) {
+ AnotherChainedType object;
+ object._num = obj["field2"].numberLong();
+ return object;
+ }
+
+ void serialize(BSONObjBuilder* builder) const {
+ builder->append("field2", _num);
+ }
+
+ std::int64_t getField2() const {
+ return _num;
+ }
+ void setField2(std::int64_t value) {
+ _num = value;
+ }
+
+private:
+ std::int64_t _num;
+};
+
} // namespace mongo
diff --git a/src/mongo/idl/unittest.idl b/src/mongo/idl/unittest.idl
index 7aecd720ad4..6b6337ed69a 100644
--- a/src/mongo/idl/unittest.idl
+++ b/src/mongo/idl/unittest.idl
@@ -97,6 +97,27 @@ types:
##################################################################################################
#
+# Test types used in parser chaining testing
+#
+##################################################################################################
+
+ ChainedType:
+ bson_serialization_type: chain
+ description: "An Chain Type to test chaining"
+ cpp_type: "mongo::ChainedType"
+ serializer: mongo::ChainedType::serialize
+ deserializer: mongo::ChainedType::parse
+
+ AnotherChainedType:
+ bson_serialization_type: chain
+ description: "Another Chain Type to test chaining"
+ cpp_type: "mongo::AnotherChainedType"
+ serializer: mongo::AnotherChainedType::serialize
+ deserializer: mongo::AnotherChainedType::parse
+
+
+##################################################################################################
+#
# Unit test structs for a single value to ensure type validation works correctly
#
##################################################################################################
@@ -368,4 +389,55 @@ structs:
field6o:
type: array<one_string>
optional: true
-#
+
+##################################################################################################
+#
+# Test Chained Types
+#
+##################################################################################################
+
+ chained_string_basic_type:
+ description: Base struct type for a chained string
+ strict: false
+ fields:
+ stringField: string
+
+ chained_any_basic_type:
+ description: Base struct type for a chained any
+ strict: false
+ fields:
+ anyField: any_basic_type
+
+ chained_object_basic_type:
+ description: Base struct type for a chained object
+ strict: false
+ fields:
+ objectField: object_basic_type
+
+ chained_struct_only:
+ description: UnitTest for chained struct with only chained types
+ strict: false
+ chained_types:
+ - ChainedType
+ - AnotherChainedType
+
+ chained_struct_mixed:
+ description: Chained struct with chained structs and fields
+ strict: true
+ chained_structs:
+ - chained_any_basic_type
+ - chained_object_basic_type
+ fields:
+ field3: string
+
+ chained_struct_type_mixed:
+ description: Chained struct with chained types, structs, and fields
+ strict: false
+ chained_types:
+ - ChainedType
+ - AnotherChainedType
+ chained_structs:
+ - chained_string_basic_type
+ fields:
+ field3:
+ type: int