THRIFT-3122 Javascript struct constructor should properly initialize struct and container members from plain js arguments
Patch:  Igor Tkach

This closes #519
diff --git a/lib/nodejs/test/deep-constructor.test.js b/lib/nodejs/test/deep-constructor.test.js
new file mode 100644
index 0000000..41e7dd0
--- /dev/null
+++ b/lib/nodejs/test/deep-constructor.test.js
@@ -0,0 +1,233 @@
+var ttypes = require('./gen-nodejs/JsDeepConstructorTest_types');
+var thrift = require('thrift');
+var test = require('tape');
+var bufferEquals = require('buffer-equals');
+
+function serializeBinary(data) {
+  var buff;
+  var transport = new thrift.TBufferedTransport(null, function(msg){
+    buff = msg;
+  });
+  var prot = new thrift.TBinaryProtocol(transport);
+  data.write(prot);
+  prot.flush();
+  return buff;
+
+}
+
+
+function deserializeBinary(serialized, type) {
+  var t = new thrift.TFramedTransport(serialized);
+  var p = new thrift.TBinaryProtocol(t);
+  var data = new type();
+  data.read(p);
+  return data;
+}
+
+
+function serializeJSON(data) {
+  var buff;
+  var transport = new thrift.TBufferedTransport(null, function(msg){
+    buff = msg;
+  });
+  var protocol  = new thrift.TJSONProtocol(transport);
+  protocol.writeMessageBegin("", 0, 0);
+  data.write(protocol);
+  protocol.writeMessageEnd();
+  protocol.flush();
+  return buff;
+}
+
+
+function deserializeJSON(serialized, type) {
+  var transport = new thrift.TFramedTransport(serialized);
+  var protocol  = new thrift.TJSONProtocol(transport);
+  protocol.readMessageBegin();
+  var data = new type();
+  data.read(protocol);
+  protocol.readMessageEnd();
+  return data;
+}
+
+
+function createThriftObj() {
+
+  return new ttypes.Complex({
+
+    struct_field: new ttypes.Simple({value: 'a'}),
+
+    struct_list_field: [
+      new ttypes.Simple({value: 'b'}),
+      new ttypes.Simple({value: 'c'}),
+    ],
+
+    struct_set_field: [
+      new ttypes.Simple({value: 'd'}),
+      new ttypes.Simple({value: 'e'}),
+    ],
+
+    struct_map_field: {
+      A: new ttypes.Simple({value: 'f'}),
+      B: new ttypes.Simple({value: 'g'})
+    },
+
+    struct_nested_containers_field: [
+      [
+        {
+          C: [
+            new ttypes.Simple({value: 'h'}),
+            new ttypes.Simple({value: 'i'})
+          ]
+        }
+      ]
+    ],
+
+    struct_nested_containers_field2: {
+      D: [
+        {
+          DA: new ttypes.Simple({value: 'j'})
+        },
+        {
+          DB: new ttypes.Simple({value: 'k'})
+        }
+      ]
+    }
+  }
+  );
+}
+
+
+function createJsObj() {
+
+  return {
+
+    struct_field: {value: 'a'},
+
+    struct_list_field: [
+      {value: 'b'},
+      {value: 'c'},
+    ],
+
+    struct_set_field: [
+      {value: 'd'},
+      {value: 'e'},
+    ],
+
+    struct_map_field: {
+      A: {value: 'f'},
+      B: {value: 'g'}
+    },
+
+    struct_nested_containers_field: [
+      [
+        {
+          C: [
+            {value: 'h'},
+            {value: 'i'}
+          ]
+        }
+      ]
+    ],
+
+    struct_nested_containers_field2: {
+      D: [
+        {
+          DA: {value: 'j'}
+        },
+        {
+          DB: {value: 'k'}
+        }
+      ]
+    }
+  };
+}
+
+
+function assertValues(obj, assert) {
+    assert.equals(obj.struct_field.value, 'a');
+    assert.equals(obj.struct_list_field[0].value, 'b');
+    assert.equals(obj.struct_list_field[1].value, 'c');
+    assert.equals(obj.struct_set_field[0].value, 'd');
+    assert.equals(obj.struct_set_field[1].value, 'e');
+    assert.equals(obj.struct_map_field.A.value, 'f');
+    assert.equals(obj.struct_map_field.B.value, 'g');
+    assert.equals(obj.struct_nested_containers_field[0][0].C[0].value, 'h');
+    assert.equals(obj.struct_nested_containers_field[0][0].C[1].value, 'i');
+    assert.equals(obj.struct_nested_containers_field2.D[0].DA.value, 'j');
+    assert.equals(obj.struct_nested_containers_field2.D[1].DB.value, 'k');
+}
+
+function createTestCases(serialize, deserialize) {
+
+  var cases = {
+
+    "Serialize/deserialize should return equal object": function(assert){
+      var tObj = createThriftObj();
+      var received = deserialize(serialize(tObj), ttypes.Complex);
+      assert.ok(tObj !== received, 'not the same object');
+      assert.deepEqual(tObj, received);
+      assert.end();
+    },
+
+    "Nested structs and containers initialized from plain js objects should serialize same as if initialized from thrift objects": function(assert) {
+      var tObj1 = createThriftObj();
+      var tObj2 = new ttypes.Complex(createJsObj());
+      assertValues(tObj2, assert);
+      var s1 = serialize(tObj1);
+      var s2 = serialize(tObj2);
+      assert.ok(bufferEquals(s1, s2));
+      assert.end();
+    },
+
+    "Modifications to args object should not affect constructed Thrift object": function (assert) {
+
+      var args = createJsObj();
+      assertValues(args, assert);
+
+      var tObj = new ttypes.Complex(args);
+      assertValues(tObj, assert);
+
+      args.struct_field.value = 'ZZZ';
+      args.struct_list_field[0].value = 'ZZZ';
+      args.struct_list_field[1].value = 'ZZZ';
+      args.struct_set_field[0].value = 'ZZZ';
+      args.struct_set_field[1].value = 'ZZZ';
+      args.struct_map_field.A.value = 'ZZZ';
+      args.struct_map_field.B.value = 'ZZZ';
+      args.struct_nested_containers_field[0][0].C[0] = 'ZZZ';
+      args.struct_nested_containers_field[0][0].C[1] = 'ZZZ';
+      args.struct_nested_containers_field2.D[0].DA = 'ZZZ';
+      args.struct_nested_containers_field2.D[0].DB = 'ZZZ';
+
+      assertValues(tObj, assert);
+      assert.end();
+    },
+
+    "nulls are ok": function(assert) {
+      var tObj = new ttypes.Complex({
+        struct_field: null,
+        struct_list_field: null,
+        struct_set_field: null,
+        struct_map_field: null,
+        struct_nested_containers_field: null,
+        struct_nested_containers_field2: null
+      });
+      var received = deserialize(serialize(tObj), ttypes.Complex);
+      assert.ok(tObj !== received);
+      assert.deepEqual(tObj, received);
+      assert.end();
+    }
+
+  };
+  return cases;
+}
+
+
+function run(name, cases){
+  Object.keys(cases).forEach(function(caseName) {
+    test(name + ': ' + caseName, cases[caseName]);
+  });
+}
+
+run('binary', createTestCases(serializeBinary, deserializeBinary));
+run('json', createTestCases(serializeJSON, deserializeJSON));
diff --git a/lib/nodejs/test/test-cases.js b/lib/nodejs/test/test-cases.js
index c396ca9..7872295 100644
--- a/lib/nodejs/test/test-cases.js
+++ b/lib/nodejs/test/test-cases.js
@@ -107,6 +107,22 @@
     })]
 });
 
+var crazy2 = new ttypes.Insanity({
+  "userMap":{ "5":5, "8":8 },
+  "xtructs":[{
+      "string_thing":"Goodbye4",
+      "byte_thing":4,
+      "i32_thing":4,
+      "i64_thing":4
+    }, {
+      "string_thing":"Hello2",
+      "byte_thing":2,
+      "i32_thing":2,
+      "i64_thing":2
+    }]
+});
+
+
 var insanity = {
   "1":{ "2": crazy, "3": crazy },
   "2":{ "6":{ "userMap":{}, "xtructs":[] } }
@@ -119,4 +135,5 @@
 module.exports.out = out;
 module.exports.out2 = out2;
 module.exports.crazy = crazy;
+module.exports.crazy2 = crazy2;
 module.exports.insanity = insanity;
diff --git a/lib/nodejs/test/testAll.sh b/lib/nodejs/test/testAll.sh
index fd11425..38b284a 100755
--- a/lib/nodejs/test/testAll.sh
+++ b/lib/nodejs/test/testAll.sh
@@ -71,10 +71,12 @@
 #generating thrift code
 
 ${DIR}/../../../compiler/cpp/thrift -o ${DIR} --gen js:node ${DIR}/../../../test/ThriftTest.thrift
+${DIR}/../../../compiler/cpp/thrift -o ${DIR} --gen js:node ${DIR}/../../../test/JsDeepConstructorTest.thrift
 
 #unit tests
 
 node ${DIR}/binary.test.js || TESTOK=1
+node ${DIR}/deep-constructor.test.js || TESTOK=1
 
 #integration tests
 
diff --git a/lib/nodejs/test/test_driver.js b/lib/nodejs/test/test_driver.js
index 6e472ad..27ffd63 100644
--- a/lib/nodejs/test/test_driver.js
+++ b/lib/nodejs/test/test_driver.js
@@ -80,6 +80,11 @@
       checkRecursively(testCases.insanity, response, 'testInsanity');
     });
 
+    client.testInsanity(testCases.crazy2, function(err, response) {
+      assert.error(err, 'testInsanity2: no callback error');
+      checkRecursively(testCases.insanity, response, 'testInsanity2');
+    });
+
     client.testException('TException', function(err, response) {
       assert.ok(err instanceof TException, 'testException: correct error type');
       assert.ok(!response, 'testException: no response');
@@ -161,6 +166,12 @@
       })
       .fail(fail('testInsanity'));
 
+    client.testInsanity(testCases.crazy2)
+      .then(function(response) {
+        checkRecursively(testCases.insanity, response, 'testInsanity2');
+      })
+      .fail(fail('testInsanity2'));
+
     client.testException('TException')
       .then(function(response) {
         fail('testException: TException');