diff options
author | Mark Benvenuto <mark.benvenuto@mongodb.com> | 2017-04-19 10:08:26 -0400 |
---|---|---|
committer | Mark Benvenuto <mark.benvenuto@mongodb.com> | 2017-04-19 15:30:27 -0400 |
commit | ebd361aae87504474e7db011f08da7aeb0923167 (patch) | |
tree | 3b07cb1415b5397dc1c5285622a300efc47c4dea /buildscripts/idl | |
parent | 72f19039beebcb3e087dc1efbe6fac31526d2fd0 (diff) | |
download | mongo-ebd361aae87504474e7db011f08da7aeb0923167.tar.gz |
SERVER-28514 Add Array support for IDL
Diffstat (limited to 'buildscripts/idl')
-rw-r--r-- | buildscripts/idl/idl/ast.py | 5 | ||||
-rw-r--r-- | buildscripts/idl/idl/binder.py | 14 | ||||
-rw-r--r-- | buildscripts/idl/idl/errors.py | 17 | ||||
-rw-r--r-- | buildscripts/idl/idl/generator.py | 498 | ||||
-rw-r--r-- | buildscripts/idl/idl/syntax.py | 36 | ||||
-rw-r--r-- | buildscripts/idl/sample/sample.idl | 4 | ||||
-rw-r--r-- | buildscripts/idl/tests/test_binder.py | 71 |
7 files changed, 518 insertions, 127 deletions
diff --git a/buildscripts/idl/idl/ast.py b/buildscripts/idl/idl/ast.py index 1337958b718..6c350d92459 100644 --- a/buildscripts/idl/idl/ast.py +++ b/buildscripts/idl/idl/ast.py @@ -110,7 +110,10 @@ class Field(common.SourceLocation): self.bindata_subtype = None # type: unicode self.default = None # type: unicode - # Properties specific to fields with are structs. + # Properties specific to fields which are structs. self.struct_type = None # type: unicode + # Properties specific to fields which are arrays. + self.array = False # type: bool + super(Field, self).__init__(file_name, line, column) diff --git a/buildscripts/idl/idl/binder.py b/buildscripts/idl/idl/binder.py index 39aa0ee4b63..b46eec84c33 100644 --- a/buildscripts/idl/idl/binder.py +++ b/buildscripts/idl/idl/binder.py @@ -89,7 +89,7 @@ def _validate_type(ctxt, idl_type): """Validate each type is correct.""" # Validate naming restrictions - if idl_type.name.startswith("array"): + if idl_type.name.startswith("array<"): ctxt.add_array_not_valid_error(idl_type, "type", idl_type.name) _validate_type_properties(ctxt, idl_type, 'type') @@ -196,7 +196,7 @@ def _bind_struct(ctxt, parsed_spec, struct): ast_struct.strict = struct.strict # Validate naming restrictions - if ast_struct.name.startswith("array"): + if ast_struct.name.startswith("array<"): ctxt.add_array_not_valid_error(ast_struct, "struct", ast_struct.name) for field in struct.fields: @@ -237,7 +237,7 @@ def _bind_field(ctxt, parsed_spec, field): ast_field.optional = field.optional # Validate naming restrictions - if ast_field.name.startswith("array"): + if ast_field.name.startswith("array<"): ctxt.add_array_not_valid_error(ast_field, "field", ast_field.name) if field.ignore: @@ -245,11 +245,17 @@ def _bind_field(ctxt, parsed_spec, field): _validate_ignored_field(ctxt, field) return ast_field - # TODO: support array (struct, idltype) = parsed_spec.symbols.resolve_field_type(ctxt, field) if not struct and not idltype: return None + # If the field type is an array, mark the AST version as such. + if syntax.parse_array_type(field.type): + ast_field.array = True + + if field.default or (idltype and idltype.default): + ctxt.add_array_no_default(field, field.name) + # Copy over only the needed information if this a struct or a type if struct: ast_field.struct_type = struct.name diff --git a/buildscripts/idl/idl/errors.py b/buildscripts/idl/idl/errors.py index 86e3b119d88..47986c47435 100644 --- a/buildscripts/idl/idl/errors.py +++ b/buildscripts/idl/idl/errors.py @@ -55,6 +55,8 @@ ERROR_ID_FIELD_MUST_BE_EMPTY_FOR_STRUCT = "ID0019" ERROR_ID_CUSTOM_SCALAR_SERIALIZATION_NOT_SUPPORTED = "ID0020" ERROR_ID_BAD_ANY_TYPE_USE = "ID0021" ERROR_ID_BAD_NUMERIC_CPP_TYPE = "ID0022" +ERROR_ID_BAD_ARRAY_TYPE_NAME = "ID0023" +ERROR_ID_ARRAY_NO_DEFAULT = "ID0024" class IDLError(Exception): @@ -388,6 +390,21 @@ 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): + # 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): + # type: (common.SourceLocation, unicode) -> None + """Add an error about an array having a type with a default value.""" + self._add_error( + location, ERROR_ID_ARRAY_NO_DEFAULT, + "Field '%s' is not allowed to have both a default value and be an array type" % + (field_name)) + def _assert_unique_error_messages(): # type: () -> None diff --git a/buildscripts/idl/idl/generator.py b/buildscripts/idl/idl/generator.py index d458572a717..90f18f02129 100644 --- a/buildscripts/idl/idl/generator.py +++ b/buildscripts/idl/idl/generator.py @@ -12,6 +12,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 """IDL C++ Code Generator.""" from __future__ import absolute_import, print_function, unicode_literals @@ -21,7 +22,7 @@ import os import string import sys import textwrap -from typing import List, Union +from typing import List, Mapping, Union from . import ast from . import bson @@ -120,21 +121,36 @@ def _qualify_optional_type(cpp_type, field): return cpp_type -def _get_field_parameter_type(field): +def _qualify_array_type(cpp_type, field): + # type: (unicode, ast.Field) -> unicode + """Qualify the type if the field is an array.""" + if field.array: + cpp_type = "std::vector<%s>" % (cpp_type) + + return cpp_type + + +def _get_field_getter_setter_type(field): # type: (ast.Field) -> unicode - """Get the C++ type name for a parameter for a field.""" + """Get the C++ type name for the getter/setter parameter for a field.""" assert field.cpp_type is not None or field.struct_type is not None - cpp_type = _get_view_type(_get_field_cpp_type(field)) + cpp_type = _get_field_cpp_type(field) + + cpp_type = _get_view_type(cpp_type) + + cpp_type = _qualify_array_type(cpp_type, field) return _qualify_optional_type(cpp_type, field) -def _get_field_member_type(field): +def _get_field_storage_type(field): # type: (ast.Field) -> unicode - """Get the C++ type name for a class member for a field.""" + """Get the C++ type name for the storage of class member for a field.""" cpp_type = _get_field_cpp_type(field) + cpp_type = _qualify_array_type(cpp_type, field) + return _qualify_optional_type(cpp_type, field) @@ -144,8 +160,37 @@ def _get_field_member_name(field): return '_%s' % (_camel_case(field.name)) -def _get_bson_type_check(field): - # type: (ast.Field) -> unicode +def _get_return_by_reference(field): + # type: (ast.Field) -> bool + """Return True if the type should be returned by reference.""" + # For non-view types, return a reference for types: + # 1. arrays + # 2. nested structs + # But do not return a reference for: + # 1. std::int32_t and other primitive types + # 2. optional types + cpp_type = _get_field_cpp_type(field) + + if not _is_view_type(cpp_type) and (not field.optional and + (not _is_primitive_type(cpp_type) or field.array)): + return True + + return False + + +def _get_disable_xvalue(field): + # type: (ast.Field) -> bool + """Return True if the type should have the xvalue getter disabled.""" + # Any we return references or view types, we should disable the xvalue. + # For view types like StringData, the return type and member storage types are different + # so returning a reference is not supported. + cpp_type = _get_field_cpp_type(field) + + return _is_view_type(cpp_type) or _get_return_by_reference(field) + + +def _get_bson_type_check(bson_element, ctxt_name, field): + # type: (unicode, unicode, ast.Field) -> unicode """Get the C++ bson type check for a field.""" bson_types = field.bson_serialization_type if len(bson_types) == 1: @@ -154,13 +199,14 @@ def _get_bson_type_check(field): return None if not bson_types[0] == 'bindata': - return 'ctxt.checkAndAssertType(element, %s)' % bson.cpp_bson_type_name(bson_types[0]) + return '%s.checkAndAssertType(%s, %s)' % (ctxt_name, bson_element, + bson.cpp_bson_type_name(bson_types[0])) else: - return 'ctxt.checkAndAssertBinDataType(element, %s)' % bson.cpp_bindata_subtype_type_name( - field.bindata_subtype) + return '%s.checkAndAssertBinDataType(%s, %s)' % ( + ctxt_name, bson_element, bson.cpp_bindata_subtype_type_name(field.bindata_subtype)) else: type_list = '{%s}' % (', '.join([bson.cpp_bson_type_name(b) for b in bson_types])) - return 'ctxt.checkAndAssertTypes(element, %s)' % type_list + return '%s.checkAndAssertTypes(%s, %s)' % (ctxt_name, bson_element, type_list) def _access_member(field): @@ -185,6 +231,15 @@ def fill_spaces(count): return fill +def _template_format(template, template_params): + # type: (unicode, Mapping[unicode,unicode]) -> unicode + """Write a template to the stream.""" + # Ignore the types since we use unicode literals and this expects str but works fine with + # unicode. + # See https://docs.python.org/2/library/string.html#template-strings + return string.Template(template).substitute(template_params) # type: ignore + + def indent_text(count, unindented_text): # type: (int, unicode) -> unicode """Indent each line of a multi-line string.""" @@ -207,10 +262,11 @@ class _IndentedTextWriter(object): """Create an indented text writer.""" self._stream = stream self._indent = 0 + self._template_context = None # type: Mapping[unicode, unicode] def write_unindented_line(self, msg): # type: (unicode) -> None - """Write an unindented line to the stream.""" + """Write an unindented line to the stream, no template formatting applied.""" self._stream.write(msg) self._stream.write("\n") @@ -227,7 +283,26 @@ class _IndentedTextWriter(object): def write_line(self, msg): # type: (unicode) -> None - """Write a line to the stream.""" + """Write a line to the stream, no template formatting applied.""" + self._stream.write(indent_text(self._indent, msg)) + self._stream.write("\n") + + def set_template_mapping(self, template_params): + # type: (Mapping[unicode,unicode]) -> None + """Set the current template mapping parameters for string.Template formatting.""" + assert not self._template_context + self._template_context = template_params + + def clear_template_mapping(self): + # type: () -> None + """Clear the current template mapping parameters for string.Template formatting.""" + assert self._template_context + self._template_context = None + + def write_template(self, template): + # type: (unicode) -> None + """Write a template to the stream.""" + msg = _template_format(template, self._template_context) self._stream.write(indent_text(self._indent, msg)) self._stream.write("\n") @@ -237,6 +312,26 @@ class _IndentedTextWriter(object): self._stream.write("\n") +class _TemplateContext(object): + """Set the template context for the writer.""" + + def __init__(self, writer, template_params): + # type: (_IndentedTextWriter, Mapping[unicode,unicode]) -> None + """Create a template context.""" + self._writer = writer + self._template_context = template_params + + def __enter__(self): + # type: () -> None + """Set the template mapping for the writer.""" + self._writer.set_template_mapping(self._template_context) + + def __exit__(self, *args): + # type: (*str) -> None + """Clear the template mapping for the writer.""" + self._writer.clear_template_mapping() + + class _EmptyBlock(object): """Do not generate an indented block.""" @@ -278,7 +373,7 @@ class _UnindentedScopedBlock(object): class _IndentedScopedBlock(object): - """Generate a block, and indent the contents.""" + """Generate a block, template the parameters, and indent the contents.""" def __init__(self, writer, opening, closing): # type: (_IndentedTextWriter, unicode, unicode) -> None @@ -290,14 +385,14 @@ class _IndentedScopedBlock(object): def __enter__(self): # type: () -> None """Write the beginning of the block and then indent.""" - self._writer.write_line(self._opening) + self._writer.write_template(self._opening) self._writer.indent() def __exit__(self, *args): # type: (*str) -> None """Unindent the block and print the ending.""" self._writer.unindent() - self._writer.write_line(self._closing) + self._writer.write_template(self._closing) class _FieldUsageChecker(object): @@ -400,6 +495,11 @@ class _CppFileWriterBase(object): * %s */""" % (description))) + def _with_template(self, template_params): + # type: (Mapping[unicode,unicode]) -> _TemplateContext + """Generate a template context for the current parameters.""" + return _TemplateContext(self._writer, template_params) + def _block(self, opening, closing): # type: (unicode, unicode) -> Union[_IndentedScopedBlock,_EmptyBlock] """Generate an indented block if opening is not empty.""" @@ -451,68 +551,127 @@ class _CppHeaderFileWriter(_CppFileWriterBase): # type: (ast.Field) -> None """Generate the C++ getter definition for a field.""" cpp_type = _get_field_cpp_type(field) - param_type = _get_field_parameter_type(field) + param_type = _get_field_getter_setter_type(field) member_name = _get_field_member_name(field) optional_ampersand = "" - disable_xvalue = False + if _get_return_by_reference(field): + optional_ampersand = "&" + + disable_xvalue = _get_disable_xvalue(field) + if not _is_view_type(cpp_type): - if not field.optional and not _is_primitive_type(cpp_type): - optional_ampersand = '&' - disable_xvalue = True - body = 'return %s;' % (member_name) + body_template = 'return $member_name;' else: - body = 'return %s{%s};' % (param_type, member_name) - disable_xvalue = True + if field.array: + # Delegate to a function to the do the transformation between vectors. + if field.optional: + body_template = """\ + if (${member_name}.is_initialized()) { + return transformVector(${member_name}.get()); + } else { + return boost::none; + } + """ + else: + body_template = 'return transformVector(${member_name});' + else: + body_template = 'return ${param_type}{${member_name}};' + + template_params = { + 'method_name': _title_case(field.name), + 'member_name': member_name, + 'optional_ampersand': optional_ampersand, + 'param_type': param_type, + } + + body = _template_format(body_template, template_params) # Generate a getter that disables xvalue for view types (i.e. StringData), constructed # optional types, and non-primitive types. - if disable_xvalue: - self._writer.write_line('const %s%s get%s() const& { %s }' % - (param_type, optional_ampersand, _title_case(field.name), body)) - self._writer.write_line("const %s%s get%s() && = delete;" % - (param_type, optional_ampersand, _title_case(field.name))) - else: - self._writer.write_line('const %s%s get%s() const { %s }' % - (param_type, optional_ampersand, _title_case(field.name), body)) + template_params['body'] = body + + with self._with_template(template_params): + + if disable_xvalue: + self._writer.write_template( + 'const ${param_type}${optional_ampersand} get${method_name}() const& { ${body} }' + ) + self._writer.write_template( + 'const ${param_type}${optional_ampersand} get${method_name}() && = delete;') + else: + self._writer.write_template( + 'const ${param_type}${optional_ampersand} get${method_name}() const { ${body} }') def gen_setter(self, field): # type: (ast.Field) -> None """Generate the C++ setter definition for a field.""" cpp_type = _get_field_cpp_type(field) - param_type = _get_field_parameter_type(field) + param_type = _get_field_getter_setter_type(field) member_name = _get_field_member_name(field) + template_params = { + 'method_name': _title_case(field.name), + 'member_name': member_name, + 'param_type': param_type, + } + if _is_view_type(cpp_type): - if not field.optional: - self._writer.write_line('void set%s(%s value) & { %s = value.%s(); }' % - (_title_case(field.name), param_type, member_name, - _get_view_type_to_base_method(cpp_type))) + template_params['view_to_base_method'] = _get_view_type_to_base_method(cpp_type) - else: - # We need to convert between two different types of optional<T> and yet retain the - # ability for the user to specific an uninitialized optional. This occurs for - # mongo::StringData and std::string paired together. - with self._block('void set%s(%s value) {' % (_title_case(field.name), param_type), - "}"): - self._writer.write_line( - textwrap.dedent("""\ - if (value.is_initialized()) { - %s = value.get().%s(); - } else { - %s = boost::none; - } - """ % (member_name, _get_view_type_to_base_method(cpp_type), member_name))) + with self._with_template(template_params): + + if field.array: + if not field.optional: + self._writer.write_template( + 'void set${method_name}(${param_type} value) & { ${member_name} = transformVector(value); }' + ) + + else: + # We need to convert between two different types of optional<T> and yet provide + # the ability for the user to specific an uninitialized optional. This occurs + # for vector<mongo::StringData> and vector<std::string> paired together. + with self._block('void set${method_name}(${param_type} value) & {', "}"): + self._writer.write_template( + textwrap.dedent("""\ + if (value.is_initialized()) { + ${member_name} = transformVector(value.get()); + } else { + ${member_name} = boost::none; + } + """)) + else: + if not field.optional: + self._writer.write_template( + 'void set${method_name}(${param_type} value) & { ${member_name} = value.${view_to_base_method}(); }' + ) + + else: + # We need to convert between two different types of optional<T> and yet provide + # the ability for the user to specific an uninitialized optional. This occurs + # for mongo::StringData and std::string paired together. + with self._block('void set${method_name}(${param_type} value) & {', "}"): + self._writer.write_template( + textwrap.dedent("""\ + if (value.is_initialized()) { + ${member_name} = value.get().${view_to_base_method}(); + } else { + ${member_name} = boost::none; + } + """)) else: - self._writer.write_line('void set%s(%s value) { %s = std::move(value); }' % - (_title_case(field.name), param_type, member_name)) + with self._with_template(template_params): + self._writer.write_template( + 'void set${method_name}(${param_type} value) & { ${member_name} = std::move(value); }' + ) + self._writer.write_empty_line() def gen_member(self, field): # type: (ast.Field) -> None """Generate the C++ class member definition for a field.""" - member_type = _get_field_member_type(field) + member_type = _get_field_storage_type(field) member_name = _get_field_member_name(field) self._writer.write_line('%s %s;' % (member_type, member_name)) @@ -531,6 +690,7 @@ class _CppHeaderFileWriter(_CppFileWriterBase): 'boost/optional.hpp', 'cstdint', 'string', + 'vector', ] header_list.sort() @@ -592,49 +752,101 @@ class _CppSourceFileWriter(_CppFileWriterBase): """Create a C++ .cpp file code writer.""" super(_CppSourceFileWriter, self).__init__(writer) - def gen_field_deserializer(self, field): - # type: (ast.Field) -> None - """Generate the C++ deserializer piece for a few field.""" - # May be an empty block if the type is any - type_predicate = _get_bson_type_check(field) + def _gen_field_deserializer_expression(self, element_name, field): + # type: (unicode, ast.Field) -> unicode + # pylint: disable=invalid-name + """ + Generate the C++ deserializer piece for a field. + + Writes multiple lines into the generated file. + Returns the final statement to access the deserialized value. + """ - with self._predicate(type_predicate): + if field.struct_type: + self._writer.write_line('IDLParserErrorContext tempContext("%s", &ctxt);' % + (field.name)) + self._writer.write_line('const auto localObject = %s.Obj();' % (element_name)) + return '%s::parse(tempContext, localObject);' % (_title_case(field.struct_type)) + elif 'BSONElement::' in field.deserializer: + method_name = _get_method_name(field.deserializer) + return '%s.%s()' % (element_name, method_name) + else: + # Custom method, call the method on object. + # TODO: avoid this string hack in the future + if len(field.bson_serialization_type) == 1 and field.bson_serialization_type[ + 0] == 'string': + # Call a method like: Class::method(StringData value) + self._writer.write_line('auto tempValue = %s.valueStringData();' % (element_name)) - if field.struct_type: - self._writer.write_line('IDLParserErrorContext tempContext("%s", &ctxt);' % - (field.name)) - self._writer.write_line('const auto localObject = element.Obj();') - self._writer.write_line('object.%s = %s::parse(tempContext, localObject);' % ( - _get_field_member_name(field), _title_case(field.struct_type))) - elif 'BSONElement::' in field.deserializer: method_name = _get_method_name(field.deserializer) - self._writer.write_line('object.%s = element.%s();' % - (_get_field_member_name(field), method_name)) + + return '%s(tempValue)' % (method_name) + elif len(field.bson_serialization_type) == 1 and field.bson_serialization_type[ + 0] == 'object': + # Call a method like: Class::method(const BSONObj& value) + method_name = _get_method_name_from_qualified_method_name(field.deserializer) + self._writer.write_line('const BSONObj localObject = %s.Obj();' % (element_name)) + return '%s(localObject)' % (method_name) else: - # Custom method, call the method on object - # TODO: avoid this string hack in the future - if len(field.bson_serialization_type) == 1 and field.bson_serialization_type[ - 0] == 'string': - # Call a method like: Class::method(StringData value) - self._writer.write_line('auto tempValue = element.valueStringData();') - - method_name = _get_method_name(field.deserializer) - - self._writer.write_line('object.%s = %s(tempValue);' % - (_get_field_member_name(field), method_name)) - elif len(field.bson_serialization_type) == 1 and field.bson_serialization_type[ - 0] == 'object': - # Call a method like: Class::method(const BSONObj& value) - method_name = _get_method_name_from_qualified_method_name(field.deserializer) - self._writer.write_line('const BSONObj localObject = element.Obj();') - self._writer.write_line('object.%s = %s(localObject);' % - (_get_field_member_name(field), method_name)) - else: - # Call a method like: Class::method(const BSONElement& value) - method_name = _get_method_name_from_qualified_method_name(field.deserializer) + # Call a method like: Class::method(const BSONElement& value) + method_name = _get_method_name_from_qualified_method_name(field.deserializer) + + return '%s(%s)' % (method_name, element_name) + + def _gen_array_deserializer(self, field): + # type: (ast.Field) -> None + """Generate the C++ deserializer piece for an array field.""" + cpp_type = _get_field_cpp_type(field) + + self._writer.write_line('std::uint32_t expectedFieldNumber{0};') + self._writer.write_line('const IDLParserErrorContext arrayCtxt("%s", &ctxt);' % + (field.name)) + self._writer.write_line('std::vector<%s> values;' % (cpp_type)) + self._writer.write_empty_line() + + self._writer.write_line('const BSONObj arrayObject = element.Obj();') + + with self._block('for (const auto& arrayElement : arrayObject) {', '}'): + + self._writer.write_line( + 'const auto arrayFieldName = arrayElement.fieldNameStringData();') + self._writer.write_line('std::uint32_t fieldNumber;') + self._writer.write_empty_line() + + # Check the array field names are integers + self._writer.write_line( + 'Status status = parseNumberFromString(arrayFieldName, &fieldNumber);') + with self._predicate('status.isOK()'): + + # Check that the array field names are sequential + with self._predicate('fieldNumber != expectedFieldNumber'): + self._writer.write_line('arrayCtxt.throwBadArrayFieldNumberSequence(' + + 'fieldNumber, expectedFieldNumber);') + + with self._predicate(_get_bson_type_check('arrayElement', 'arrayCtxt', field)): + array_value = self._gen_field_deserializer_expression('arrayElement', field) + self._writer.write_line('values.emplace_back(%s);' % (array_value)) + + with self._block('else {', '}'): + self._writer.write_line('arrayCtxt.throwBadArrayFieldNumberValue(arrayFieldName);') + + self._writer.write_line('++expectedFieldNumber;') - self._writer.write_line('object.%s = %s(element);' % - (_get_field_member_name(field), method_name)) + self._writer.write_line('object.%s = std::move(values);' % (_get_field_member_name(field))) + + def gen_field_deserializer(self, field): + # type: (ast.Field) -> None + """Generate the C++ deserializer piece for a field.""" + if field.array: + 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)): + + object_value = self._gen_field_deserializer_expression('element', field) + self._writer.write_line('object.%s = %s;' % + (_get_field_member_name(field), object_value)) def gen_deserializer_method(self, struct): # type: (ast.Struct) -> None @@ -643,6 +855,7 @@ class _CppSourceFileWriter(_CppFileWriterBase): func_def = '%s %s::parse(const IDLParserErrorContext& ctxt, const BSONObj& bsonObject)' % ( _title_case(struct.name), _title_case(struct.name)) with self._block('%s {' % (func_def), '}'): + self._writer.write_line('%s object;' % _title_case(struct.name)) field_usage_check = _FieldUsageChecker(self._writer) @@ -659,7 +872,7 @@ class _CppSourceFileWriter(_CppFileWriterBase): first_field = True for field in struct.fields: - field_predicate = 'fieldName == "%s"' % field.name + field_predicate = 'fieldName == "%s"' % (field.name) field_usage_check.add(field) with self._predicate(field_predicate, not first_field): @@ -685,6 +898,70 @@ class _CppSourceFileWriter(_CppFileWriterBase): self._writer.write_line('return object;') + def _gen_serializer_method_custom(self, field): + # type: (ast.Field) -> None + """Generate the serialize method definition for a custom type.""" + + # Generate custom serialization + method_name = _get_method_name(field.serializer) + + template_params = { + 'field_name': field.name, + 'method_name': method_name, + 'access_member': _access_member(field), + } + + with self._with_template(template_params): + + if len(field.bson_serialization_type) == 1 and \ + field.bson_serialization_type[0] == 'string': + # TODO: expand this out to be less then a string only hack + + if field.array: + self._writer.write_template( + 'BSONArrayBuilder arrayBuilder(builder->subarrayStart("${field_name}"));') + with self._block('for (const auto& item : ${access_member}) {', '}'): + # self._writer.write_template('auto tempValue = ;') + self._writer.write_template('arrayBuilder.append(item.${method_name}());') + else: + # self._writer.write_template( + # 'auto tempValue = ;') + self._writer.write_template( + 'builder->append("${field_name}", ${access_member}.${method_name}());') + else: + if field.array: + self._writer.write_template( + 'BSONArrayBuilder arrayBuilder(builder->subarrayStart("${field_name}"));') + with self._block('for (const auto& item : ${access_member}) {', '}'): + self._writer.write_line( + 'BSONObjBuilder subObjBuilder(arrayBuilder.subobjStart());') + self._writer.write_template('item.${method_name}(&subObjBuilder);') + else: + self._writer.write_template('${access_member}.${method_name}(builder);') + + def _gen_serializer_method_struct(self, field): + # type: (ast.Field) -> None + """Generate the serialize method definition for a struct type.""" + + template_params = { + 'field_name': field.name, + 'access_member': _access_member(field), + } + + with self._with_template(template_params): + + if field.array: + self._writer.write_template( + 'BSONArrayBuilder arrayBuilder(builder->subarrayStart(""${field_name}"));') + with self._block('for (const auto& item : ${access_member}) {', '}'): + self._writer.write_line( + 'BSONObjBuilder subObjBuilder(arrayBuilder.subobjStart());') + self._writer.write_line('item.serialize(&subObjBuilder);') + else: + self._writer.write_template( + 'BSONObjBuilder subObjBuilder(builder->subobjStart("${field_name}"));') + self._writer.write_template('${access_member}.serialize(&subObjBuilder);') + def gen_serializer_method(self, struct): # type: (ast.Struct) -> None """Generate the serialize method definition.""" @@ -702,7 +979,7 @@ class _CppSourceFileWriter(_CppFileWriterBase): optional_block_start = None if field.optional: optional_block_start = 'if (%s) {' % (member_name) - elif field.struct_type: + elif field.struct_type or field.serializer: # Introduce a new scope for required nested object serialization. optional_block_start = '{' @@ -710,31 +987,15 @@ class _CppSourceFileWriter(_CppFileWriterBase): if not field.struct_type: if field.serializer: - # Generate custom serialization - method_name = _get_method_name(field.serializer) - - if len(field.bson_serialization_type) == 1 and \ - field.bson_serialization_type[0] == 'string': - # TODO: expand this out to be less then a string only hack - self._writer.write_line('auto tempValue = %s.%s();' % - (_access_member(field), method_name)) - self._writer.write_line( - 'builder->append("%s", std::move(tempValue));' % (field.name)) - else: - self._writer.write_line('%s.%s(builder);' % - (_access_member(field), method_name)) - + self._gen_serializer_method_custom(field) else: # Generate default serialization using BSONObjBuilder::append + # Note: BSONObjBuilder::append has overrides for std::vector also self._writer.write_line('builder->append("%s", %s);' % (field.name, _access_member(field))) - else: - self._writer.write_line( - 'BSONObjBuilder subObjBuilder(builder->subobjStart("%s"));' % - (field.name)) - self._writer.write_line('%s.serialize(&subObjBuilder);' % - (_access_member(field))) + self._gen_serializer_method_struct(field) + # Add a blank line after each block self._writer.write_empty_line() @@ -805,8 +1066,11 @@ def generate_code(spec, output_base_dir, header_file_name, source_file_name): _generate_header(spec, header_file_name) - include_h_file_name = os.path.relpath( - os.path.normpath(header_file_name), os.path.normpath(output_base_dir)) + if output_base_dir: + include_h_file_name = os.path.relpath( + os.path.normpath(header_file_name), os.path.normpath(output_base_dir)) + else: + include_h_file_name = header_file_name # Normalize to POSIX style for consistency across Windows and POSIX. include_h_file_name = include_h_file_name.replace("\\", "/") diff --git a/buildscripts/idl/idl/syntax.py b/buildscripts/idl/idl/syntax.py index 0470a52e0cf..9ec520c0235 100644 --- a/buildscripts/idl/idl/syntax.py +++ b/buildscripts/idl/idl/syntax.py @@ -55,6 +55,22 @@ class IDLSpec(object): #TODO self.imports = None # type: Optional[Imports] +def parse_array_type(name): + # type: (unicode) -> unicode + """Parse a type name of the form 'array<type>' and extract type.""" + if not name.startswith("array<") and not name.endswith(">"): + return None + + name = name[len("array<"):] + name = name[:-1] + + # V1 restriction, ban nested array types to reduce scope. + if name.startswith("array<") and name.endswith(">"): + return None + + return name + + class SymbolTable(object): """ IDL Symbol Table. @@ -106,16 +122,28 @@ class SymbolTable(object): def resolve_field_type(self, ctxt, field): # type: (errors.ParserContext, Field) -> 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) + + def _resolve_field_type(self, ctxt, field, type_name): + # type: (errors.ParserContext, Field, 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 == field.type: + if idltype.name == type_name: return (None, idltype) for struct in self.structs: - if struct.name == field.type: + if struct.name == type_name: return (struct, None) - # TODO: handle array - ctxt.add_unknown_type_error(field, field.name, field.type) + 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) + return (None, None) + + return self._resolve_field_type(ctxt, field, array_type_name) + + ctxt.add_unknown_type_error(field, field.name, type_name) return (None, None) diff --git a/buildscripts/idl/sample/sample.idl b/buildscripts/idl/sample/sample.idl index bb836cfe2a4..f0c3e8121a5 100644 --- a/buildscripts/idl/sample/sample.idl +++ b/buildscripts/idl/sample/sample.idl @@ -77,3 +77,7 @@ structs: type: string optional: true description: "An optional string" + vectorField: + type: array<int> + description: "An example int array field with default value" + diff --git a/buildscripts/idl/tests/test_binder.py b/buildscripts/idl/tests/test_binder.py index 612a2eb8960..ff1d0351c57 100644 --- a/buildscripts/idl/tests/test_binder.py +++ b/buildscripts/idl/tests/test_binder.py @@ -455,7 +455,6 @@ class TestBinder(testcase.IDLTestcase): bson_serialization_type: string serializer: foo deserializer: foo - default: foo """) # Short type @@ -491,6 +490,35 @@ class TestBinder(testcase.IDLTestcase): default: bar """)) + # Test array as field type + self.assert_bind(test_preamble + textwrap.dedent(""" + structs: + foo: + description: foo + strict: true + fields: + foo: array<string> + """)) + + # Test array as field type + self.assert_bind( + textwrap.dedent(""" + types: + arrayfake: + description: foo + cpp_type: foo + bson_serialization_type: string + serializer: foo + deserializer: foo + + structs: + foo: + description: foo + strict: true + fields: + arrayOfString: arrayfake + """)) + def test_field_negative(self): # type: () -> None """Negative field tests.""" @@ -517,6 +545,47 @@ class TestBinder(testcase.IDLTestcase): array<foo>: string """), idl.errors.ERROR_ID_ARRAY_NOT_VALID_TYPE) + # Test recursive array as field type + self.assert_bind_fail(test_preamble + textwrap.dedent(""" + structs: + foo: + description: foo + strict: true + fields: + foo: array<array<string>> + """), idl.errors.ERROR_ID_BAD_ARRAY_TYPE_NAME) + + # Test inherited default with array + self.assert_bind_fail(test_preamble + textwrap.dedent(""" + structs: + foo: + description: foo + strict: true + fields: + foo: array<string> + """), idl.errors.ERROR_ID_ARRAY_NO_DEFAULT) + + # Test non-inherited default with array + self.assert_bind_fail( + textwrap.dedent(""" + types: + string: + description: foo + cpp_type: foo + bson_serialization_type: string + serializer: foo + deserializer: foo + + structs: + foo: + description: foo + strict: true + fields: + foo: + type: array<string> + default: 123 + """), idl.errors.ERROR_ID_ARRAY_NO_DEFAULT) + def test_ignored_field_negative(self): # type: () -> None """Test that if a field is marked as ignored, no other properties are set.""" |