Move server-side service stubs to .pbserver.dart

This should break all dependencies on .pbjson.dart files for client-side
code, reducing the number of files loaded into Dartium.

However, it is a breaking API change: server-side code needs to be modified
to import .pbserver.dart files as well as .pb.dart files.

BUG=
R=sgjesse@google.com

Review URL: https://chromiumcodereview.appspot.com//2013343002 .
diff --git a/Makefile b/Makefile
index 03df907..60575c4 100644
--- a/Makefile
+++ b/Makefile
@@ -33,9 +33,14 @@
 	toplevel_import \
 	toplevel
 TEST_PROTO_DIR=$(OUTPUT_DIR)/protos
-TEST_PROTO_LIBS=$(foreach proto, $(TEST_PROTO_LIST), $(TEST_PROTO_DIR)/$(proto).pb.dart $(TEST_PROTO_DIR)/$(proto).pbjson.dart)
+TEST_PROTO_LIBS=$(foreach f, $(TEST_PROTO_LIST), \
+  $(TEST_PROTO_DIR)/$(f).pb.dart \
+	$(TEST_PROTO_DIR)/$(f).pbenum.dart \
+	$(TEST_PROTO_DIR)/$(f).pbserver.dart \
+	$(TEST_PROTO_DIR)/$(f).pbjson.dart)
 TEST_PROTO_SRC_DIR=test/protos
-TEST_PROTO_SRCS=$(foreach proto, $(TEST_PROTO_LIST), $(TEST_PROTO_SRC_DIR)/$(proto).proto)
+TEST_PROTO_SRCS=$(foreach proto, $(TEST_PROTO_LIST), \
+  $(TEST_PROTO_SRC_DIR)/$(proto).proto)
 
 PREGENERATED_SRCS=lib/descriptor.proto lib/plugin.proto
 
diff --git a/lib/file_generator.dart b/lib/file_generator.dart
index 148e2a6..65cba47 100644
--- a/lib/file_generator.dart
+++ b/lib/file_generator.dart
@@ -97,23 +97,6 @@
     return '${s[0].toUpperCase()}${s.substring(1)}';
   }
 
-  /// Returns the library name at the top of the .pb.dart file.
-  ///
-  /// (This should be unique to avoid warnings about duplicate Dart libraries.)
-  String _generateLibraryName(Uri protoFilePath) {
-    var libraryName =
-        _fileNameWithoutExtension(protoFilePath).replaceAll('-', '_');
-
-    if (_fileDescriptor.package != '') {
-      // Two .protos can be in the same proto package.
-      // It isn't unique enough to use as a Dart library name.
-      // But we can prepend it.
-      return _fileDescriptor.package + "_" + libraryName;
-    }
-
-    return libraryName;
-  }
-
   /// Generates all the Dart files for this .proto file.
   List<CodeGeneratorResponse_File> generateFiles(OutputConfiguration config) {
     if (!_linked) throw new StateError("not linked");
@@ -129,6 +112,7 @@
     return [
       makeFile(".pb.dart", generateMainFile(config)),
       makeFile(".pbenum.dart", generateEnumFile(config)),
+      makeFile(".pbserver.dart", generateServerFile(config)),
       makeFile(".pbjson.dart", generateJsonFile(config)),
     ];
   }
@@ -167,21 +151,13 @@
     for (ClientApiGenerator c in clientApiGenerators) {
       c.generate(out);
     }
-    for (ServiceGenerator s in serviceGenerators) {
-      s.generate(out);
-    }
-
     return out.toString();
   }
 
   /// Writes the header and imports for the .pb.dart file.
   void writeMainHeader(IndentingWriter out,
       [OutputConfiguration config = const DefaultOutputConfiguration()]) {
-    String libraryName = _generateLibraryName(protoFileUri);
-    out.println('///\n'
-        '//  Generated code. Do not modify.\n'
-        '///\n'
-        'library $libraryName;\n');
+    _writeLibraryHeading(out);
 
     // We only add the dart:async import if there are services in the
     // FileDescriptorProto.
@@ -212,34 +188,16 @@
     var enumImports = new Set<FileGenerator>.identity();
     _findProtosToImport(imports, enumImports);
 
-    void writeImport(FileGenerator target, String extension) {
-      Uri resolvedImport =
-          config.resolveImport(target.protoFileUri, protoFileUri, extension);
-      out.print("import '$resolvedImport'");
-      if (package != target.package && target.package.isNotEmpty) {
-        out.print(' as ${target.packageImportPrefix}');
-      }
-      out.println(';');
-    }
-
     for (var target in imports) {
-      writeImport(target, ".pb.dart");
+      _writeImport(out, config, target, ".pb.dart");
     }
     if (imports.isNotEmpty) out.println();
 
     for (var target in enumImports) {
-      writeImport(target, ".pbenum.dart");
+      _writeImport(out, config, target, ".pbenum.dart");
     }
     if (enumImports.isNotEmpty) out.println();
 
-    // Services also depend on the json imports.
-    if (serviceGenerators.isNotEmpty) {
-      Uri resolvedImport =
-          config.resolveImport(protoFileUri, protoFileUri, ".pbjson.dart");
-      out.println("import '$resolvedImport';");
-      out.println();
-    }
-
     // Export enums in main file for backward compatibility.
     if (enumCount > 0) {
       Uri resolvedImport =
@@ -262,8 +220,7 @@
   bool get _needsProtobufImport =>
       messageGenerators.isNotEmpty ||
       extensionGenerators.isNotEmpty ||
-      clientApiGenerators.isNotEmpty ||
-      serviceGenerators.isNotEmpty;
+      clientApiGenerators.isNotEmpty;
 
   /// Returns the generator for each .pb.dart file we need to import.
   void _findProtosToImport(
@@ -274,6 +231,7 @@
     for (var x in extensionGenerators) {
       x.addImportsTo(imports, enumImports);
     }
+    // Add imports needed for client-side services.
     for (var x in serviceGenerators) {
       x.addImportsTo(imports);
     }
@@ -310,22 +268,9 @@
   String generateEnumFile(
       [OutputConfiguration config = const DefaultOutputConfiguration()]) {
     if (!_linked) throw new StateError("not linked");
-    Uri filePath = new Uri.file(_fileDescriptor.name);
-    if (filePath.isAbsolute) {
-      // protoc should never generate a file descriptor with an absolute path.
-      throw "FAILURE: File with an absolute path is not supported";
-    }
 
-    var baseLibraryName = _generateLibraryName(filePath);
-    var libraryName = baseLibraryName + "_pbenum";
     var out = new IndentingWriter();
-    out.print('''
-///
-//  Generated code. Do not modify.
-///
-library $libraryName;
-
-''');
+    _writeLibraryHeading(out, "pbenum");
 
     if (enumCount > 0) {
       out.println("import 'package:protobuf/protobuf.dart';");
@@ -352,39 +297,61 @@
     return count;
   }
 
+  /// Returns the contents of the .pbserver.dart file for this .proto file.
+  String generateServerFile(
+      [OutputConfiguration config = const DefaultOutputConfiguration()]) {
+    if (!_linked) throw new StateError("not linked");
+    var out = new IndentingWriter();
+    _writeLibraryHeading(out, "pbserver");
+
+    if (serviceGenerators.isNotEmpty) {
+      out.println('''
+import 'dart:async';
+
+import 'package:protobuf/protobuf.dart';
+''');
+    }
+
+    // Import .pb.dart files needed for requests and responses.
+    var imports = new Set<FileGenerator>();
+    for (var x in serviceGenerators) {
+      x.addImportsTo(imports);
+    }
+    for (var target in imports) {
+      _writeImport(out, config, target, ".pb.dart");
+    }
+
+    // Import .pbjson.dart file needed for $json and $messageJson.
+    if (serviceGenerators.isNotEmpty) {
+      _writeImport(out, config, this, ".pbjson.dart");
+      out.println();
+    }
+
+    Uri resolvedImport =
+        config.resolveImport(protoFileUri, protoFileUri, ".pb.dart");
+    out.println("export '$resolvedImport';");
+    out.println();
+
+    for (ServiceGenerator s in serviceGenerators) {
+      s.generate(out);
+    }
+
+    return out.toString();
+  }
+
   /// Returns the contents of the .pbjson.dart file for this .proto file.
   String generateJsonFile(
       [OutputConfiguration config = const DefaultOutputConfiguration()]) {
     if (!_linked) throw new StateError("not linked");
-    Uri filePath = new Uri.file(_fileDescriptor.name);
-    if (filePath.isAbsolute) {
-      // protoc should never generate a file descriptor with an absolute path.
-      throw "FAILURE: File with an absolute path is not supported";
-    }
-
-    var baseLibraryName = _generateLibraryName(filePath);
-    var libraryName = baseLibraryName + "_pbjson";
     var out = new IndentingWriter();
-    out.print('''
-///
-//  Generated code. Do not modify.
-///
-library $libraryName;
-
-''');
+    _writeLibraryHeading(out, "pbjson");
 
     // Import the .pbjson.dart files we depend on.
-    var importList = _findJsonProtosToImport();
-    for (var imported in importList) {
-      Uri resolvedImport = config.resolveImport(
-          imported.protoFileUri, protoFileUri, ".pbjson.dart");
-      out.print("import '$resolvedImport'");
-      if (package != imported.package && imported.package.isNotEmpty) {
-        out.print(' as ${imported.packageImportPrefix}');
-      }
-      out.println(';');
+    var imports = _findJsonProtosToImport();
+    for (var target in imports) {
+      _writeImport(out, config, target, ".pbjson.dart");
     }
-    if (importList.isNotEmpty) out.println();
+    if (imports.isNotEmpty) out.println();
 
     for (var e in enumGenerators) {
       e.generateConstants(out);
@@ -415,4 +382,43 @@
     imports.remove(this); // Don't need to import self.
     return imports;
   }
+
+  /// Writes the library name at the top of the dart file.
+  ///
+  /// (This should be unique to avoid warnings about duplicate Dart libraries.)
+  void _writeLibraryHeading(IndentingWriter out, [String extension]) {
+    Uri filePath = new Uri.file(_fileDescriptor.name);
+    if (filePath.isAbsolute) {
+      // protoc should never generate a file descriptor with an absolute path.
+      throw "FAILURE: File with an absolute path is not supported";
+    }
+
+    var libraryName = _fileNameWithoutExtension(filePath).replaceAll('-', '_');
+    if (extension != null) libraryName += "_$extension";
+    if (_fileDescriptor.package != '') {
+      // Two .protos can be in the same proto package.
+      // It isn't unique enough to use as a Dart library name.
+      // But we can prepend it.
+      libraryName = _fileDescriptor.package + "_" + libraryName;
+    }
+    out.println('''
+///
+//  Generated code. Do not modify.
+///
+library $libraryName;
+''');
+  }
+
+  /// Writes an import of a .dart file corresponding to a .proto file.
+  /// (Possibly the same .proto file.)
+  void _writeImport(IndentingWriter out, OutputConfiguration config,
+      FileGenerator target, String extension) {
+    Uri resolvedImport =
+        config.resolveImport(target.protoFileUri, protoFileUri, extension);
+    out.print("import '$resolvedImport'");
+    if (package != target.package && target.package.isNotEmpty) {
+      out.print(' as ${target.packageImportPrefix}');
+    }
+    out.println(';');
+  }
 }
diff --git a/pubspec.yaml b/pubspec.yaml
index f913957..9c52c02 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: protoc_plugin
-version: 0.5.2
+version: 0.6
 author: Dart Team <misc@dartlang.org>
 description: Protoc compiler plugin to generate Dart code
 homepage: https://github.com/dart-lang/dart-protoc-plugin
diff --git a/test/file_generator_test.dart b/test/file_generator_test.dart
index bac4d15..380647d 100644
--- a/test/file_generator_test.dart
+++ b/test/file_generator_test.dart
@@ -297,8 +297,8 @@
     expect(writer.toString(), expected);
   });
 
-  test('FileGenerator outputs extra imports for a service', () {
-    String expected = r'''
+  test('FileGenerator outputs files for a service', () {
+    String expectedClient = r'''
 ///
 //  Generated code. Do not modify.
 ///
@@ -308,12 +308,93 @@
 
 import 'package:protobuf/protobuf.dart';
 
-import 'test.pbjson.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 {}
+
+class TestApi {
+  RpcClient _client;
+  TestApi(this._client);
+
+  Future<Empty> ping(ClientContext ctx, Empty request) {
+    var emptyResponse = new Empty();
+    return _client.invoke(ctx, 'Test', 'Ping', request, emptyResponse);
+  }
+}
 
 ''';
+
+    String expectedServer = r'''
+///
+//  Generated code. Do not modify.
+///
+library test_pbserver;
+
+import 'dart:async';
+
+import 'package:protobuf/protobuf.dart';
+
+import 'test.pb.dart';
+import 'test.pbjson.dart';
+
+export 'test.pb.dart';
+
+abstract class TestServiceBase extends GeneratedService {
+  Future<Empty> ping(ServerContext ctx, Empty request);
+
+  GeneratedMessage createRequest(String method) {
+    switch (method) {
+      case 'Ping': return new Empty();
+      default: throw new ArgumentError('Unknown method: $method');
+    }
+  }
+
+  Future<GeneratedMessage> handleCall(ServerContext ctx, String method, GeneratedMessage request) {
+    switch (method) {
+      case 'Ping': return ping(ctx, request);
+      default: throw new ArgumentError('Unknown method: $method');
+    }
+  }
+
+  Map<String, dynamic> get $json => Test$json;
+  Map<String, dynamic> get $messageJson => Test$messageJson;
+}
+
+''';
+
+    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'
-      ..service.add(new ServiceDescriptorProto());
+      ..messageType.add(empty)
+      ..service.add(sd);
 
     var options = parseGenerationOptions(
         new CodeGeneratorRequest(), new CodeGeneratorResponse());
@@ -323,7 +404,8 @@
 
     var writer = new IndentingWriter();
     fg.writeMainHeader(writer);
-    expect(writer.toString(), expected);
+    expect(fg.generateMainFile(), expectedClient);
+    expect(fg.generateServerFile(), expectedServer);
   });
 
   test('FileGenerator handles field_name options', () {
diff --git a/test/service_test.dart b/test/service_test.dart
index 2628a1f..a4aedd7 100644
--- a/test/service_test.dart
+++ b/test/service_test.dart
@@ -5,7 +5,7 @@
 import 'package:protobuf/protobuf.dart';
 import 'package:test/test.dart';
 
-import '../out/protos/service.pb.dart' as pb;
+import '../out/protos/service.pbserver.dart' as pb;
 import '../out/protos/service2.pb.dart' as pb2;
 import '../out/protos/service3.pb.dart' as pb3;