Initial support for generating client and server stubs for Dart.

R=sgjesse@google.com, skybrian@google.com
BUG=

Review URL: https://chromiumcodereview.appspot.com//1196293003
diff --git a/Makefile b/Makefile
index 5ec982d..45172c2 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,7 @@
 PLUGIN_SRC = \
 						 prepend.dart \
 						 bin/protoc_plugin.dart \
+						 lib/client_generator.dart \
 						 lib/code_generator.dart \
 						 lib/enum_generator.dart \
 						 lib/exceptions.dart \
@@ -12,6 +13,7 @@
 						 lib/output_config.dart \
 						 lib/protobuf_field.dart \
 						 lib/protoc.dart \
+						 lib/service_generator.dart \
 						 lib/src/descriptor.pb.dart \
 						 lib/src/plugin.pb.dart \
 						 lib/writer.dart
@@ -36,6 +38,7 @@
 						 package1 \
 						 package2 \
 						 package3 \
+						 service \
 						 toplevel_import \
 						 toplevel
 TEST_PROTO_DIR=$(OUTPUT_DIR)/protos
diff --git a/lib/client_generator.dart b/lib/client_generator.dart
new file mode 100644
index 0000000..bffbcae
--- /dev/null
+++ b/lib/client_generator.dart
@@ -0,0 +1,56 @@
+// Copyright (c) 2015, 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 ClientApiGenerator extends ProtobufContainer {
+  final String classname;
+  final String fqname;
+
+  final ProtobufContainer _parent;
+  final GenerationContext _context;
+  final ServiceDescriptorProto _descriptor;
+
+  ClientApiGenerator(ServiceDescriptorProto descriptor,
+      ProtobufContainer parent, this._context)
+      : _descriptor = descriptor,
+        _parent = parent,
+        classname = descriptor.name,
+        fqname = (parent == null || parent.fqname == null)
+            ? descriptor.name
+            : (parent.fqname == '.'
+                ? '.${descriptor.name}'
+                : '${parent.fqname}.${descriptor.name}') {
+    _context.register(this);
+  }
+
+  String get package => _parent.package;
+
+  String _shortType(String typename) {
+    return typename.substring(typename.lastIndexOf('.')+1);
+  }
+
+  void generate(IndentingWriter out) {
+    out.addBlock('class ${classname}Api {', '}', () {
+      out.println('RpcClient _client;');
+      out.println('${classname}Api(this._client);');
+      out.println();
+      for (MethodDescriptorProto m in _descriptor.method) {
+        // lowercase first letter in method name.
+        var methodName =
+            m.name.substring(0,1).toLowerCase() + m.name.substring(1);
+        out.addBlock('Future<${_shortType(m.outputType)}> $methodName('
+            'ClientContext ctx, ${_shortType(m.inputType)} request) '
+            'async {', '}', () {
+          out.println('var emptyResponse = new ${_shortType(m.outputType)}();');
+          out.println('var result = await _client.invoke(ctx, '
+              '\'${_descriptor.name}\', \'${m.name}\', '
+              'request, emptyResponse);');
+          out.println('return result;');
+        });
+      }
+    });
+    out.println();
+  }
+}
diff --git a/lib/file_generator.dart b/lib/file_generator.dart
index c7f85d2..58fa8a3 100644
--- a/lib/file_generator.dart
+++ b/lib/file_generator.dart
@@ -28,6 +28,8 @@
   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>[];
 
   FileGenerator(this._fileDescriptor, this._parent, this._context) {
     _context.register(this);
@@ -46,6 +48,10 @@
       extensionGenerators.add(
           new ExtensionGenerator(extension, this, _context));
     }
+    for (ServiceDescriptorProto service in _fileDescriptor.service) {
+      serviceGenerators.add(new ServiceGenerator(service, this, _context));
+      clientApiGenerators.add(new ClientApiGenerator(service, this, _context));
+    }
   }
 
   String get package => _fileDescriptor.package;
@@ -90,12 +96,17 @@
 
     String libraryName = _generateLibraryName(filePath);
 
+    // Print header and imports. We only add the dart:async import if there
+    // are services in the FileDescriptorProto.
     out.println(
       '///\n'
       '//  Generated code. Do not modify.\n'
       '///\n'
-      'library $libraryName;\n'
-      '\n'
+      'library $libraryName;\n');
+    if (_fileDescriptor.service.isNotEmpty) {
+      out.println("import 'dart:async';\n");
+    }
+    out.println(
       "import 'package:fixnum/fixnum.dart';\n"
       "import 'package:protobuf/protobuf.dart';"
     );
@@ -158,6 +169,13 @@
         out.println('}');
       });
     }
+
+    for (ClientApiGenerator c in clientApiGenerators) {
+      c.generate(out);
+    }
+    for (ServiceGenerator s in serviceGenerators) {
+      s.generate(out);
+    }
   }
 
   /// Returns a map from import names to the Dart symbols to be imported.
diff --git a/lib/indenting_writer.dart b/lib/indenting_writer.dart
index 1b7dfca..e22a935 100644
--- a/lib/indenting_writer.dart
+++ b/lib/indenting_writer.dart
@@ -12,13 +12,17 @@
 
   IndentingWriter(this._indentSequence, this._writer);
 
-  void addBlock(String start, String end, void body()) {
+  void addBlock(String start, String end, void body(), {endWithNewline: true}) {
     println(start);
     var oldIndent = _currentIndent;
     _currentIndent = '$_currentIndent$_indentSequence';
     body();
     _currentIndent = oldIndent;
-    println(end);
+    if (endWithNewline) {
+      println(end);
+    } else {
+      print(end);
+    }
   }
 
   void print(String stringToPrint) {
diff --git a/lib/protoc.dart b/lib/protoc.dart
index 7289bac..c5160c6 100644
--- a/lib/protoc.dart
+++ b/lib/protoc.dart
@@ -11,6 +11,7 @@
 import 'src/plugin.pb.dart';
 import 'src/dart_options.pb.dart';
 
+part 'client_generator.dart';
 part 'code_generator.dart';
 part 'enum_generator.dart';
 part 'exceptions.dart';
@@ -21,4 +22,5 @@
 part 'options.dart';
 part 'output_config.dart';
 part 'protobuf_field.dart';
+part 'service_generator.dart';
 part 'writer.dart';
diff --git a/lib/service_generator.dart b/lib/service_generator.dart
new file mode 100644
index 0000000..22e1d8b
--- /dev/null
+++ b/lib/service_generator.dart
@@ -0,0 +1,95 @@
+// Copyright (c) 2015, 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 ServiceGenerator extends ProtobufContainer {
+  final String classname;
+  final String fqname;
+
+  final ProtobufContainer _parent;
+  final GenerationContext _context;
+  final ServiceDescriptorProto _descriptor;
+  final List<MethodDescriptorProto> _methodDescriptors;
+
+  ServiceGenerator(ServiceDescriptorProto descriptor, ProtobufContainer parent,
+      this._context)
+      : _descriptor = descriptor,
+        _parent = parent,
+        classname = descriptor.name,
+        fqname = _qualifiedName(descriptor, parent),
+        _methodDescriptors = descriptor.method {
+    _context.register(this);
+  }
+
+  static String  _qualifiedName(ServiceDescriptorProto descriptor,
+      ProtobufContainer parent) {
+    if (parent == null || parent.fqname == null) {
+      return descriptor.name;
+    } else if (parent.fqname == '.') {
+      return '.${descriptor.name}';
+    } else {
+      return '${parent.fqname}.${descriptor.name}';
+    }
+  }
+
+  static String _serviceClassName(descriptor) {
+    if (descriptor.name.endsWith("Service")) {
+      return descriptor.name + "Base"; // avoid: ServiceServiceBase
+    } else {
+      return descriptor.name + "ServiceBase";
+    }
+  }
+
+  String get package => _parent.package;
+
+  String _shortType(String typename) {
+    return typename.substring(typename.lastIndexOf('.') + 1);
+  }
+
+  String _methodName(String name) =>
+      name.substring(0,1).toLowerCase() + name.substring(1);
+
+  void generate(IndentingWriter out) {
+    out.addBlock(
+        'abstract class ${_serviceClassName(_descriptor)} extends '
+        'GeneratedService {',
+        '}', () {
+      for (MethodDescriptorProto m in _methodDescriptors) {
+        var methodName = _methodName(m.name);
+        out.println('Future<${_shortType(m.outputType)}> $methodName('
+            'ServerContext ctx, ${_shortType(m.inputType)} request);');
+      }
+      out.println();
+
+      out.addBlock(
+          'GeneratedMessage createRequest(String method) {', '}', () {
+        out.addBlock("switch (method) {", "}", () {
+          for (MethodDescriptorProto m in _methodDescriptors) {
+            out.println(
+              "case '${m.name}': return new ${_shortType(m.inputType)}();");
+          }
+          out.println("default: "
+            "throw new ArgumentError('Unknown method: \$method');");
+        });
+      });
+      out.println();
+
+      out.addBlock(
+          'Future<GeneratedMessage> handleCall(ServerContext ctx, '
+          'String method, GeneratedMessage request) async {', '}', () {
+        out.addBlock("switch (method) {", "}", () {
+          for (MethodDescriptorProto m in _methodDescriptors) {
+            var methodName = _methodName(m.name);
+            out.println(
+              "case '${m.name}': return await $methodName(ctx, request);");
+          }
+          out.println("default: "
+            "throw new ArgumentError('Unknown method: \$method');");
+        });
+      });
+    });
+    out.println();
+  }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 79ce039..8eeffff 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,12 +1,12 @@
 name: protoc_plugin
-version: 0.3.9
+version: 0.3.10
 author: Dart Team <misc@dartlang.org>
 description: Protoc compiler plugin to generate Dart code
 homepage: https://github.com/dart-lang/dart-protoc-plugin
 environment:
   sdk: '>=1.0.0 <2.0.0'
 dependencies:
-  protobuf: '>=0.3.9 <0.4.0'
+  protobuf: '>=0.3.10 <0.4.0'
   path: '>=1.0.0 <2.0.0'
 dev_dependencies:
   unittest: '>=0.9.0 <0.11.0'
diff --git a/test/all_tests.dart b/test/all_tests.dart
index 802da60..e362197 100755
--- a/test/all_tests.dart
+++ b/test/all_tests.dart
@@ -5,36 +5,42 @@
 
 library protoc_plugin_all_tests;
 
+import 'client_generator_test.dart' as cgt;
 import 'enum_generator_test.dart' as egt;
+import 'file_generator_test.dart' as fgt;
 import 'generated_message_test.dart' as gmt;
+import 'hash_code_test.dart' as hct;
 import 'indenting_writer_test.dart' as iwt;
 import 'json_test.dart' as jt;
 import 'map_test.dart' as map_test;
 import 'message_generator_test.dart' as mgt;
-import 'file_generator_test.dart' as fgt;
 import 'message_test.dart' as mt;
 import 'protoc_options_test.dart' as pot;
+import 'reserved_names_test.dart' as rnt;
+import 'service_test.dart' as st;
+import 'service_generator_test.dart' as sgt;
 import 'unknown_field_set_test.dart' as ufst;
 import 'validate_fail_test.dart' as vft;
 import 'wire_format_test.dart' as wft;
-import 'reserved_names_test.dart' as rnt;
-import 'hash_code_test.dart' as hct;
 import 'package:unittest/compact_vm_config.dart';
 
 void main() {
   useCompactVMConfiguration();
+  cgt.main();
   egt.main();
+  fgt.main();
   gmt.main();
+  hct.main();
   iwt.main();
   jt.main();
   map_test.main();
   mgt.main();
-  fgt.main();
   mt.main();
   pot.main();
+  rnt.main();
+  st.main();
+  sgt.main();
   ufst.main();
   vft.main();
   wft.main();
-  rnt.main();
-  hct.main();
 }
diff --git a/test/client_generator_test.dart b/test/client_generator_test.dart
new file mode 100644
index 0000000..db5fa38
--- /dev/null
+++ b/test/client_generator_test.dart
@@ -0,0 +1,63 @@
+#!/usr/bin/env dart
+// Copyright (c) 2015, 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 client_generator_test;
+
+import 'package:protoc_plugin/src/descriptor.pb.dart';
+import 'package:protoc_plugin/src/plugin.pb.dart';
+import 'package:protoc_plugin/protoc.dart';
+import 'package:unittest/unittest.dart';
+
+ServiceDescriptorProto buildServiceDescriptor() {
+  ServiceDescriptorProto sd = new ServiceDescriptorProto()
+    ..name = 'Test'
+    ..method.addAll([
+      new MethodDescriptorProto()
+        ..name = 'AMethod'
+        ..inputType = 'SomeRequest'
+        ..outputType = 'SomeReply',
+      new MethodDescriptorProto()
+        ..name = 'AnotherMethod'
+        ..inputType = '.foo.bar.EmptyMessage'
+        ..outputType = '.foo.bar.AnotherReply',
+    ]);
+  return sd;
+}
+
+void main() {
+  test('testClientGenerator', () {
+    // NOTE: Below > 80 cols because it is matching generated code > 80 cols.
+    String expected = r'''
+class TestApi {
+  RpcClient _client;
+  TestApi(this._client);
+
+  Future<SomeReply> aMethod(ClientContext ctx, SomeRequest request) async {
+    var emptyResponse = new SomeReply();
+    var result = await _client.invoke(ctx, 'Test', 'AMethod', request, emptyResponse);
+    return result;
+  }
+  Future<AnotherReply> anotherMethod(ClientContext ctx, EmptyMessage request) async {
+    var emptyResponse = new AnotherReply();
+    var result = await _client.invoke(ctx, 'Test', 'AnotherMethod', request, emptyResponse);
+    return result;
+  }
+}
+
+''';
+    var options = parseGenerationOptions(
+        new CodeGeneratorRequest(), new CodeGeneratorResponse());
+    var context =
+        new GenerationContext(options, new DefaultOutputConfiguration());
+    var fd = new FileDescriptorProto();
+    var fg = new FileGenerator(fd, null, context);
+    ServiceDescriptorProto sd = buildServiceDescriptor();
+    MemoryWriter buffer = new MemoryWriter();
+    IndentingWriter writer = new IndentingWriter('  ', buffer);
+    ClientApiGenerator cag = new ClientApiGenerator(sd, fg, context);
+    cag.generate(writer);
+    expect(buffer.toString(), expected);
+  });
+}
diff --git a/test/protos/service.proto b/test/protos/service.proto
new file mode 100644
index 0000000..5616552
--- /dev/null
+++ b/test/protos/service.proto
@@ -0,0 +1,13 @@
+syntax = "proto2";
+
+message SearchRequest {
+  optional string query = 1;
+}
+
+message SearchResponse {
+  repeated string result = 1;
+}
+
+service SearchService {
+	rpc Search (SearchRequest) returns (SearchResponse);
+}
diff --git a/test/service_generator_test.dart b/test/service_generator_test.dart
new file mode 100644
index 0000000..67c735e
--- /dev/null
+++ b/test/service_generator_test.dart
@@ -0,0 +1,68 @@
+#!/usr/bin/env dart
+// Copyright (c) 2015, 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 service_generator_test;
+
+import 'package:protoc_plugin/src/descriptor.pb.dart';
+import 'package:protoc_plugin/src/plugin.pb.dart';
+import 'package:protoc_plugin/protoc.dart';
+import 'package:unittest/unittest.dart';
+
+ServiceDescriptorProto buildServiceDescriptor() {
+  ServiceDescriptorProto sd = new ServiceDescriptorProto()
+    ..name = 'Test'
+    ..method.addAll([
+      new MethodDescriptorProto()
+        ..name = 'AMethod'
+        ..inputType = 'SomeRequest'
+        ..outputType = 'SomeReply',
+      new MethodDescriptorProto()
+        ..name = 'AnotherMethod'
+        ..inputType = '.foo.bar.EmptyMessage'
+        ..outputType = '.foo.bar.AnotherReply',
+    ]);
+  return sd;
+}
+
+void main() {
+  test('testServiceGenerator', () {
+    // NOTE: Below > 80 cols because it is matching generated code > 80 cols.
+    String expected = r'''
+abstract class TestServiceBase extends GeneratedService {
+  Future<SomeReply> aMethod(ServerContext ctx, SomeRequest request);
+  Future<AnotherReply> anotherMethod(ServerContext ctx, EmptyMessage request);
+
+  GeneratedMessage createRequest(String method) {
+    switch (method) {
+      case 'AMethod': return new SomeRequest();
+      case 'AnotherMethod': return new EmptyMessage();
+      default: throw new ArgumentError('Unknown method: $method');
+    }
+  }
+
+  Future<GeneratedMessage> handleCall(ServerContext ctx, String method, GeneratedMessage request) async {
+    switch (method) {
+      case 'AMethod': return await aMethod(ctx, request);
+      case 'AnotherMethod': return await anotherMethod(ctx, request);
+      default: throw new ArgumentError('Unknown method: $method');
+    }
+  }
+}
+
+''';
+    var options = parseGenerationOptions(
+        new CodeGeneratorRequest(), new CodeGeneratorResponse());
+    var context =
+        new GenerationContext(options, new DefaultOutputConfiguration());
+    var fd = new FileDescriptorProto();
+    var fg = new FileGenerator(fd, null, context);
+    ServiceDescriptorProto sd = buildServiceDescriptor();
+    MemoryWriter buffer = new MemoryWriter();
+    IndentingWriter writer = new IndentingWriter('  ', buffer);
+    var sg = new ServiceGenerator(sd, fg, context);
+    sg.generate(writer);
+    expect(buffer.toString(), expected);
+  });
+}
diff --git a/test/service_test.dart b/test/service_test.dart
new file mode 100644
index 0000000..5d0f806
--- /dev/null
+++ b/test/service_test.dart
@@ -0,0 +1,65 @@
+library service_test;
+
+import 'dart:async' show Future;
+
+import 'package:protobuf/protobuf.dart';
+import 'package:unittest/unittest.dart';
+
+import '../out/protos/service.pb.dart' as pb;
+
+class SearchService extends pb.SearchServiceBase {
+    Future<pb.SearchResponse> search(
+        ServerContext ctx, pb.SearchRequest request) async {
+      var out = new pb.SearchResponse();
+      if (request.query == 'hello' || request.query == 'world') {
+        out.result.add('hello, world!');
+      }
+      return out;
+    }
+}
+
+class FakeJsonServer {
+  final GeneratedService searchService;
+  FakeJsonServer(this.searchService);
+
+  Future<String> messageHandler(
+      String serviceName, String methodName, String requestJson) async {
+    if (serviceName == 'SearchService') {
+      GeneratedMessage request = searchService.createRequest(methodName);
+      request.mergeFromJson(requestJson);
+      var ctx = new ServerContext();
+      var reply = await searchService.handleCall(ctx, methodName, request);
+      return reply.writeToJson();
+    } else {
+      throw 'unknown service: $serviceName';
+    }
+  }
+}
+
+class FakeJsonClient implements RpcClient {
+  final FakeJsonServer server;
+  FakeJsonClient(this.server);
+
+  Future<GeneratedMessage> invoke(
+    ClientContext ctx, String serviceName, String methodName,
+    GeneratedMessage request, GeneratedMessage response) async {
+
+    String requestJson = request.writeToJson();
+    String replyJson =
+        await server.messageHandler(serviceName, methodName, requestJson);
+    response.mergeFromJson(replyJson);
+    return response;
+  }
+}
+
+void main() {
+  var server = new FakeJsonServer(new SearchService());
+  var api = new pb.SearchServiceApi(new FakeJsonClient(server));
+
+  test('end to end RPC using JSON', () async {
+    var request = new pb.SearchRequest()
+      ..query = "hello";
+    var reply = await api.search(new ClientContext(), request);
+    expect(reply.result, ["hello, world!"]);
+  });
+}