Merge pull request #5288 from murgatroid99/node_proto_options

Add options to Node library to modify ProtoBuf behavior
diff --git a/src/node/index.js b/src/node/index.js
index 7eacdc6..1c19772 100644
--- a/src/node/index.js
+++ b/src/node/index.js
@@ -56,17 +56,18 @@
 /**
  * Load a gRPC object from an existing ProtoBuf.Reflect object.
  * @param {ProtoBuf.Reflect.Namespace} value The ProtoBuf object to load.
+ * @param {Object=} options Options to apply to the loaded object
  * @return {Object<string, *>} The resulting gRPC object
  */
-exports.loadObject = function loadObject(value) {
+exports.loadObject = function loadObject(value, options) {
   var result = {};
   if (value.className === 'Namespace') {
     _.each(value.children, function(child) {
-      result[child.name] = loadObject(child);
+      result[child.name] = loadObject(child, options);
     });
     return result;
   } else if (value.className === 'Service') {
-    return client.makeProtobufClientConstructor(value);
+    return client.makeProtobufClientConstructor(value, options);
   } else if (value.className === 'Message' || value.className === 'Enum') {
     return value.build();
   } else {
@@ -77,28 +78,45 @@
 var loadObject = exports.loadObject;
 
 /**
- * Load a gRPC object from a .proto file.
- * @param {string} filename The file to load
+ * Load a gRPC object from a .proto file. The options object can provide the
+ * following options:
+ * - convertFieldsToCamelCase: Loads this file with that option on protobuf.js
+ *   set as specified. See
+ *   https://github.com/dcodeIO/protobuf.js/wiki/Advanced-options for details
+ * - binaryAsBase64: deserialize bytes values as base64 strings instead of
+ *   Buffers. Defaults to false
+ * - longsAsStrings: deserialize long values as strings instead of objects.
+ *   Defaults to true
+ * @param {string|{root: string, file: string}} filename The file to load
  * @param {string=} format The file format to expect. Must be either 'proto' or
  *     'json'. Defaults to 'proto'
+ * @param {Object=} options Options to apply to the loaded file
  * @return {Object<string, *>} The resulting gRPC object
  */
-exports.load = function load(filename, format) {
+exports.load = function load(filename, format, options) {
   if (!format) {
     format = 'proto';
   }
-  var builder;
-  switch(format) {
-    case 'proto':
-    builder = ProtoBuf.loadProtoFile(filename);
-    break;
-    case 'json':
-    builder = ProtoBuf.loadJsonFile(filename);
-    break;
-    default:
-    throw new Error('Unrecognized format "' + format + '"');
+  var convertFieldsToCamelCaseOriginal = ProtoBuf.convertFieldsToCamelCase;
+  if(options && options.hasOwnProperty('convertFieldsToCamelCase')) {
+    ProtoBuf.convertFieldsToCamelCase = options.convertFieldsToCamelCase;
   }
-  return loadObject(builder.ns);
+  var builder;
+  try {
+    switch(format) {
+      case 'proto':
+      builder = ProtoBuf.loadProtoFile(filename);
+      break;
+      case 'json':
+      builder = ProtoBuf.loadJsonFile(filename);
+      break;
+      default:
+      throw new Error('Unrecognized format "' + format + '"');
+    }
+  } finally {
+    ProtoBuf.convertFieldsToCamelCase = convertFieldsToCamelCaseOriginal;
+  }
+  return loadObject(builder.ns, options);
 };
 
 /**
diff --git a/src/node/src/client.js b/src/node/src/client.js
index b5247a6..c02c447 100644
--- a/src/node/src/client.js
+++ b/src/node/src/client.js
@@ -698,13 +698,16 @@
  * Creates a constructor for clients for the given service
  * @param {ProtoBuf.Reflect.Service} service The service to generate a client
  *     for
+ * @param {Object=} options Options to apply to the client
  * @return {function(string, Object)} New client constructor
  */
-exports.makeProtobufClientConstructor =  function(service) {
-  var method_attrs = common.getProtobufServiceAttrs(service, service.name);
+exports.makeProtobufClientConstructor =  function(service, options) {
+  var method_attrs = common.getProtobufServiceAttrs(service, service.name,
+                                                    options);
   var Client = exports.makeClientConstructor(
       method_attrs, common.fullyQualifiedName(service));
   Client.service = service;
+  Client.service.grpc_options = options;
   return Client;
 };
 
diff --git a/src/node/src/common.js b/src/node/src/common.js
index 2e6c01c..e521760 100644
--- a/src/node/src/common.js
+++ b/src/node/src/common.js
@@ -44,9 +44,20 @@
 /**
  * Get a function that deserializes a specific type of protobuf.
  * @param {function()} cls The constructor of the message type to deserialize
+ * @param {bool=} binaryAsBase64 Deserialize bytes fields as base64 strings
+ *     instead of Buffers. Defaults to false
+ * @param {bool=} longsAsStrings Deserialize long values as strings instead of
+ *     objects. Defaults to true
  * @return {function(Buffer):cls} The deserialization function
  */
-exports.deserializeCls = function deserializeCls(cls) {
+exports.deserializeCls = function deserializeCls(cls, binaryAsBase64,
+                                                 longsAsStrings) {
+  if (binaryAsBase64 === undefined || binaryAsBase64 === null) {
+    binaryAsBase64 = false;
+  }
+  if (longsAsStrings === undefined || longsAsStrings === null) {
+    longsAsStrings = true;
+  }
   /**
    * Deserialize a buffer to a message object
    * @param {Buffer} arg_buf The buffer to deserialize
@@ -55,7 +66,7 @@
   return function deserialize(arg_buf) {
     // Convert to a native object with binary fields as Buffers (first argument)
     // and longs as strings (second argument)
-    return cls.decode(arg_buf).toRaw(false, true);
+    return cls.decode(arg_buf).toRaw(binaryAsBase64, longsAsStrings);
   };
 };
 
@@ -119,19 +130,28 @@
 /**
  * Return a map from method names to method attributes for the service.
  * @param {ProtoBuf.Reflect.Service} service The service to get attributes for
+ * @param {Object=} options Options to apply to these attributes
  * @return {Object} The attributes map
  */
-exports.getProtobufServiceAttrs = function getProtobufServiceAttrs(service) {
+exports.getProtobufServiceAttrs = function getProtobufServiceAttrs(service,
+                                                                   options) {
   var prefix = '/' + fullyQualifiedName(service) + '/';
+  var binaryAsBase64, longsAsStrings;
+  if (options) {
+    binaryAsBase64 = options.binaryAsBase64;
+    longsAsStrings = options.longsAsStrings;
+  }
   return _.object(_.map(service.children, function(method) {
     return [_.camelCase(method.name), {
       path: prefix + method.name,
       requestStream: method.requestStream,
       responseStream: method.responseStream,
       requestSerialize: serializeCls(method.resolvedRequestType.build()),
-      requestDeserialize: deserializeCls(method.resolvedRequestType.build()),
+      requestDeserialize: deserializeCls(method.resolvedRequestType.build(),
+                                     binaryAsBase64, longsAsStrings),
       responseSerialize: serializeCls(method.resolvedResponseType.build()),
-      responseDeserialize: deserializeCls(method.resolvedResponseType.build())
+      responseDeserialize: deserializeCls(method.resolvedResponseType.build(),
+                                     binaryAsBase64, longsAsStrings)
     }];
   }));
 };
diff --git a/src/node/src/server.js b/src/node/src/server.js
index e5aadcd..0cf7ba3 100644
--- a/src/node/src/server.js
+++ b/src/node/src/server.js
@@ -737,7 +737,12 @@
  *     method implementation for the provided service.
  */
 Server.prototype.addProtoService = function(service, implementation) {
-  this.addService(common.getProtobufServiceAttrs(service), implementation);
+  var options;
+  if (service.grpc_options) {
+    options = service.grpc_options;
+  }
+  this.addService(common.getProtobufServiceAttrs(service, options),
+                  implementation);
 };
 
 /**
diff --git a/src/node/test/common_test.js b/src/node/test/common_test.js
index 08ba429..66a4205 100644
--- a/src/node/test/common_test.js
+++ b/src/node/test/common_test.js
@@ -1,6 +1,6 @@
 /*
  *
- * Copyright 2015, Google Inc.
+ * Copyright 2015-2016, Google Inc.
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -42,7 +42,7 @@
 var messages_proto = ProtoBuf.loadProtoFile(
     __dirname + '/test_messages.proto').build();
 
-describe('Proto message serialize and deserialize', function() {
+describe('Proto message long int serialize and deserialize', function() {
   var longSerialize = common.serializeCls(messages_proto.LongValues);
   var longDeserialize = common.deserializeCls(messages_proto.LongValues);
   var pos_value = '314159265358979';
@@ -87,4 +87,52 @@
     assert.strictEqual(longDeserialize(serialized).sfixed_64.toString(),
                        neg_value);
   });
+  it('should deserialize as a number with the right option set', function() {
+    var longNumDeserialize = common.deserializeCls(messages_proto.LongValues,
+                                                   false, false);
+    var serialized = longSerialize({int_64: pos_value});
+    assert.strictEqual(typeof longDeserialize(serialized).int_64, 'string');
+    /* With the longsAsStrings option disabled, long values are represented as
+     * objects with 3 keys: low, high, and unsigned */
+    assert.strictEqual(typeof longNumDeserialize(serialized).int_64, 'object');
+  });
+});
+describe('Proto message bytes serialize and deserialize', function() {
+  var sequenceSerialize = common.serializeCls(messages_proto.SequenceValues);
+  var sequenceDeserialize = common.deserializeCls(
+      messages_proto.SequenceValues);
+  var sequenceBase64Deserialize = common.deserializeCls(
+      messages_proto.SequenceValues, true);
+  var buffer_val = new Buffer([0x69, 0xb7]);
+  var base64_val = 'abc=';
+  it('should preserve a buffer', function() {
+    var serialized = sequenceSerialize({bytes_field: buffer_val});
+    var deserialized = sequenceDeserialize(serialized);
+    assert.strictEqual(deserialized.bytes_field.compare(buffer_val), 0);
+  });
+  it('should accept base64 encoded strings', function() {
+    var serialized = sequenceSerialize({bytes_field: base64_val});
+    var deserialized = sequenceDeserialize(serialized);
+    assert.strictEqual(deserialized.bytes_field.compare(buffer_val), 0);
+  });
+  it('should output base64 encoded strings with an option set', function() {
+    var serialized = sequenceSerialize({bytes_field: base64_val});
+    var deserialized = sequenceBase64Deserialize(serialized);
+    assert.strictEqual(deserialized.bytes_field, base64_val);
+  });
+  /* The next two tests are specific tests to verify that issue
+   * https://github.com/grpc/grpc/issues/5174 has been fixed. They are skipped
+   * because they will not pass until a protobuf.js release has been published
+   * with a fix for https://github.com/dcodeIO/protobuf.js/issues/390 */
+  it.skip('should serialize a repeated field as packed by default', function() {
+    var expected_serialize = new Buffer([0x12, 0x01, 0x01, 0x0a]);
+    var serialized = sequenceSerialize({repeated_field: [10]});
+    assert.strictEqual(expected_serialize.compare(serialized), 0);
+  });
+  it.skip('should deserialize packed or unpacked repeated', function() {
+    var serialized = new Buffer([0x12, 0x01, 0x01, 0x0a]);
+    assert.doesNotThrow(function() {
+      sequenceDeserialize(serialized);
+    });
+  });
 });
diff --git a/src/node/test/test_messages.proto b/src/node/test/test_messages.proto
index c77a937..9b8cb87 100644
--- a/src/node/test/test_messages.proto
+++ b/src/node/test/test_messages.proto
@@ -36,3 +36,8 @@
   fixed64 fixed_64 = 4;
   sfixed64 sfixed_64 = 5;
 }
+
+message SequenceValues {
+  bytes bytes_field = 1;
+  repeated int32 repeated_field = 2;
+}