Add support for map fields. (#137)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 731e10d..650143a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,16 @@
+## 0.11.0
+
+* Breaking change: Support for [map fields](https://developers.google.com/protocol-buffers/docs/proto3#maps)
+  Generated files require package:protobuf version 0.10.5 or newer.
+  Protobuf map fields such as:
+
+  message Foo {
+    map<int32, string> map_field = 1;
+  }
+  are now no longer represented as List<Foo_MapFieldEntry> but as Map<int, String>.
+
+  All code handling these fields needs to be updated.
+
 ## 0.10.5
 
 * Generated files now import `dart:async` with a prefix to prevent name
diff --git a/Makefile b/Makefile
index c6c338d..e75116c 100644
--- a/Makefile
+++ b/Makefile
@@ -24,6 +24,7 @@
 	import_clash \
 	map_api \
 	map_api2 \
+	map_field \
 	mixins \
 	multiple_files_test \
 	nested_extension \
diff --git a/lib/file_generator.dart b/lib/file_generator.dart
index 9dfaf63..0b9d2a3 100644
--- a/lib/file_generator.dart
+++ b/lib/file_generator.dart
@@ -245,7 +245,7 @@
     // Make sure any other symbols in dart:core don't cause name conflicts with
     // protobuf classes that have the same name.
     out.println("// ignore: UNUSED_SHOWN_NAME\n"
-        "import 'dart:core' show int, bool, double, String, List, override;\n");
+        "import 'dart:core' show int, bool, double, String, List, Map, override;\n");
 
     if (_needsFixnumImport) {
       out.println("import 'package:fixnum/fixnum.dart';");
diff --git a/lib/message_generator.dart b/lib/message_generator.dart
index 7a82ebb..365953a 100644
--- a/lib/message_generator.dart
+++ b/lib/message_generator.dart
@@ -213,6 +213,9 @@
     checkResolved();
 
     for (MessageGenerator m in _messageGenerators) {
+      // Don't output the generated map entry type. Instead, the `PbMap` type
+      // from the protobuf library is used to hold the keys and values.
+      if (m._descriptor.options.hasMapEntry()) continue;
       m.generate(out);
     }
 
@@ -381,8 +384,8 @@
     var names = field.memberNames;
 
     _emitOverrideIf(field.overridesGetter, out);
-    var getterExpr = _getterExpression(
-        fieldTypeString, field.index, defaultExpr, field.isRepeated);
+    final getterExpr = _getterExpression(fieldTypeString, field.index,
+        defaultExpr, field.isRepeated, field.isMapField);
     out.println('${fieldTypeString} get ${names.fieldName} => ${getterExpr};');
 
     if (field.isRepeated) {
@@ -421,8 +424,11 @@
     }
   }
 
-  String _getterExpression(
-      String fieldType, int index, String defaultExpr, bool isRepeated) {
+  String _getterExpression(String fieldType, int index, String defaultExpr,
+      bool isRepeated, bool isMapField) {
+    if (isMapField) {
+      return '\$_getMap($index)';
+    }
     if (fieldType == 'String') {
       return '\$_getS($index, $defaultExpr)';
     }
diff --git a/lib/protobuf_field.dart b/lib/protobuf_field.dart
index 13764ca..ce53c7d 100644
--- a/lib/protobuf_field.dart
+++ b/lib/protobuf_field.dart
@@ -72,11 +72,24 @@
   /// True if this field uses the Int64 from the fixnum package.
   bool get needsFixnumImport => baseType.unprefixed == "Int64";
 
+  /// True if this field is a map field definition: `map<key_type, value_type> map_field = N`.
+  bool get isMapField {
+    if (!isRepeated || !baseType.isMessage) return false;
+    MessageGenerator generator = baseType.generator;
+    return generator._descriptor.options.hasMapEntry();
+  }
+
   /// Returns the expression to use for the Dart type.
   ///
   /// This will be a List for repeated types.
   /// [fileGen] represents the .proto file where we are generating code.
   String getDartType(FileGenerator fileGen) {
+    if (isMapField) {
+      MessageGenerator d = baseType.generator;
+      String keyType = d._fieldList[0].baseType.getDartType(fileGen);
+      String valueType = d._fieldList[1].baseType.getDartType(fileGen);
+      return 'Map<$keyType, $valueType>';
+    }
     if (isRepeated) return baseType.getRepeatedDartType(fileGen);
     return baseType.getDartType(fileGen);
   }
@@ -106,18 +119,41 @@
     String quotedName = "'$dartFieldName'";
     String type = baseType.getDartType(fileGen);
 
+    if (isMapField) {
+      MessageGenerator generator = baseType.generator;
+      ProtobufField key = generator._fieldList[0];
+      ProtobufField value = generator._fieldList[1];
+      String keyType = key.baseType.getDartType(fileGen);
+      String valueType = value.baseType.getDartType(fileGen);
+      String keyTypeConstant = key.typeConstant;
+      String valTypeConstant = value.typeConstant;
+
+      if (value.baseType.isMessage || value.baseType.isGroup) {
+        return '..m<$keyType, $valueType>($number, $quotedName, '
+            '$keyTypeConstant, $valTypeConstant, $valueType.create)';
+      }
+      if (value.baseType.isEnum) {
+        return '..m<$keyType, $valueType>($number, $quotedName, '
+            '$keyTypeConstant, $valTypeConstant, null, $valueType.valueOf, '
+            '$valueType.values)';
+      }
+      return '..m<$keyType, $valueType>($number, $quotedName, '
+          '$keyTypeConstant, $valTypeConstant)';
+    }
+
     if (isRepeated) {
       if (baseType.isMessage || baseType.isGroup) {
         return '..pp<$type>($number, $quotedName, $typeConstant,'
             ' $type.$checkItem, $type.create)';
-      } else if (baseType.isEnum) {
+      }
+      if (baseType.isEnum) {
         return '..pp<$type>($number, $quotedName, $typeConstant,'
             ' $type.$checkItem, null, $type.valueOf, $type.values)';
-      } else if (typeConstant == '$_protobufImportPrefix.PbFieldType.PS') {
-        return '..pPS($number, $quotedName)';
-      } else {
-        return '..p<$type>($number, $quotedName, $typeConstant)';
       }
+      if (typeConstant == '$_protobufImportPrefix.PbFieldType.PS') {
+        return '..pPS($number, $quotedName)';
+      }
+      return '..p<$type>($number, $quotedName, $typeConstant)';
     }
 
     String makeDefault = generateDefaultFunction(fileGen);
diff --git a/lib/src/dart_options.pb.dart b/lib/src/dart_options.pb.dart
index 25816e1..058f220 100644
--- a/lib/src/dart_options.pb.dart
+++ b/lib/src/dart_options.pb.dart
@@ -5,7 +5,7 @@
 // ignore_for_file: non_constant_identifier_names,library_prefixes,unused_import
 
 // ignore: UNUSED_SHOWN_NAME
-import 'dart:core' show int, bool, double, String, List, override;
+import 'dart:core' show int, bool, double, String, List, Map, override;
 
 import 'package:protobuf/protobuf.dart' as $pb;
 
diff --git a/lib/src/descriptor.pb.dart b/lib/src/descriptor.pb.dart
index 925d8ab..09c44e0 100644
--- a/lib/src/descriptor.pb.dart
+++ b/lib/src/descriptor.pb.dart
@@ -5,7 +5,7 @@
 // ignore_for_file: non_constant_identifier_names,library_prefixes,unused_import
 
 // ignore: UNUSED_SHOWN_NAME
-import 'dart:core' show int, bool, double, String, List, override;
+import 'dart:core' show int, bool, double, String, List, Map, override;
 
 import 'package:fixnum/fixnum.dart';
 import 'package:protobuf/protobuf.dart' as $pb;
diff --git a/lib/src/plugin.pb.dart b/lib/src/plugin.pb.dart
index cb34f63..f8a6ba2 100644
--- a/lib/src/plugin.pb.dart
+++ b/lib/src/plugin.pb.dart
@@ -5,7 +5,7 @@
 // ignore_for_file: non_constant_identifier_names,library_prefixes,unused_import
 
 // ignore: UNUSED_SHOWN_NAME
-import 'dart:core' show int, bool, double, String, List, override;
+import 'dart:core' show int, bool, double, String, List, Map, override;
 
 import 'package:protobuf/protobuf.dart' as $pb;
 
diff --git a/pubspec.yaml b/pubspec.yaml
index d01ed26..5d44764 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -10,7 +10,7 @@
 dependencies:
   fixnum: ^0.10.5
   path: ^1.0.0
-  protobuf: ^0.10.4
+  protobuf: ^0.10.5
   dart_style: ^1.0.6
 
 dev_dependencies:
diff --git a/test/goldens/grpc_service.pb b/test/goldens/grpc_service.pb
index 9bf1ff9..2515d87 100644
--- a/test/goldens/grpc_service.pb
+++ b/test/goldens/grpc_service.pb
@@ -5,7 +5,7 @@
 // ignore_for_file: non_constant_identifier_names,library_prefixes,unused_import
 
 // ignore: UNUSED_SHOWN_NAME
-import 'dart:core' show int, bool, double, String, List, override;
+import 'dart:core' show int, bool, double, String, List, Map, override;
 
 import 'package:protobuf/protobuf.dart' as $pb;
 
diff --git a/test/goldens/header_in_package.pb b/test/goldens/header_in_package.pb
index 7b74077..0399a53 100644
--- a/test/goldens/header_in_package.pb
+++ b/test/goldens/header_in_package.pb
@@ -5,7 +5,7 @@
 // ignore_for_file: non_constant_identifier_names,library_prefixes,unused_import
 
 // ignore: UNUSED_SHOWN_NAME
-import 'dart:core' show int, bool, double, String, List, override;
+import 'dart:core' show int, bool, double, String, List, Map, override;
 
 import 'package:protobuf/protobuf.dart' as $pb;
 
diff --git a/test/goldens/header_with_fixnum.pb b/test/goldens/header_with_fixnum.pb
index 4d353b1..9df6c14 100644
--- a/test/goldens/header_with_fixnum.pb
+++ b/test/goldens/header_with_fixnum.pb
@@ -5,7 +5,7 @@
 // ignore_for_file: non_constant_identifier_names,library_prefixes,unused_import
 
 // ignore: UNUSED_SHOWN_NAME
-import 'dart:core' show int, bool, double, String, List, override;
+import 'dart:core' show int, bool, double, String, List, Map, override;
 
 import 'package:fixnum/fixnum.dart';
 import 'package:protobuf/protobuf.dart' as $pb;
diff --git a/test/goldens/imports.pb b/test/goldens/imports.pb
index f75afa7..2c384a9 100644
--- a/test/goldens/imports.pb
+++ b/test/goldens/imports.pb
@@ -5,7 +5,7 @@
 // ignore_for_file: non_constant_identifier_names,library_prefixes,unused_import
 
 // ignore: UNUSED_SHOWN_NAME
-import 'dart:core' show int, bool, double, String, List, override;
+import 'dart:core' show int, bool, double, String, List, Map, override;
 
 import 'package:protobuf/protobuf.dart' as $pb;
 
diff --git a/test/goldens/oneMessage.pb b/test/goldens/oneMessage.pb
index 0278d17..7ca3006 100644
--- a/test/goldens/oneMessage.pb
+++ b/test/goldens/oneMessage.pb
@@ -5,7 +5,7 @@
 // ignore_for_file: non_constant_identifier_names,library_prefixes,unused_import
 
 // ignore: UNUSED_SHOWN_NAME
-import 'dart:core' show int, bool, double, String, List, override;
+import 'dart:core' show int, bool, double, String, List, Map, override;
 
 import 'package:protobuf/protobuf.dart' as $pb;
 
diff --git a/test/goldens/service.pb b/test/goldens/service.pb
index ba27e9b..507b6bf 100644
--- a/test/goldens/service.pb
+++ b/test/goldens/service.pb
@@ -6,7 +6,7 @@
 
 import 'dart:async' as $async;
 // ignore: UNUSED_SHOWN_NAME
-import 'dart:core' show int, bool, double, String, List, override;
+import 'dart:core' show int, bool, double, String, List, Map, override;
 
 import 'package:protobuf/protobuf.dart' as $pb;
 
diff --git a/test/goldens/topLevelEnum.pb b/test/goldens/topLevelEnum.pb
index af9ca99..bb968a8 100644
--- a/test/goldens/topLevelEnum.pb
+++ b/test/goldens/topLevelEnum.pb
@@ -5,7 +5,7 @@
 // ignore_for_file: non_constant_identifier_names,library_prefixes,unused_import
 
 // ignore: UNUSED_SHOWN_NAME
-import 'dart:core' show int, bool, double, String, List, override;
+import 'dart:core' show int, bool, double, String, List, Map, override;
 
 export 'test.pbenum.dart';
 
diff --git a/test/map_field_test.dart b/test/map_field_test.dart
new file mode 100644
index 0000000..594250f
--- /dev/null
+++ b/test/map_field_test.dart
@@ -0,0 +1,271 @@
+#!/usr/bin/env dart
+// Copyright (c) 2018, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library map_field_test;
+
+import 'dart:convert';
+
+import 'package:test/test.dart';
+
+import '../out/protos/map_field.pb.dart';
+
+void main() {
+  void _setValues(TestMap testMap) {
+    testMap
+      ..int32ToInt32Field[1] = 11
+      ..int32ToInt32Field[2] = 22
+      ..int32ToInt32Field[3] = 33
+      ..int32ToStringField[1] = '11'
+      ..int32ToStringField[2] = '22'
+      ..int32ToStringField[3] = '33'
+      ..int32ToBytesField[1] = utf8.encode('11')
+      ..int32ToBytesField[2] = utf8.encode('22')
+      ..int32ToBytesField[3] = utf8.encode('33')
+      ..int32ToEnumField[1] = TestMap_EnumValue.FOO
+      ..int32ToEnumField[2] = TestMap_EnumValue.BAR
+      ..int32ToEnumField[3] = TestMap_EnumValue.BAZ
+      ..int32ToMessageField[1] = (TestMap_MessageValue()..value = 11)
+      ..int32ToMessageField[2] = (TestMap_MessageValue()..value = 22)
+      ..int32ToMessageField[3] = (TestMap_MessageValue()..value = 33)
+      ..stringToInt32Field['1'] = 11
+      ..stringToInt32Field['2'] = 22
+      ..stringToInt32Field['3'] = 33;
+  }
+
+  void _updateValues(TestMap testMap) {
+    testMap
+      ..int32ToInt32Field[1] = 111
+      ..int32ToInt32Field.remove(2)
+      ..int32ToInt32Field[4] = 44
+      ..int32ToStringField[1] = '111'
+      ..int32ToStringField.remove(2)
+      ..int32ToStringField[4] = '44'
+      ..int32ToBytesField[1] = utf8.encode('111')
+      ..int32ToBytesField.remove(2)
+      ..int32ToBytesField[4] = utf8.encode('44')
+      ..int32ToEnumField[1] = TestMap_EnumValue.BAR
+      ..int32ToEnumField.remove(2)
+      ..int32ToEnumField[4] = TestMap_EnumValue.ZOP
+      ..int32ToMessageField[1] = (TestMap_MessageValue()..value = 111)
+      ..int32ToMessageField.remove(2)
+      ..int32ToMessageField[4] = (TestMap_MessageValue()..value = 44)
+      ..stringToInt32Field['1'] = 111
+      ..stringToInt32Field.remove('2')
+      ..stringToInt32Field['4'] = 44;
+  }
+
+  void _expectEmpty(TestMap testMap) {
+    expect(testMap.int32ToInt32Field, isEmpty);
+    expect(testMap.int32ToStringField, isEmpty);
+    expect(testMap.int32ToBytesField, isEmpty);
+    expect(testMap.int32ToEnumField, isEmpty);
+    expect(testMap.int32ToMessageField, isEmpty);
+    expect(testMap.stringToInt32Field, isEmpty);
+  }
+
+  void _expectMapValuesSet(TestMap testMap) {
+    expect(testMap.int32ToInt32Field[1], 11);
+    expect(testMap.int32ToInt32Field[2], 22);
+    expect(testMap.int32ToInt32Field[3], 33);
+
+    expect(testMap.int32ToStringField[1], '11');
+    expect(testMap.int32ToStringField[2], '22');
+    expect(testMap.int32ToStringField[3], '33');
+
+    expect(testMap.int32ToBytesField[1], utf8.encode('11'));
+    expect(testMap.int32ToBytesField[2], utf8.encode('22'));
+    expect(testMap.int32ToBytesField[3], utf8.encode('33'));
+
+    expect(testMap.int32ToEnumField[1], TestMap_EnumValue.FOO);
+    expect(testMap.int32ToEnumField[2], TestMap_EnumValue.BAR);
+    expect(testMap.int32ToEnumField[3], TestMap_EnumValue.BAZ);
+
+    expect(testMap.int32ToMessageField[1].value, 11);
+    expect(testMap.int32ToMessageField[2].value, 22);
+    expect(testMap.int32ToMessageField[3].value, 33);
+
+    expect(testMap.stringToInt32Field['1'], 11);
+    expect(testMap.stringToInt32Field['2'], 22);
+    expect(testMap.stringToInt32Field['3'], 33);
+  }
+
+  void _expectMapValuesUpdated(TestMap testMap) {
+    expect(testMap.int32ToInt32Field.length, 3);
+    expect(testMap.int32ToInt32Field[1], 111);
+    expect(testMap.int32ToInt32Field[3], 33);
+    expect(testMap.int32ToInt32Field[4], 44);
+
+    expect(testMap.int32ToStringField.length, 3);
+    expect(testMap.int32ToStringField[1], '111');
+    expect(testMap.int32ToStringField[3], '33');
+    expect(testMap.int32ToStringField[4], '44');
+
+    expect(testMap.int32ToBytesField.length, 3);
+    expect(testMap.int32ToBytesField[1], utf8.encode('111'));
+    expect(testMap.int32ToBytesField[3], utf8.encode('33'));
+    expect(testMap.int32ToBytesField[4], utf8.encode('44'));
+
+    expect(testMap.int32ToEnumField.length, 3);
+    expect(testMap.int32ToEnumField[1], TestMap_EnumValue.BAR);
+    expect(testMap.int32ToEnumField[3], TestMap_EnumValue.BAZ);
+    expect(testMap.int32ToEnumField[4], TestMap_EnumValue.ZOP);
+
+    expect(testMap.int32ToMessageField.length, 3);
+    expect(testMap.int32ToMessageField[1].value, 111);
+    expect(testMap.int32ToMessageField[3].value, 33);
+    expect(testMap.int32ToMessageField[4].value, 44);
+
+    expect(testMap.stringToInt32Field.length, 3);
+    expect(testMap.stringToInt32Field['1'], 111);
+    expect(testMap.stringToInt32Field['3'], 33);
+    expect(testMap.stringToInt32Field['4'], 44);
+  }
+
+  test('set and clear values', () {
+    TestMap testMap = TestMap();
+    _expectEmpty(testMap);
+
+    _setValues(testMap);
+    _expectMapValuesSet(testMap);
+
+    testMap.clear();
+    _expectEmpty(testMap);
+  });
+
+  test('update map values', () {
+    TestMap testMap = TestMap();
+    _setValues(testMap);
+    _updateValues(testMap);
+    _expectMapValuesUpdated(testMap);
+  });
+
+  test('null keys and value are not allowed', () {
+    TestMap testMap = TestMap();
+
+    try {
+      testMap.stringToInt32Field[null] = 1;
+      fail('Should have thrown an exception.');
+    } on ArgumentError catch (e) {
+      expect(e.message, "Can't add a null to a map field");
+    }
+
+    try {
+      testMap.int32ToBytesField[1] = null;
+      fail('Should have thrown an exception.');
+    } on ArgumentError catch (e) {
+      expect(e.message, "Can't add a null to a map field");
+    }
+
+    try {
+      testMap.int32ToStringField[1] = null;
+      fail('Should have thrown an exception.');
+    } on ArgumentError catch (e) {
+      expect(e.message, "Can't add a null to a map field");
+    }
+
+    try {
+      testMap.int32ToEnumField[1] = null;
+      fail('Should have thrown an exception.');
+    } on ArgumentError catch (e) {
+      expect(e.message, "Can't add a null to a map field");
+    }
+
+    try {
+      testMap.int32ToMessageField[1] = null;
+      fail('Should have thrown an exception.');
+    } on ArgumentError catch (e) {
+      expect(e.message, "Can't add a null to a map field");
+    }
+  });
+
+  test('Serialize and parse map', () {
+    TestMap testMap = TestMap();
+    _setValues(testMap);
+
+    testMap = TestMap.fromBuffer(testMap.writeToBuffer());
+    _expectMapValuesSet(testMap);
+
+    _updateValues(testMap);
+    testMap = TestMap.fromBuffer(testMap.writeToBuffer());
+    _expectMapValuesUpdated(testMap);
+
+    testMap.clear();
+    testMap = TestMap.fromBuffer(testMap.writeToBuffer());
+    _expectEmpty(testMap);
+  });
+
+  test('json serialize map', () {
+    TestMap testMap = TestMap();
+    _setValues(testMap);
+
+    testMap = TestMap.fromJson(testMap.writeToJson());
+    _expectMapValuesSet(testMap);
+
+    _updateValues(testMap);
+    testMap = TestMap.fromJson(testMap.writeToJson());
+    _expectMapValuesUpdated(testMap);
+
+    testMap.clear();
+    testMap = TestMap.fromJson(testMap.writeToJson());
+    _expectEmpty(testMap);
+  });
+
+  test('merge from other message', () {
+    TestMap testMap = TestMap();
+    _setValues(testMap);
+
+    TestMap other = TestMap();
+    other.mergeFromMessage(testMap);
+    _expectMapValuesSet(other);
+
+    testMap = TestMap()
+      ..int32ToMessageField[1] = (TestMap_MessageValue()..value = 42)
+      ..int32ToMessageField[2] = (TestMap_MessageValue()..value = 44);
+    other = TestMap()
+      ..int32ToMessageField[1] = (TestMap_MessageValue()..secondValue = 43);
+    testMap.mergeFromMessage(other);
+
+    expect(testMap.int32ToMessageField[1].value, 0);
+    expect(testMap.int32ToMessageField[1].secondValue, 43);
+    expect(testMap.int32ToMessageField[2].value, 44);
+  });
+
+  test('parse duplicate keys', () {
+    TestMap testMap = TestMap()..int32ToStringField[1] = 'foo';
+    TestMap testMap2 = TestMap()..int32ToStringField[1] = 'bar';
+
+    TestMap merge = TestMap.fromBuffer(
+        []..addAll(testMap.writeToBuffer())..addAll(testMap2.writeToBuffer()));
+
+    // When parsing from the wire, if there are duplicate map keys the last key
+    // seen should be used.
+    expect(merge.int32ToStringField.length, 1);
+    expect(merge.int32ToStringField[1], 'bar');
+  });
+
+  // TODO(zarah): remove skip once https://github.com/dart-lang/protobuf/issues/139 is fixed.
+  test('Deep merge from other message', () {
+    Inner i1 = Inner()..innerMap['a'] = 'a';
+    Inner i2 = Inner()..innerMap['b'] = 'b';
+
+    Outer o1 = Outer()..i = i1;
+    Outer o2 = Outer()..i = i2;
+
+    o1.mergeFromMessage(o2);
+    expect(o1.i.innerMap.length, 2);
+  }, skip: true);
+
+  test('retain explicit default values of sub-messages', () {
+    TestMap testMap = TestMap()
+      ..int32ToMessageField[1] = TestMap_MessageValue();
+    expect(testMap.int32ToMessageField[1].secondValue, 42);
+
+    TestMap testMap2 = TestMap()
+      ..int32ToMessageField[2] = TestMap_MessageValue();
+
+    testMap.mergeFromBuffer(testMap2.writeToBuffer());
+    expect(testMap.int32ToMessageField[2].secondValue, 42);
+  });
+}
diff --git a/test/protos/map_field.proto b/test/protos/map_field.proto
new file mode 100644
index 0000000..d5af64f
--- /dev/null
+++ b/test/protos/map_field.proto
@@ -0,0 +1,38 @@
+// Copyright (c) 2018, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+syntax = "proto2";
+
+package protobuf_unittest;
+
+message TestMap {
+    message MessageValue {
+        optional int32 value = 1;
+        optional int32 secondValue = 2 [default = 42];
+    }
+
+    enum EnumValue {
+        FOO = 0;
+        BAR = 1;
+        BAZ = 2;
+        ZOP = 3;
+    }
+
+    map<int32, int32>        int32_to_int32_field = 1;
+    map<int32, string>       int32_to_string_field = 2;
+    map<int32, bytes>        int32_to_bytes_field = 3;
+    map<int32, EnumValue>    int32_to_enum_field = 4;
+    map<int32, MessageValue> int32_to_message_field = 5;
+    map<string, int32>       string_to_int32_field = 6;
+    map<uint32, int32>       uint32_to_int32_field = 7;
+    map<int64, int32>        int64_to_int32_field = 8;
+}
+
+message Inner  {
+    map<string, string> inner_map = 1;
+}
+
+message Outer {
+    optional Inner i = 1;
+}