gRPC stub generation. (#79)

Added support for generating gRPC stubs. gRPC mode is selected by adding
the option 'grpc' to the `--dart_out` argument, as in
`--dart_out=grpc:<path>`.

When gRPC mode is selected, the legacy ("generic") RPC stubs will not be
emitted.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b0b1f50..4a21ebc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+## Unreleased
+
+### gRPC support
+
+* Added gRPC stub generation.
+* Updated descriptor.proto from google/protobuf v3.3.0.
+
 ## 0.7.2 - 2017-06-12
 
 * Added CHANGELOG.md
diff --git a/lib/code_generator.dart b/lib/code_generator.dart
index 9c9182e..ecb10e0 100644
--- a/lib/code_generator.dart
+++ b/lib/code_generator.dart
@@ -55,7 +55,7 @@
       // (We may import it even if we don't generate the .pb.dart file.)
       List<FileGenerator> generators = <FileGenerator>[];
       for (FileDescriptorProto file in request.protoFile) {
-        generators.add(new FileGenerator(file));
+        generators.add(new FileGenerator(file, options));
       }
 
       // Collect field types and importable files.
diff --git a/lib/file_generator.dart b/lib/file_generator.dart
index 6ea65d2..838ca03 100644
--- a/lib/file_generator.dart
+++ b/lib/file_generator.dart
@@ -5,6 +5,7 @@
 part of protoc;
 
 final _dartIdentifier = new RegExp(r'^\w+$');
+final _formatter = new DartFormatter();
 
 /// Generates the Dart output files for one .proto input file.
 ///
@@ -90,22 +91,23 @@
   }
 
   final FileDescriptorProto descriptor;
+  final GenerationOptions options;
 
   // The relative path used to import the .proto file, as a URI.
   final Uri protoFileUri;
 
-  final List<EnumGenerator> enumGenerators = <EnumGenerator>[];
-  final List<MessageGenerator> messageGenerators = <MessageGenerator>[];
-  final List<ExtensionGenerator> extensionGenerators = <ExtensionGenerator>[];
-  final List<ClientApiGenerator> clientApiGenerators = <ClientApiGenerator>[];
-  final List<ServiceGenerator> serviceGenerators = <ServiceGenerator>[];
+  final enumGenerators = <EnumGenerator>[];
+  final messageGenerators = <MessageGenerator>[];
+  final extensionGenerators = <ExtensionGenerator>[];
+  final clientApiGenerators = <ClientApiGenerator>[];
+  final serviceGenerators = <ServiceGenerator>[];
+  final grpcGenerators = <GrpcServiceGenerator>[];
 
   /// True if cross-references have been resolved.
   bool _linked = false;
 
-  FileGenerator(FileDescriptorProto descriptor)
-      : descriptor = descriptor,
-        protoFileUri = new Uri.file(descriptor.name) {
+  FileGenerator(this.descriptor, this.options)
+      : protoFileUri = new Uri.file(descriptor.name) {
     if (protoFileUri.isAbsolute) {
       // protoc should never generate an import with an absolute path.
       throw "FAILURE: Import with absolute path is not supported";
@@ -133,9 +135,13 @@
       extensionGenerators.add(new ExtensionGenerator(extension, this));
     }
     for (ServiceDescriptorProto service in descriptor.service) {
-      var serviceGen = new ServiceGenerator(service, this);
-      serviceGenerators.add(serviceGen);
-      clientApiGenerators.add(new ClientApiGenerator(serviceGen));
+      if (options.useGrpc) {
+        grpcGenerators.add(new GrpcServiceGenerator(service, this));
+      } else {
+        var serviceGen = new ServiceGenerator(service, this);
+        serviceGenerators.add(serviceGen);
+        clientApiGenerators.add(new ClientApiGenerator(serviceGen));
+      }
     }
   }
 
@@ -178,12 +184,19 @@
         ..content = content;
     }
 
-    return [
+    final files = [
       makeFile(".pb.dart", generateMainFile(config)),
       makeFile(".pbenum.dart", generateEnumFile(config)),
-      makeFile(".pbserver.dart", generateServerFile(config)),
       makeFile(".pbjson.dart", generateJsonFile(config)),
     ];
+    if (options.useGrpc) {
+      if (grpcGenerators.isNotEmpty) {
+        files.add(makeFile(".pbgrpc.dart", generateGrpcFile(config)));
+      }
+    } else {
+      files.add(makeFile(".pbserver.dart", generateServerFile(config)));
+    }
+    return files;
   }
 
   /// Returns the contents of the .pb.dart file for this .proto file.
@@ -417,6 +430,40 @@
     return out.toString();
   }
 
+  /// Returns the contents of the .pbgrpc.dart file for this .proto file.
+  String generateGrpcFile(
+      [OutputConfiguration config = const DefaultOutputConfiguration()]) {
+    if (!_linked) throw new StateError("not linked");
+    var out = new IndentingWriter();
+    _writeLibraryHeading(out, "pbgrpc");
+
+    out.println('''
+import 'dart:async';
+
+import 'package:grpc/grpc.dart';
+''');
+
+    // Import .pb.dart files needed for requests and responses.
+    var imports = new Set<FileGenerator>();
+    for (var generator in grpcGenerators) {
+      generator.addImportsTo(imports);
+    }
+    for (var target in imports) {
+      _writeImport(out, config, target, ".pb.dart");
+    }
+
+    var resolvedImport =
+        config.resolveImport(protoFileUri, protoFileUri, ".pb.dart");
+    out.println("export '$resolvedImport';");
+    out.println();
+
+    for (var generator in grpcGenerators) {
+      generator.generate(out);
+    }
+
+    return _formatter.format(out.toString());
+  }
+
   /// Returns the contents of the .pbjson.dart file for this .proto file.
   String generateJsonFile(
       [OutputConfiguration config = const DefaultOutputConfiguration()]) {
diff --git a/lib/grpc_generator.dart b/lib/grpc_generator.dart
new file mode 100644
index 0000000..22d2fcc
--- /dev/null
+++ b/lib/grpc_generator.dart
@@ -0,0 +1,266 @@
+// Copyright (c) 2017, 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.
+
+part of protoc;
+
+class GrpcServiceGenerator {
+  final ServiceDescriptorProto _descriptor;
+
+  /// The generator of the .pb.dart file that will contain this service.
+  final FileGenerator fileGen;
+
+  /// The message types needed directly by this service.
+  ///
+  /// The key is the fully qualified name.
+  /// Populated by [resolve].
+  final _deps = <String, MessageGenerator>{};
+
+  /// Maps each undefined type to a string describing its location.
+  ///
+  /// Populated by [resolve].
+  final _undefinedDeps = <String, String>{};
+
+  /// Fully-qualified gRPC service name.
+  String _fullServiceName;
+
+  /// Dart class name for client stub.
+  String _clientClassname;
+
+  /// Dart class name for server stub.
+  String _serviceClassname;
+
+  /// List of gRPC methods.
+  final _methods = <_GrpcMethod>[];
+
+  GrpcServiceGenerator(this._descriptor, this.fileGen) {
+    final name = _descriptor.name;
+    final package = fileGen.package;
+
+    if (package != null && package.isNotEmpty) {
+      _fullServiceName = '$package.$name';
+    } else {
+      _fullServiceName = name;
+    }
+
+    // avoid: ClientClient
+    _clientClassname = name.endsWith('Client') ? name : name + 'Client';
+    // avoid: ServiceServiceBase
+    _serviceClassname =
+        name.endsWith('Service') ? name + 'Base' : name + 'ServiceBase';
+  }
+
+  /// Finds all message types used by this service.
+  ///
+  /// Puts the types found in [_deps]. If a type name can't be resolved, puts it
+  /// in [_undefinedDeps].
+  /// Precondition: messages have been registered and resolved.
+  void resolve(GenerationContext ctx) {
+    for (var method in _descriptor.method) {
+      _methods.add(new _GrpcMethod(this, ctx, method));
+    }
+  }
+
+  /// Adds a dependency on the given message type.
+  ///
+  /// If the type name can't be resolved, adds it to [_undefinedDeps].
+  void _addDependency(GenerationContext ctx, String fqname, String location) {
+    if (_deps.containsKey(fqname)) return; // Already added.
+
+    MessageGenerator mg = ctx.getFieldType(fqname);
+    if (mg == null) {
+      _undefinedDeps[fqname] = location;
+      return;
+    }
+    mg.checkResolved();
+    _deps[mg.fqname] = mg;
+  }
+
+  /// Adds dependencies of [generate] to [imports].
+  ///
+  /// For each .pb.dart file that the generated code needs to import,
+  /// add its generator.
+  void addImportsTo(Set<FileGenerator> imports) {
+    for (var mg in _deps.values) {
+      imports.add(mg.fileGen);
+    }
+  }
+
+  /// Returns the Dart class name to use for a message type.
+  ///
+  /// Throws an exception if it can't be resolved.
+  String _getDartClassName(String fqname) {
+    var mg = _deps[fqname];
+    if (mg == null) {
+      var location = _undefinedDeps[fqname];
+      // TODO(jakobr): Throw more actionable error.
+      throw 'FAILURE: Unknown type reference (${fqname}) for ${location}';
+    }
+    if (fileGen.package == mg.fileGen.package || mg.fileGen.package == "") {
+      // It's either the same file, or another file with the same package.
+      // (In the second case, we import it without using "as".)
+      return mg.classname;
+    }
+    return mg.packageImportPrefix + "." + mg.classname;
+  }
+
+  void generate(IndentingWriter out) {
+    _generateClient(out);
+    out.println();
+    _generateService(out);
+  }
+
+  void _generateClient(IndentingWriter out) {
+    out.addBlock('class $_clientClassname {', '}', () {
+      out.println('final ClientChannel _channel;');
+      out.println();
+      for (final method in _methods) {
+        method.generateClientMethodDescriptor(out);
+      }
+      out.println();
+      out.println('$_clientClassname(this._channel);');
+      for (final method in _methods) {
+        method.generateClientStub(out);
+      }
+    });
+  }
+
+  void _generateService(IndentingWriter out) {
+    out.addBlock('abstract class $_serviceClassname extends Service {', '}',
+        () {
+      out.println('String get \$name => \'$_fullServiceName\';');
+      out.println();
+      out.addBlock('$_serviceClassname() {', '}', () {
+        for (final method in _methods) {
+          method.generateServiceMethodRegistration(out);
+        }
+      });
+      out.println();
+      for (final method in _methods) {
+        method.generateServiceMethodPreamble(out);
+      }
+      for (final method in _methods) {
+        method.generateServiceMethodStub(out);
+      }
+    });
+  }
+}
+
+class _GrpcMethod {
+  final String _grpcName;
+  final String _dartName;
+  final String _serviceName;
+
+  final bool _clientStreaming;
+  final bool _serverStreaming;
+
+  final String _requestType;
+  final String _responseType;
+
+  final String _argumentType;
+  final String _clientReturnType;
+  final String _serverReturnType;
+
+  _GrpcMethod._(
+      this._grpcName,
+      this._dartName,
+      this._serviceName,
+      this._clientStreaming,
+      this._serverStreaming,
+      this._requestType,
+      this._responseType,
+      this._argumentType,
+      this._clientReturnType,
+      this._serverReturnType);
+
+  factory _GrpcMethod(GrpcServiceGenerator service, GenerationContext ctx,
+      MethodDescriptorProto method) {
+    final grpcName = method.name;
+    final dartName =
+        grpcName.substring(0, 1).toLowerCase() + grpcName.substring(1);
+
+    final clientStreaming = method.clientStreaming;
+    final serverStreaming = method.serverStreaming;
+
+    service._addDependency(ctx, method.inputType, "input type of $grpcName");
+    service._addDependency(ctx, method.outputType, "output type of $grpcName");
+
+    final requestType = service._getDartClassName(method.inputType);
+    final responseType = service._getDartClassName(method.outputType);
+
+    final argumentType = clientStreaming ? 'Stream<$requestType>' : requestType;
+    final clientReturnType = serverStreaming
+        ? 'ResponseStream<$responseType>'
+        : 'ResponseFuture<$responseType>';
+    final serverReturnType =
+        serverStreaming ? 'Stream<$responseType>' : 'Future<$responseType>';
+
+    return new _GrpcMethod._(
+        grpcName,
+        dartName,
+        service._fullServiceName,
+        clientStreaming,
+        serverStreaming,
+        requestType,
+        responseType,
+        argumentType,
+        clientReturnType,
+        serverReturnType);
+  }
+
+  void generateClientMethodDescriptor(IndentingWriter out) {
+    out.println(
+        'static final _\$$_dartName = new ClientMethod<$_requestType, $_responseType>(');
+    out.println('\'/$_serviceName/$_grpcName\',');
+    out.println('($_requestType value) => value.writeToBuffer(),');
+    out.println('(List<int> value) => new $_responseType.fromBuffer(value));');
+  }
+
+  void generateClientStub(IndentingWriter out) {
+    out.println();
+    out.addBlock('$_clientReturnType $_dartName($_argumentType request) {', '}',
+        () {
+      out.println('final call = new ClientCall(_channel, _\$$_dartName);');
+      if (_clientStreaming) {
+        out.println('request.pipe(call.request);');
+      } else {
+        out.println('call.request..add(request)..close();');
+      }
+      if (_serverStreaming) {
+        out.println('return new ResponseStream(call);');
+      } else {
+        out.println('return new ResponseFuture(call);');
+      }
+    });
+  }
+
+  void generateServiceMethodRegistration(IndentingWriter out) {
+    out.println('\$addMethod(new ServiceMethod(');
+    out.println('\'$_grpcName\',');
+    out.println('$_dartName${_clientStreaming ? '' : '_Pre'},');
+    out.println('$_clientStreaming,');
+    out.println('$_serverStreaming,');
+    out.println('(List<int> value) => new $_requestType.fromBuffer(value),');
+    out.println('($_responseType value) => value.writeToBuffer()));');
+  }
+
+  void generateServiceMethodPreamble(IndentingWriter out) {
+    if (_clientStreaming) return;
+
+    out.addBlock(
+        '$_serverReturnType ${_dartName}_Pre(ServiceCall call, Future<$_requestType> request) async${_serverStreaming ? '*' : ''} {',
+        '}', () {
+      if (_serverStreaming) {
+        out.println('yield* $_dartName(call, await request);');
+      } else {
+        out.println('return $_dartName(call, await request);');
+      }
+    });
+    out.println();
+  }
+
+  void generateServiceMethodStub(IndentingWriter out) {
+    out.println(
+        '$_serverReturnType $_dartName(ServiceCall call, $_argumentType request);');
+  }
+}
diff --git a/lib/linker.dart b/lib/linker.dart
index fa1ab9d..cf275d7 100644
--- a/lib/linker.dart
+++ b/lib/linker.dart
@@ -30,6 +30,9 @@
     for (var s in f.serviceGenerators) {
       s.resolve(ctx);
     }
+    for (var s in f.grpcGenerators) {
+      s.resolve(ctx);
+    }
   }
 }
 
diff --git a/lib/options.dart b/lib/options.dart
index fcea308..c8fc4a6 100644
--- a/lib/options.dart
+++ b/lib/options.dart
@@ -46,7 +46,9 @@
 
 /// Options expected by the protoc code generation compiler.
 class GenerationOptions {
-  GenerationOptions();
+  final bool useGrpc;
+
+  GenerationOptions({this.useGrpc = false});
 }
 
 /// A parser for a name-value pair option. Options parsed in
@@ -60,17 +62,33 @@
   void parse(String name, String value, onError(String details));
 }
 
-/// Parser used by the compiler, which supports the `field_name` option (see
-/// [FieldNameOptionParser]) and any additional option added in [parsers]. If
-/// [parsers] has a key for `field_name`, it will be ignored.
+class GrpcOptionParser implements SingleOptionParser {
+  bool grpcEnabled = false;
+
+  @override
+  void parse(String name, String value, onError(String details)) {
+    if (value != null) {
+      onError('Invalid grpc option. No value expected.');
+      return;
+    }
+    grpcEnabled = true;
+  }
+}
+
+/// Parser used by the compiler, which supports the `rpc` option (see
+/// [RpcOptionParser]) and any additional option added in [parsers]. If
+/// [parsers] has a key for `rpc`, it will be ignored.
 GenerationOptions parseGenerationOptions(
     CodeGeneratorRequest request, CodeGeneratorResponse response,
     [Map<String, SingleOptionParser> parsers]) {
-  var newParsers = <String, SingleOptionParser>{};
+  final newParsers = <String, SingleOptionParser>{};
   if (parsers != null) newParsers.addAll(parsers);
 
+  final grpcOptionParser = new GrpcOptionParser();
+  newParsers['grpc'] = grpcOptionParser;
+
   if (genericOptionsParser(request, response, newParsers)) {
-    return new GenerationOptions();
+    return new GenerationOptions(useGrpc: grpcOptionParser.grpcEnabled);
   }
   return null;
 }
diff --git a/lib/protoc.dart b/lib/protoc.dart
index 94cb333..6a1b390 100644
--- a/lib/protoc.dart
+++ b/lib/protoc.dart
@@ -3,6 +3,7 @@
 import 'dart:async';
 import 'dart:io';
 
+import 'package:dart_style/dart_style.dart';
 import 'package:protobuf/mixins_meta.dart';
 import 'package:protobuf/protobuf.dart';
 import 'package:path/path.dart' as path;
@@ -21,6 +22,7 @@
 part 'enum_generator.dart';
 part 'extension_generator.dart';
 part 'file_generator.dart';
+part 'grpc_generator.dart';
 part 'linker.dart';
 part 'message_generator.dart';
 part 'options.dart';
diff --git a/pubspec.yaml b/pubspec.yaml
index b962b21..5c59ddb 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -9,6 +9,7 @@
   fixnum: ^0.10.5
   path: ^1.0.0
   protobuf: ^0.5.4
+  dart_style: ^1.0.6
 dev_dependencies:
   browser: any
   test: ^0.12.0
diff --git a/test/client_generator_test.dart b/test/client_generator_test.dart
index 2150082..7b6cc74 100644
--- a/test/client_generator_test.dart
+++ b/test/client_generator_test.dart
@@ -30,12 +30,13 @@
 }
 
 ''';
+    var options = new GenerationOptions();
     var fd = buildFileDescriptor("testpkg", ["SomeRequest", "SomeReply"]);
     fd.service.add(buildServiceDescriptor());
-    var fg = new FileGenerator(fd);
+    var fg = new FileGenerator(fd, options);
 
     var fd2 = buildFileDescriptor("foo.bar", ["EmptyMessage", "AnotherReply"]);
-    var fg2 = new FileGenerator(fd2);
+    var fg2 = new FileGenerator(fd2, options);
 
     link(new GenerationOptions(), [fg, fg2]);
 
diff --git a/test/file_generator_test.dart b/test/file_generator_test.dart
index 3ab1e20..b7d01fa 100644
--- a/test/file_generator_test.dart
+++ b/test/file_generator_test.dart
@@ -129,7 +129,7 @@
     FileDescriptorProto fd = buildFileDescriptor();
     var options = parseGenerationOptions(
         new CodeGeneratorRequest(), new CodeGeneratorResponse());
-    FileGenerator fg = new FileGenerator(fd);
+    FileGenerator fg = new FileGenerator(fd, options);
     link(options, [fg]);
     expect(fg.generateMainFile(), expected);
   });
@@ -157,7 +157,7 @@
     FileDescriptorProto fd = buildFileDescriptor();
     var options = parseGenerationOptions(
         new CodeGeneratorRequest(), new CodeGeneratorResponse());
-    FileGenerator fg = new FileGenerator(fd);
+    FileGenerator fg = new FileGenerator(fd, options);
     link(options, [fg]);
     expect(fg.generateJsonFile(), expected);
   });
@@ -220,7 +220,7 @@
     var options = parseGenerationOptions(
         new CodeGeneratorRequest(), new CodeGeneratorResponse());
 
-    FileGenerator fg = new FileGenerator(fd);
+    FileGenerator fg = new FileGenerator(fd, options);
     link(options, [fg]);
     expect(fg.generateMainFile(), expected);
     expect(fg.generateEnumFile(), expectedEnum);
@@ -252,7 +252,7 @@
     var options = parseGenerationOptions(
         new CodeGeneratorRequest(), new CodeGeneratorResponse());
 
-    FileGenerator fg = new FileGenerator(fd);
+    FileGenerator fg = new FileGenerator(fd, options);
     link(options, [fg]);
     expect(fg.generateJsonFile(), expected);
   });
@@ -276,7 +276,7 @@
     var options = parseGenerationOptions(
         new CodeGeneratorRequest(), new CodeGeneratorResponse());
 
-    FileGenerator fg = new FileGenerator(fd);
+    FileGenerator fg = new FileGenerator(fd, options);
     link(options, [fg]);
 
     var writer = new IndentingWriter();
@@ -313,7 +313,7 @@
     var options = parseGenerationOptions(
         new CodeGeneratorRequest(), new CodeGeneratorResponse());
 
-    FileGenerator fg = new FileGenerator(fd);
+    FileGenerator fg = new FileGenerator(fd, options);
     link(options, [fg]);
 
     var writer = new IndentingWriter();
@@ -429,7 +429,7 @@
     var options = parseGenerationOptions(
         new CodeGeneratorRequest(), new CodeGeneratorResponse());
 
-    FileGenerator fg = new FileGenerator(fd);
+    FileGenerator fg = new FileGenerator(fd, options);
     link(options, [fg]);
 
     var writer = new IndentingWriter();
@@ -438,6 +438,236 @@
     expect(fg.generateServerFile(), expectedServer);
   });
 
+  test('FileGenerator does not output legacy service stubs if gRPC is selected',
+      () {
+    String expectedClient = r'''
+///
+//  Generated code. Do not modify.
+///
+// ignore_for_file: non_constant_identifier_names
+// ignore_for_file: library_prefixes
+library test;
+
+// ignore: UNUSED_SHOWN_NAME
+import 'dart:core' show int, bool, double, String, List, override;
+import 'dart:async';
+
+import 'package:protobuf/protobuf.dart';
+
+class Empty extends GeneratedMessage {
+  static final BuilderInfo _i = new BuilderInfo('Empty')
+    ..hasRequiredFields = false
+  ;
+
+  Empty() : super();
+  Empty.fromBuffer(List<int> i, [ExtensionRegistry r = ExtensionRegistry.EMPTY]) : super.fromBuffer(i, r);
+  Empty.fromJson(String i, [ExtensionRegistry r = ExtensionRegistry.EMPTY]) : super.fromJson(i, r);
+  Empty clone() => new Empty()..mergeFromMessage(this);
+  BuilderInfo get info_ => _i;
+  static Empty create() => new Empty();
+  static PbList<Empty> createRepeated() => new PbList<Empty>();
+  static Empty getDefault() {
+    if (_defaultInstance == null) _defaultInstance = new _ReadonlyEmpty();
+    return _defaultInstance;
+  }
+  static Empty _defaultInstance;
+  static void $checkItem(Empty v) {
+    if (v is !Empty) checkItemFailed(v, 'Empty');
+  }
+}
+
+class _ReadonlyEmpty extends Empty with ReadonlyMessageMixin {}
+
+''';
+
+    DescriptorProto empty = new DescriptorProto()..name = "Empty";
+
+    ServiceDescriptorProto sd = new ServiceDescriptorProto()
+      ..name = 'Test'
+      ..method.add(new MethodDescriptorProto()
+        ..name = 'Ping'
+        ..inputType = '.Empty'
+        ..outputType = '.Empty');
+
+    FileDescriptorProto fd = new FileDescriptorProto()
+      ..name = 'test'
+      ..messageType.add(empty)
+      ..service.add(sd);
+
+    var options = new GenerationOptions(useGrpc: true);
+
+    FileGenerator fg = new FileGenerator(fd, options);
+    link(options, [fg]);
+
+    var writer = new IndentingWriter();
+    fg.writeMainHeader(writer);
+    expect(fg.generateMainFile(), expectedClient);
+  });
+
+  test('FileGenerator outputs gRPC stubs if gRPC is selected', () {
+    final expectedGrpc = r'''
+///
+//  Generated code. Do not modify.
+///
+// ignore_for_file: non_constant_identifier_names
+// ignore_for_file: library_prefixes
+library test_pbgrpc;
+
+import 'dart:async';
+
+import 'package:grpc/grpc.dart';
+
+import 'test.pb.dart';
+export 'test.pb.dart';
+
+class TestClient {
+  final ClientChannel _channel;
+
+  static final _$unary = new ClientMethod<Input, Output>(
+      '/Test/Unary',
+      (Input value) => value.writeToBuffer(),
+      (List<int> value) => new Output.fromBuffer(value));
+  static final _$clientStreaming = new ClientMethod<Input, Output>(
+      '/Test/ClientStreaming',
+      (Input value) => value.writeToBuffer(),
+      (List<int> value) => new Output.fromBuffer(value));
+  static final _$serverStreaming = new ClientMethod<Input, Output>(
+      '/Test/ServerStreaming',
+      (Input value) => value.writeToBuffer(),
+      (List<int> value) => new Output.fromBuffer(value));
+  static final _$bidirectional = new ClientMethod<Input, Output>(
+      '/Test/Bidirectional',
+      (Input value) => value.writeToBuffer(),
+      (List<int> value) => new Output.fromBuffer(value));
+
+  TestClient(this._channel);
+
+  ResponseFuture<Output> unary(Input request) {
+    final call = new ClientCall(_channel, _$unary);
+    call.request
+      ..add(request)
+      ..close();
+    return new ResponseFuture(call);
+  }
+
+  ResponseFuture<Output> clientStreaming(Stream<Input> request) {
+    final call = new ClientCall(_channel, _$clientStreaming);
+    request.pipe(call.request);
+    return new ResponseFuture(call);
+  }
+
+  ResponseStream<Output> serverStreaming(Input request) {
+    final call = new ClientCall(_channel, _$serverStreaming);
+    call.request
+      ..add(request)
+      ..close();
+    return new ResponseStream(call);
+  }
+
+  ResponseStream<Output> bidirectional(Stream<Input> request) {
+    final call = new ClientCall(_channel, _$bidirectional);
+    request.pipe(call.request);
+    return new ResponseStream(call);
+  }
+}
+
+abstract class TestServiceBase extends Service {
+  String get $name => 'Test';
+
+  TestServiceBase() {
+    $addMethod(new ServiceMethod(
+        'Unary',
+        unary_Pre,
+        false,
+        false,
+        (List<int> value) => new Input.fromBuffer(value),
+        (Output value) => value.writeToBuffer()));
+    $addMethod(new ServiceMethod(
+        'ClientStreaming',
+        clientStreaming,
+        true,
+        false,
+        (List<int> value) => new Input.fromBuffer(value),
+        (Output value) => value.writeToBuffer()));
+    $addMethod(new ServiceMethod(
+        'ServerStreaming',
+        serverStreaming_Pre,
+        false,
+        true,
+        (List<int> value) => new Input.fromBuffer(value),
+        (Output value) => value.writeToBuffer()));
+    $addMethod(new ServiceMethod(
+        'Bidirectional',
+        bidirectional,
+        true,
+        true,
+        (List<int> value) => new Input.fromBuffer(value),
+        (Output value) => value.writeToBuffer()));
+  }
+
+  Future<Output> unary_Pre(ServiceCall call, Future<Input> request) async {
+    return unary(call, await request);
+  }
+
+  Stream<Output> serverStreaming_Pre(
+      ServiceCall call, Future<Input> request) async* {
+    yield* serverStreaming(call, await request);
+  }
+
+  Future<Output> unary(ServiceCall call, Input request);
+  Future<Output> clientStreaming(ServiceCall call, Stream<Input> request);
+  Stream<Output> serverStreaming(ServiceCall call, Input request);
+  Stream<Output> bidirectional(ServiceCall call, Stream<Input> request);
+}
+''';
+
+    final input = new DescriptorProto()..name = "Input";
+    final output = new DescriptorProto()..name = "Output";
+
+    final unary = new MethodDescriptorProto()
+      ..name = 'Unary'
+      ..inputType = '.Input'
+      ..outputType = '.Output'
+      ..clientStreaming = false
+      ..serverStreaming = false;
+    final clientStreaming = new MethodDescriptorProto()
+      ..name = 'ClientStreaming'
+      ..inputType = '.Input'
+      ..outputType = '.Output'
+      ..clientStreaming = true
+      ..serverStreaming = false;
+    final serverStreaming = new MethodDescriptorProto()
+      ..name = 'ServerStreaming'
+      ..inputType = '.Input'
+      ..outputType = '.Output'
+      ..clientStreaming = false
+      ..serverStreaming = true;
+    final bidirectional = new MethodDescriptorProto()
+      ..name = 'Bidirectional'
+      ..inputType = '.Input'
+      ..outputType = '.Output'
+      ..clientStreaming = true
+      ..serverStreaming = true;
+
+    ServiceDescriptorProto sd = new ServiceDescriptorProto()
+      ..name = 'Test'
+      ..method.addAll([unary, clientStreaming, serverStreaming, bidirectional]);
+
+    FileDescriptorProto fd = new FileDescriptorProto()
+      ..name = 'test'
+      ..messageType.addAll([input, output])
+      ..service.add(sd);
+
+    var options = new GenerationOptions(useGrpc: true);
+
+    FileGenerator fg = new FileGenerator(fd, options);
+    link(options, [fg]);
+
+    var writer = new IndentingWriter();
+    fg.writeMainHeader(writer);
+    expect(fg.generateGrpcFile(), expectedGrpc);
+  });
+
   test('FileGenerator generates imports for .pb.dart files', () {
     // NOTE: Below > 80 cols because it is matching generated code > 80 cols.
     String expected = r'''
@@ -614,8 +844,9 @@
     var response = new CodeGeneratorResponse();
     var options = parseGenerationOptions(request, response);
 
-    FileGenerator fg = new FileGenerator(fd);
-    link(options, [fg, new FileGenerator(fd1), new FileGenerator(fd2)]);
+    FileGenerator fg = new FileGenerator(fd, options);
+    link(options,
+        [fg, new FileGenerator(fd1, options), new FileGenerator(fd2, options)]);
     expect(fg.generateMainFile(), expected);
     expect(fg.generateJsonFile(), expectedJson);
   });
diff --git a/test/message_generator_test.dart b/test/message_generator_test.dart
index 80bf551..2b3c13a 100755
--- a/test/message_generator_test.dart
+++ b/test/message_generator_test.dart
@@ -127,7 +127,7 @@
     var options = parseGenerationOptions(
         new CodeGeneratorRequest(), new CodeGeneratorResponse());
 
-    FileGenerator fg = new FileGenerator(fd);
+    FileGenerator fg = new FileGenerator(fd, options);
     MessageGenerator mg = new MessageGenerator(md, fg, {}, null);
 
     var ctx = new GenerationContext(options);
diff --git a/test/service_generator_test.dart b/test/service_generator_test.dart
index b917228..fe5c82f 100644
--- a/test/service_generator_test.dart
+++ b/test/service_generator_test.dart
@@ -40,12 +40,13 @@
 
 ''';
 
+    var options = new GenerationOptions();
     var fd = buildFileDescriptor("testpkg", ["SomeRequest", "SomeReply"]);
     fd.service.add(buildServiceDescriptor());
-    var fg = new FileGenerator(fd);
+    var fg = new FileGenerator(fd, options);
 
     var fd2 = buildFileDescriptor("foo.bar", ["EmptyMessage", "AnotherReply"]);
-    var fg2 = new FileGenerator(fd2);
+    var fg2 = new FileGenerator(fd2, options);
 
     link(new GenerationOptions(), [fg, fg2]);