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/js/Gruntfile.js b/lib/js/Gruntfile.js
index 3298d5c..32c8834 100644
--- a/lib/js/Gruntfile.js
+++ b/lib/js/Gruntfile.js
@@ -35,7 +35,7 @@
           'dist/<%= pkg.name %>.min.js': ['<%= concat.dist.dest %>']
         }
       }
-    },  
+    },
     shell: {
       InstallThriftJS: {
         command: 'mkdir test/build; mkdir test/build/js; cp src/thrift.js test/build/js/thrift.js'
@@ -48,6 +48,9 @@
       },
       ThriftGenJQ: {
         command: 'thrift -gen js:jquery -gen js:node -o test ../../test/ThriftTest.thrift'
+      },
+      ThriftGenDeepConstructor: {
+        command: 'thrift -gen js -o test ../../test/JsDeepConstructorTest.thrift'
       }
     },
     external_daemon: {
@@ -123,6 +126,13 @@
             'https://localhost:8089/testws.html'
           ]
         }
+      },
+      ThriftDeepConstructor: {
+        options: {
+          urls: [
+            'http://localhost:8088/test-deep-constructor.html'
+          ]
+        }
       }
     },
     jshint: {
@@ -147,13 +157,14 @@
   grunt.loadNpmTasks('grunt-external-daemon');
   grunt.loadNpmTasks('grunt-shell');
 
-  grunt.registerTask('test', ['jshint', 'shell:InstallThriftJS', 'shell:InstallThriftNodeJSDep', 'shell:ThriftGen', 
-                              'external_daemon:ThriftTestServer', 'external_daemon:ThriftTestServer_TLS', 
+  grunt.registerTask('test', ['jshint', 'shell:InstallThriftJS', 'shell:InstallThriftNodeJSDep', 'shell:ThriftGen',
+                              'external_daemon:ThriftTestServer', 'external_daemon:ThriftTestServer_TLS',
+                              'shell:ThriftGenDeepConstructor', 'qunit:ThriftDeepConstructor',
                               'qunit:ThriftJS', 'qunit:ThriftJS_TLS',
                               'shell:ThriftGenJQ', 'qunit:ThriftJSJQ', 'qunit:ThriftJSJQ_TLS'
                              ]);
-  grunt.registerTask('default', ['jshint', 'shell:InstallThriftJS', 'shell:InstallThriftNodeJSDep', 'shell:ThriftGen', 
-                                 'external_daemon:ThriftTestServer', 'external_daemon:ThriftTestServer_TLS', 
+  grunt.registerTask('default', ['jshint', 'shell:InstallThriftJS', 'shell:InstallThriftNodeJSDep', 'shell:ThriftGen',
+                                 'external_daemon:ThriftTestServer', 'external_daemon:ThriftTestServer_TLS',
                                  'qunit:ThriftJS', 'qunit:ThriftJS_TLS',
                                  'shell:ThriftGenJQ', 'qunit:ThriftJSJQ', 'qunit:ThriftJSJQ_TLS',
                                  'concat', 'uglify', 'jsdoc'
diff --git a/lib/js/src/thrift.js b/lib/js/src/thrift.js
index af45d9c..9bd1198 100644
--- a/lib/js/src/thrift.js
+++ b/lib/js/src/thrift.js
@@ -1203,7 +1203,7 @@
         r.size = list.shift();
 
         this.rpos.push(this.rstack.length);
-        this.rstack.push(list);
+        this.rstack.push(list.shift());
 
         return r;
     },
@@ -1439,3 +1439,69 @@
 
 
 
+var copyList, copyMap;
+
+copyList = function(lst, types) {
+
+  if (!lst) {return lst; }
+
+  var type;
+
+  if (types.shift === undefined) {
+    type = types;
+  }
+  else {
+    type = types[0];
+  }
+  var Type = type;
+
+  var len = lst.length, result = [], i, val;
+  for (i = 0; i < len; i++) {
+    val = lst[i];
+    if (type === null) {
+      result.push(val);
+    }
+    else if (type === copyMap || type === copyList) {
+      result.push(type(val, types.slice(1)));
+    }
+    else {
+      result.push(new Type(val));
+    }
+  }
+  return result;
+};
+
+copyMap = function(obj, types){
+
+  if (!obj) {return obj; }
+
+  var type;
+
+  if (types.shift === undefined) {
+    type = types;
+  }
+  else {
+    type = types[0];
+  }
+  var Type = type;
+
+  var result = {}, val;
+  for(var prop in obj) {
+    if(obj.hasOwnProperty(prop)) {
+      val = obj[prop];
+      if (type === null) {
+        result[prop] = val;
+      }
+      else if (type === copyMap || type === copyList) {
+        result[prop] = type(val, types.slice(1));
+      }
+      else {
+        result[prop] = new Type(val);
+      }
+    }
+  }
+  return result;
+};
+
+Thrift.copyMap = copyMap;
+Thrift.copyList = copyList;
diff --git a/lib/js/test/deep-constructor.test.js b/lib/js/test/deep-constructor.test.js
new file mode 100644
index 0000000..9a19809
--- /dev/null
+++ b/lib/js/test/deep-constructor.test.js
@@ -0,0 +1,195 @@
+function serialize(data) {
+  var transport = new Thrift.Transport("/service");
+  var protocol  = new Thrift.Protocol(transport);
+  protocol.writeMessageBegin("", 0, 0);
+  data.write(protocol);
+  protocol.writeMessageEnd();
+  return transport.send_buf;
+}
+
+function deserialize(serialized, type) {
+  var transport = new Thrift.Transport("/service");
+  transport.setRecvBuffer(serialized);
+  var protocol  = new Thrift.Protocol(transport);
+  protocol.readMessageBegin();
+  var data = new type();
+  data.read(protocol);
+  protocol.readMessageEnd();
+  return data;
+}
+
+
+function createThriftObj() {
+
+  return new Complex({
+
+    struct_field: new Simple({value: 'a'}),
+
+    struct_list_field: [
+      new Simple({value: 'b'}),
+      new Simple({value: 'c'}),
+    ],
+
+    struct_set_field: [
+      new Simple({value: 'd'}),
+      new Simple({value: 'e'}),
+    ],
+
+    struct_map_field: {
+      A: new Simple({value: 'f'}),
+      B: new Simple({value: 'g'})
+    },
+
+    struct_nested_containers_field: [
+      [
+        {
+          C: [
+            new Simple({value: 'h'}),
+            new Simple({value: 'i'})
+          ]
+        }
+      ]
+    ],
+
+
+    struct_nested_containers_field2: {
+      D: [
+        {
+          DA: new Simple({value: 'j'})
+        },
+        {
+          DB: new 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.equal(obj.struct_field.value, 'a');
+    assert.equal(obj.struct_list_field[0].value, 'b');
+    assert.equal(obj.struct_list_field[1].value, 'c');
+    assert.equal(obj.struct_set_field[0].value, 'd');
+    assert.equal(obj.struct_set_field[1].value, 'e');
+    assert.equal(obj.struct_map_field.A.value, 'f');
+    assert.equal(obj.struct_map_field.B.value, 'g');
+    assert.equal(obj.struct_nested_containers_field[0][0].C[0].value, 'h');
+    assert.equal(obj.struct_nested_containers_field[0][0].C[1].value, 'i');
+    assert.equal(obj.struct_nested_containers_field2.D[0].DA.value, 'j');
+    assert.equal(obj.struct_nested_containers_field2.D[1].DB.value, 'k');
+}
+
+var cases = {
+
+  "Serialize/deserialize simple struct should return equal object": function(assert){
+    var tObj = new Simple({value: 'a'});
+    var received = deserialize(serialize(tObj), Simple);
+    assert.ok(tObj !== received);
+    assert.deepEqual(received, tObj);
+  },
+
+
+  "Serialize/deserialize should return equal object": function(assert){
+    var tObj = createThriftObj();
+    var received = deserialize(serialize(tObj), Complex);
+    assert.ok(tObj !== received);
+    assert.deepEqual(received, tObj);
+  },
+
+  "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 Complex(createJsObj());
+    assertValues(tObj2, assert);
+    assert.equal(serialize(tObj2), serialize(tObj1));
+  },
+
+  "Modifications to args object should not affect constructed Thrift object": function (assert) {
+
+    var args = createJsObj();
+    assertValues(args, assert);
+
+    var tObj = new 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);
+  },
+
+  "nulls are ok": function(assert) {
+    var tObj = new 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), Complex);
+    assert.ok(tObj !== received);
+    assert.deepEqual(tObj, received);
+  }
+
+};
+
+Object.keys(cases).forEach(function(caseName) {
+  test(caseName, cases[caseName]);
+});
diff --git a/lib/js/test/test-deep-constructor.html b/lib/js/test/test-deep-constructor.html
new file mode 100755
index 0000000..5835dc8
--- /dev/null
+++ b/lib/js/test/test-deep-constructor.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements. See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership. The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License. You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied. See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+  <title>Thrift Javascript Bindings: Unit Test</title>
+
+  <script src="build/js/thrift.js"         type="text/javascript" charset="utf-8"></script>
+  <script src="gen-js/JsDeepConstructorTest_types.js" type="text/javascript" charset="utf-8"></script>
+  <!-- jQuery -->
+  <script type="text/javascript" src="http://code.jquery.com/jquery-1.7.2.js" charset="utf-8"></script>
+
+  <!-- QUnit Test framework-->
+  <script type="text/javascript" src="http://code.jquery.com/qunit/qunit-1.14.0.js" charset="utf-8"></script>
+  <link rel="stylesheet" href="http://code.jquery.com/qunit/qunit-1.14.0.css" type="text/css" media="screen" />
+
+  <!-- the Test Suite-->
+  <script type="text/javascript" src="deep-constructor.test.js" charset="utf-8"></script>
+</head>
+<body>
+  <h1 id="qunit-header">Thrift Javascript Bindings: Deep Constructor Test (<a href="https://git-wip-us.apache.org/repos/asf?p=thrift.git;a=blob;f=test/JsDeepConstructorTest.thrift;hb=HEAD">JsDeepConstructorTest.thrift</a>)</h1>
+  <h2 id="qunit-banner"></h2>
+  <div id="qunit-testrunner-toolbar"></div>
+  <h2 id="qunit-userAgent"></h2>
+  <ol id="qunit-tests"><li><!-- get valid xhtml strict--></li></ol>
+  <p>
+      <a href="http://validator.w3.org/check/referer"><img
+          src="http://www.w3.org/Icons/valid-xhtml10"
+          alt="Valid XHTML 1.0!" height="31" width="88" /></a>
+  </p>
+</body>
+</html>