Browse Source

增加三方上传

15954078560 2 years ago
parent
commit
2db8af6db3
100 changed files with 20053 additions and 0 deletions
  1. 1 0
      addons/cos/.addonrc
  2. 153 0
      addons/cos/Cos.php
  3. 263 0
      addons/cos/bootstrap.js
  4. 273 0
      addons/cos/config.php
  5. 347 0
      addons/cos/controller/Index.php
  6. 10 0
      addons/cos/info.ini
  7. 19 0
      addons/cos/library/Guzzle/command/LICENSE
  8. 134 0
      addons/cos/library/Guzzle/command/README.md
  9. 36 0
      addons/cos/library/Guzzle/command/composer.json
  10. 55 0
      addons/cos/library/Guzzle/command/src/Command.php
  11. 39 0
      addons/cos/library/Guzzle/command/src/CommandInterface.php
  12. 7 0
      addons/cos/library/Guzzle/command/src/Exception/CommandClientException.php
  13. 109 0
      addons/cos/library/Guzzle/command/src/Exception/CommandException.php
  14. 7 0
      addons/cos/library/Guzzle/command/src/Exception/CommandServerException.php
  15. 60 0
      addons/cos/library/Guzzle/command/src/HasDataTrait.php
  16. 18 0
      addons/cos/library/Guzzle/command/src/Result.php
  17. 9 0
      addons/cos/library/Guzzle/command/src/ResultInterface.php
  18. 217 0
      addons/cos/library/Guzzle/command/src/ServiceClient.php
  19. 92 0
      addons/cos/library/Guzzle/command/src/ServiceClientInterface.php
  20. 16 0
      addons/cos/library/Guzzle/command/src/ToArrayInterface.php
  21. 4 0
      addons/cos/library/Guzzle/guzzle-services/.gitignore
  22. 23 0
      addons/cos/library/Guzzle/guzzle-services/.travis.yml
  23. 351 0
      addons/cos/library/Guzzle/guzzle-services/CHANGELOG.md
  24. 19 0
      addons/cos/library/Guzzle/guzzle-services/LICENSE
  25. 15 0
      addons/cos/library/Guzzle/guzzle-services/Makefile
  26. 129 0
      addons/cos/library/Guzzle/guzzle-services/README.md
  27. 49 0
      addons/cos/library/Guzzle/guzzle-services/composer.json
  28. 14 0
      addons/cos/library/Guzzle/guzzle-services/phpunit.xml.dist
  29. 265 0
      addons/cos/library/Guzzle/guzzle-services/src/Description.php
  30. 107 0
      addons/cos/library/Guzzle/guzzle-services/src/DescriptionInterface.php
  31. 294 0
      addons/cos/library/Guzzle/guzzle-services/src/Deserializer.php
  32. 169 0
      addons/cos/library/Guzzle/guzzle-services/src/GuzzleClient.php
  33. 82 0
      addons/cos/library/Guzzle/guzzle-services/src/Handler/ValidatedDescriptionHandler.php
  34. 312 0
      addons/cos/library/Guzzle/guzzle-services/src/Operation.php
  35. 655 0
      addons/cos/library/Guzzle/guzzle-services/src/Parameter.php
  36. 13 0
      addons/cos/library/Guzzle/guzzle-services/src/QuerySerializer/QuerySerializerInterface.php
  37. 33 0
      addons/cos/library/Guzzle/guzzle-services/src/QuerySerializer/Rfc3986Serializer.php
  38. 101 0
      addons/cos/library/Guzzle/guzzle-services/src/RequestLocation/AbstractLocation.php
  39. 49 0
      addons/cos/library/Guzzle/guzzle-services/src/RequestLocation/BodyLocation.php
  40. 84 0
      addons/cos/library/Guzzle/guzzle-services/src/RequestLocation/FormParamLocation.php
  41. 67 0
      addons/cos/library/Guzzle/guzzle-services/src/RequestLocation/HeaderLocation.php
  42. 85 0
      addons/cos/library/Guzzle/guzzle-services/src/RequestLocation/JsonLocation.php
  43. 76 0
      addons/cos/library/Guzzle/guzzle-services/src/RequestLocation/MultiPartLocation.php
  44. 92 0
      addons/cos/library/Guzzle/guzzle-services/src/RequestLocation/QueryLocation.php
  45. 44 0
      addons/cos/library/Guzzle/guzzle-services/src/RequestLocation/RequestLocationInterface.php
  46. 328 0
      addons/cos/library/Guzzle/guzzle-services/src/RequestLocation/XmlLocation.php
  47. 69 0
      addons/cos/library/Guzzle/guzzle-services/src/ResponseLocation/AbstractLocation.php
  48. 39 0
      addons/cos/library/Guzzle/guzzle-services/src/ResponseLocation/BodyLocation.php
  49. 47 0
      addons/cos/library/Guzzle/guzzle-services/src/ResponseLocation/HeaderLocation.php
  50. 176 0
      addons/cos/library/Guzzle/guzzle-services/src/ResponseLocation/JsonLocation.php
  51. 41 0
      addons/cos/library/Guzzle/guzzle-services/src/ResponseLocation/ReasonPhraseLocation.php
  52. 61 0
      addons/cos/library/Guzzle/guzzle-services/src/ResponseLocation/ResponseLocationInterface.php
  53. 39 0
      addons/cos/library/Guzzle/guzzle-services/src/ResponseLocation/StatusCodeLocation.php
  54. 311 0
      addons/cos/library/Guzzle/guzzle-services/src/ResponseLocation/XmlLocation.php
  55. 141 0
      addons/cos/library/Guzzle/guzzle-services/src/SchemaFormatter.php
  56. 297 0
      addons/cos/library/Guzzle/guzzle-services/src/SchemaValidator.php
  57. 164 0
      addons/cos/library/Guzzle/guzzle-services/src/Serializer.php
  58. 13 0
      addons/cos/library/Guzzle/guzzle-services/tests/Asset/Exception/CustomCommandException.php
  59. 13 0
      addons/cos/library/Guzzle/guzzle-services/tests/Asset/Exception/OtherCustomCommandException.php
  60. 10 0
      addons/cos/library/Guzzle/guzzle-services/tests/Asset/test.html
  61. 184 0
      addons/cos/library/Guzzle/guzzle-services/tests/DescriptionTest.php
  62. 386 0
      addons/cos/library/Guzzle/guzzle-services/tests/DeserializerTest.php
  63. 1037 0
      addons/cos/library/Guzzle/guzzle-services/tests/GuzzleClientTest.php
  64. 112 0
      addons/cos/library/Guzzle/guzzle-services/tests/Handler/ValidatedDescriptionHandlerTest.php
  65. 227 0
      addons/cos/library/Guzzle/guzzle-services/tests/OperationTest.php
  66. 378 0
      addons/cos/library/Guzzle/guzzle-services/tests/ParameterTest.php
  67. 35 0
      addons/cos/library/Guzzle/guzzle-services/tests/QuerySerializer/Rfc3986SerializerTest.php
  68. 26 0
      addons/cos/library/Guzzle/guzzle-services/tests/RequestLocation/BodyLocationTest.php
  69. 52 0
      addons/cos/library/Guzzle/guzzle-services/tests/RequestLocation/FormParamLocationTest.php
  70. 52 0
      addons/cos/library/Guzzle/guzzle-services/tests/RequestLocation/HeaderLocationTest.php
  71. 91 0
      addons/cos/library/Guzzle/guzzle-services/tests/RequestLocation/JsonLocationTest.php
  72. 33 0
      addons/cos/library/Guzzle/guzzle-services/tests/RequestLocation/MultiPartLocationTest.php
  73. 77 0
      addons/cos/library/Guzzle/guzzle-services/tests/RequestLocation/QueryLocationTest.php
  74. 525 0
      addons/cos/library/Guzzle/guzzle-services/tests/RequestLocation/XmlLocationTest.php
  75. 30 0
      addons/cos/library/Guzzle/guzzle-services/tests/ResponseLocation/BodyLocationTest.php
  76. 31 0
      addons/cos/library/Guzzle/guzzle-services/tests/ResponseLocation/HeaderLocationTest.php
  77. 581 0
      addons/cos/library/Guzzle/guzzle-services/tests/ResponseLocation/JsonLocationTest.php
  78. 30 0
      addons/cos/library/Guzzle/guzzle-services/tests/ResponseLocation/ReasonPhraseLocationTest.php
  79. 27 0
      addons/cos/library/Guzzle/guzzle-services/tests/ResponseLocation/StatusCodeLocationTest.php
  80. 795 0
      addons/cos/library/Guzzle/guzzle-services/tests/ResponseLocation/XmlLocationTest.php
  81. 60 0
      addons/cos/library/Guzzle/guzzle-services/tests/SchemaFormatterTest.php
  82. 330 0
      addons/cos/library/Guzzle/guzzle-services/tests/SchemaValidatorTest.php
  83. 39 0
      addons/cos/library/Guzzle/guzzle-services/tests/SerializerTest.php
  84. 283 0
      addons/cos/library/Qcloud/Cos/Client.php
  85. 163 0
      addons/cos/library/Qcloud/Cos/CommandToRequestTransformer.php
  86. 48 0
      addons/cos/library/Qcloud/Cos/Common.php
  87. 141 0
      addons/cos/library/Qcloud/Cos/Copy.php
  88. 162 0
      addons/cos/library/Qcloud/Cos/CosTransformer.php
  89. 7 0
      addons/cos/library/Qcloud/Cos/Exception/CosException.php
  90. 189 0
      addons/cos/library/Qcloud/Cos/Exception/ServiceResponseException.php
  91. 72 0
      addons/cos/library/Qcloud/Cos/ExceptionMiddleware.php
  92. 112 0
      addons/cos/library/Qcloud/Cos/ExceptionParser.php
  93. 136 0
      addons/cos/library/Qcloud/Cos/MultipartUpload.php
  94. 49 0
      addons/cos/library/Qcloud/Cos/Request/BodyLocation.php
  95. 127 0
      addons/cos/library/Qcloud/Cos/ResultTransformer.php
  96. 79 0
      addons/cos/library/Qcloud/Cos/Serializer.php
  97. 4843 0
      addons/cos/library/Qcloud/Cos/Service.php
  98. 48 0
      addons/cos/library/Qcloud/Cos/Signature.php
  99. 28 0
      addons/cos/library/Qcloud/Cos/SignatureMiddleware.php
  100. 1682 0
      addons/cos/library/Qcloud/Cos/Tests/Test.php

+ 1 - 0
addons/cos/.addonrc

@@ -0,0 +1 @@
+{"files":["public\\assets\\addons\\cos\\js\\spark.js"],"license":"regular","licenseto":"19079","licensekey":"89y20RnioHNsmUOh snZITlBSzhouRaP0mH+V3A==","domains":["baoming.com"],"licensecodes":[],"validations":["bdeb21c6994449a78f37e38ceb4cbdb2"]}

+ 153 - 0
addons/cos/Cos.php

@@ -0,0 +1,153 @@
+<?php
+
+namespace addons\cos;
+
+use addons\cos\library\Auth;
+use app\common\library\Menu;
+use fast\Http;
+use Qcloud\Cos\Client;
+use think\Addons;
+use think\App;
+use think\Loader;
+
+/**
+ * COS插件
+ */
+class Cos extends Addons
+{
+
+    /**
+     * 插件安装方法
+     * @return bool
+     */
+    public function install()
+    {
+
+        return true;
+    }
+
+    /**
+     * 插件卸载方法
+     * @return bool
+     */
+    public function uninstall()
+    {
+
+        return true;
+    }
+
+    /**
+     * 插件启用方法
+     * @return bool
+     */
+    public function enable()
+    {
+
+        return true;
+    }
+
+    /**
+     * 插件禁用方法
+     * @return bool
+     */
+    public function disable()
+    {
+
+        return true;
+    }
+
+    /**
+     * 渲染命名空间配置信息
+     */
+    public function appInit()
+    {
+        if (!class_exists('\Qcloud\Cos\Client')) {
+            Loader::addNamespace('Qcloud\Cos', $this->addons_path . str_replace('/', DS, 'library/Qcloud/Cos/'));
+        }
+        if (!class_exists('\GuzzleHttp\Command\Command')) {
+            Loader::addNamespace('GuzzleHttp\Command', $this->addons_path . str_replace('/', DS, 'library/Guzzle/command/src/'));
+        }
+        if (!class_exists('\GuzzleHttp\Command\Guzzle\Description')) {
+            Loader::addNamespace('GuzzleHttp\Command\Guzzle', $this->addons_path . str_replace('/', DS, 'library/Guzzle/guzzle-services/src/'));
+        }
+
+    }
+
+    /**
+     * 判断是否来源于API上传
+     */
+    public function moduleInit($request)
+    {
+        $config = $this->getConfig();
+        $module = strtolower($request->module());
+        if ($module == 'api' && ($config['apiupload'] ?? 0) && in_array($module, explode(',', $config['uploadmodule'] ?? '')) &&
+            strtolower($request->controller()) == 'common' &&
+            strtolower($request->action()) == 'upload') {
+            request()->param('isApi', true);
+            App::invokeMethod(["\\addons\\cos\\controller\\Index", "upload"], ['isApi' => true]);
+        }
+    }
+
+    /**
+     *
+     */
+    public function uploadConfigInit(&$upload)
+    {
+        $config = $this->getConfig();
+        $module = request()->module();
+        $module = $module ? strtolower($module) : 'index';
+
+        $uploadModule = array_filter(explode(',', $config['uploadmodule'] ?? ''));
+        if (!in_array($module, $uploadModule)) {
+            return $upload;
+        }
+
+        $data = ['deadline' => time() + $config['expire']];
+        $signature = hash_hmac('sha1', json_encode($data), $config['secretKey'], true);
+
+        $token = '';
+        $noNeedLogin = array_filter(explode(',', $config['noneedlogin'] ?? ''));
+        if (in_array($module, $noNeedLogin) || ($module == 'admin' && \app\admin\library\Auth::instance()->id) || ($module != 'admin' && \app\common\library\Auth::instance()->id)) {
+            $token = $config['appId'] . ':' . base64_encode($signature) . ':' . base64_encode(json_encode($data));
+        }
+        $multipart = [
+            'costoken' => $token
+        ];
+
+        $upload = array_merge($upload, [
+            'cdnurl'     => $config['cdnurl'],
+            'uploadurl'  => $config['uploadmode'] == 'client' ? $config['uploadurl'] : addon_url('cos/index/upload', [], false, true),
+            'uploadmode' => $config['uploadmode'],
+            'bucket'     => $config['bucket'],
+            'maxsize'    => $config['maxsize'],
+            'mimetype'   => $config['mimetype'],
+            'savekey'    => $config['savekey'],
+            'chunking'   => (bool)($config['chunking'] ?? $upload['chunking']),
+            'chunksize'  => (int)($config['chunksize'] ?? $upload['chunksize']),
+            'multipart'  => $multipart,
+            'storage'    => $this->getName(),
+            'multiple'   => $config['multiple'] ? true : false,
+        ]);
+    }
+
+    /**
+     * 附件删除后
+     */
+    public function uploadDelete($attachment)
+    {
+        $config = $this->getConfig();
+        if ($attachment['storage'] == 'cos' && isset($config['syncdelete']) && $config['syncdelete']) {
+            $cosConfig = array(
+                'region'      => $config['region'],
+                'schema'      => 'https', //协议头部,默认为http
+                'credentials' => array(
+                    'secretId'  => $config['secretId'],
+                    'secretKey' => $config['secretKey']
+                )
+            );
+            $oss = new Client($cosConfig);
+            $ret = $oss->deleteObject(array('Bucket' => $config['bucket'], 'Key' => ltrim($attachment->url, '/')));
+        }
+        return true;
+    }
+}

+ 263 - 0
addons/cos/bootstrap.js

@@ -0,0 +1,263 @@
+if (typeof Config.upload.storage !== 'undefined' && Config.upload.storage === 'cos') {
+    require(['upload'], function (Upload) {
+        //获取文件MD5值
+        var getFileMd5 = function (file, cb) {
+            require(['../addons/cos/js/spark'], function (SparkMD5) {
+                var blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice,
+                    chunkSize = 10 * 1024 * 1024,
+                    chunks = Math.ceil(file.size / chunkSize),
+                    currentChunk = 0,
+                    spark = new SparkMD5.ArrayBuffer(),
+                    fileReader = new FileReader();
+
+                fileReader.onload = function (e) {
+                    spark.append(e.target.result);
+                    currentChunk++;
+                    if (currentChunk < chunks) {
+                        loadNext();
+                    } else {
+                        cb && cb(spark.end());
+                    }
+                };
+
+                fileReader.onerror = function () {
+                    console.warn('文件读取错误');
+                };
+
+                function loadNext() {
+                    var start = currentChunk * chunkSize,
+                        end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
+
+                    fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
+                }
+
+                loadNext();
+            });
+        };
+
+        var _onInit = Upload.events.onInit;
+        //初始化中完成判断
+        Upload.events.onInit = function () {
+            _onInit.apply(this, Array.prototype.slice.apply(arguments));
+            //如果上传接口不是COS,则不处理
+            if (this.options.url !== Config.upload.uploadurl) {
+                return;
+            }
+            $.extend(this.options, {
+                //关闭自动处理队列功能
+                autoQueue: false,
+                params: function (files, xhr, chunk) {
+                    var params = Config.upload.multipart;
+                    if (chunk) {
+                        return $.extend({}, params, {
+                            filesize: chunk.file.size,
+                            filename: chunk.file.name,
+                            chunkid: chunk.file.upload.uuid,
+                            chunkindex: chunk.index,
+                            chunkcount: chunk.file.upload.totalChunkCount,
+                            chunkfilesize: chunk.dataBlock.data.size,
+                            chunksize: this.options.chunkSize,
+                            width: chunk.file.width || 0,
+                            height: chunk.file.height || 0,
+                            type: chunk.file.type,
+                            uploadId: chunk.file.uploadId,
+                            key: chunk.file.key,
+                        });
+                    } else {
+                        params = $.extend({}, params, files[0].params);
+                        params.category = files[0].category || '';
+                    }
+                    return params;
+                },
+                chunkSuccess: function (chunk, file, response) {
+                    var etag = chunk.xhr.getResponseHeader("ETag").replace(/(^")|("$)/g, '');
+                    this.etags = this.etags ? this.etags : [];
+                    this.etags[chunk.index] = etag;
+                },
+                chunksUploaded: function (file, done) {
+                    var that = this;
+
+                    Fast.api.ajax({
+                        url: "/addons/cos/index/upload",
+                        data: {
+                            action: 'merge',
+                            filesize: file.size,
+                            filename: file.name,
+                            chunkid: file.upload.uuid,
+                            chunkcount: file.upload.totalChunkCount,
+                            md5: file.md5,
+                            key: file.key,
+                            uploadId: file.uploadId,
+                            etags: this.etags,
+                            category: file.category || '',
+                            costoken: Config.upload.multipart.costoken,
+                        },
+                    }, function (data, ret) {
+                        done(JSON.stringify(ret));
+                        return false;
+                    }, function (data, ret) {
+                        file.accepted = false;
+                        that._errorProcessing([file], ret.msg);
+                        return false;
+                    });
+
+                },
+            });
+
+            var _success = this.options.success;
+            //先移除已有的事件
+            this.off("success", _success).on("success", function (file, response) {
+                var ret = {code: 0, msg: response};
+                try {
+                    if (response) {
+                        ret = typeof response === 'string' ? JSON.parse(response) : response;
+                    }
+                    if (file.xhr.status === 200 || file.xhr.status === 204) {
+                        if (Config.upload.uploadmode === 'client') {
+                            ret = {code: 1, data: {url: '/' + file.key}};
+                        }
+
+                        if (ret.code == 1) {
+                            var url = ret.data.url || '';
+                            Fast.api.ajax({
+                                url: "/addons/cos/index/notify",
+                                data: {name: file.name, url: url, md5: file.md5, size: file.size, width: file.width || 0, height: file.height || 0, type: file.type, category: file.category || '', costoken: Config.upload.multipart.costoken}
+                            }, function () {
+                                return false;
+                            }, function () {
+                                return false;
+                            });
+                        } else {
+                            console.error(ret);
+                        }
+                    } else {
+                        console.error(file.xhr);
+                    }
+                } catch (e) {
+                    console.error(e);
+                }
+                _success.call(this, file, ret);
+            });
+
+            this.on("addedfile", function (file) {
+                var that = this;
+                setTimeout(function () {
+                    if (file.status === 'error') {
+                        return;
+                    }
+                    getFileMd5(file, function (md5) {
+                        var chunk = that.options.chunking && file.size > that.options.chunkSize ? 1 : 0;
+                        var params = $(that.element).data("params") || {};
+                        var category = typeof params.category !== 'undefined' ? params.category : ($(that.element).data("category") || '');
+                        category = typeof category === 'function' ? category.call(that, file) : category;
+                        Fast.api.ajax({
+                            url: "/addons/cos/index/params",
+                            data: {method: 'POST', category: category, md5: md5, name: file.name, type: file.type, size: file.size, chunk: chunk, chunksize: that.options.chunkSize, costoken: Config.upload.multipart.costoken},
+                        }, function (data) {
+                            file.md5 = md5;
+                            file.id = data.id;
+                            file.key = data.key;
+                            file.date = data.date;
+                            file.uploadId = data.uploadId;
+                            file.policy = data.policy;
+                            file.signature = data.signature;
+                            file.partsAuthorization = data.partsAuthorization;
+                            file.params = data;
+                            file.category = category;
+
+                            if (file.status != 'error') {
+                                //开始上传
+                                that.enqueueFile(file);
+                            } else {
+                                that.removeFile(file);
+                            }
+                            return false;
+                        }, function () {
+                            that.removeFile(file);
+                        });
+                    });
+                }, 0);
+            });
+
+
+            if (Config.upload.uploadmode === 'client') {
+                var _method = this.options.method;
+                var _url = this.options.url;
+                this.options.method = function (files) {
+                    if (files[0].upload.chunked) {
+                        var chunk = null;
+                        files[0].upload.chunks.forEach(function (item) {
+                            if (item.status === 'uploading') {
+                                chunk = item;
+                            }
+                        });
+                        if (!chunk) {
+                            return "POST";
+                        } else {
+                            return "PUT";
+                        }
+                    }
+                    return _method;
+                };
+                this.options.url = function (files) {
+                    if (files[0].upload.chunked) {
+                        var chunk = null;
+                        files[0].upload.chunks.forEach(function (item) {
+                            if (item.status === 'uploading') {
+                                chunk = item;
+                            }
+                        });
+                        var index = chunk.dataBlock.chunkIndex;
+                        // debugger;
+                        this.options.headers = {"Authorization": files[0]['partsAuthorization'][index], "x-date": files[0]['date']};
+                        if (!chunk) {
+                            return Config.upload.uploadurl + "/" + files[0].key + "?uploadId=" + files[0].uploadId;
+                        } else {
+                            return Config.upload.uploadurl + "/" + files[0].key + "?partNumber=" + (index + 1) + "&uploadId=" + files[0].uploadId;
+                        }
+                    }
+                    return _url;
+                };
+                this.options.params = function (files, xhr, chunk) {
+                    var params = Config.upload.multipart;
+                    if (chunk) {
+                        return $.extend({}, params, {
+                            filesize: chunk.file.size,
+                            filename: chunk.file.name,
+                            chunkid: chunk.file.upload.uuid,
+                            chunkindex: chunk.index,
+                            chunkcount: chunk.file.upload.totalChunkCount,
+                            chunkfilesize: chunk.dataBlock.data.size,
+                            width: chunk.file.width || 0,
+                            height: chunk.file.height || 0,
+                            type: chunk.file.type,
+                        });
+                    } else {
+                        var retParams = $.extend({}, params, files[0].params || {});
+                        delete retParams.costoken;
+                        return retParams;
+                    }
+                };
+                this.on("sending", function (file, xhr, formData) {
+                    var that = this;
+                    if (file.upload.chunked) {
+                        var _send = xhr.send;
+                        xhr.send = function () {
+                            var chunk = null;
+                            file.upload.chunks.forEach(function (item) {
+                                if (item.status == 'uploading') {
+                                    chunk = item;
+                                }
+                            });
+                            if (chunk) {
+                                _send.call(xhr, chunk.dataBlock.data);
+                            }
+                        };
+                    } else {
+
+                    }
+                });
+            }
+        };
+    });
+}

+ 273 - 0
addons/cos/config.php

@@ -0,0 +1,273 @@
+<?php
+
+return [
+    [
+        'name' => 'appId',
+        'title' => 'AppID',
+        'type' => 'string',
+        'content' => [],
+        'value' => '1309974405',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '请前往腾讯控制台 > 访问管理 > API密钥',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'secretId',
+        'title' => 'SecretId',
+        'type' => 'string',
+        'content' => [],
+        'value' => 'AKIDhDTSBdoTs0rS4bx3rQijSu61f3B2cv3y',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '请前往腾讯控制台 > 访问管理 > API密钥',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'secretKey',
+        'title' => 'SecretKey',
+        'type' => 'string',
+        'content' => [],
+        'value' => 'f3A1btufAOL2SbT4ORDiwtZy7yYooY1D',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '请前往腾讯控制台 > 访问管理 > API密钥',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'bucket',
+        'title' => '存储桶名称',
+        'type' => 'string',
+        'content' => [],
+        'value' => 'fireflytra-1309974405',
+        'rule' => 'required;bucket',
+        'msg' => '',
+        'tip' => '存储空间名称',
+        'ok' => '',
+        'extend' => 'data-rule-bucket="[/^[0-9a-z_\\-]{3,63}$/, \'请输入正确的存储桶名称\']"',
+    ],
+    [
+        'name' => 'region',
+        'title' => '地域名称',
+        'type' => 'string',
+        'content' => [],
+        'value' => 'ap-beijing',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '请输入地域简称,请注意使用英文',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'uploadmode',
+        'title' => '上传模式',
+        'type' => 'select',
+        'content' => [
+            'client' => '客户端直传(速度快,无备份)',
+            'server' => '服务器中转(占用服务器带宽,有备份)',
+        ],
+        'value' => 'client',
+        'rule' => '',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'serverbackup',
+        'title' => '服务器中转模式备份',
+        'type' => 'radio',
+        'content' => [
+            1 => '备份(附件管理将产生2条记录)',
+            0 => '不备份',
+        ],
+        'value' => '0',
+        'rule' => '',
+        'msg' => '',
+        'tip' => '服务器中转模式下是否备份文件',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'uploadurl',
+        'title' => '上传接口地址',
+        'type' => 'string',
+        'content' => [],
+        'value' => 'https://fireflytra-1309974405.cos.ap-beijing.myqcloud.com',
+        'rule' => 'required;uploadurl',
+        'msg' => '',
+        'tip' => '请输入你的上传接口地址',
+        'ok' => '',
+        'extend' => 'data-rule-uploadurl="[/^http(s)?:\\/\\/.*$/, \'必需以http(s)://开头\']"',
+    ],
+    [
+        'name' => 'cdnurl',
+        'title' => 'CDN地址',
+        'type' => 'string',
+        'content' => [],
+        'value' => 'https://fireflytra-1309974405.cos.ap-beijing.myqcloud.com',
+        'rule' => 'required;cdnurl',
+        'msg' => '',
+        'tip' => '请配置你的CDN地址或在存储桶基础配置中获取',
+        'ok' => '',
+        'extend' => 'data-rule-cdnurl="[/^http(s)?:\\/\\/.*$/, \'必需以http(s)://开头\']"',
+    ],
+    [
+        'name' => 'savekey',
+        'title' => '保存文件名',
+        'type' => 'string',
+        'content' => [],
+        'value' => '/uploads/{year}{mon}{day}/{filemd5}{.suffix}',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'expire',
+        'title' => '上传有效时长',
+        'type' => 'string',
+        'content' => [],
+        'value' => '600',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '用户停留页面上传有效时长,单位秒',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'maxsize',
+        'title' => '最大可上传',
+        'type' => 'string',
+        'content' => [],
+        'value' => '10M',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'mimetype',
+        'title' => '可上传后缀格式',
+        'type' => 'string',
+        'content' => [],
+        'value' => 'jpg,png,bmp,jpeg,gif,zip,rar,xls,xlsx',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'multiple',
+        'title' => '多文件上传',
+        'type' => 'bool',
+        'content' => [],
+        'value' => '0',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'thumbstyle',
+        'title' => '缩略图样式',
+        'type' => 'string',
+        'content' => [],
+        'value' => '',
+        'rule' => '',
+        'msg' => '',
+        'tip' => '用于后台列表缩略图样式,可使用:?imageMogr2/thumbnail/120x90',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'chunking',
+        'title' => '分片上传',
+        'type' => 'radio',
+        'content' => [
+            1 => '开启',
+            0 => '关闭',
+        ],
+        'value' => '0',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'chunksize',
+        'title' => '分片大小',
+        'type' => 'number',
+        'content' => [],
+        'value' => '4194304',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'syncdelete',
+        'title' => '附件删除时是否同步删除文件',
+        'type' => 'bool',
+        'content' => [],
+        'value' => '0',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'apiupload',
+        'title' => 'API接口使用云存储',
+        'type' => 'bool',
+        'content' => [],
+        'value' => '1',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'noneedlogin',
+        'title' => '免登录上传',
+        'type' => 'checkbox',
+        'content' => [
+            'api' => 'API',
+            'index' => '前台',
+            'admin' => '后台',
+        ],
+        'value' => 'api,index,admin',
+        'rule' => '',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'uploadmodule',
+        'title' => '上传开启模块',
+        'type' => 'checkbox',
+        'content' => [
+            'api' => 'API',
+            'index' => '前台',
+            'admin' => '后台',
+        ],
+        'value' => 'api,index,admin',
+        'rule' => '',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+];

+ 347 - 0
addons/cos/controller/Index.php

@@ -0,0 +1,347 @@
+<?php
+
+namespace addons\cos\controller;
+
+use app\common\exception\UploadException;
+use app\common\library\Upload;
+use app\common\model\Attachment;
+use Qcloud\Cos\Client;
+use Qcloud\Cos\Signature;
+use think\addons\Controller;
+use think\Config;
+
+/**
+ * COS云储存
+ *
+ */
+class Index extends Controller
+{
+
+    protected $cosConfig = [];
+
+
+    public function _initialize()
+    {
+        //跨域检测
+        check_cors_request();
+
+        parent::_initialize();
+        Config::set('default_return_type', 'json');
+
+        $config = get_addon_config('cos');
+        $this->cosConfig = array(
+            'region'      => $config['region'],
+            'schema'      => 'https', //协议头部,默认为http
+            'credentials' => array(
+                'secretId'  => $config['secretId'],
+                'secretKey' => $config['secretKey']
+            )
+        );
+    }
+
+    public function index()
+    {
+        Config::set('default_return_type', 'html');
+        $this->error("当前插件暂无前台页面");
+    }
+
+    public function params()
+    {
+        $this->check();
+
+        $config = get_addon_config('cos');
+        $name = $this->request->post('name');
+        $md5 = $this->request->post('md5');
+        $chunk = $this->request->post('chunk');
+
+        $key = (new Upload())->getSavekey($config['savekey'], $name, $md5);
+        $key = ltrim($key, "/");
+        $params = [
+            'key' => $key,
+            'md5' => $md5
+        ];
+        if ($chunk) {
+            $fileSize = $this->request->post('size');
+            $oss = new Client($this->cosConfig);
+
+            $result = $oss->createMultipartUpload(array(
+                'Bucket' => $config['bucket'],
+                'Key'    => $key,
+            ));
+            $uploadId = $result['UploadId'];
+
+            $sig = new Signature($config['secretId'], $config['secretKey']);
+
+            $partSize = $this->request->post("chunksize");
+            $i = 0;
+            $size_count = $fileSize;
+            $values = array();
+            while ($size_count > 0) {
+                $size_count -= $partSize;
+                $values[] = array(
+                    $partSize * $i,
+                    ($size_count > 0) ? $partSize : ($size_count + $partSize),
+                );
+                $i++;
+            }
+
+            $params['key'] = $key;
+            $params['uploadId'] = $uploadId;
+            $params['partsAuthorization'] = [];
+            $date = gmdate('D, d M Y H:i:s \G\M\T');
+            foreach ($values as $index => $part) {
+                $partNumber = $index + 1;
+                $options = array(
+                    'Bucket'     => $config['bucket'],
+                    'Key'        => $key,
+                    'UploadId'   => $uploadId,
+                    'PartNumber' => $partNumber,
+                    'Body'       => ''
+                );
+                $command = $oss->getCommand('uploadPart', $options);
+                $request = $oss->commandToRequestTransformer($command);
+                $authorization = $sig->createAuthorization($request);
+
+                $params['partsAuthorization'][$index] = $authorization;
+            }
+            $params['date'] = $date;
+        } else {
+            if ($config['uploadmode'] == 'client') {
+                $expiretime = time() + $config['expire'];
+                $expiration = gmdate("Y-m-d\TH:i:s.414\Z", $expiretime);
+                $keytime = (time() - 60) . ';' . $expiretime;
+                $policy = json_encode([
+                    'expiration' => $expiration,
+                    'conditions' => [
+                        ['q-sign-algorithm' => 'sha1'],
+                        ['q-ak' => $config['secretId']],
+                        ['q-sign-time' => $keytime]
+                    ]
+                ]);
+                $signature = hash_hmac('sha1', sha1($policy), hash_hmac('sha1', $keytime, $config['secretKey']));
+                $params = [
+                    'key'              => $key,
+                    'policy'           => base64_encode($policy),
+                    'q-sign-algorithm' => 'sha1',
+                    'q-ak'             => $config['secretId'],
+                    'q-key-time'       => $keytime,
+                    'q-sign-time'      => $keytime,
+                    'q-signature'      => $signature
+                ];
+            }
+        }
+
+        $this->success('', null, $params);
+        return;
+    }
+
+    /**
+     * 服务器中转上传文件
+     * 上传分片
+     * 合并分片
+     * @param bool $isApi
+     */
+    public function upload($isApi = false)
+    {
+        if ($isApi === true) {
+            if (!$this->auth->isLogin()) {
+                $this->error("请登录后再进行操作");
+            }
+        } else {
+            $this->check();
+        }
+        $config = get_addon_config('cos');
+        $oss = new Client($this->cosConfig);
+
+        //检测删除文件或附件
+        $checkDeleteFile = function ($attachment, $upload, $force = false) use ($config) {
+            //如果设定为不备份则删除文件和记录 或 强制删除
+            if ((isset($config['serverbackup']) && !$config['serverbackup']) || $force) {
+                if ($attachment && !empty($attachment['id'])) {
+                    $attachment->delete();
+                }
+                if ($upload) {
+                    //文件绝对路径
+                    $filePath = $upload->getFile()->getRealPath() ?: $upload->getFile()->getPathname();
+                    @unlink($filePath);
+                }
+            }
+        };
+
+        $chunkid = $this->request->post("chunkid");
+        if ($chunkid) {
+            $action = $this->request->post("action");
+            $chunkindex = $this->request->post("chunkindex/d");
+            $chunkcount = $this->request->post("chunkcount/d");
+            $filesize = $this->request->post("filesize");
+            $filename = $this->request->post("filename");
+            $method = $this->request->method(true);
+            $key = $this->request->post("key");
+            $uploadId = $this->request->post("uploadId");
+
+            if ($action == 'merge') {
+                $attachment = null;
+                $upload = null;
+                //合并分片
+                if ($config['uploadmode'] == 'server') {
+                    //合并分片文件
+                    try {
+                        $upload = new Upload();
+                        $attachment = $upload->merge($chunkid, $chunkcount, $filename);
+                    } catch (UploadException $e) {
+                        $this->error($e->getMessage());
+                    }
+                }
+
+                $etags = $this->request->post("etags/a", []);
+                if (count($etags) != $chunkcount) {
+                    $checkDeleteFile($attachment, $upload, true);
+                    $this->error("分片数据错误");
+                }
+                $listParts = [];
+                for ($i = 0; $i < $chunkcount; $i++) {
+                    $listParts[] = array("PartNumber" => $i + 1, "ETag" => $etags[$i]);
+                }
+                try {
+                    $result = $oss->completeMultipartUpload(array(
+                            'Bucket'   => $config['bucket'],
+                            'Key'      => $key,
+                            'UploadId' => $uploadId,
+                            'Parts'    => $listParts
+                        )
+                    );
+                } catch (\Exception $e) {
+                    $checkDeleteFile($attachment, $upload, true);
+                    $this->error($e->getMessage());
+                }
+
+                if (!isset($result['Key'])) {
+                    $checkDeleteFile($attachment, $upload, true);
+                    $this->error("上传失败");
+                } else {
+                    $checkDeleteFile($attachment, $upload);
+                    $this->success("上传成功", '', ['url' => "/" . $key, 'fullurl' => cdnurl("/" . $key, true)]);
+                }
+            } else {
+                //默认普通上传文件
+                $file = $this->request->file('file');
+                try {
+                    $upload = new Upload($file);
+                    $file = $upload->chunk($chunkid, $chunkindex, $chunkcount);
+                } catch (UploadException $e) {
+                    $this->error($e->getMessage());
+                }
+                try {
+                    $params = array(
+                        'Bucket'     => $config['bucket'],
+                        'Key'        => $key,
+                        'UploadId'   => $uploadId,
+                        'PartNumber' => $chunkindex + 1,
+                        'Body'       => $file->fread($file->getSize())
+                    );
+                    $ret = $oss->uploadPart($params);
+                    $etag = $ret['ETag'];
+                } catch (\Exception $e) {
+                    $this->error($e->getMessage());
+                }
+
+                $this->success("上传成功", "", [], 3, ['ETag' => $etag]);
+            }
+        } else {
+            $attachment = null;
+            //默认普通上传文件
+            $file = $this->request->file('file');
+            try {
+                $upload = new Upload($file);
+                $attachment = $upload->upload();
+            } catch (UploadException $e) {
+                $this->error($e->getMessage());
+            }
+
+            //文件绝对路径
+            $filePath = $upload->getFile()->getRealPath() ?: $upload->getFile()->getPathname();
+
+            $url = $attachment->url;
+
+            try {
+                $ret = $oss->upload($config['bucket'], ltrim($attachment->url, "/"), $upload->getFile());
+                //成功不做任何操作
+            } catch (\Exception $e) {
+                $checkDeleteFile($attachment, $upload, true);
+                $this->error("上传失败");
+            }
+            $checkDeleteFile($attachment, $upload);
+
+            $this->success("上传成功", '', ['url' => $url, 'fullurl' => cdnurl($url, true)]);
+        }
+        return;
+    }
+
+    /**
+     * 回调
+     */
+    public function notify()
+    {
+        $this->check();
+        $size = $this->request->post('size/d');
+        $name = $this->request->post('name', '');
+        $md5 = $this->request->post('md5', '');
+        $type = $this->request->post('type', '');
+        $url = $this->request->post('url', '');
+        $width = $this->request->post('width/d');
+        $height = $this->request->post('height/d');
+        $category = $this->request->post('category', '');
+        $category = array_key_exists($category, config('site.attachmentcategory') ?? []) ? $category : '';
+        $suffix = strtolower(pathinfo($name, PATHINFO_EXTENSION));
+        $suffix = $suffix && preg_match("/^[a-zA-Z0-9]+$/", $suffix) ? $suffix : 'file';
+        $attachment = Attachment::where('url', $url)->where('storage', 'cos')->find();
+        if (!$attachment) {
+            $params = array(
+                'category'    => $category,
+                'admin_id'    => (int)session('admin.id'),
+                'user_id'     => (int)cookie('uid'),
+                'filesize'    => $size,
+                'filename'    => $name,
+                'imagewidth'  => $width,
+                'imageheight' => $height,
+                'imagetype'   => $suffix,
+                'imageframes' => 0,
+                'mimetype'    => $type,
+                'url'         => $url,
+                'uploadtime'  => time(),
+                'storage'     => 'cos',
+                'sha1'        => $md5,
+            );
+            Attachment::create($params, true);
+        }
+        $this->success();
+        return;
+    }
+
+    /**
+     * 检查签名是否正确或过期
+     */
+    protected function check()
+    {
+        $costoken = $this->request->post('costoken', '', 'trim');
+        if (!$costoken) {
+            $this->error("参数不正确");
+        }
+        $config = get_addon_config('cos');
+        list($appId, $sign, $data) = explode(':', $costoken);
+        if (!$appId || !$sign || !$data) {
+            $this->error("参数不正确");
+        }
+        if ($appId !== $config['appId']) {
+            $this->error("参数不正确");
+        }
+        if ($sign !== base64_encode(hash_hmac('sha1', base64_decode($data), $config['secretKey'], true))) {
+            $this->error("签名不正确");
+        }
+        $json = json_decode(base64_decode($data), true);
+        if ($json['deadline'] < time()) {
+            $this->error("请求已经超时");
+        }
+    }
+
+}

+ 10 - 0
addons/cos/info.ini

@@ -0,0 +1,10 @@
+name = cos
+title = 腾讯COS云存储
+intro = 使用腾讯COS云存储作为默认云储存
+author = FastAdmin
+website = https://www.fastadmin.net
+version = 1.2.0
+state = 1
+url = /addons/cos
+license = regular
+licenseto = 19079

+ 19 - 0
addons/cos/library/Guzzle/command/LICENSE

@@ -0,0 +1,19 @@
+Copyright (c) 2015 Michael Dowling, https://github.com/mtdowling <mtdowling@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 134 - 0
addons/cos/library/Guzzle/command/README.md

@@ -0,0 +1,134 @@
+# Guzzle Commands
+
+[![License](https://poser.pugx.org/guzzlehttp/command/license)](https://packagist.org/packages/guzzlehttp/command)
+[![Build Status](https://travis-ci.org/guzzle/command.svg?branch=master)](https://travis-ci.org/guzzle/command) 
+[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/guzzle/command/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/guzzle/command/?branch=master) 
+[![Code Coverage](https://scrutinizer-ci.com/g/guzzle/command/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/guzzle/command/?branch=master) 
+[![SensioLabsInsight](https://insight.sensiolabs.com/projects/7a93338e-50cd-42f7-9299-17c44d92148f/mini.png)](https://insight.sensiolabs.com/projects/7a93338e-50cd-42f7-9299-17c44d92148f)
+[![Latest Stable Version](https://poser.pugx.org/guzzlehttp/command/v/stable)](https://packagist.org/packages/guzzlehttp/command)
+[![Latest Unstable Version](https://poser.pugx.org/guzzlehttp/command/v/unstable)](https://packagist.org/packages/guzzlehttp/command)
+[![Total Downloads](https://poser.pugx.org/guzzlehttp/command/downloads)](https://packagist.org/packages/guzzlehttp/command)
+
+This library uses Guzzle (``guzzlehttp/guzzle``, version 6.x) and provides the
+foundations to create fully-featured web service clients by abstracting Guzzle
+HTTP **requests** and **responses** into higher-level **commands** and
+**results**. A **middleware** system, analogous to — but separate from — the one
+in the HTTP layer may be used to customize client behavior when preparing
+commands into requests and processing responses into results.
+
+### Commands
+    
+Key-value pair objects representing an operation of a web service. Commands have a name and a set of parameters.
+
+### Results
+
+Key-value pair objects representing the processed result of executing an operation of a web service.
+
+## Installing
+
+This project can be installed using Composer:
+
+``composer require guzzlehttp/command``
+
+For **Guzzle 5**, use ``composer require guzzlehttp/command:0.8.*``. The source
+code for the Guzzle 5 version is available on the
+`0.8 branch <https://github.com/guzzle/command/tree/0.8>`_.
+
+**Note:** If Composer is not
+`installed globally <https://getcomposer.org/doc/00-intro.md#globally>`_,
+then you may need to run the preceding Composer commands using
+``php composer.phar`` (where ``composer.phar`` is the path to your copy of
+Composer), instead of just ``composer``.
+
+## Service Clients
+
+Service Clients are web service clients that implement the
+``GuzzleHttp\Command\ServiceClientInterface`` and use an underlying Guzzle HTTP
+client (``GuzzleHttp\Client``) to communicate with the service. Service clients
+create and execute **commands** (``GuzzleHttp\Command\CommandInterface``),
+which encapsulate operations within the web service, including the operation
+name and parameters. This library provides a generic implementation of a service
+client: the ``GuzzleHttp\Command\ServiceClient`` class.
+
+## Instantiating a Service Client
+
+@TODO Add documentation
+
+* ``ServiceClient``'s constructor
+* Transformer functions (``$commandToRequestTransformer`` and ``$responseToResultTransformer``)
+* The ``HandlerStack``
+
+## Executing Commands
+
+Service clients create command objects using the ``getCommand()`` method.
+
+```php
+$commandName = 'foo';
+$arguments = ['baz' => 'bar'];
+$command = $client->getCommand($commandName, $arguments);
+
+```
+
+After creating a command, you may execute the command using the ``execute()``
+method of the client.
+
+```php
+$result = $client->execute($command);
+```
+
+The result of executing a command will be a ``GuzzleHttp\Command\ResultInterface``
+object. Result objects are ``ArrayAccess``-ible and contain the data parsed from
+HTTP response.
+
+Service clients have magic methods that act as shortcuts to executing commands
+by name without having to create the ``Command`` object in a separate step
+before executing it.
+
+```php
+$result = $client->foo(['baz' => 'bar']);
+```
+
+## Asynchronous Commands
+
+@TODO Add documentation
+
+* ``-Async`` suffix for client methods
+* Promises
+
+```php
+// Create and execute an asynchronous command.
+$command = $command = $client->getCommand('foo', ['baz' => 'bar']);
+$promise = $client->executeAsync($command);
+
+// Use asynchronous commands with magic methods.
+$promise = $client->fooAsync(['baz' => 'bar']);
+```
+
+@TODO Add documentation
+
+* ``wait()``-ing on promises.
+
+```php
+$result = $promise->wait();
+
+echo $result['fizz']; //> 'buzz' 
+```
+
+## Concurrent Requests
+
+@TODO Add documentation
+
+* ``executeAll()``
+* ``executeAllAsync()``.
+* Options (``fulfilled``, ``rejected``, ``concurrency``)
+
+## Middleware: Extending the Client
+
+Middleware can be added to the service client or underlying HTTP client to
+implement additional behavior and customize the ``Command``-to-``Result`` and
+``Request``-to-``Response`` lifecycles, respectively.
+
+## Todo
+
+* Middleware system and command vs request layers
+* The ``HandlerStack``

+ 36 - 0
addons/cos/library/Guzzle/command/composer.json

@@ -0,0 +1,36 @@
+{
+    "name": "guzzlehttp/command",
+    "description": "Provides the foundation for building command-based web service clients",
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "Michael Dowling",
+            "email": "mtdowling@gmail.com",
+            "homepage": "https://github.com/mtdowling"
+        },
+        {
+            "name": "Jeremy Lindblom",
+            "email": "jeremeamia@gmail.com",
+            "homepage": "https://github.com/jeremeamia"
+        }
+    ],
+    "require": {
+        "php": ">=5.5.0",
+        "guzzlehttp/guzzle": "^6.2",
+        "guzzlehttp/promises": "~1.3",
+        "guzzlehttp/psr7": "~1.0"
+    },
+    "require-dev": {
+        "phpunit/phpunit": "~4.0|~5.0"
+    },
+    "autoload": {
+        "psr-4": {
+            "GuzzleHttp\\Command\\": "src/"
+        }
+    },
+    "extra": {
+        "branch-alias": {
+            "dev-master": "0.9-dev"
+        }
+    }
+}

+ 55 - 0
addons/cos/library/Guzzle/command/src/Command.php

@@ -0,0 +1,55 @@
+<?php
+namespace GuzzleHttp\Command;
+
+use GuzzleHttp\HandlerStack;
+
+/**
+ * Default command implementation.
+ */
+class Command implements CommandInterface
+{
+    use HasDataTrait;
+
+    /** @var string */
+    private $name;
+
+    /** @var HandlerStack */
+    private $handlerStack;
+
+    /**
+     * @param string       $name         Name of the command
+     * @param array        $args         Arguments to pass to the command
+     * @param HandlerStack $handlerStack Stack of middleware for the command
+     */
+    public function __construct(
+        $name,
+        array $args = [],
+        HandlerStack $handlerStack = null
+    ) {
+        $this->name = $name;
+        $this->data = $args;
+        $this->handlerStack = $handlerStack;
+    }
+
+    public function getHandlerStack()
+    {
+        return $this->handlerStack;
+    }
+
+    public function getName()
+    {
+        return $this->name;
+    }
+
+    public function hasParam($name)
+    {
+        return array_key_exists($name, $this->data);
+    }
+
+    public function __clone()
+    {
+        if ($this->handlerStack) {
+            $this->handlerStack = clone $this->handlerStack;
+        }
+    }
+}

+ 39 - 0
addons/cos/library/Guzzle/command/src/CommandInterface.php

@@ -0,0 +1,39 @@
+<?php
+namespace GuzzleHttp\Command;
+
+use GuzzleHttp\HandlerStack;
+
+/**
+ * A command object encapsulates the input parameters used to control the
+ * creation of a HTTP request and processing of a HTTP response.
+ *
+ * Using the getParams() method will return the input parameters of the command
+ * as an associative array.
+ */
+interface CommandInterface extends \ArrayAccess, \IteratorAggregate, \Countable, ToArrayInterface
+{
+    /**
+     * Retrieves the handler stack specific to this command's execution.
+     *
+     * This can be used to add middleware that is specific to the command instance.
+     *
+     * @return HandlerStack
+     */
+    public function getHandlerStack();
+
+    /**
+     * Get the name of the command.
+     *
+     * @return string
+     */
+    public function getName();
+
+    /**
+     * Check if the command has a parameter by name.
+     *
+     * @param string $name Name of the parameter to check.
+     *
+     * @return bool
+     */
+    public function hasParam($name);
+}

+ 7 - 0
addons/cos/library/Guzzle/command/src/Exception/CommandClientException.php

@@ -0,0 +1,7 @@
+<?php
+namespace GuzzleHttp\Command\Exception;
+
+/**
+ * Exception encountered when a 4xx level response is received for a request
+ */
+class CommandClientException extends CommandException {}

+ 109 - 0
addons/cos/library/Guzzle/command/src/Exception/CommandException.php

@@ -0,0 +1,109 @@
+<?php
+namespace GuzzleHttp\Command\Exception;
+
+use GuzzleHttp\Exception\GuzzleException;
+use GuzzleHttp\Exception\RequestException;
+use GuzzleHttp\Command\CommandInterface;
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
+
+/**
+ * Exception encountered while executing a command.
+ */
+class CommandException extends \RuntimeException implements GuzzleException
+{
+    /** @var CommandInterface */
+    private $command;
+
+    /** @var RequestInterface */
+    private $request;
+
+    /** @var ResponseInterface */
+    private $response;
+
+    /**
+     * @param CommandInterface $command
+     * @param \Exception $prev
+     * @return CommandException
+     */
+    public static function fromPrevious(CommandInterface $command, \Exception $prev)
+    {
+        // If the exception is already a command exception, return it.
+        if ($prev instanceof self && $command === $prev->getCommand()) {
+            return $prev;
+        }
+
+        // If the exception is a RequestException, get the Request and Response.
+        $request = $response = null;
+        if ($prev instanceof RequestException) {
+            $request = $prev->getRequest();
+            $response = $prev->getResponse();
+        }
+
+        // Throw a more specific exception for 4XX or 5XX responses.
+        $class = self::class;
+        $statusCode = $response ? $response->getStatusCode() : 0;
+        if ($statusCode >= 400 && $statusCode < 500) {
+            $class = CommandClientException::class;
+        } elseif ($statusCode >= 500 && $statusCode < 600) {
+            $class = CommandServerException::class;
+        }
+
+        // Prepare the message.
+        $message = 'There was an error executing the ' . $command->getName()
+            . ' command: ' . $prev->getMessage();
+
+        // Create the exception.
+        return new $class($message, $command, $prev, $request, $response);
+    }
+
+    /**
+     * @param string $message Exception message
+     * @param CommandInterface $command
+     * @param \Exception $previous Previous exception (if any)
+     * @param RequestInterface $request
+     * @param ResponseInterface $response
+     */
+    public function __construct(
+        $message,
+        CommandInterface $command,
+        \Exception $previous = null,
+        RequestInterface $request = null,
+        ResponseInterface $response = null
+    ) {
+        $this->command = $command;
+        $this->request = $request;
+        $this->response = $response;
+        parent::__construct($message, 0, $previous);
+    }
+
+    /**
+     * Gets the command that failed.
+     *
+     * @return CommandInterface
+     */
+    public function getCommand()
+    {
+        return $this->command;
+    }
+
+    /**
+     * Gets the request that caused the exception
+     *
+     * @return RequestInterface|null
+     */
+    public function getRequest()
+    {
+        return $this->request;
+    }
+
+    /**
+     * Gets the associated response
+     *
+     * @return ResponseInterface|null
+     */
+    public function getResponse()
+    {
+        return $this->response;
+    }
+}

+ 7 - 0
addons/cos/library/Guzzle/command/src/Exception/CommandServerException.php

@@ -0,0 +1,7 @@
+<?php
+namespace GuzzleHttp\Command\Exception;
+
+/**
+ * Exception encountered when a 5xx level response is received for a request
+ */
+class CommandServerException extends CommandException {}

+ 60 - 0
addons/cos/library/Guzzle/command/src/HasDataTrait.php

@@ -0,0 +1,60 @@
+<?php
+
+namespace GuzzleHttp\Command;
+
+/**
+ * Basic collection behavior for Command and Result objects.
+ *
+ * The methods in the class are primarily for implementing the ArrayAccess,
+ * Countable, and IteratorAggregate interfaces.
+ */
+trait HasDataTrait
+{
+    /** @var array Data stored in the collection. */
+    protected $data;
+
+    public function __toString()
+    {
+        return print_r($this, true);
+    }
+
+    public function __debugInfo()
+    {
+        return $this->data;
+    }
+
+    public function offsetExists($offset)
+    {
+        return array_key_exists($offset, $this->data);
+    }
+
+    public function offsetGet($offset)
+    {
+        return isset($this->data[$offset]) ? $this->data[$offset] : null;
+    }
+
+    public function offsetSet($offset, $value)
+    {
+        $this->data[$offset] = $value;
+    }
+
+    public function offsetUnset($offset)
+    {
+        unset($this->data[$offset]);
+    }
+
+    public function count()
+    {
+        return count($this->data);
+    }
+
+    public function getIterator()
+    {
+        return new \ArrayIterator($this->data);
+    }
+
+    public function toArray()
+    {
+        return $this->data;
+    }
+}

+ 18 - 0
addons/cos/library/Guzzle/command/src/Result.php

@@ -0,0 +1,18 @@
+<?php
+namespace GuzzleHttp\Command;
+
+/**
+ * Default command implementation.
+ */
+class Result implements ResultInterface
+{
+    use HasDataTrait;
+
+    /**
+     * @param array $data
+     */
+    public function __construct(array $data = [])
+    {
+        $this->data = $data;
+    }
+}

+ 9 - 0
addons/cos/library/Guzzle/command/src/ResultInterface.php

@@ -0,0 +1,9 @@
+<?php
+namespace GuzzleHttp\Command;
+
+/**
+ * An array-like object that represents the result of executing a command.
+ */
+interface ResultInterface extends \ArrayAccess, \IteratorAggregate, \Countable, ToArrayInterface
+{
+}

+ 217 - 0
addons/cos/library/Guzzle/command/src/ServiceClient.php

@@ -0,0 +1,217 @@
+<?php
+namespace GuzzleHttp\Command;
+
+use GuzzleHttp\ClientInterface as HttpClient;
+use GuzzleHttp\Command\Exception\CommandException;
+use GuzzleHttp\HandlerStack;
+use GuzzleHttp\Promise;
+use GuzzleHttp\Promise\PromiseInterface;
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
+
+/**
+ * The Guzzle ServiceClient serves as the foundation for creating web service
+ * clients that interact with RPC-style APIs.
+ */
+class ServiceClient implements ServiceClientInterface
+{
+    /** @var HttpClient HTTP client used to send requests */
+    private $httpClient;
+
+    /** @var HandlerStack */
+    private $handlerStack;
+    
+    /** @var callable */
+    private $commandToRequestTransformer;
+
+    /** @var callable */
+    private $responseToResultTransformer;
+
+    /**
+     * Instantiates a Guzzle ServiceClient for making requests to a web service.
+     *
+     * @param HttpClient $httpClient A fully-configured Guzzle HTTP client that
+     *     will be used to perform the underlying HTTP requests.
+     * @param callable $commandToRequestTransformer A callable that transforms
+     *     a Command into a Request. The function should accept a
+     *     `GuzzleHttp\Command\CommandInterface` object and return a
+     *     `Psr\Http\Message\RequestInterface` object.
+     * @param callable $responseToResultTransformer A callable that transforms a
+     *     Response into a Result. The function should accept a
+     *     `Psr\Http\Message\ResponseInterface` object (and optionally a
+     *     `Psr\Http\Message\RequestInterface` object) and return a
+     *     `GuzzleHttp\Command\ResultInterface` object.
+     * @param HandlerStack $commandHandlerStack A Guzzle HandlerStack, which can
+     *     be used to add command-level middleware to the service client.
+     */
+    public function __construct(
+        HttpClient $httpClient,
+        callable $commandToRequestTransformer,
+        callable $responseToResultTransformer,
+        HandlerStack $commandHandlerStack = null
+    ) {
+        $this->httpClient = $httpClient;
+        $this->commandToRequestTransformer = $commandToRequestTransformer;
+        $this->responseToResultTransformer = $responseToResultTransformer;
+        $this->handlerStack = $commandHandlerStack ?: new HandlerStack();
+        $this->handlerStack->setHandler($this->createCommandHandler());
+    }
+
+    public function getHttpClient()
+    {
+        return $this->httpClient;
+    }
+
+    public function getHandlerStack()
+    {
+        return $this->handlerStack;
+    }
+
+    public function getCommand($name, array $params = [])
+    {
+        return new Command($name, $params, clone $this->handlerStack);
+    }
+
+    public function execute(CommandInterface $command)
+    {
+        return $this->executeAsync($command)->wait();
+    }
+
+    public function executeAsync(CommandInterface $command)
+    {
+        $stack = $command->getHandlerStack() ?: $this->handlerStack;
+        $handler = $stack->resolve();
+
+        return $handler($command);
+    }
+
+    public function executeAll($commands, array $options = [])
+    {
+        // Modify provided callbacks to track results.
+        $results = [];
+        $options['fulfilled'] = function ($v, $k) use (&$results, $options) {
+            if (isset($options['fulfilled'])) {
+                $options['fulfilled']($v, $k);
+            }
+            $results[$k] = $v;
+        };
+        $options['rejected'] = function ($v, $k) use (&$results, $options) {
+            if (isset($options['rejected'])) {
+                $options['rejected']($v, $k);
+            }
+            $results[$k] = $v;
+        };
+
+        // Execute multiple commands synchronously, then sort and return the results.
+        return $this->executeAllAsync($commands, $options)
+            ->then(function () use (&$results) {
+                ksort($results);
+                return $results;
+            })
+            ->wait();
+    }
+
+    public function executeAllAsync($commands, array $options = [])
+    {
+        // Apply default concurrency.
+        if (!isset($options['concurrency'])) {
+            $options['concurrency'] = 25;
+        }
+
+        // Convert the iterator of commands to a generator of promises.
+        $commands = Promise\iter_for($commands);
+        $promises = function () use ($commands) {
+            foreach ($commands as $key => $command) {
+                if (!$command instanceof CommandInterface) {
+                    throw new \InvalidArgumentException('The iterator must '
+                        . 'yield instances of ' . CommandInterface::class);
+                }
+                yield $key => $this->executeAsync($command);
+            }
+        };
+
+        // Execute the commands using a pool.
+        return (new Promise\EachPromise($promises(), $options))->promise();
+    }
+
+    /**
+     * Creates and executes a command for an operation by name.
+     *
+     * @param string $name Name of the command to execute.
+     * @param array $args Arguments to pass to the getCommand method.
+     *
+     * @return ResultInterface|PromiseInterface
+     * @see \GuzzleHttp\Command\ServiceClientInterface::getCommand
+     */
+    public function __call($name, array $args)
+    {
+        $args = isset($args[0]) ? $args[0] : [];
+        if (substr($name, -5) === 'Async') {
+            $command = $this->getCommand(substr($name, 0, -5), $args);
+            return $this->executeAsync($command);
+        } else {
+            return $this->execute($this->getCommand($name, $args));
+        }
+    }
+
+    /**
+     * Defines the main handler for commands that uses the HTTP client.
+     *
+     * @return callable
+     */
+    private function createCommandHandler()
+    {
+        return function (CommandInterface $command) {
+            return Promise\coroutine(function () use ($command) {
+                // Prepare the HTTP options.
+                $opts = $command['@http'] ?: [];
+                unset($command['@http']);
+
+                try {
+                    // Prepare the request from the command and send it.
+                    $request = $this->transformCommandToRequest($command);
+                    $promise = $this->httpClient->sendAsync($request, $opts);
+
+                    // Create a result from the response.
+                    $response = (yield $promise);
+                    yield $this->transformResponseToResult($response, $request, $command);
+                } catch (\Exception $e) {
+                    throw CommandException::fromPrevious($command, $e);
+                }
+            });
+        };
+    }
+
+    /**
+     * Transforms a Command object into a Request object.
+     *
+     * @param CommandInterface $command
+     * @return RequestInterface
+     */
+    private function transformCommandToRequest(CommandInterface $command)
+    {
+        $transform = $this->commandToRequestTransformer;
+
+        return $transform($command);
+    }
+
+
+    /**
+     * Transforms a Response object, also using data from the Request object,
+     * into a Result object.
+     *
+     * @param ResponseInterface $response
+     * @param RequestInterface $request
+     * @param CommandInterface $command
+     * @return ResultInterface
+     */
+    private function transformResponseToResult(
+        ResponseInterface $response,
+        RequestInterface $request,
+        CommandInterface $command
+    ) {
+        $transform = $this->responseToResultTransformer;
+
+        return $transform($response, $request, $command);
+    }
+}

+ 92 - 0
addons/cos/library/Guzzle/command/src/ServiceClientInterface.php

@@ -0,0 +1,92 @@
+<?php
+namespace GuzzleHttp\Command;
+
+use GuzzleHttp\ClientInterface;
+use GuzzleHttp\Command\Exception\CommandException;
+use GuzzleHttp\HandlerStack;
+use GuzzleHttp\Promise\PromiseInterface;
+
+/**
+ * Web service client interface.
+ */
+interface ServiceClientInterface
+{
+    /**
+     * Create a command for an operation name.
+     *
+     * Special keys may be set on the command to control how it behaves.
+     * Implementations SHOULD be able to utilize the following keys or throw
+     * an exception if unable.
+     *
+     * @param string $name Name of the operation to use in the command
+     * @param array  $args Arguments to pass to the command
+     *
+     * @return CommandInterface
+     * @throws \InvalidArgumentException if no command can be found by name
+     */
+    public function getCommand($name, array $args = []);
+
+    /**
+     * Execute a single command.
+     *
+     * @param CommandInterface $command Command to execute
+     *
+     * @return ResultInterface The result of the executed command
+     * @throws CommandException
+     */
+    public function execute(CommandInterface $command);
+
+    /**
+     * Execute a single command asynchronously
+     *
+     * @param CommandInterface $command Command to execute
+     *
+     * @return PromiseInterface A Promise that resolves to a Result.
+     */
+    public function executeAsync(CommandInterface $command);
+
+    /**
+     * Executes multiple commands concurrently using a fixed pool size.
+     *
+     * @param array|\Iterator $commands Array or iterator that contains
+     *     CommandInterface objects to execute with the client.
+     * @param array $options Associative array of options to apply.
+     *     - concurrency: (int) Max number of commands to execute concurrently.
+     *     - fulfilled: (callable) Function to invoke when a command completes.
+     *     - rejected: (callable) Function to invoke when a command fails.
+     *
+     * @return array
+     * @see GuzzleHttp\Command\ServiceClientInterface::createPool for options.
+     */
+    public function executeAll($commands, array $options = []);
+
+    /**
+     * Executes multiple commands concurrently and asynchronously using a
+     * fixed pool size.
+     *
+     * @param array|\Iterator $commands Array or iterator that contains
+     *     CommandInterface objects to execute with the client.
+     * @param array $options Associative array of options to apply.
+     *     - concurrency: (int) Max number of commands to execute concurrently.
+     *     - fulfilled: (callable) Function to invoke when a command completes.
+     *     - rejected: (callable) Function to invoke when a command fails.
+     *
+     * @return PromiseInterface
+     * @see GuzzleHttp\Command\ServiceClientInterface::createPool for options.
+     */
+    public function executeAllAsync($commands, array $options = []);
+
+    /**
+     * Get the HTTP client used to send requests for the web service client
+     *
+     * @return ClientInterface
+     */
+    public function getHttpClient();
+
+    /**
+     * Get the HandlerStack which can be used to add middleware to the client.
+     *
+     * @return HandlerStack
+     */
+    public function getHandlerStack();
+}

+ 16 - 0
addons/cos/library/Guzzle/command/src/ToArrayInterface.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace GuzzleHttp\Command;
+
+/**
+ * An object that can be represented as an array
+ */
+interface ToArrayInterface
+{
+    /**
+     * Get the array representation of an object
+     *
+     * @return array
+     */
+    public function toArray();
+}

+ 4 - 0
addons/cos/library/Guzzle/guzzle-services/.gitignore

@@ -0,0 +1,4 @@
+phpunit.xml
+composer.lock
+vendor/
+artifacts/

+ 23 - 0
addons/cos/library/Guzzle/guzzle-services/.travis.yml

@@ -0,0 +1,23 @@
+sudo: false
+language: php
+
+php:
+  - 5.5
+  - 5.6
+  - 7.0
+  - 7.1
+  - hhvm
+  - nightly
+
+matrix:
+  fast_finish: true
+  allow_failures:
+    - php: 7.1
+    - php: hhvm
+    - php: nightly
+
+before_script:
+  - composer self-update
+  - composer install --no-interaction --prefer-source --dev
+
+script: make test

+ 351 - 0
addons/cos/library/Guzzle/guzzle-services/CHANGELOG.md

@@ -0,0 +1,351 @@
+# Change Log
+
+## [1.1.3](https://github.com/guzzle/guzzle-services/tree/1.1.3) (2017-10-06)
+
+[Full Changelog](https://github.com/guzzle/guzzle-services/compare/1.1.2...HEAD)
+
+**Closed issues:**
+
+- Parameter type configuration causes issue when filters change input type [\#147](https://github.com/guzzle/guzzle-services/issues/147)
+
+**Merged pull requests:**
+
+- Use wire name when visiting array [\#152](https://github.com/guzzle/guzzle-services/pull/152) ([my2ter](https://github.com/my2ter))
+
+- Adding descriptive error message on parameter failure [\#144](https://github.com/guzzle/guzzle-services/pull/144) ([igorsantos07](https://github.com/igorsantos07))
+
+## [1.1.2](https://github.com/guzzle/guzzle-services/tree/1.1.2) (2017-05-19)
+
+[Full Changelog](https://github.com/guzzle/guzzle-services/compare/1.1.1...1.1.2)
+
+**Closed issues:**
+
+- Default values ignored in 1.1 [\#146](https://github.com/guzzle/guzzle-services/issues/146)
+
+- Operations extends is broken in 1.1.1 [\#145](https://github.com/guzzle/guzzle-services/issues/145)
+
+## [1.1.1](https://github.com/guzzle/guzzle-services/tree/1.1.1) (2017-05-15)
+
+[Full Changelog](https://github.com/guzzle/guzzle-services/compare/1.1.0...1.1.1)
+
+**Closed issues:**
+
+- Filters are applied twice [\#134](https://github.com/guzzle/guzzle-services/issues/134)
+
+- Is it possible to NOT urlencode a specific uri parameter value? [\#97](https://github.com/guzzle/guzzle-services/issues/97)
+
+**Merged pull requests:**
+
+- Fix minor typos in documentation. [\#139](https://github.com/guzzle/guzzle-services/pull/139) ([forevermatt](https://github.com/forevermatt))
+
+- Do not mutate command at validation [\#135](https://github.com/guzzle/guzzle-services/pull/135) ([danizord](https://github.com/danizord))
+
+- Added tests for JSON array of arrays and array of objects [\#131](https://github.com/guzzle/guzzle-services/pull/131) ([selfcatering](https://github.com/selfcatering))
+
+- Allow filters on response model [\#138](https://github.com/guzzle/guzzle-services/pull/138) ([danizord](https://github.com/danizord))
+
+- Exposing properties to a parent class [\#136](https://github.com/guzzle/guzzle-services/pull/136) ([Napas](https://github.com/Napas))
+
+## [1.1.0](https://github.com/guzzle/guzzle-services/tree/1.1.0) (2017-01-31)
+
+[Full Changelog](https://github.com/guzzle/guzzle-services/compare/1.0.1...1.1.0)
+
+**Closed issues:**
+
+- Grab a list of objects when they are not located at top level of a json response \(HATEOAS\) [\#90](https://github.com/guzzle/guzzle-services/issues/90)
+
+- Regression of Issue \#51 - XmlLocation response not handling multiple tags of the same name correctly [\#82](https://github.com/guzzle/guzzle-services/issues/82)
+
+- PUT requests with parameters with location of "postField" result in Exception [\#78](https://github.com/guzzle/guzzle-services/issues/78)
+
+- Allow to provide Post Body as an Array [\#77](https://github.com/guzzle/guzzle-services/issues/77)
+
+**Merged pull requests:**
+
+- Bring more flexibility to query params serialization [\#132](https://github.com/guzzle/guzzle-services/pull/132) ([bakura10](https://github.com/bakura10))
+
+- Allow to fix validation for parameters with a format [\#130](https://github.com/guzzle/guzzle-services/pull/130) ([bakura10](https://github.com/bakura10))
+
+## [1.0.1](https://github.com/guzzle/guzzle-services/tree/1.0.1) (2017-01-13)
+
+[Full Changelog](https://github.com/guzzle/guzzle-services/compare/1.0.0...1.0.1)
+
+**Implemented enhancements:**
+
+- Set a name when pushing ValidatedDescriptionHandler to stack [\#127](https://github.com/guzzle/guzzle-services/issues/127)
+
+**Fixed bugs:**
+
+- combine method in Uri [\#101](https://github.com/guzzle/guzzle-services/issues/101)
+
+- Undefined Variable [\#88](https://github.com/guzzle/guzzle-services/issues/88)
+
+- Regression in array parameter serialization [\#128](https://github.com/guzzle/guzzle-services/issues/128)
+
+- Unable to POST multiple multipart parameters [\#123](https://github.com/guzzle/guzzle-services/issues/123)
+
+**Closed issues:**
+
+- Tag pre 1.0.0 release [\#121](https://github.com/guzzle/guzzle-services/issues/121)
+
+- Adjust inline documentation of Parameter [\#120](https://github.com/guzzle/guzzle-services/issues/120)
+
+- postField location not recognized after upgrading to 1.0 [\#119](https://github.com/guzzle/guzzle-services/issues/119)
+
+- Create a new release for the guzzle6 branch [\#118](https://github.com/guzzle/guzzle-services/issues/118)
+
+- Compatibility problem with PHP7.0 ? [\#116](https://github.com/guzzle/guzzle-services/issues/116)
+
+- What is the correct type of Parameter static option [\#113](https://github.com/guzzle/guzzle-services/issues/113)
+
+- Improve the construction of baseUri in Description [\#112](https://github.com/guzzle/guzzle-services/issues/112)
+
+- Please create version tag for current master branch [\#110](https://github.com/guzzle/guzzle-services/issues/110)
+
+- Problems with postField params [\#98](https://github.com/guzzle/guzzle-services/issues/98)
+
+**Merged pull requests:**
+
+- Fix serialization of query params [\#129](https://github.com/guzzle/guzzle-services/pull/129) ([bakura10](https://github.com/bakura10))
+
+## [1.0.0](https://github.com/guzzle/guzzle-services/tree/1.0.0) (2016-11-24)
+
+[Full Changelog](https://github.com/guzzle/guzzle-services/compare/0.6.0...1.0.0)
+
+**Closed issues:**
+
+- AbstractClient' not found [\#117](https://github.com/guzzle/guzzle-services/issues/117)
+
+**Merged pull requests:**
+
+- Make Guzzle Services compatible with Guzzle6 [\#109](https://github.com/guzzle/guzzle-services/pull/109) ([Konafets](https://github.com/Konafets))
+
+## [0.6.0](https://github.com/guzzle/guzzle-services/tree/0.6.0) (2016-10-21)
+
+[Full Changelog](https://github.com/guzzle/guzzle-services/compare/0.5.0...0.6.0)
+
+**Closed issues:**
+
+- Broken composer install [\#111](https://github.com/guzzle/guzzle-services/issues/111)
+
+- The visit\(\) method is expected to return a RequestInterface but it doesn't in JsonLocation [\#106](https://github.com/guzzle/guzzle-services/issues/106)
+
+- Allow parameters in baseUrl [\#102](https://github.com/guzzle/guzzle-services/issues/102)
+
+- Have default params at client construction, gone away? [\#100](https://github.com/guzzle/guzzle-services/issues/100)
+
+- Runtime Exception Error is always empty [\#99](https://github.com/guzzle/guzzle-services/issues/99)
+
+- PHP Fatal error:  Unsupported operand types in guzzlehttp/guzzle-services/src/GuzzleClient.php on line 72 [\#95](https://github.com/guzzle/guzzle-services/issues/95)
+
+- Date of next version [\#94](https://github.com/guzzle/guzzle-services/issues/94)
+
+- Map null reponse values to defined reponse model properties [\#91](https://github.com/guzzle/guzzle-services/issues/91)
+
+- Map a json-array into a Model [\#80](https://github.com/guzzle/guzzle-services/issues/80)
+
+- If property specified in json model but empty, notice raised [\#75](https://github.com/guzzle/guzzle-services/issues/75)
+
+- Allow primitive response types for operations [\#73](https://github.com/guzzle/guzzle-services/issues/73)
+
+- Allow shortened definition of properties in models [\#71](https://github.com/guzzle/guzzle-services/issues/71)
+
+- Where's the ServiceDescriptionLoader/AbstractConfigLoader? [\#68](https://github.com/guzzle/guzzle-services/issues/68)
+
+- errorResposnes from operation is never used [\#66](https://github.com/guzzle/guzzle-services/issues/66)
+
+- Updating the description  [\#65](https://github.com/guzzle/guzzle-services/issues/65)
+
+- Parameter type validation is too strict [\#7](https://github.com/guzzle/guzzle-services/issues/7)
+
+**Merged pull requests:**
+
+- fix code example [\#115](https://github.com/guzzle/guzzle-services/pull/115) ([snoek09](https://github.com/snoek09))
+
+- Bug Fix for GuzzleClient constructor [\#96](https://github.com/guzzle/guzzle-services/pull/96) ([peterfox](https://github.com/peterfox))
+
+- add plugin section to readme [\#93](https://github.com/guzzle/guzzle-services/pull/93) ([gimler](https://github.com/gimler))
+
+- Allow mapping null response values to defined response model properties [\#92](https://github.com/guzzle/guzzle-services/pull/92) ([shaun785](https://github.com/shaun785))
+
+- Updated exception message for better debugging [\#85](https://github.com/guzzle/guzzle-services/pull/85) ([stovak](https://github.com/stovak))
+
+- Gracefully handle null return from $this-\>getConfig\('defaults'\) [\#84](https://github.com/guzzle/guzzle-services/pull/84) ([fuhry](https://github.com/fuhry))
+
+- Fixing issue \#82 to address regression for handling elements with the sa... [\#83](https://github.com/guzzle/guzzle-services/pull/83) ([sprak3000](https://github.com/sprak3000))
+
+- Fix for specified property but no value in json \(notice for undefined in... [\#76](https://github.com/guzzle/guzzle-services/pull/76) ([rfink](https://github.com/rfink))
+
+- Add ErrorHandler subscriber [\#67](https://github.com/guzzle/guzzle-services/pull/67) ([bakura10](https://github.com/bakura10))
+
+- Fix combine base url and command uri [\#108](https://github.com/guzzle/guzzle-services/pull/108) ([vlastv](https://github.com/vlastv))
+
+- Fixing JsonLocation::visit\(\) not returning a request \#106 [\#107](https://github.com/guzzle/guzzle-services/pull/107) ([Pinolo](https://github.com/Pinolo))
+
+- Fix call to undefined method "GuzzleHttp\Psr7\Uri::combine" [\#105](https://github.com/guzzle/guzzle-services/pull/105) ([horrorin](https://github.com/horrorin))
+
+- fix description for get request example [\#87](https://github.com/guzzle/guzzle-services/pull/87) ([snoek09](https://github.com/snoek09))
+
+- Allow raw values \(non array/object\) for root model definitions [\#74](https://github.com/guzzle/guzzle-services/pull/74) ([rfink](https://github.com/rfink))
+
+- Allow shortened definition of properties by assigning them directly to a type [\#72](https://github.com/guzzle/guzzle-services/pull/72) ([rfink](https://github.com/rfink))
+
+## [0.5.0](https://github.com/guzzle/guzzle-services/tree/0.5.0) (2014-12-23)
+
+[Full Changelog](https://github.com/guzzle/guzzle-services/compare/0.4.0...0.5.0)
+
+**Closed issues:**
+
+- Does it supports custom class instantiate to define an operation using a service description [\#62](https://github.com/guzzle/guzzle-services/issues/62)
+
+- Tag version 0.4.0 [\#61](https://github.com/guzzle/guzzle-services/issues/61)
+
+- XmlLocation not adding attributes to non-leaf child nodes [\#52](https://github.com/guzzle/guzzle-services/issues/52)
+
+- XmlLocation response not handling multiple tags of the same name correctly [\#51](https://github.com/guzzle/guzzle-services/issues/51)
+
+- Validation Bug [\#47](https://github.com/guzzle/guzzle-services/issues/47)
+
+- CommandException doesn't contain response data [\#44](https://github.com/guzzle/guzzle-services/issues/44)
+
+- \[Fix included\] XmlLocation requires text value to have attributes [\#37](https://github.com/guzzle/guzzle-services/issues/37)
+
+- Question: Mocking a Response does not throw exception [\#35](https://github.com/guzzle/guzzle-services/issues/35)
+
+- allow default 'location' on Model [\#26](https://github.com/guzzle/guzzle-services/issues/26)
+
+- create mock subscriber requests from descriptions [\#25](https://github.com/guzzle/guzzle-services/issues/25)
+
+**Merged pull requests:**
+
+- Documentation: Add 'boolean-string' as a supported "format" value [\#63](https://github.com/guzzle/guzzle-services/pull/63) ([jwcobb](https://github.com/jwcobb))
+
+## [0.4.0](https://github.com/guzzle/guzzle-services/tree/0.4.0) (2014-11-03)
+
+[Full Changelog](https://github.com/guzzle/guzzle-services/compare/0.3.0...0.4.0)
+
+**Closed issues:**
+
+- Exceptions Thrown From Subscribers Are Ignored? [\#58](https://github.com/guzzle/guzzle-services/issues/58)
+
+- Totally Broken With Guzzle 5 [\#57](https://github.com/guzzle/guzzle-services/issues/57)
+
+- GuzzleHTTP/Command Dependency fail [\#50](https://github.com/guzzle/guzzle-services/issues/50)
+
+- Request parameter PathLocation [\#46](https://github.com/guzzle/guzzle-services/issues/46)
+
+- Requesting a new version tag [\#45](https://github.com/guzzle/guzzle-services/issues/45)
+
+- CommandException expects second parameter to be CommandTransaction instance  [\#43](https://github.com/guzzle/guzzle-services/issues/43)
+
+- Cannot add Autorization header to my requests [\#39](https://github.com/guzzle/guzzle-services/issues/39)
+
+- Resouce Itterators [\#36](https://github.com/guzzle/guzzle-services/issues/36)
+
+- Question [\#33](https://github.com/guzzle/guzzle-services/issues/33)
+
+- query location array can be comma separated [\#31](https://github.com/guzzle/guzzle-services/issues/31)
+
+- Automatically returns array from command? [\#30](https://github.com/guzzle/guzzle-services/issues/30)
+
+- Arrays nested under objects in JSON response broken? [\#27](https://github.com/guzzle/guzzle-services/issues/27)
+
+- Question? [\#23](https://github.com/guzzle/guzzle-services/issues/23)
+
+**Merged pull requests:**
+
+- Bump the version in the readme [\#60](https://github.com/guzzle/guzzle-services/pull/60) ([GrahamCampbell](https://github.com/GrahamCampbell))
+
+- Bump the next version to 0.4 [\#56](https://github.com/guzzle/guzzle-services/pull/56) ([GrahamCampbell](https://github.com/GrahamCampbell))
+
+- Fixed the guzzlehttp/command version constraint [\#55](https://github.com/guzzle/guzzle-services/pull/55) ([GrahamCampbell](https://github.com/GrahamCampbell))
+
+- Work with latest Guzzle 5 and Command updates [\#54](https://github.com/guzzle/guzzle-services/pull/54) ([mtdowling](https://github.com/mtdowling))
+
+- Addressing Issue \#51 & Issue \#52 [\#53](https://github.com/guzzle/guzzle-services/pull/53) ([sprak3000](https://github.com/sprak3000))
+
+- added description interface to extend it [\#49](https://github.com/guzzle/guzzle-services/pull/49) ([danieledangeli](https://github.com/danieledangeli))
+
+- Update readme to improve documentation \(\#46\) [\#48](https://github.com/guzzle/guzzle-services/pull/48) ([bonndan](https://github.com/bonndan))
+
+- Fixed the readme version constraint [\#42](https://github.com/guzzle/guzzle-services/pull/42) ([GrahamCampbell](https://github.com/GrahamCampbell))
+
+- Update .travis.yml [\#41](https://github.com/guzzle/guzzle-services/pull/41) ([GrahamCampbell](https://github.com/GrahamCampbell))
+
+- Added a branch alias [\#40](https://github.com/guzzle/guzzle-services/pull/40) ([GrahamCampbell](https://github.com/GrahamCampbell))
+
+- Fixes Response\XmlLocation requires text value [\#38](https://github.com/guzzle/guzzle-services/pull/38) ([magnetik](https://github.com/magnetik))
+
+- Removing unnecessary \(\) from docblock [\#32](https://github.com/guzzle/guzzle-services/pull/32) ([jamiehannaford](https://github.com/jamiehannaford))
+
+- Fix JSON response location so that both is supported: arrays nested unde... [\#28](https://github.com/guzzle/guzzle-services/pull/28) ([ukautz](https://github.com/ukautz))
+
+- Throw Any Exceptions On Process [\#59](https://github.com/guzzle/guzzle-services/pull/59) ([GrahamCampbell](https://github.com/GrahamCampbell))
+
+- Allow extension to work recursively over models [\#34](https://github.com/guzzle/guzzle-services/pull/34) ([jamiehannaford](https://github.com/jamiehannaford))
+
+- A custom class can be configured for command instances. [\#29](https://github.com/guzzle/guzzle-services/pull/29) ([robinvdvleuten](https://github.com/robinvdvleuten))
+
+- \[WIP\] doing some experimentation [\#24](https://github.com/guzzle/guzzle-services/pull/24) ([cordoval](https://github.com/cordoval))
+
+## [0.3.0](https://github.com/guzzle/guzzle-services/tree/0.3.0) (2014-06-01)
+
+[Full Changelog](https://github.com/guzzle/guzzle-services/compare/0.2.0...0.3.0)
+
+**Closed issues:**
+
+- Testing Guzzle Services doesn't work [\#19](https://github.com/guzzle/guzzle-services/issues/19)
+
+- Description factory [\#18](https://github.com/guzzle/guzzle-services/issues/18)
+
+- support to load service description from file [\#15](https://github.com/guzzle/guzzle-services/issues/15)
+
+- Update dependency on guzzlehttp/command [\#11](https://github.com/guzzle/guzzle-services/issues/11)
+
+**Merged pull requests:**
+
+- Add license file [\#22](https://github.com/guzzle/guzzle-services/pull/22) ([siwinski](https://github.com/siwinski))
+
+- Fix 'Invalid argument supplied for foreach\(\)' [\#21](https://github.com/guzzle/guzzle-services/pull/21) ([Olden](https://github.com/Olden))
+
+- Fixed string zero \('0'\) values not being filtered in XML. [\#20](https://github.com/guzzle/guzzle-services/pull/20) ([dragonwize](https://github.com/dragonwize))
+
+- baseUrl can be a string or an uri template [\#16](https://github.com/guzzle/guzzle-services/pull/16) ([robinvdvleuten](https://github.com/robinvdvleuten))
+
+## [0.2.0](https://github.com/guzzle/guzzle-services/tree/0.2.0) (2014-03-30)
+
+[Full Changelog](https://github.com/guzzle/guzzle-services/compare/0.1.0...0.2.0)
+
+**Closed issues:**
+
+- please remove wiki [\#13](https://github.com/guzzle/guzzle-services/issues/13)
+
+- Parameter validation fails for union types [\#12](https://github.com/guzzle/guzzle-services/issues/12)
+
+- question on integration with Guzzle4 [\#8](https://github.com/guzzle/guzzle-services/issues/8)
+
+- typehints for operations property [\#6](https://github.com/guzzle/guzzle-services/issues/6)
+
+- improve exception message [\#5](https://github.com/guzzle/guzzle-services/issues/5)
+
+**Merged pull requests:**
+
+- Update composer.json [\#14](https://github.com/guzzle/guzzle-services/pull/14) ([GrahamCampbell](https://github.com/GrahamCampbell))
+
+- Update composer.json [\#9](https://github.com/guzzle/guzzle-services/pull/9) ([GrahamCampbell](https://github.com/GrahamCampbell))
+
+- some fixes [\#4](https://github.com/guzzle/guzzle-services/pull/4) ([cordoval](https://github.com/cordoval))
+
+- Fix the CommandException path used in ValidateInput [\#2](https://github.com/guzzle/guzzle-services/pull/2) ([mookle](https://github.com/mookle))
+
+- Minor improvements [\#1](https://github.com/guzzle/guzzle-services/pull/1) ([GrahamCampbell](https://github.com/GrahamCampbell))
+
+- Use latest guzzlehttp/command to fix dependencies [\#10](https://github.com/guzzle/guzzle-services/pull/10) ([sbward](https://github.com/sbward))
+
+- some collaboration using Gush :\) [\#3](https://github.com/guzzle/guzzle-services/pull/3) ([cordoval](https://github.com/cordoval))
+
+## [0.1.0](https://github.com/guzzle/guzzle-services/tree/0.1.0) (2014-03-15)
+
+
+
+\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*

+ 19 - 0
addons/cos/library/Guzzle/guzzle-services/LICENSE

@@ -0,0 +1,19 @@
+Copyright (c) 2014 Michael Dowling, https://github.com/mtdowling <mtdowling@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 15 - 0
addons/cos/library/Guzzle/guzzle-services/Makefile

@@ -0,0 +1,15 @@
+all: clean test
+
+test:
+	vendor/bin/phpunit
+
+coverage:
+	vendor/bin/phpunit --coverage-html=artifacts/coverage
+
+view-coverage:
+	open artifacts/coverage/index.html
+
+clean:
+	rm -rf artifacts/*
+
+.PHONY: coverage

+ 129 - 0
addons/cos/library/Guzzle/guzzle-services/README.md

@@ -0,0 +1,129 @@
+# Guzzle Services
+
+[![License](https://poser.pugx.org/guzzlehttp/guzzle-services/license)](https://packagist.org/packages/guzzlehttp/guzzle-services)
+[![Build Status](https://travis-ci.org/guzzle/guzzle-services.svg?branch=master)](https://travis-ci.org/guzzle/guzzle-services)
+[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/guzzle/guzzle-services/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/guzzle/guzzle-services/?branch=master)
+[![Code Coverage](https://scrutinizer-ci.com/g/guzzle/guzzle-services/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/guzzle/guzzle-services/?branch=master)
+[![SensioLabsInsight](https://insight.sensiolabs.com/projects/b08be676-b209-40b7-a6df-b6d13e8dff62/mini.png)](https://insight.sensiolabs.com/projects/b08be676-b209-40b7-a6df-b6d13e8dff62)
+[![Latest Stable Version](https://poser.pugx.org/guzzlehttp/guzzle-services/v/stable)](https://packagist.org/packages/guzzlehttp/guzzle-services)
+[![Latest Unstable Version](https://poser.pugx.org/guzzlehttp/guzzle-services/v/unstable)](https://packagist.org/packages/guzzlehttp/guzzle-services)
+[![Total Downloads](https://poser.pugx.org/guzzlehttp/guzzle-services/downloads)](https://packagist.org/packages/guzzlehttp/guzzle-services)
+
+Provides an implementation of the Guzzle Command library that uses Guzzle service descriptions to describe web services, serialize requests, and parse responses into easy to use model structures.
+
+```php
+use GuzzleHttp\Client;
+use GuzzleHttp\Command\Guzzle\GuzzleClient;
+use GuzzleHttp\Command\Guzzle\Description;
+
+$client = new Client();
+$description = new Description([
+	'baseUri' => 'http://httpbin.org/',
+	'operations' => [
+		'testing' => [
+			'httpMethod' => 'GET',
+			'uri' => '/get{?foo}',
+			'responseModel' => 'getResponse',
+			'parameters' => [
+				'foo' => [
+					'type' => 'string',
+					'location' => 'uri'
+				],
+				'bar' => [
+					'type' => 'string',
+					'location' => 'query'
+				]
+			]
+		]
+	],
+	'models' => [
+		'getResponse' => [
+			'type' => 'object',
+			'additionalProperties' => [
+				'location' => 'json'
+			]
+		]
+	]
+]);
+
+$guzzleClient = new GuzzleClient($client, $description);
+
+$result = $guzzleClient->testing(['foo' => 'bar']);
+echo $result['args']['foo'];
+// bar
+```
+
+## Installing
+
+This project can be installed using Composer:
+
+``composer require guzzlehttp/guzzle-services``
+
+For **Guzzle 5**, use ``composer require guzzlehttp/guzzle-services:0.6``.
+
+**Note:** If Composer is not installed [globally](https://getcomposer.org/doc/00-intro.md#globally) then you may need to run the preceding Composer commands using ``php composer.phar`` (where ``composer.phar`` is the path to your copy of Composer), instead of just ``composer``.
+
+## Plugins
+
+* Load Service description from file [https://github.com/gimler/guzzle-description-loader]
+
+## Transition guide from Guzzle 5.0 to 6.0
+ 
+### Change regarding PostField and PostFile
+
+The request locations `postField` and `postFile` were removed in favor of `formParam` and `multipart`. If your description looks like
+ 
+```php
+[
+    'baseUri' => 'http://httpbin.org/',
+    'operations' => [
+        'testing' => [
+            'httpMethod' => 'GET',
+            'uri' => '/get{?foo}',
+            'responseModel' => 'getResponse',
+            'parameters' => [
+                'foo' => [
+                    'type' => 'string',
+                    'location' => 'postField'
+                ],
+                'bar' => [
+                    'type' => 'string',
+                    'location' => 'postFile'
+                ]
+            ]
+        ]
+    ],
+]
+```
+
+you need to change `postField` to `formParam` and `postFile` to `multipart`. 
+
+More documentation coming soon.
+
+## Cookbook
+
+### Changing the way query params are serialized
+
+By default, query params are serialized using strict RFC3986 rules, using `http_build_query` method. With this, array params are serialized this way:
+
+```php
+$client->myMethod(['foo' => ['bar', 'baz']]);
+
+// Query params will be foo[0]=bar&foo[1]=baz
+```
+
+However, a lot of APIs in the wild require the numeric indices to be removed, so that the query params end up being `foo[]=bar&foo[]=baz`. You
+can easily change the behaviour by creating your own serializer and overriding the "query" request location:
+
+```php
+use GuzzleHttp\Command\Guzzle\GuzzleClient;
+use GuzzleHttp\Command\Guzzle\RequestLocation\QueryLocation;
+use GuzzleHttp\Command\Guzzle\QuerySerializer\Rfc3986Serializer;
+use GuzzleHttp\Command\Guzzle\Serializer;
+
+$queryLocation   = new QueryLocation('query', new Rfc3986Serializer(true));
+$serializer      = new Serializer($description, ['query' => $queryLocation]);
+$guzzleClient    = new GuzzleClient($client, $description, $serializer);
+```
+
+You can also create your own serializer if you have specific needs.

+ 49 - 0
addons/cos/library/Guzzle/guzzle-services/composer.json

@@ -0,0 +1,49 @@
+{
+    "name": "guzzlehttp/guzzle-services",
+    "description": "Provides an implementation of the Guzzle Command library that uses Guzzle service descriptions to describe web services, serialize requests, and parse responses into easy to use model structures.",
+    "type": "library",
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "Michael Dowling",
+            "email": "mtdowling@gmail.com",
+            "homepage": "https://github.com/mtdowling"
+        },
+        {
+            "name": "Jeremy Lindblom",
+            "email": "jeremeamia@gmail.com",
+            "homepage": "https://github.com/jeremeamia"
+        },
+        {
+            "name": "Stefano Kowalke",
+            "email": "blueduck@mail.org",
+            "homepage": "https://github.com/konafets"
+        }
+    ],
+    "require": {
+        "php": ">=5.5",
+        "guzzlehttp/guzzle": "^6.2",
+        "guzzlehttp/command": "~1.0"
+    },
+    "require-dev": {
+        "phpunit/phpunit": "~4.0"
+    },
+    "autoload": {
+        "psr-4": {
+            "GuzzleHttp\\Command\\Guzzle\\": "src/"
+        }
+    },
+    "autoload-dev": {
+        "psr-4": {
+            "GuzzleHttp\\Tests\\Command\\Guzzle\\": "tests/"
+        }
+    },
+    "suggest": {
+        "gimler/guzzle-description-loader": "^0.0.4"
+    },
+    "extra": {
+        "branch-alias": {
+            "dev-master": "1.0.x-dev"
+        }
+    }
+}

+ 14 - 0
addons/cos/library/Guzzle/guzzle-services/phpunit.xml.dist

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit bootstrap="./vendor/autoload.php"
+         colors="true">
+    <testsuites>
+        <testsuite>
+            <directory>tests</directory>
+        </testsuite>
+    </testsuites>
+    <filter>
+        <whitelist>
+            <directory suffix=".php">src</directory>
+        </whitelist>
+    </filter>
+</phpunit>

+ 265 - 0
addons/cos/library/Guzzle/guzzle-services/src/Description.php

@@ -0,0 +1,265 @@
+<?php
+namespace GuzzleHttp\Command\Guzzle;
+
+use GuzzleHttp\Psr7\Uri;
+
+/**
+ * Represents a Guzzle service description
+ */
+class Description implements DescriptionInterface
+{
+    /** @var array Array of {@see OperationInterface} objects */
+    private $operations = [];
+
+    /** @var array Array of API models */
+    private $models = [];
+
+    /** @var string Name of the API */
+    private $name;
+
+    /** @var string API version */
+    private $apiVersion;
+
+    /** @var string Summary of the API */
+    private $description;
+
+    /** @var array Any extra API data */
+    private $extraData = [];
+
+    /** @var Uri baseUri/basePath */
+    private $baseUri;
+
+    /** @var SchemaFormatter */
+    private $formatter;
+
+    /**
+     * @param array $config  Service description data
+     * @param array $options Custom options to apply to the description
+     *     - formatter: Can provide a custom SchemaFormatter class
+     *
+     * @throws \InvalidArgumentException
+     */
+    public function __construct(array $config, array $options = [])
+    {
+        // Keep a list of default keys used in service descriptions that is
+        // later used to determine extra data keys.
+        static $defaultKeys = ['name', 'models', 'apiVersion', 'description'];
+
+        // Pull in the default configuration values
+        foreach ($defaultKeys as $key) {
+            if (isset($config[$key])) {
+                $this->{$key} = $config[$key];
+            }
+        }
+
+        // Set the baseUri
+        // Account for the old style of using baseUrl
+        if (isset($config['baseUrl'])) {
+            $config['baseUri'] = $config['baseUrl'];
+        }
+        $this->baseUri = isset($config['baseUri']) ? new Uri($config['baseUri']) : new Uri();
+
+        // Ensure that the models and operations properties are always arrays
+        $this->models = (array) $this->models;
+        $this->operations = (array) $this->operations;
+
+        // We want to add operations differently than adding the other properties
+        $defaultKeys[] = 'operations';
+
+        // Create operations for each operation
+        if (isset($config['operations'])) {
+            foreach ($config['operations'] as $name => $operation) {
+                if (!is_array($operation)) {
+                    throw new \InvalidArgumentException('Operations must be arrays');
+                }
+                $this->operations[$name] = $operation;
+            }
+        }
+
+        // Get all of the additional properties of the service description and
+        // store them in a data array
+        foreach (array_diff(array_keys($config), $defaultKeys) as $key) {
+            $this->extraData[$key] = $config[$key];
+        }
+
+        // Configure the schema formatter
+        if (isset($options['formatter'])) {
+            $this->formatter = $options['formatter'];
+        } else {
+            static $defaultFormatter;
+            if (!$defaultFormatter) {
+                $defaultFormatter = new SchemaFormatter();
+            }
+            $this->formatter = $defaultFormatter;
+        }
+    }
+
+    /**
+     * Get the basePath/baseUri of the description
+     *
+     * @return Uri
+     */
+    public function getBaseUri()
+    {
+        return $this->baseUri;
+    }
+
+    /**
+     * Get the API operations of the service
+     *
+     * @return Operation[] Returns an array of {@see Operation} objects
+     */
+    public function getOperations()
+    {
+        return $this->operations;
+    }
+
+    /**
+     * Check if the service has an operation by name
+     *
+     * @param string $name Name of the operation to check
+     *
+     * @return bool
+     */
+    public function hasOperation($name)
+    {
+        return isset($this->operations[$name]);
+    }
+
+    /**
+     * Get an API operation by name
+     *
+     * @param string $name Name of the command
+     *
+     * @return Operation
+     * @throws \InvalidArgumentException if the operation is not found
+     */
+    public function getOperation($name)
+    {
+        if (!$this->hasOperation($name)) {
+            throw new \InvalidArgumentException("No operation found named $name");
+        }
+
+        // Lazily create operations as they are retrieved
+        if (!($this->operations[$name] instanceof Operation)) {
+            $this->operations[$name]['name'] = $name;
+            $this->operations[$name] = new Operation($this->operations[$name], $this);
+        }
+
+        return $this->operations[$name];
+    }
+
+    /**
+     * Get a shared definition structure.
+     *
+     * @param string $id ID/name of the model to retrieve
+     *
+     * @return Parameter
+     * @throws \InvalidArgumentException if the model is not found
+     */
+    public function getModel($id)
+    {
+        if (!$this->hasModel($id)) {
+            throw new \InvalidArgumentException("No model found named $id");
+        }
+
+        // Lazily create models as they are retrieved
+        if (!($this->models[$id] instanceof Parameter)) {
+            $this->models[$id] = new Parameter(
+                $this->models[$id],
+                ['description' => $this]
+            );
+        }
+
+        return $this->models[$id];
+    }
+
+    /**
+     * Get all models of the service description.
+     *
+     * @return array
+     */
+    public function getModels()
+    {
+        $models = [];
+        foreach ($this->models as $name => $model) {
+            $models[$name] = $this->getModel($name);
+        }
+
+        return $models;
+    }
+
+    /**
+     * Check if the service description has a model by name.
+     *
+     * @param string $id Name/ID of the model to check
+     *
+     * @return bool
+     */
+    public function hasModel($id)
+    {
+        return isset($this->models[$id]);
+    }
+
+    /**
+     * Get the API version of the service
+     *
+     * @return string
+     */
+    public function getApiVersion()
+    {
+        return $this->apiVersion;
+    }
+
+    /**
+     * Get the name of the API
+     *
+     * @return string
+     */
+    public function getName()
+    {
+        return $this->name;
+    }
+
+    /**
+     * Get a summary of the purpose of the API
+     *
+     * @return string
+     */
+    public function getDescription()
+    {
+        return $this->description;
+    }
+
+    /**
+     * Format a parameter using named formats.
+     *
+     * @param string $format Format to convert it to
+     * @param mixed  $input  Input string
+     *
+     * @return mixed
+     */
+    public function format($format, $input)
+    {
+        return $this->formatter->format($format, $input);
+    }
+
+    /**
+     * Get arbitrary data from the service description that is not part of the
+     * Guzzle service description specification.
+     *
+     * @param string $key Data key to retrieve or null to retrieve all extra
+     *
+     * @return null|mixed
+     */
+    public function getData($key = null)
+    {
+        if ($key === null) {
+            return $this->extraData;
+        } elseif (isset($this->extraData[$key])) {
+            return $this->extraData[$key];
+        } else {
+            return null;
+        }
+    }
+}

+ 107 - 0
addons/cos/library/Guzzle/guzzle-services/src/DescriptionInterface.php

@@ -0,0 +1,107 @@
+<?php
+namespace GuzzleHttp\Command\Guzzle;
+
+use GuzzleHttp\Psr7\Uri;
+
+interface DescriptionInterface
+{
+    /**
+     * Get the basePath/baseUri of the description
+     *
+     * @return Uri
+     */
+    public function getBaseUri();
+
+    /**
+     * Get the API operations of the service
+     *
+     * @return Operation[] Returns an array of {@see Operation} objects
+     */
+    public function getOperations();
+
+    /**
+     * Check if the service has an operation by name
+     *
+     * @param string $name Name of the operation to check
+     *
+     * @return bool
+     */
+    public function hasOperation($name);
+
+    /**
+     * Get an API operation by name
+     *
+     * @param string $name Name of the command
+     *
+     * @return Operation
+     * @throws \InvalidArgumentException if the operation is not found
+     */
+    public function getOperation($name);
+
+    /**
+     * Get a shared definition structure.
+     *
+     * @param string $id ID/name of the model to retrieve
+     *
+     * @return Parameter
+     * @throws \InvalidArgumentException if the model is not found
+     */
+    public function getModel($id);
+
+    /**
+     * Get all models of the service description.
+     *
+     * @return array
+     */
+    public function getModels();
+
+    /**
+     * Check if the service description has a model by name.
+     *
+     * @param string $id Name/ID of the model to check
+     *
+     * @return bool
+     */
+    public function hasModel($id);
+
+    /**
+     * Get the API version of the service
+     *
+     * @return string
+     */
+    public function getApiVersion();
+
+    /**
+     * Get the name of the API
+     *
+     * @return string
+     */
+    public function getName();
+
+    /**
+     * Get a summary of the purpose of the API
+     *
+     * @return string
+     */
+    public function getDescription();
+
+    /**
+     * Format a parameter using named formats.
+     *
+     * @param string $format Format to convert it to
+     * @param mixed  $input  Input string
+     *
+     * @return mixed
+     */
+    public function format($format, $input);
+
+    /**
+     * Get arbitrary data from the service description that is not part of the
+     * Guzzle service description specification.
+     *
+     * @param string $key Data key to retrieve or null to retrieve all extra
+     *
+     * @return null|mixed
+     */
+    public function getData($key = null);
+}

+ 294 - 0
addons/cos/library/Guzzle/guzzle-services/src/Deserializer.php

@@ -0,0 +1,294 @@
+<?php
+namespace GuzzleHttp\Command\Guzzle;
+
+use GuzzleHttp\Command\CommandInterface;
+use GuzzleHttp\Command\Guzzle\ResponseLocation\BodyLocation;
+use GuzzleHttp\Command\Guzzle\ResponseLocation\HeaderLocation;
+use GuzzleHttp\Command\Guzzle\ResponseLocation\JsonLocation;
+use GuzzleHttp\Command\Guzzle\ResponseLocation\ReasonPhraseLocation;
+use GuzzleHttp\Command\Guzzle\ResponseLocation\ResponseLocationInterface;
+use GuzzleHttp\Command\Guzzle\ResponseLocation\StatusCodeLocation;
+use GuzzleHttp\Command\Guzzle\ResponseLocation\XmlLocation;
+use GuzzleHttp\Command\Result;
+use GuzzleHttp\Command\ResultInterface;
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
+
+/**
+ * Handler used to create response models based on an HTTP response and
+ * a service description.
+ *
+ * Response location visitors are registered with this Handler to handle
+ * locations (e.g., 'xml', 'json', 'header'). All of the locations of a response
+ * model that will be visited first have their ``before`` method triggered.
+ * After the before method is called on every visitor that will be walked, each
+ * visitor is triggered using the ``visit()`` method. After all of the visitors
+ * are visited, the ``after()`` method is called on each visitor. This is the
+ * place in which you should handle things like additionalProperties with
+ * custom locations (i.e., this is how it is handled in the JSON visitor).
+ */
+class Deserializer
+{
+    /** @var ResponseLocationInterface[] $responseLocations */
+    private $responseLocations;
+
+    /** @var DescriptionInterface $description */
+    private $description;
+
+    /** @var boolean $process */
+    private $process;
+
+    /**
+     * @param DescriptionInterface $description
+     * @param bool $process
+     * @param ResponseLocationInterface[] $responseLocations Extra response locations
+     */
+    public function __construct(
+        DescriptionInterface $description,
+        $process,
+        array $responseLocations = []
+    ) {
+        static $defaultResponseLocations;
+        if (!$defaultResponseLocations) {
+            $defaultResponseLocations = [
+                'body'         => new BodyLocation(),
+                'header'       => new HeaderLocation(),
+                'reasonPhrase' => new ReasonPhraseLocation(),
+                'statusCode'   => new StatusCodeLocation(),
+                'xml'          => new XmlLocation(),
+                'json'         => new JsonLocation(),
+            ];
+        }
+
+        $this->responseLocations = $responseLocations + $defaultResponseLocations;
+        $this->description = $description;
+        $this->process = $process;
+    }
+
+    /**
+     * Deserialize the response into the specified result representation
+     *
+     * @param ResponseInterface     $response
+     * @param RequestInterface|null $request
+     * @param CommandInterface      $command
+     * @return Result|ResultInterface|void|ResponseInterface
+     */
+    public function __invoke(ResponseInterface $response, RequestInterface $request, CommandInterface $command)
+    {
+        // If the user don't want to process the result, just return the plain response here
+        if ($this->process === false) {
+            return $response;
+        }
+
+        $name = $command->getName();
+        $operation = $this->description->getOperation($name);
+
+        $this->handleErrorResponses($response, $request, $command, $operation);
+
+        // Add a default Model as the result if no matching schema was found
+        if (!($modelName = $operation->getResponseModel())) {
+            // Not sure if this should be empty or contains the response.
+            // Decided to do it how it was in the old version for now.
+            return new Result();
+        }
+
+        $model = $operation->getServiceDescription()->getModel($modelName);
+        if (!$model) {
+            throw new \RuntimeException("Unknown model: {$modelName}");
+        }
+
+        return $this->visit($model, $response);
+    }
+
+    /**
+     * Handles visit() and after() methods of the Response locations
+     *
+     * @param Parameter         $model
+     * @param ResponseInterface $response
+     * @return Result|ResultInterface|void
+     */
+    protected function visit(Parameter $model, ResponseInterface $response)
+    {
+        $result = new Result();
+        $context = ['visitors' => []];
+
+        if ($model->getType() === 'object') {
+            $result = $this->visitOuterObject($model, $result, $response, $context);
+        } elseif ($model->getType() === 'array') {
+            $result = $this->visitOuterArray($model, $result, $response, $context);
+        } else {
+            throw new \InvalidArgumentException('Invalid response model: ' . $model->getType());
+        }
+
+        // Call the after() method of each found visitor
+        /** @var ResponseLocationInterface $visitor */
+        foreach ($context['visitors'] as $visitor) {
+            $result = $visitor->after($result, $response, $model);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Handles the before() method of Response locations
+     *
+     * @param string            $location
+     * @param Parameter         $model
+     * @param ResultInterface   $result
+     * @param ResponseInterface $response
+     * @param array             $context
+     * @return ResultInterface
+     */
+    private function triggerBeforeVisitor(
+        $location,
+        Parameter $model,
+        ResultInterface $result,
+        ResponseInterface $response,
+        array &$context
+    ) {
+        if (!isset($this->responseLocations[$location])) {
+            throw new \RuntimeException("Unknown location: $location");
+        }
+
+        $context['visitors'][$location] = $this->responseLocations[$location];
+
+        $result = $this->responseLocations[$location]->before(
+            $result,
+            $response,
+            $model
+        );
+
+        return $result;
+    }
+
+    /**
+     * Visits the outer object
+     *
+     * @param Parameter         $model
+     * @param ResultInterface   $result
+     * @param ResponseInterface $response
+     * @param array             $context
+     * @return ResultInterface
+     */
+    private function visitOuterObject(
+        Parameter $model,
+        ResultInterface $result,
+        ResponseInterface $response,
+        array &$context
+    ) {
+        $parentLocation = $model->getLocation();
+
+        // If top-level additionalProperties is a schema, then visit it
+        $additional = $model->getAdditionalProperties();
+        if ($additional instanceof Parameter) {
+            // Use the model location if none set on additionalProperties.
+            $location = $additional->getLocation() ?: $parentLocation;
+            $result = $this->triggerBeforeVisitor($location, $model, $result, $response, $context);
+        }
+
+        // Use 'location' from all individual defined properties, but fall back
+        // to the model location if no per-property location is set. Collect
+        // the properties that need to be visited into an array.
+        $visitProperties = [];
+        foreach ($model->getProperties() as $schema) {
+            $location = $schema->getLocation() ?: $parentLocation;
+            if ($location) {
+                $visitProperties[] = [$location, $schema];
+                // Trigger the before method on each unique visitor location
+                if (!isset($context['visitors'][$location])) {
+                    $result = $this->triggerBeforeVisitor($location, $model, $result, $response, $context);
+                }
+            }
+        }
+
+        // Actually visit each response element
+        foreach ($visitProperties as $property) {
+            $result = $this->responseLocations[$property[0]]->visit($result, $response, $property[1]);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Visits the outer array
+     *
+     * @param Parameter         $model
+     * @param ResultInterface   $result
+     * @param ResponseInterface $response
+     * @param array             $context
+     * @return ResultInterface|void
+     */
+    private function visitOuterArray(
+        Parameter $model,
+        ResultInterface $result,
+        ResponseInterface $response,
+        array &$context
+    ) {
+        // Use 'location' defined on the top of the model
+        if (!($location = $model->getLocation())) {
+            return;
+        }
+
+        // Trigger the before method on each unique visitor location
+        if (!isset($context['visitors'][$location])) {
+            $result = $this->triggerBeforeVisitor($location, $model, $result, $response, $context);
+        }
+
+        // Visit each item in the response
+        $result = $this->responseLocations[$location]->visit($result, $response, $model);
+
+        return $result;
+    }
+
+    /**
+     * Reads the "errorResponses" from commands, and trigger appropriate exceptions
+     *
+     * In order for the exception to be properly triggered, all your exceptions must be instance
+     * of "GuzzleHttp\Command\Exception\CommandException". If that's not the case, your exceptions will be wrapped
+     * around a CommandException
+     *
+     * @param ResponseInterface $response
+     * @param RequestInterface  $request
+     * @param CommandInterface  $command
+     * @param Operation         $operation
+     */
+    protected function handleErrorResponses(
+        ResponseInterface $response,
+        RequestInterface $request,
+        CommandInterface $command,
+        Operation $operation
+    ) {
+        $errors = $operation->getErrorResponses();
+
+        // We iterate through each errors in service description. If the descriptor contains both a phrase and
+        // status code, there must be an exact match of both. Otherwise, a match of status code is enough
+        $bestException = null;
+
+        foreach ($errors as $error) {
+            $code = (int) $error['code'];
+
+            if ($response->getStatusCode() !== $code) {
+                continue;
+            }
+
+            if (isset($error['phrase']) && ! ($error['phrase'] === $response->getReasonPhrase())) {
+                continue;
+            }
+
+            $bestException = $error['class'];
+
+            // If there is an exact match of phrase + code, then we cannot find a more specialized exception in
+            // the array, so we can break early instead of iterating the remaining ones
+            if (isset($error['phrase'])) {
+                break;
+            }
+        }
+
+        if (null !== $bestException) {
+            throw new $bestException($response->getReasonPhrase(), $command, null, $request, $response);
+        }
+
+        // If we reach here, no exception could be match from descriptor, and Guzzle exception will propagate if
+        // option "http_errors" is set to true, which is the default setting.
+    }
+}

+ 169 - 0
addons/cos/library/Guzzle/guzzle-services/src/GuzzleClient.php

@@ -0,0 +1,169 @@
+<?php
+namespace GuzzleHttp\Command\Guzzle;
+
+use GuzzleHttp\ClientInterface;
+use GuzzleHttp\Command\CommandInterface;
+use GuzzleHttp\Command\Guzzle\Handler\ValidatedDescriptionHandler;
+use GuzzleHttp\Command\ServiceClient;
+use GuzzleHttp\HandlerStack;
+
+/**
+ * Default Guzzle web service client implementation.
+ */
+class GuzzleClient extends ServiceClient
+{
+    /** @var array $config */
+    private $config;
+
+    /** @var DescriptionInterface Guzzle service description */
+    private $description;
+
+    /**
+     * The client constructor accepts an associative array of configuration
+     * options:
+     *
+     * - defaults: Associative array of default command parameters to add to
+     *   each command created by the client.
+     * - validate: Specify if command input is validated (defaults to true).
+     *   Changing this setting after the client has been created will have no
+     *   effect.
+     * - process: Specify if HTTP responses are parsed (defaults to true).
+     *   Changing this setting after the client has been created will have no
+     *   effect.
+     * - response_locations: Associative array of location types mapping to
+     *   ResponseLocationInterface objects.
+     *
+     * @param ClientInterface $client HTTP client to use.
+     * @param DescriptionInterface $description Guzzle service description
+     * @param callable $commandToRequestTransformer
+     * @param callable $responseToResultTransformer
+     * @param HandlerStack $commandHandlerStack
+     * @param array $config Configuration options
+     */
+    public function __construct(
+        ClientInterface $client,
+        DescriptionInterface $description,
+        callable $commandToRequestTransformer = null,
+        callable $responseToResultTransformer = null,
+        HandlerStack $commandHandlerStack = null,
+        array $config = []
+    ) {
+        $this->config = $config;
+        $this->description = $description;
+        $serializer = $this->getSerializer($commandToRequestTransformer);
+        $deserializer = $this->getDeserializer($responseToResultTransformer);
+
+        parent::__construct($client, $serializer, $deserializer, $commandHandlerStack);
+        $this->processConfig($config);
+    }
+
+    /**
+     * Returns the command if valid; otherwise an Exception
+     * @param string $name
+     * @param array  $args
+     * @return CommandInterface
+     * @throws \InvalidArgumentException
+     */
+    public function getCommand($name, array $args = [])
+    {
+        if (!$this->description->hasOperation($name)) {
+            $name = ucfirst($name);
+            if (!$this->description->hasOperation($name)) {
+                throw new \InvalidArgumentException(
+                    "No operation found named {$name}"
+                );
+            }
+        }
+
+        // Merge in default command options
+        $args += $this->getConfig('defaults');
+
+        return parent::getCommand($name, $args);
+    }
+
+    /**
+     * Return the description
+     *
+     * @return DescriptionInterface
+     */
+    public function getDescription()
+    {
+        return $this->description;
+    }
+
+    /**
+     * Returns the passed Serializer when set, a new instance otherwise
+     *
+     * @param callable|null $commandToRequestTransformer
+     * @return \GuzzleHttp\Command\Guzzle\Serializer
+     */
+    private function getSerializer($commandToRequestTransformer)
+    {
+        return $commandToRequestTransformer ==! null
+            ? $commandToRequestTransformer
+            : new Serializer($this->description);
+    }
+
+    /**
+     * Returns the passed Deserializer when set, a new instance otherwise
+     *
+     * @param callable|null $responseToResultTransformer
+     * @return \GuzzleHttp\Command\Guzzle\Deserializer
+     */
+    private function getDeserializer($responseToResultTransformer)
+    {
+        $process = (! isset($this->config['process']) || $this->config['process'] === true);
+
+        return $responseToResultTransformer ==! null
+            ? $responseToResultTransformer
+            : new Deserializer($this->description, $process);
+    }
+
+    /**
+     * Get the config of the client
+     *
+     * @param array|string $option
+     * @return mixed
+     */
+    public function getConfig($option = null)
+    {
+        return $option === null
+            ? $this->config
+            : (isset($this->config[$option]) ? $this->config[$option] : []);
+    }
+
+    /**
+     * @param $option
+     * @param $value
+     */
+    public function setConfig($option, $value)
+    {
+        $this->config[$option] = $value;
+    }
+
+    /**
+     * Prepares the client based on the configuration settings of the client.
+     *
+     * @param array $config Constructor config as an array
+     */
+    protected function processConfig(array $config)
+    {
+        // set defaults as an array if not provided
+        if (!isset($config['defaults'])) {
+            $config['defaults'] = [];
+        }
+
+        // Add the handlers based on the configuration option
+        $stack = $this->getHandlerStack();
+
+        if (!isset($config['validate']) || $config['validate'] === true) {
+            $stack->push(new ValidatedDescriptionHandler($this->description), 'validate_description');
+        }
+
+        if (!isset($config['process']) || $config['process'] === true) {
+            // TODO: This belongs to the Deserializer and should be handled there.
+            // Question: What is the result when the Deserializer is bypassed?
+            // Possible answer: The raw response.
+        }
+    }
+}

+ 82 - 0
addons/cos/library/Guzzle/guzzle-services/src/Handler/ValidatedDescriptionHandler.php

@@ -0,0 +1,82 @@
+<?php namespace GuzzleHttp\Command\Guzzle\Handler;
+
+use GuzzleHttp\Command\CommandInterface;
+use GuzzleHttp\Command\Exception\CommandException;
+use GuzzleHttp\Command\Guzzle\DescriptionInterface;
+use GuzzleHttp\Command\Guzzle\SchemaValidator;
+
+/**
+ * Handler used to validate command input against a service description.
+ *
+ * @author Stefano Kowalke <info@arroba-it.de>
+ */
+class ValidatedDescriptionHandler
+{
+    /** @var SchemaValidator $validator */
+    private $validator;
+
+    /** @var DescriptionInterface $description */
+    private $description;
+
+    /**
+     * ValidatedDescriptionHandler constructor.
+     *
+     * @param DescriptionInterface $description
+     * @param SchemaValidator|null $schemaValidator
+     */
+    public function __construct(DescriptionInterface $description, SchemaValidator $schemaValidator = null)
+    {
+        $this->description = $description;
+        $this->validator = $schemaValidator ?: new SchemaValidator();
+    }
+
+    /**
+     * @param callable $handler
+     * @return \Closure
+     */
+    public function __invoke(callable $handler)
+    {
+        return function (CommandInterface $command) use ($handler) {
+            $errors = [];
+            $operation = $this->description->getOperation($command->getName());
+
+            foreach ($operation->getParams() as $name => $schema) {
+                $value = $command[$name];
+
+                if ($value) {
+                    $value = $schema->filter($value);
+                }
+
+                if (! $this->validator->validate($schema, $value)) {
+                    $errors = array_merge($errors, $this->validator->getErrors());
+                } elseif ($value !== $command[$name]) {
+                    // Update the config value if it changed and no validation errors were encountered.
+                    // This happen when the user extending an operation
+                    // See https://github.com/guzzle/guzzle-services/issues/145
+                    $command[$name] = $value;
+                }
+            }
+
+            if ($params = $operation->getAdditionalParameters()) {
+                foreach ($command->toArray() as $name => $value) {
+                    // It's only additional if it isn't defined in the schema
+                    if (! $operation->hasParam($name)) {
+                        // Always set the name so that error messages are useful
+                        $params->setName($name);
+                        if (! $this->validator->validate($params, $value)) {
+                            $errors = array_merge($errors, $this->validator->getErrors());
+                        } elseif ($value !== $command[$name]) {
+                            $command[$name] = $value;
+                        }
+                    }
+                }
+            }
+
+            if ($errors) {
+                throw new CommandException('Validation errors: ' . implode("\n", $errors), $command);
+            }
+
+            return $handler($command);
+        };
+    }
+}

+ 312 - 0
addons/cos/library/Guzzle/guzzle-services/src/Operation.php

@@ -0,0 +1,312 @@
+<?php
+namespace GuzzleHttp\Command\Guzzle;
+
+use GuzzleHttp\Command\ToArrayInterface;
+
+/**
+ * Guzzle operation
+ */
+class Operation implements ToArrayInterface
+{
+    /** @var array Parameters */
+    private $parameters = [];
+
+    /** @var Parameter Additional parameters schema */
+    private $additionalParameters;
+
+    /** @var DescriptionInterface */
+    private $description;
+
+    /** @var array Config data */
+    private $config;
+
+    /**
+     * Builds an Operation object using an array of configuration data.
+     *
+     * - name: (string) Name of the command
+     * - httpMethod: (string) HTTP method of the operation
+     * - uri: (string) URI template that can create a relative or absolute URL
+     * - parameters: (array) Associative array of parameters for the command.
+     *   Each value must be an array that is used to create {@see Parameter}
+     *   objects.
+     * - summary: (string) This is a short summary of what the operation does
+     * - notes: (string) A longer description of the operation.
+     * - documentationUrl: (string) Reference URL providing more information
+     *   about the operation.
+     * - responseModel: (string) The model name used for processing response.
+     * - deprecated: (bool) Set to true if this is a deprecated command
+     * - errorResponses: (array) Errors that could occur when executing the
+     *   command. Array of hashes, each with a 'code' (the HTTP response code),
+     *   'phrase' (response reason phrase or description of the error), and
+     *   'class' (a custom exception class that would be thrown if the error is
+     *   encountered).
+     * - data: (array) Any extra data that might be used to help build or
+     *   serialize the operation
+     * - additionalParameters: (null|array) Parameter schema to use when an
+     *   option is passed to the operation that is not in the schema
+     *
+     * @param array                 $config      Array of configuration data
+     * @param DescriptionInterface  $description Service description used to resolve models if $ref tags are found
+     * @throws \InvalidArgumentException
+     */
+    public function __construct(array $config = [], DescriptionInterface $description = null)
+    {
+        static $defaults = [
+            'name' => '',
+            'httpMethod' => '',
+            'uri' => '',
+            'responseModel' => null,
+            'notes' => '',
+            'summary' => '',
+            'documentationUrl' => null,
+            'deprecated' => false,
+            'data' => [],
+            'parameters' => [],
+            'additionalParameters' => null,
+            'errorResponses' => []
+        ];
+
+        $this->description = $description === null ? new Description([]) : $description;
+
+        if (isset($config['extends'])) {
+            $config = $this->resolveExtends($config['extends'], $config);
+        }
+
+        $this->config = $config + $defaults;
+
+        // Account for the old style of using responseClass
+        if (isset($config['responseClass'])) {
+            $this->config['responseModel'] = $config['responseClass'];
+        }
+
+        $this->resolveParameters();
+    }
+
+    /**
+     * @return array
+     */
+    public function toArray()
+    {
+        return $this->config;
+    }
+
+    /**
+     * Get the service description that the operation belongs to
+     *
+     * @return Description
+     */
+    public function getServiceDescription()
+    {
+        return $this->description;
+    }
+
+    /**
+     * Get the params of the operation
+     *
+     * @return Parameter[]
+     */
+    public function getParams()
+    {
+        return $this->parameters;
+    }
+
+    /**
+     * Get additionalParameters of the operation
+     *
+     * @return Parameter|null
+     */
+    public function getAdditionalParameters()
+    {
+        return $this->additionalParameters;
+    }
+
+    /**
+     * Check if the operation has a specific parameter by name
+     *
+     * @param string $name Name of the param
+     *
+     * @return bool
+     */
+    public function hasParam($name)
+    {
+        return isset($this->parameters[$name]);
+    }
+
+    /**
+     * Get a single parameter of the operation
+     *
+     * @param string $name Parameter to retrieve by name
+     *
+     * @return Parameter|null
+     */
+    public function getParam($name)
+    {
+        return isset($this->parameters[$name])
+            ? $this->parameters[$name]
+            : null;
+    }
+
+    /**
+     * Get the HTTP method of the operation
+     *
+     * @return string|null
+     */
+    public function getHttpMethod()
+    {
+        return $this->config['httpMethod'];
+    }
+
+    /**
+     * Get the name of the operation
+     *
+     * @return string|null
+     */
+    public function getName()
+    {
+        return $this->config['name'];
+    }
+
+    /**
+     * Get a short summary of what the operation does
+     *
+     * @return string|null
+     */
+    public function getSummary()
+    {
+        return $this->config['summary'];
+    }
+
+    /**
+     * Get a longer text field to explain the behavior of the operation
+     *
+     * @return string|null
+     */
+    public function getNotes()
+    {
+        return $this->config['notes'];
+    }
+
+    /**
+     * Get the documentation URL of the operation
+     *
+     * @return string|null
+     */
+    public function getDocumentationUrl()
+    {
+        return $this->config['documentationUrl'];
+    }
+
+    /**
+     * Get the name of the model used for processing the response.
+     *
+     * @return string
+     */
+    public function getResponseModel()
+    {
+        return $this->config['responseModel'];
+    }
+
+    /**
+     * Get whether or not the operation is deprecated
+     *
+     * @return bool
+     */
+    public function getDeprecated()
+    {
+        return $this->config['deprecated'];
+    }
+
+    /**
+     * Get the URI that will be merged into the generated request
+     *
+     * @return string
+     */
+    public function getUri()
+    {
+        return $this->config['uri'];
+    }
+
+    /**
+     * Get the errors that could be encountered when executing the operation
+     *
+     * @return array
+     */
+    public function getErrorResponses()
+    {
+        return $this->config['errorResponses'];
+    }
+
+    /**
+     * Get extra data from the operation
+     *
+     * @param string $name Name of the data point to retrieve or null to
+     *     retrieve all of the extra data.
+     *
+     * @return mixed|null
+     */
+    public function getData($name = null)
+    {
+        if ($name === null) {
+            return $this->config['data'];
+        } elseif (isset($this->config['data'][$name])) {
+            return $this->config['data'][$name];
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * @param $name
+     * @param array $config
+     * @return array
+     */
+    private function resolveExtends($name, array $config)
+    {
+        if (!$this->description->hasOperation($name)) {
+            throw new \InvalidArgumentException('No operation named ' . $name);
+        }
+
+        // Merge parameters together one level deep
+        $base = $this->description->getOperation($name)->toArray();
+        $result = $config + $base;
+
+        if (isset($base['parameters']) && isset($config['parameters'])) {
+            $result['parameters'] = $config['parameters'] + $base['parameters'];
+        }
+
+        return $result;
+    }
+
+    /**
+     * Process the description and extract the parameter config
+     *
+     * @return void
+     */
+    private function resolveParameters()
+    {
+        // Parameters need special handling when adding
+        foreach ($this->config['parameters'] as $name => $param) {
+            if (!is_array($param)) {
+                throw new \InvalidArgumentException(
+                    "Parameters must be arrays, {$this->config['name']}.$name is ".gettype($param)
+                );
+            }
+            $param['name'] = $name;
+            $this->parameters[$name] = new Parameter(
+                $param,
+                ['description' => $this->description]
+            );
+        }
+
+        if ($this->config['additionalParameters']) {
+            if (is_array($this->config['additionalParameters'])) {
+                $this->additionalParameters = new Parameter(
+                    $this->config['additionalParameters'],
+                    ['description' => $this->description]
+                );
+            } else {
+                $this->additionalParameters = $this->config['additionalParameters'];
+            }
+        }
+    }
+}

+ 655 - 0
addons/cos/library/Guzzle/guzzle-services/src/Parameter.php

@@ -0,0 +1,655 @@
+<?php
+namespace GuzzleHttp\Command\Guzzle;
+
+use GuzzleHttp\Command\ToArrayInterface;
+
+/**
+ * API parameter object used with service descriptions
+ */
+class Parameter implements ToArrayInterface
+{
+    private $originalData;
+
+    /** @var string $name */
+    private $name;
+
+    /** @var string $description */
+    private $description;
+
+    /** @var string|array $type */
+    private $type;
+
+    /** @var bool $required*/
+    private $required;
+
+    /** @var array|null $enum */
+    private $enum;
+
+    /** @var string $pattern */
+    private $pattern;
+
+    /** @var int $minimum*/
+    private $minimum;
+
+    /** @var int $maximum */
+    private $maximum;
+
+    /** @var int $minLength */
+    private $minLength;
+
+    /** @var int $maxLength */
+    private $maxLength;
+
+    /** @var int $minItems */
+    private $minItems;
+
+    /** @var int $maxItems */
+    private $maxItems;
+
+    /** @var mixed $default */
+    private $default;
+
+    /** @var bool $static */
+    private $static;
+
+    /** @var array $filters */
+    private $filters;
+
+    /** @var string $location */
+    private $location;
+
+    /** @var string $sentAs */
+    private $sentAs;
+
+    /** @var array $data */
+    private $data;
+
+    /** @var array $properties */
+    private $properties = [];
+
+    /** @var array|bool|Parameter $additionalProperties */
+    private $additionalProperties;
+
+    /** @var array|Parameter $items */
+    private $items;
+
+    /** @var string $format */
+    private $format;
+
+    private $propertiesCache = null;
+
+    /** @var Description */
+    private $serviceDescription;
+
+    /**
+     * Create a new Parameter using an associative array of data.
+     *
+     * The array can contain the following information:
+     *
+     * - name: (string) Unique name of the parameter
+     *
+     * - type: (string|array) Type of variable (string, number, integer,
+     *   boolean, object, array, numeric, null, any). Types are used for
+     *   validation and determining the structure of a parameter. You can use a
+     *   union type by providing an array of simple types. If one of the union
+     *   types matches the provided value, then the value is valid.
+     *
+     * - required: (bool) Whether or not the parameter is required
+     *
+     * - default: (mixed) Default value to use if no value is supplied
+     *
+     * - static: (bool) Set to true to specify that the parameter value cannot
+     *   be changed from the default.
+     *
+     * - description: (string) Documentation of the parameter
+     *
+     * - location: (string) The location of a request used to apply a parameter.
+     *   Custom locations can be registered with a command, but the defaults
+     *   are uri, query, header, body, json, xml, formParam, multipart.
+     *
+     * - sentAs: (string) Specifies how the data being modeled is sent over the
+     *   wire. For example, you may wish to include certain headers in a
+     *   response model that have a normalized casing of FooBar, but the actual
+     *   header is x-foo-bar. In this case, sentAs would be set to x-foo-bar.
+     *
+     * - filters: (array) Array of static method names to run a parameter
+     *   value through. Each value in the array must be a string containing the
+     *   full class path to a static method or an array of complex filter
+     *   information. You can specify static methods of classes using the full
+     *   namespace class name followed by '::' (e.g. Foo\Bar::baz). Some
+     *   filters require arguments in order to properly filter a value. For
+     *   complex filters, use a hash containing a 'method' key pointing to a
+     *   static method, and an 'args' key containing an array of positional
+     *   arguments to pass to the method. Arguments can contain keywords that
+     *   are replaced when filtering a value: '@value' is replaced with the
+     *   value being validated, '@api' is replaced with the Parameter object.
+     *
+     * - properties: When the type is an object, you can specify nested parameters
+     *
+     * - additionalProperties: (array) This attribute defines a schema for all
+     *   properties that are not explicitly defined in an object type
+     *   definition. If specified, the value MUST be a schema or a boolean. If
+     *   false is provided, no additional properties are allowed beyond the
+     *   properties defined in the schema. The default value is an empty schema
+     *   which allows any value for additional properties.
+     *
+     * - items: This attribute defines the allowed items in an instance array,
+     *   and MUST be a schema or an array of schemas. The default value is an
+     *   empty schema which allows any value for items in the instance array.
+     *   When this attribute value is a schema and the instance value is an
+     *   array, then all the items in the array MUST be valid according to the
+     *   schema.
+     *
+     * - pattern: When the type is a string, you can specify the regex pattern
+     *   that a value must match
+     *
+     * - enum: When the type is a string, you can specify a list of acceptable
+     *   values.
+     *
+     * - minItems: (int) Minimum number of items allowed in an array
+     *
+     * - maxItems: (int) Maximum number of items allowed in an array
+     *
+     * - minLength: (int) Minimum length of a string
+     *
+     * - maxLength: (int) Maximum length of a string
+     *
+     * - minimum: (int) Minimum value of an integer
+     *
+     * - maximum: (int) Maximum value of an integer
+     *
+     * - data: (array) Any additional custom data to use when serializing,
+     *   validating, etc
+     *
+     * - format: (string) Format used to coax a value into the correct format
+     *   when serializing or unserializing. You may specify either an array of
+     *   filters OR a format, but not both. Supported values: date-time, date,
+     *   time, timestamp, date-time-http, and boolean-string.
+     *
+     * - $ref: (string) String referencing a service description model. The
+     *   parameter is replaced by the schema contained in the model.
+     *
+     * @param array $data    Array of data as seen in service descriptions
+     * @param array $options Options used when creating the parameter. You can
+     *     specify a Guzzle service description in the 'description' key.
+     *
+     * @throws \InvalidArgumentException
+     */
+    public function __construct(array $data = [], array $options = [])
+    {
+        $this->originalData = $data;
+
+        if (isset($options['description'])) {
+            $this->serviceDescription = $options['description'];
+            if (!($this->serviceDescription instanceof DescriptionInterface)) {
+                throw new \InvalidArgumentException('description must be a Description');
+            }
+            if (isset($data['$ref'])) {
+                if ($model = $this->serviceDescription->getModel($data['$ref'])) {
+                    $name = isset($data['name']) ? $data['name'] : null;
+                    $data = $model->toArray() + $data;
+                    if ($name) {
+                        $data['name'] = $name;
+                    }
+                }
+            } elseif (isset($data['extends'])) {
+                // If this parameter extends from another parameter then start
+                // with the actual data union in the parent's data (e.g. actual
+                // supersedes parent)
+                if ($extends = $this->serviceDescription->getModel($data['extends'])) {
+                    $data += $extends->toArray();
+                }
+            }
+        }
+
+        // Pull configuration data into the parameter
+        foreach ($data as $key => $value) {
+            $this->{$key} = $value;
+        }
+
+        $this->required = (bool) $this->required;
+        $this->data = (array) $this->data;
+
+        if ($this->filters) {
+            $this->setFilters((array) $this->filters);
+        }
+
+        if ($this->type == 'object' && $this->additionalProperties === null) {
+            $this->additionalProperties = true;
+        }
+    }
+
+    /**
+     * Convert the object to an array
+     *
+     * @return array
+     */
+    public function toArray()
+    {
+        return $this->originalData;
+    }
+
+    /**
+     * Get the default or static value of the command based on a value
+     *
+     * @param string $value Value that is currently set
+     *
+     * @return mixed Returns the value, a static value if one is present, or a default value
+     */
+    public function getValue($value)
+    {
+        if ($this->static || ($this->default !== null && $value === null)) {
+            return $this->default;
+        }
+
+        return $value;
+    }
+
+    /**
+     * Run a value through the filters OR format attribute associated with the
+     * parameter.
+     *
+     * @param mixed $value Value to filter
+     *
+     * @return mixed Returns the filtered value
+     * @throws \RuntimeException when trying to format when no service
+     *     description is available.
+     */
+    public function filter($value)
+    {
+        // Formats are applied exclusively and supersed filters
+        if ($this->format) {
+            if (!$this->serviceDescription) {
+                throw new \RuntimeException('No service description was set so '
+                    . 'the value cannot be formatted.');
+            }
+            return $this->serviceDescription->format($this->format, $value);
+        }
+
+        // Convert Boolean values
+        if ($this->type == 'boolean' && !is_bool($value)) {
+            $value = filter_var($value, FILTER_VALIDATE_BOOLEAN);
+        }
+
+        // Apply filters to the value
+        if ($this->filters) {
+            foreach ($this->filters as $filter) {
+                if (is_array($filter)) {
+                    // Convert complex filters that hold value place holders
+                    foreach ($filter['args'] as &$data) {
+                        if ($data == '@value') {
+                            $data = $value;
+                        } elseif ($data == '@api') {
+                            $data = $this;
+                        }
+                    }
+                    $value = call_user_func_array(
+                        $filter['method'],
+                        $filter['args']
+                    );
+                } else {
+                    $value = call_user_func($filter, $value);
+                }
+            }
+        }
+
+        return $value;
+    }
+
+    /**
+     * Get the name of the parameter
+     *
+     * @return string
+     */
+    public function getName()
+    {
+        return $this->name;
+    }
+
+    /**
+     * Set the name of the parameter
+     *
+     * @param string $name Name to set
+     */
+    public function setName($name)
+    {
+        $this->name = $name;
+    }
+
+    /**
+     * Get the key of the parameter, where sentAs will supersede name if it is
+     * set.
+     *
+     * @return string
+     */
+    public function getWireName()
+    {
+        return $this->sentAs ?: $this->name;
+    }
+
+    /**
+     * Get the type(s) of the parameter
+     *
+     * @return string|array
+     */
+    public function getType()
+    {
+        return $this->type;
+    }
+
+    /**
+     * Get if the parameter is required
+     *
+     * @return bool
+     */
+    public function isRequired()
+    {
+        return $this->required;
+    }
+
+    /**
+     * Get the default value of the parameter
+     *
+     * @return string|null
+     */
+    public function getDefault()
+    {
+        return $this->default;
+    }
+
+    /**
+     * Get the description of the parameter
+     *
+     * @return string|null
+     */
+    public function getDescription()
+    {
+        return $this->description;
+    }
+
+    /**
+     * Get the minimum acceptable value for an integer
+     *
+     * @return int|null
+     */
+    public function getMinimum()
+    {
+        return $this->minimum;
+    }
+
+    /**
+     * Get the maximum acceptable value for an integer
+     *
+     * @return int|null
+     */
+    public function getMaximum()
+    {
+        return $this->maximum;
+    }
+
+    /**
+     * Get the minimum allowed length of a string value
+     *
+     * @return int
+     */
+    public function getMinLength()
+    {
+        return $this->minLength;
+    }
+
+    /**
+     * Get the maximum allowed length of a string value
+     *
+     * @return int|null
+     */
+    public function getMaxLength()
+    {
+        return $this->maxLength;
+    }
+
+    /**
+     * Get the maximum allowed number of items in an array value
+     *
+     * @return int|null
+     */
+    public function getMaxItems()
+    {
+        return $this->maxItems;
+    }
+
+    /**
+     * Get the minimum allowed number of items in an array value
+     *
+     * @return int
+     */
+    public function getMinItems()
+    {
+        return $this->minItems;
+    }
+
+    /**
+     * Get the location of the parameter
+     *
+     * @return string|null
+     */
+    public function getLocation()
+    {
+        return $this->location;
+    }
+
+    /**
+     * Get the sentAs attribute of the parameter that used with locations to
+     * sentAs an attribute when it is being applied to a location.
+     *
+     * @return string|null
+     */
+    public function getSentAs()
+    {
+        return $this->sentAs;
+    }
+
+    /**
+     * Retrieve a known property from the parameter by name or a data property
+     * by name. When no specific name value is passed, all data properties
+     * will be returned.
+     *
+     * @param string|null $name Specify a particular property name to retrieve
+     *
+     * @return array|mixed|null
+     */
+    public function getData($name = null)
+    {
+        if (!$name) {
+            return $this->data;
+        } elseif (isset($this->data[$name])) {
+            return $this->data[$name];
+        } elseif (isset($this->{$name})) {
+            return $this->{$name};
+        }
+
+        return null;
+    }
+
+    /**
+     * Get whether or not the default value can be changed
+     *
+     * @return bool
+     */
+    public function isStatic()
+    {
+        return $this->static;
+    }
+
+    /**
+     * Get an array of filters used by the parameter
+     *
+     * @return array
+     */
+    public function getFilters()
+    {
+        return $this->filters ?: [];
+    }
+
+    /**
+     * Get the properties of the parameter
+     *
+     * @return Parameter[]
+     */
+    public function getProperties()
+    {
+        if (!$this->propertiesCache) {
+            $this->propertiesCache = [];
+            foreach (array_keys($this->properties) as $name) {
+                $this->propertiesCache[$name] = $this->getProperty($name);
+            }
+        }
+
+        return $this->propertiesCache;
+    }
+
+    /**
+     * Get a specific property from the parameter
+     *
+     * @param string $name Name of the property to retrieve
+     *
+     * @return null|Parameter
+     */
+    public function getProperty($name)
+    {
+        if (!isset($this->properties[$name])) {
+            return null;
+        }
+
+        if (!($this->properties[$name] instanceof self)) {
+            $this->properties[$name]['name'] = $name;
+            $this->properties[$name] = new static(
+                $this->properties[$name],
+                ['description' => $this->serviceDescription]
+            );
+        }
+
+        return $this->properties[$name];
+    }
+
+    /**
+     * Get the additionalProperties value of the parameter
+     *
+     * @return bool|Parameter|null
+     */
+    public function getAdditionalProperties()
+    {
+        if (is_array($this->additionalProperties)) {
+            $this->additionalProperties = new static(
+                $this->additionalProperties,
+                ['description' => $this->serviceDescription]
+            );
+        }
+
+        return $this->additionalProperties;
+    }
+
+    /**
+     * Get the item data of the parameter
+     *
+     * @return Parameter
+     */
+    public function getItems()
+    {
+        if (is_array($this->items)) {
+            $this->items = new static(
+                $this->items,
+                ['description' => $this->serviceDescription]
+            );
+        }
+
+        return $this->items;
+    }
+
+    /**
+     * Get the enum of strings that are valid for the parameter
+     *
+     * @return array|null
+     */
+    public function getEnum()
+    {
+        return $this->enum;
+    }
+
+    /**
+     * Get the regex pattern that must match a value when the value is a string
+     *
+     * @return string
+     */
+    public function getPattern()
+    {
+        return $this->pattern;
+    }
+
+    /**
+     * Get the format attribute of the schema
+     *
+     * @return string
+     */
+    public function getFormat()
+    {
+        return $this->format;
+    }
+
+    /**
+     * Set the array of filters used by the parameter
+     *
+     * @param array $filters Array of functions to use as filters
+     *
+     * @return self
+     */
+    private function setFilters(array $filters)
+    {
+        $this->filters = [];
+        foreach ($filters as $filter) {
+            $this->addFilter($filter);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Add a filter to the parameter
+     *
+     * @param string|array $filter Method to filter the value through
+     *
+     * @return self
+     * @throws \InvalidArgumentException
+     */
+    private function addFilter($filter)
+    {
+        if (is_array($filter)) {
+            if (!isset($filter['method'])) {
+                throw new \InvalidArgumentException(
+                    'A [method] value must be specified for each complex filter'
+                );
+            }
+        }
+
+        if (!$this->filters) {
+            $this->filters = [$filter];
+        } else {
+            $this->filters[] = $filter;
+        }
+
+        return $this;
+    }
+
+    /**
+     * Check if a parameter has a specific variable and if it set.
+     *
+     * @param string $var
+     * @return bool
+     */
+    public function has($var)
+    {
+        if (!is_string($var)) {
+            throw new \InvalidArgumentException('Expected a string. Got: ' . (is_object($var) ? get_class($var) : gettype($var)));
+        }
+        return isset($this->{$var}) && !empty($this->{$var});
+    }
+}

+ 13 - 0
addons/cos/library/Guzzle/guzzle-services/src/QuerySerializer/QuerySerializerInterface.php

@@ -0,0 +1,13 @@
+<?php
+namespace GuzzleHttp\Command\Guzzle\QuerySerializer;
+
+interface QuerySerializerInterface
+{
+    /**
+     * Aggregate query params and transform them into a string
+     *
+     * @param  array $queryParams
+     * @return string
+     */
+    public function aggregate(array $queryParams);
+}

+ 33 - 0
addons/cos/library/Guzzle/guzzle-services/src/QuerySerializer/Rfc3986Serializer.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace GuzzleHttp\Command\Guzzle\QuerySerializer;
+
+class Rfc3986Serializer implements QuerySerializerInterface
+{
+    /**
+     * @var bool
+     */
+    private $removeNumericIndices;
+
+    /**
+     * @param bool $removeNumericIndices
+     */
+    public function __construct($removeNumericIndices = false)
+    {
+        $this->removeNumericIndices = $removeNumericIndices;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function aggregate(array $queryParams)
+    {
+        $queryString = http_build_query($queryParams, null, '&', PHP_QUERY_RFC3986);
+
+        if ($this->removeNumericIndices) {
+            $queryString = preg_replace('/%5B[0-9]+%5D/simU', '%5B%5D', $queryString);
+        }
+
+        return $queryString;
+    }
+}

+ 101 - 0
addons/cos/library/Guzzle/guzzle-services/src/RequestLocation/AbstractLocation.php

@@ -0,0 +1,101 @@
+<?php
+namespace GuzzleHttp\Command\Guzzle\RequestLocation;
+
+use GuzzleHttp\Command\CommandInterface;
+use GuzzleHttp\Command\Guzzle\Operation;
+use GuzzleHttp\Command\Guzzle\Parameter;
+use Psr\Http\Message\RequestInterface;
+
+abstract class AbstractLocation implements RequestLocationInterface
+{
+    /** @var string */
+    protected $locationName;
+
+    /**
+     * Set the name of the location
+     *
+     * @param $locationName
+     */
+    public function __construct($locationName)
+    {
+        $this->locationName = $locationName;
+    }
+
+    /**
+     * @param CommandInterface $command
+     * @param RequestInterface $request
+     * @param Parameter $param
+     * @return RequestInterface
+     */
+    public function visit(
+        CommandInterface $command,
+        RequestInterface $request,
+        Parameter $param
+    ) {
+        return $request;
+    }
+
+    /**
+     * @param CommandInterface $command
+     * @param RequestInterface $request
+     * @param Operation $operation
+     * @return RequestInterface
+     */
+    public function after(
+        CommandInterface $command,
+        RequestInterface $request,
+        Operation $operation
+    ) {
+        return $request;
+    }
+
+    /**
+     * Prepare (filter and set desired name for request item) the value for
+     * request.
+     *
+     * @param mixed     $value
+     * @param Parameter $param
+     *
+     * @return array|mixed
+     */
+    protected function prepareValue($value, Parameter $param)
+    {
+        return is_array($value)
+            ? $this->resolveRecursively($value, $param)
+            : $param->filter($value);
+    }
+
+    /**
+     * Recursively prepare and filter nested values.
+     *
+     * @param array     $value Value to map
+     * @param Parameter $param Parameter related to the current key.
+     *
+     * @return array Returns the mapped array
+     */
+    protected function resolveRecursively(array $value, Parameter $param)
+    {
+        foreach ($value as $name => &$v) {
+            switch ($param->getType()) {
+                case 'object':
+                    if ($subParam = $param->getProperty($name)) {
+                        $key = $subParam->getWireName();
+                        $value[$key] = $this->prepareValue($v, $subParam);
+                        if ($name != $key) {
+                            unset($value[$name]);
+                        }
+                    } elseif ($param->getAdditionalProperties() instanceof Parameter) {
+                        $v = $this->prepareValue($v, $param->getAdditionalProperties());
+                    }
+                    break;
+                case 'array':
+                    if ($items = $param->getItems()) {
+                        $v = $this->prepareValue($v, $items);
+                    }
+                    break;
+            }
+        }
+
+        return $param->filter($value);
+    }
+}

+ 49 - 0
addons/cos/library/Guzzle/guzzle-services/src/RequestLocation/BodyLocation.php

@@ -0,0 +1,49 @@
+<?php
+namespace GuzzleHttp\Command\Guzzle\RequestLocation;
+
+use GuzzleHttp\Command\CommandInterface;
+use GuzzleHttp\Command\Guzzle\Parameter;
+use GuzzleHttp\Psr7;
+use Psr\Http\Message\MessageInterface;
+use Psr\Http\Message\RequestInterface;
+
+/**
+ * Adds a body to a request
+ */
+class BodyLocation extends AbstractLocation
+{
+
+    /**
+     * Set the name of the location
+     *
+     * @param string $locationName
+     */
+    public function __construct($locationName = 'body')
+    {
+        parent::__construct($locationName);
+    }
+
+    /**
+     * @param CommandInterface $command
+     * @param RequestInterface $request
+     * @param Parameter        $param
+     *
+     * @return MessageInterface
+     */
+    public function visit(
+        CommandInterface $command,
+        RequestInterface $request,
+        Parameter $param
+    ) {
+        $oldValue = $request->getBody()->getContents();
+
+        $value = $command[$param->getName()];
+        $value = $param->getName() . '=' . $param->filter($value);
+
+        if ($oldValue !== '') {
+            $value = $oldValue . '&' . $value;
+        }
+
+        return $request->withBody(Psr7\stream_for($value));
+    }
+}

+ 84 - 0
addons/cos/library/Guzzle/guzzle-services/src/RequestLocation/FormParamLocation.php

@@ -0,0 +1,84 @@
+<?php
+namespace GuzzleHttp\Command\Guzzle\RequestLocation;
+
+use GuzzleHttp\Command\CommandInterface;
+use GuzzleHttp\Command\Guzzle\Operation;
+use GuzzleHttp\Command\Guzzle\Parameter;
+use GuzzleHttp\Psr7;
+use Psr\Http\Message\RequestInterface;
+
+/**
+ * Add form_params to a request
+ */
+class FormParamLocation extends AbstractLocation
+{
+    /** @var string $contentType */
+    protected $contentType = 'application/x-www-form-urlencoded; charset=utf-8';
+
+    /** @var array $formParamsData */
+    protected $formParamsData = [];
+
+    /**
+     * Set the name of the location
+     *
+     * @param string $locationName
+     */
+    public function __construct($locationName = 'formParam')
+    {
+        parent::__construct($locationName);
+    }
+
+    /**
+     * @param CommandInterface $command
+     * @param RequestInterface $request
+     * @param Parameter        $param
+     *
+     * @return RequestInterface
+     */
+    public function visit(
+        CommandInterface $command,
+        RequestInterface $request,
+        Parameter $param
+    ) {
+        $this->formParamsData['form_params'][$param->getWireName()] = $this->prepareValue(
+            $command[$param->getName()],
+            $param
+        );
+
+        return $request;
+    }
+
+    /**
+     * @param CommandInterface $command
+     * @param RequestInterface $request
+     * @param Operation        $operation
+     *
+     * @return RequestInterface
+     */
+    public function after(
+        CommandInterface $command,
+        RequestInterface $request,
+        Operation $operation
+    ) {
+        $data = $this->formParamsData;
+        $this->formParamsData = [];
+        $modify = [];
+
+        // Add additional parameters to the form_params array
+        $additional = $operation->getAdditionalParameters();
+        if ($additional && $additional->getLocation() == $this->locationName) {
+            foreach ($command->toArray() as $key => $value) {
+                if (!$operation->hasParam($key)) {
+                    $data['form_params'][$key] = $this->prepareValue($value, $additional);
+                }
+            }
+        }
+
+        $body = http_build_query($data['form_params'], '', '&');
+        $modify['body'] = Psr7\stream_for($body);
+        $modify['set_headers']['Content-Type'] = $this->contentType;
+        $request = Psr7\modify_request($request, $modify);
+
+        return $request;
+    }
+}

+ 67 - 0
addons/cos/library/Guzzle/guzzle-services/src/RequestLocation/HeaderLocation.php

@@ -0,0 +1,67 @@
+<?php
+namespace GuzzleHttp\Command\Guzzle\RequestLocation;
+
+use GuzzleHttp\Command\CommandInterface;
+use GuzzleHttp\Command\Guzzle\Operation;
+use GuzzleHttp\Command\Guzzle\Parameter;
+use Psr\Http\Message\MessageInterface;
+use Psr\Http\Message\RequestInterface;
+
+/**
+ * Request header location
+ */
+class HeaderLocation extends AbstractLocation
+{
+
+    /**
+     * Set the name of the location
+     *
+     * @param string $locationName
+     */
+    public function __construct($locationName = 'header')
+    {
+        parent::__construct($locationName);
+    }
+
+    /**
+     * @param CommandInterface $command
+     * @param RequestInterface $request
+     * @param Parameter        $param
+     *
+     * @return MessageInterface
+     */
+    public function visit(
+        CommandInterface $command,
+        RequestInterface $request,
+        Parameter $param
+    ) {
+        $value = $command[$param->getName()];
+
+        return $request->withHeader($param->getWireName(), $param->filter($value));
+    }
+
+    /**
+     * @param CommandInterface $command
+     * @param RequestInterface $request
+     * @param Operation        $operation
+     *
+     * @return RequestInterface
+     */
+    public function after(
+        CommandInterface $command,
+        RequestInterface $request,
+        Operation $operation
+    ) {
+        /** @var Parameter $additional */
+        $additional = $operation->getAdditionalParameters();
+        if ($additional && ($additional->getLocation() === $this->locationName)) {
+            foreach ($command->toArray() as $key => $value) {
+                if (!$operation->hasParam($key)) {
+                    $request = $request->withHeader($key, $additional->filter($value));
+                }
+            }
+        }
+
+        return $request;
+    }
+}

+ 85 - 0
addons/cos/library/Guzzle/guzzle-services/src/RequestLocation/JsonLocation.php

@@ -0,0 +1,85 @@
+<?php
+namespace GuzzleHttp\Command\Guzzle\RequestLocation;
+
+use GuzzleHttp\Command\CommandInterface;
+use GuzzleHttp\Command\Guzzle\Operation;
+use GuzzleHttp\Command\Guzzle\Parameter;
+use GuzzleHttp\Psr7;
+use Psr\Http\Message\MessageInterface;
+use Psr\Http\Message\RequestInterface;
+
+/**
+ * Creates a JSON document
+ */
+class JsonLocation extends AbstractLocation
+{
+    /** @var string Whether or not to add a Content-Type header when JSON is found */
+    private $jsonContentType;
+
+    /** @var array */
+    private $jsonData;
+
+    /**
+     * @param string $locationName Name of the location
+     * @param string $contentType  Content-Type header to add to the request if
+     *     JSON is added to the body. Pass an empty string to omit.
+     */
+    public function __construct($locationName = 'json', $contentType = 'application/json')
+    {
+        parent::__construct($locationName);
+        $this->jsonContentType = $contentType;
+    }
+
+    /**
+     * @param CommandInterface $command
+     * @param RequestInterface $request
+     * @param Parameter        $param
+     *
+     * @return RequestInterface
+     */
+    public function visit(
+        CommandInterface $command,
+        RequestInterface $request,
+        Parameter $param
+    ) {
+        $this->jsonData[$param->getWireName()] = $this->prepareValue(
+            $command[$param->getName()],
+            $param
+        );
+
+        return $request->withBody(Psr7\stream_for(\GuzzleHttp\json_encode($this->jsonData)));
+    }
+
+    /**
+     * @param CommandInterface $command
+     * @param RequestInterface $request
+     * @param Operation        $operation
+     *
+     * @return MessageInterface
+     */
+    public function after(
+        CommandInterface $command,
+        RequestInterface $request,
+        Operation $operation
+    ) {
+        $data = $this->jsonData;
+        $this->jsonData = [];
+
+        // Add additional parameters to the JSON document
+        $additional = $operation->getAdditionalParameters();
+        if ($additional && ($additional->getLocation() === $this->locationName)) {
+            foreach ($command->toArray() as $key => $value) {
+                if (!$operation->hasParam($key)) {
+                    $data[$key] = $this->prepareValue($value, $additional);
+                }
+            }
+        }
+
+        // Don't overwrite the Content-Type if one is set
+        if ($this->jsonContentType && !$request->hasHeader('Content-Type')) {
+            $request = $request->withHeader('Content-Type', $this->jsonContentType);
+        }
+
+        return $request->withBody(Psr7\stream_for(\GuzzleHttp\json_encode($data)));
+    }
+}

+ 76 - 0
addons/cos/library/Guzzle/guzzle-services/src/RequestLocation/MultiPartLocation.php

@@ -0,0 +1,76 @@
+<?php
+namespace GuzzleHttp\Command\Guzzle\RequestLocation;
+
+use GuzzleHttp\Command\CommandInterface;
+use GuzzleHttp\Command\Guzzle\Operation;
+use GuzzleHttp\Command\Guzzle\Parameter;
+use GuzzleHttp\Psr7;
+use Psr\Http\Message\RequestInterface;
+
+/**
+ * Adds POST files to a request
+ */
+class MultiPartLocation extends AbstractLocation
+{
+    /** @var string $contentType */
+    protected $contentType = 'multipart/form-data; boundary=';
+
+    /** @var array $formParamsData */
+    protected $multipartData = [];
+
+    /**
+     * Set the name of the location
+     *
+     * @param string $locationName
+     */
+    public function __construct($locationName = 'multipart')
+    {
+        parent::__construct($locationName);
+    }
+
+    /**
+     * @param CommandInterface $command
+     * @param RequestInterface $request
+     * @param Parameter $param
+     * @return RequestInterface
+     */
+    public function visit(
+        CommandInterface $command,
+        RequestInterface $request,
+        Parameter $param
+    ) {
+        $this->multipartData[] = [
+            'name' => $param->getWireName(),
+            'contents' => $this->prepareValue($command[$param->getName()], $param)
+        ];
+
+        return $request;
+    }
+
+
+    /**
+     * @param CommandInterface $command
+     * @param RequestInterface $request
+     * @param Operation $operation
+     * @return RequestInterface
+     */
+    public function after(
+        CommandInterface $command,
+        RequestInterface $request,
+        Operation $operation
+    ) {
+        $data = $this->multipartData;
+        $this->multipartData = [];
+        $modify = [];
+
+        $body = new Psr7\MultipartStream($data);
+        $modify['body'] = Psr7\stream_for($body);
+        $request = Psr7\modify_request($request, $modify);
+        if ($request->getBody() instanceof Psr7\MultipartStream) {
+            // Use a multipart/form-data POST if a Content-Type is not set.
+            $request->withHeader('Content-Type', $this->contentType . $request->getBody()->getBoundary());
+        }
+
+        return $request;
+    }
+}

+ 92 - 0
addons/cos/library/Guzzle/guzzle-services/src/RequestLocation/QueryLocation.php

@@ -0,0 +1,92 @@
+<?php
+namespace GuzzleHttp\Command\Guzzle\RequestLocation;
+
+use GuzzleHttp\Command\CommandInterface;
+use GuzzleHttp\Command\Guzzle\Operation;
+use GuzzleHttp\Command\Guzzle\Parameter;
+use GuzzleHttp\Command\Guzzle\QuerySerializer\QuerySerializerInterface;
+use GuzzleHttp\Command\Guzzle\QuerySerializer\Rfc3986Serializer;
+use GuzzleHttp\Psr7;
+use Psr\Http\Message\RequestInterface;
+
+/**
+ * Adds query string values to requests
+ */
+class QueryLocation extends AbstractLocation
+{
+    /**
+     * @var QuerySerializerInterface
+     */
+    private $querySerializer;
+
+    /**
+     * Set the name of the location
+     *
+     * @param string                        $locationName
+     * @param QuerySerializerInterface|null $querySerializer
+     */
+    public function __construct($locationName = 'query', QuerySerializerInterface $querySerializer = null)
+    {
+        parent::__construct($locationName);
+
+        $this->querySerializer = $querySerializer ?: new Rfc3986Serializer();
+    }
+
+    /**
+     * @param CommandInterface $command
+     * @param RequestInterface $request
+     * @param Parameter        $param
+     *
+     * @return RequestInterface
+     */
+    public function visit(
+        CommandInterface $command,
+        RequestInterface $request,
+        Parameter $param
+    ) {
+        $uri = $request->getUri();
+        $query = Psr7\parse_query($uri->getQuery());
+
+        $query[$param->getWireName()] = $this->prepareValue(
+            $command[$param->getName()],
+            $param
+        );
+
+        $uri = $uri->withQuery($this->querySerializer->aggregate($query));
+
+        return $request->withUri($uri);
+    }
+
+    /**
+     * @param CommandInterface $command
+     * @param RequestInterface $request
+     * @param Operation        $operation
+     *
+     * @return RequestInterface
+     */
+    public function after(
+        CommandInterface $command,
+        RequestInterface $request,
+        Operation $operation
+    ) {
+        $additional = $operation->getAdditionalParameters();
+        if ($additional && $additional->getLocation() == $this->locationName) {
+            foreach ($command->toArray() as $key => $value) {
+                if (!$operation->hasParam($key)) {
+                    $uri = $request->getUri();
+                    $query = Psr7\parse_query($uri->getQuery());
+
+                    $query[$key] = $this->prepareValue(
+                        $value,
+                        $additional
+                    );
+
+                    $uri = $uri->withQuery($this->querySerializer->aggregate($query));
+                    $request = $request->withUri($uri);
+                }
+            }
+        }
+
+        return $request;
+    }
+}

+ 44 - 0
addons/cos/library/Guzzle/guzzle-services/src/RequestLocation/RequestLocationInterface.php

@@ -0,0 +1,44 @@
+<?php
+
+namespace GuzzleHttp\Command\Guzzle\RequestLocation;
+
+use GuzzleHttp\Command\CommandInterface;
+use GuzzleHttp\Command\Guzzle\Operation;
+use GuzzleHttp\Command\Guzzle\Parameter;
+use Psr\Http\Message\RequestInterface;
+
+/**
+ * Handles locations specified in a service description
+ */
+interface RequestLocationInterface
+{
+    /**
+     * Visits a location for each top-level parameter
+     *
+     * @param CommandInterface $command Command being prepared
+     * @param RequestInterface $request Request being modified
+     * @param Parameter        $param   Parameter being visited
+     *
+     * @return RequestInterface Modified request
+     */
+    public function visit(
+        CommandInterface $command,
+        RequestInterface $request,
+        Parameter $param
+    );
+
+    /**
+     * Called when all of the parameters of a command have been visited.
+     *
+     * @param CommandInterface $command   Command being prepared
+     * @param RequestInterface $request   Request being modified
+     * @param Operation        $operation Operation being serialized
+     *
+     * @return RequestInterface Modified request
+     */
+    public function after(
+        CommandInterface $command,
+        RequestInterface $request,
+        Operation $operation
+    );
+}

+ 328 - 0
addons/cos/library/Guzzle/guzzle-services/src/RequestLocation/XmlLocation.php

@@ -0,0 +1,328 @@
+<?php
+namespace GuzzleHttp\Command\Guzzle\RequestLocation;
+
+use GuzzleHttp\Command\CommandInterface;
+use GuzzleHttp\Command\Guzzle\Operation;
+use GuzzleHttp\Command\Guzzle\Parameter;
+use GuzzleHttp\Psr7;
+use Psr\Http\Message\RequestInterface;
+
+/**
+ * Creates an XML document
+ */
+class XmlLocation extends AbstractLocation
+{
+    /** @var \XMLWriter XML writer resource */
+    private $writer;
+
+    /** @var string Content-Type header added when XML is found */
+    private $contentType;
+
+    /** @var Parameter[] Buffered elements to write */
+    private $buffered = [];
+
+    /**
+     * @param string $locationName Name of the location
+     * @param string $contentType  Set to a non-empty string to add a
+     *     Content-Type header to a request if any XML content is added to the
+     *     body. Pass an empty string to disable the addition of the header.
+     */
+    public function __construct($locationName = 'xml', $contentType = 'application/xml')
+    {
+        parent::__construct($locationName);
+        $this->contentType = $contentType;
+    }
+
+    /**
+     * @param CommandInterface $command
+     * @param RequestInterface $request
+     * @param Parameter        $param
+     *
+     * @return RequestInterface
+     */
+    public function visit(
+        CommandInterface $command,
+        RequestInterface $request,
+        Parameter $param
+    ) {
+        // Buffer and order the parameters to visit based on if they are
+        // top-level attributes or child nodes.
+        // @link https://github.com/guzzle/guzzle/pull/494
+        if ($param->getData('xmlAttribute')) {
+            array_unshift($this->buffered, $param);
+        } else {
+            $this->buffered[] = $param;
+        }
+
+        return $request;
+    }
+
+    /**
+     * @param CommandInterface $command
+     * @param RequestInterface $request
+     * @param Operation        $operation
+     *
+     * @return RequestInterface
+     */
+    public function after(
+        CommandInterface $command,
+        RequestInterface $request,
+        Operation $operation
+    ) {
+        foreach ($this->buffered as $param) {
+            $this->visitWithValue(
+                $command[$param->getName()],
+                $param,
+                $operation
+            );
+        }
+
+        $this->buffered = [];
+
+        $additional = $operation->getAdditionalParameters();
+        if ($additional && $additional->getLocation() == $this->locationName) {
+            foreach ($command->toArray() as $key => $value) {
+                if (!$operation->hasParam($key)) {
+                    $additional->setName($key);
+                    $this->visitWithValue($value, $additional, $operation);
+                }
+            }
+            $additional->setName(null);
+        }
+
+        // If data was found that needs to be serialized, then do so
+        $xml = '';
+        if ($this->writer) {
+            $xml = $this->finishDocument($this->writer);
+        } elseif ($operation->getData('xmlAllowEmpty')) {
+            // Check if XML should always be sent for the command
+            $writer = $this->createRootElement($operation);
+            $xml = $this->finishDocument($writer);
+        }
+
+        if ($xml !== '') {
+            $request = $request->withBody(Psr7\stream_for($xml));
+            // Don't overwrite the Content-Type if one is set
+            if ($this->contentType && !$request->hasHeader('Content-Type')) {
+                $request = $request->withHeader('Content-Type', $this->contentType);
+            }
+        }
+
+        $this->writer = null;
+
+        return $request;
+    }
+
+    /**
+     * Create the root XML element to use with a request
+     *
+     * @param Operation $operation Operation object
+     *
+     * @return \XMLWriter
+     */
+    protected function createRootElement(Operation $operation)
+    {
+        static $defaultRoot = ['name' => 'Request'];
+        // If no root element was specified, then just wrap the XML in 'Request'
+        $root = $operation->getData('xmlRoot') ?: $defaultRoot;
+        // Allow the XML declaration to be customized with xmlEncoding
+        $encoding = $operation->getData('xmlEncoding');
+        $writer = $this->startDocument($encoding);
+        $writer->startElement($root['name']);
+
+        // Create the wrapping element with no namespaces if no namespaces were present
+        if (!empty($root['namespaces'])) {
+            // Create the wrapping element with an array of one or more namespaces
+            foreach ((array) $root['namespaces'] as $prefix => $uri) {
+                $nsLabel = 'xmlns';
+                if (!is_numeric($prefix)) {
+                    $nsLabel .= ':'.$prefix;
+                }
+                $writer->writeAttribute($nsLabel, $uri);
+            }
+        }
+
+        return $writer;
+    }
+
+    /**
+     * Recursively build the XML body
+     *
+     * @param \XMLWriter $writer XML to modify
+     * @param Parameter  $param     API Parameter
+     * @param mixed      $value     Value to add
+     */
+    protected function addXml(\XMLWriter $writer, Parameter $param, $value)
+    {
+        $value = $param->filter($value);
+        $type = $param->getType();
+        $name = $param->getWireName();
+        $prefix = null;
+        $namespace = $param->getData('xmlNamespace');
+        if (false !== strpos($name, ':')) {
+            list($prefix, $name) = explode(':', $name, 2);
+        }
+
+        if ($type == 'object' || $type == 'array') {
+            if (!$param->getData('xmlFlattened')) {
+                if ($namespace) {
+                    $writer->startElementNS(null, $name, $namespace);
+                } else {
+                    $writer->startElement($name);
+                }
+            }
+            if ($param->getType() == 'array') {
+                $this->addXmlArray($writer, $param, $value);
+            } elseif ($param->getType() == 'object') {
+                $this->addXmlObject($writer, $param, $value);
+            }
+            if (!$param->getData('xmlFlattened')) {
+                $writer->endElement();
+            }
+            return;
+        }
+        if ($param->getData('xmlAttribute')) {
+            $this->writeAttribute($writer, $prefix, $name, $namespace, $value);
+        } else {
+            $this->writeElement($writer, $prefix, $name, $namespace, $value);
+        }
+    }
+
+    /**
+     * Write an attribute with namespace if used
+     *
+     * @param  \XMLWriter $writer XMLWriter instance
+     * @param  string     $prefix    Namespace prefix if any
+     * @param  string     $name      Attribute name
+     * @param  string     $namespace The uri of the namespace
+     * @param  string     $value     The attribute content
+     */
+    protected function writeAttribute($writer, $prefix, $name, $namespace, $value)
+    {
+        if ($namespace) {
+            $writer->writeAttributeNS($prefix, $name, $namespace, $value);
+        } else {
+            $writer->writeAttribute($name, $value);
+        }
+    }
+
+    /**
+     * Write an element with namespace if used
+     *
+     * @param  \XMLWriter $writer XML writer resource
+     * @param  string     $prefix    Namespace prefix if any
+     * @param  string     $name      Element name
+     * @param  string     $namespace The uri of the namespace
+     * @param  string     $value     The element content
+     */
+    protected function writeElement(\XMLWriter $writer, $prefix, $name, $namespace, $value)
+    {
+        if ($namespace) {
+            $writer->startElementNS($prefix, $name, $namespace);
+        } else {
+            $writer->startElement($name);
+        }
+        if (strpbrk($value, '<>&')) {
+            $writer->writeCData($value);
+        } else {
+            $writer->writeRaw($value);
+        }
+        $writer->endElement();
+    }
+
+    /**
+     * Create a new xml writer and start a document
+     *
+     * @param  string $encoding document encoding
+     *
+     * @return \XMLWriter the writer resource
+     * @throws \RuntimeException if the document cannot be started
+     */
+    protected function startDocument($encoding)
+    {
+        $this->writer = new \XMLWriter();
+        if (!$this->writer->openMemory()) {
+            throw new \RuntimeException('Unable to open XML document in memory');
+        }
+        if (!$this->writer->startDocument('1.0', $encoding)) {
+            throw new \RuntimeException('Unable to start XML document');
+        }
+
+        return $this->writer;
+    }
+
+    /**
+     * End the document and return the output
+     *
+     * @param \XMLWriter $writer
+     *
+     * @return string the writer resource
+     */
+    protected function finishDocument($writer)
+    {
+        $writer->endDocument();
+
+        return $writer->outputMemory();
+    }
+
+    /**
+     * Add an array to the XML
+     *
+     * @param \XMLWriter $writer
+     * @param Parameter $param
+     * @param $value
+     */
+    protected function addXmlArray(\XMLWriter $writer, Parameter $param, &$value)
+    {
+        if ($items = $param->getItems()) {
+            foreach ($value as $v) {
+                $this->addXml($writer, $items, $v);
+            }
+        }
+    }
+
+    /**
+     * Add an object to the XML
+     *
+     * @param \XMLWriter $writer
+     * @param Parameter $param
+     * @param $value
+     */
+    protected function addXmlObject(\XMLWriter $writer, Parameter $param, &$value)
+    {
+        $noAttributes = [];
+
+        // add values which have attributes
+        foreach ($value as $name => $v) {
+            if ($property = $param->getProperty($name)) {
+                if ($property->getData('xmlAttribute')) {
+                    $this->addXml($writer, $property, $v);
+                } else {
+                    $noAttributes[] = ['value' => $v, 'property' => $property];
+                }
+            }
+        }
+
+        // now add values with no attributes
+        foreach ($noAttributes as $element) {
+            $this->addXml($writer, $element['property'], $element['value']);
+        }
+    }
+
+    /**
+     * @param $value
+     * @param Parameter $param
+     * @param Operation $operation
+     */
+    private function visitWithValue(
+        $value,
+        Parameter $param,
+        Operation $operation
+    ) {
+        if (!$this->writer) {
+            $this->createRootElement($operation);
+        }
+
+        $this->addXml($this->writer, $param, $value);
+    }
+}

+ 69 - 0
addons/cos/library/Guzzle/guzzle-services/src/ResponseLocation/AbstractLocation.php

@@ -0,0 +1,69 @@
+<?php
+namespace GuzzleHttp\Command\Guzzle\ResponseLocation;
+
+use GuzzleHttp\Command\Guzzle\Parameter;
+use GuzzleHttp\Command\ResultInterface;
+use Psr\Http\Message\ResponseInterface;
+
+/**
+ * Class AbstractLocation
+ *
+ * @package GuzzleHttp\Command\Guzzle\ResponseLocation
+ */
+abstract class AbstractLocation implements ResponseLocationInterface
+{
+    /** @var string $locationName */
+    protected $locationName;
+
+    /**
+     * Set the name of the location
+     *
+     * @param $locationName
+     */
+    public function __construct($locationName)
+    {
+        $this->locationName = $locationName;
+    }
+
+    /**
+     * @param ResultInterface $result
+     * @param ResponseInterface $response
+     * @param Parameter $model
+     * @return ResultInterface
+     */
+    public function before(
+        ResultInterface $result,
+        ResponseInterface $response,
+        Parameter $model
+    ) {
+        return $result;
+    }
+
+    /**
+     * @param ResultInterface $result
+     * @param ResponseInterface $response
+     * @param Parameter $model
+     * @return ResultInterface
+     */
+    public function after(
+        ResultInterface $result,
+        ResponseInterface $response,
+        Parameter $model
+    ) {
+        return $result;
+    }
+
+    /**
+     * @param ResultInterface $result
+     * @param ResponseInterface $response
+     * @param Parameter $param
+     * @return ResultInterface
+     */
+    public function visit(
+        ResultInterface $result,
+        ResponseInterface $response,
+        Parameter $param
+    ) {
+        return $result;
+    }
+}

+ 39 - 0
addons/cos/library/Guzzle/guzzle-services/src/ResponseLocation/BodyLocation.php

@@ -0,0 +1,39 @@
+<?php
+namespace GuzzleHttp\Command\Guzzle\ResponseLocation;
+
+use GuzzleHttp\Command\Guzzle\Parameter;
+use GuzzleHttp\Command\ResultInterface;
+use Psr\Http\Message\ResponseInterface;
+
+/**
+ * Extracts the body of a response into a result field
+ */
+class BodyLocation extends AbstractLocation
+{
+
+    /**
+     * Set the name of the location
+     *
+     * @param string $locationName
+     */
+    public function __construct($locationName = 'body')
+    {
+        parent::__construct($locationName);
+    }
+
+    /**
+     * @param ResultInterface $result
+     * @param ResponseInterface $response
+     * @param Parameter $param
+     * @return ResultInterface
+     */
+    public function visit(
+        ResultInterface $result,
+        ResponseInterface $response,
+        Parameter $param
+    ) {
+        $result[$param->getName()] = $param->filter($response->getBody());
+
+        return $result;
+    }
+}

+ 47 - 0
addons/cos/library/Guzzle/guzzle-services/src/ResponseLocation/HeaderLocation.php

@@ -0,0 +1,47 @@
+<?php
+namespace GuzzleHttp\Command\Guzzle\ResponseLocation;
+
+use GuzzleHttp\Command\Guzzle\Parameter;
+use GuzzleHttp\Command\ResultInterface;
+use Psr\Http\Message\ResponseInterface;
+
+/**
+ * Extracts headers from the response into a result fields
+ */
+class HeaderLocation extends AbstractLocation
+{
+
+    /**
+     * Set the name of the location
+     *
+     * @param string $locationName
+     */
+    public function __construct($locationName = 'header')
+    {
+        parent::__construct($locationName);
+    }
+
+    /**
+     * @param ResultInterface   $result
+     * @param ResponseInterface $response
+     * @param Parameter         $param
+     *
+     * @return ResultInterface
+     */
+    public function visit(
+        ResultInterface $result,
+        ResponseInterface $response,
+        Parameter $param
+    ) {
+        // Retrieving a single header by name
+        $name = $param->getName();
+        if ($header = $response->getHeader($param->getWireName())) {
+            if (is_array($header)) {
+                $header = array_shift($header);
+            }
+            $result[$name] = $param->filter($header);
+        }
+
+        return $result;
+    }
+}

+ 176 - 0
addons/cos/library/Guzzle/guzzle-services/src/ResponseLocation/JsonLocation.php

@@ -0,0 +1,176 @@
+<?php
+namespace GuzzleHttp\Command\Guzzle\ResponseLocation;
+
+use GuzzleHttp\Command\Guzzle\Parameter;
+use GuzzleHttp\Command\Result;
+use GuzzleHttp\Command\ResultInterface;
+use Psr\Http\Message\ResponseInterface;
+
+/**
+ * Extracts elements from a JSON document.
+ */
+class JsonLocation extends AbstractLocation
+{
+    /** @var array The JSON document being visited */
+    private $json = [];
+
+    /**
+     * Set the name of the location
+     *
+     * @param string $locationName
+     */
+    public function __construct($locationName = 'json')
+    {
+        parent::__construct($locationName);
+    }
+
+    /**
+     * @param \GuzzleHttp\Command\ResultInterface  $result
+     * @param \Psr\Http\Message\ResponseInterface  $response
+     * @param \GuzzleHttp\Command\Guzzle\Parameter $model
+     *
+     * @return \GuzzleHttp\Command\ResultInterface
+     */
+    public function before(
+        ResultInterface $result,
+        ResponseInterface $response,
+        Parameter $model
+    ) {
+        $body = (string) $response->getBody();
+        $body = $body ?: "{}";
+        $this->json = \GuzzleHttp\json_decode($body, true);
+        // relocate named arrays, so that they have the same structure as
+        //  arrays nested in objects and visit can work on them in the same way
+        if ($model->getType() === 'array' && ($name = $model->getName())) {
+            $this->json = [$name => $this->json];
+        }
+
+        return $result;
+    }
+
+    /**
+     * @param ResultInterface $result
+     * @param ResponseInterface $response
+     * @param Parameter $model
+     * @return ResultInterface
+     */
+    public function after(
+        ResultInterface $result,
+        ResponseInterface $response,
+        Parameter $model
+    ) {
+        // Handle additional, undefined properties
+        $additional = $model->getAdditionalProperties();
+        if (!($additional instanceof Parameter)) {
+            return $result;
+        }
+
+        // Use the model location as the default if one is not set on additional
+        $addLocation = $additional->getLocation() ?: $model->getLocation();
+        if ($addLocation == $this->locationName) {
+            foreach ($this->json as $prop => $val) {
+                if (!isset($result[$prop])) {
+                    // Only recurse if there is a type specified
+                    $result[$prop] = $additional->getType()
+                        ? $this->recurse($additional, $val)
+                        : $val;
+                }
+            }
+        }
+
+        $this->json = [];
+
+        return $result;
+    }
+
+    /**
+     * @param ResultInterface $result
+     * @param ResponseInterface $response
+     * @param Parameter $param
+     * @return Result|ResultInterface
+     */
+    public function visit(
+        ResultInterface $result,
+        ResponseInterface $response,
+        Parameter $param
+    ) {
+        $name = $param->getName();
+        $key = $param->getWireName();
+
+        // Check if the result should be treated as a list
+        if ($param->getType() == 'array') {
+            // Treat as javascript array
+            if ($name) {
+                // name provided, store it under a key in the array
+                $subArray = isset($this->json[$key]) ? $this->json[$key] : null;
+                $result[$name] = $this->recurse($param, $subArray);
+            } else {
+                // top-level `array` or an empty name
+                $result = new Result(array_merge(
+                    $result->toArray(),
+                    $this->recurse($param, $this->json)
+                ));
+            }
+        } elseif (isset($this->json[$key])) {
+            $result[$name] = $this->recurse($param, $this->json[$key]);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Recursively process a parameter while applying filters
+     *
+     * @param Parameter $param API parameter being validated
+     * @param mixed     $value Value to process.
+     * @return mixed|null
+     */
+    private function recurse(Parameter $param, $value)
+    {
+        if (!is_array($value)) {
+            return $param->filter($value);
+        }
+
+        $result = [];
+        $type = $param->getType();
+
+        if ($type == 'array') {
+            $items = $param->getItems();
+            foreach ($value as $val) {
+                $result[] = $this->recurse($items, $val);
+            }
+        } elseif ($type == 'object' && !isset($value[0])) {
+            // On the above line, we ensure that the array is associative and
+            // not numerically indexed
+            if ($properties = $param->getProperties()) {
+                foreach ($properties as $property) {
+                    $key = $property->getWireName();
+                    if (array_key_exists($key, $value)) {
+                        $result[$property->getName()] = $this->recurse(
+                            $property,
+                            $value[$key]
+                        );
+                        // Remove from the value so that AP can later be handled
+                        unset($value[$key]);
+                    }
+                }
+            }
+            // Only check additional properties if everything wasn't already
+            // handled
+            if ($value) {
+                $additional = $param->getAdditionalProperties();
+                if ($additional === null || $additional === true) {
+                    // Merge the JSON under the resulting array
+                    $result += $value;
+                } elseif ($additional instanceof Parameter) {
+                    // Process all child elements according to the given schema
+                    foreach ($value as $prop => $val) {
+                        $result[$prop] = $this->recurse($additional, $val);
+                    }
+                }
+            }
+        }
+
+        return $param->filter($result);
+    }
+}

+ 41 - 0
addons/cos/library/Guzzle/guzzle-services/src/ResponseLocation/ReasonPhraseLocation.php

@@ -0,0 +1,41 @@
+<?php
+namespace GuzzleHttp\Command\Guzzle\ResponseLocation;
+
+use GuzzleHttp\Command\Guzzle\Parameter;
+use GuzzleHttp\Command\ResultInterface;
+use Psr\Http\Message\ResponseInterface;
+
+/**
+ * Extracts the reason phrase of a response into a result field
+ */
+class ReasonPhraseLocation extends AbstractLocation
+{
+
+    /**
+     * Set the name of the location
+     *
+     * @param string $locationName
+     */
+    public function __construct($locationName = 'reasonPhrase')
+    {
+        parent::__construct($locationName);
+    }
+
+    /**
+     * @param ResultInterface $result
+     * @param ResponseInterface $response
+     * @param Parameter $param
+     * @return ResultInterface
+     */
+    public function visit(
+        ResultInterface $result,
+        ResponseInterface $response,
+        Parameter $param
+    ) {
+        $result[$param->getName()] = $param->filter(
+            $response->getReasonPhrase()
+        );
+
+        return $result;
+    }
+}

+ 61 - 0
addons/cos/library/Guzzle/guzzle-services/src/ResponseLocation/ResponseLocationInterface.php

@@ -0,0 +1,61 @@
+<?php
+namespace GuzzleHttp\Command\Guzzle\ResponseLocation;
+
+use GuzzleHttp\Command\Guzzle\Parameter;
+use GuzzleHttp\Command\ResultInterface;
+use Psr\Http\Message\ResponseInterface;
+
+/**
+ * Location visitor used to parse values out of a response into an associative
+ * array
+ */
+interface ResponseLocationInterface
+{
+    /**
+     * Called before visiting all parameters. This can be used for seeding the
+     * result of a command with default data (e.g. populating with JSON data in
+     * the response then adding to the parsed data).
+     *
+     * @param ResultInterface   $result   Result being created
+     * @param ResponseInterface $response Response being visited
+     * @param Parameter         $model    Response model
+     *
+     * @return ResultInterface Modified result
+     */
+    public function before(
+        ResultInterface $result,
+        ResponseInterface $response,
+        Parameter $model
+    );
+
+    /**
+     * Called after visiting all parameters
+     *
+     * @param ResultInterface   $result   Result being created
+     * @param ResponseInterface $response Response being visited
+     * @param Parameter         $model    Response model
+     *
+     * @return ResultInterface Modified result
+     */
+    public function after(
+        ResultInterface $result,
+        ResponseInterface $response,
+        Parameter $model
+    );
+
+    /**
+     * Called once for each parameter being visited that matches the location
+     * type.
+     *
+     * @param ResultInterface   $result   Result being created
+     * @param ResponseInterface $response Response being visited
+     * @param Parameter         $param    Parameter being visited
+     *
+     * @return ResultInterface Modified result
+     */
+    public function visit(
+        ResultInterface $result,
+        ResponseInterface $response,
+        Parameter $param
+    );
+}

+ 39 - 0
addons/cos/library/Guzzle/guzzle-services/src/ResponseLocation/StatusCodeLocation.php

@@ -0,0 +1,39 @@
+<?php
+namespace GuzzleHttp\Command\Guzzle\ResponseLocation;
+
+use GuzzleHttp\Command\Guzzle\Parameter;
+use GuzzleHttp\Command\ResultInterface;
+use Psr\Http\Message\ResponseInterface;
+
+/**
+ * Extracts the status code of a response into a result field
+ */
+class StatusCodeLocation extends AbstractLocation
+{
+
+    /**
+     * Set the name of the location
+     *
+     * @param string $locationName
+     */
+    public function __construct($locationName = 'statusCode')
+    {
+        parent::__construct($locationName);
+    }
+
+    /**
+     * @param ResultInterface $result
+     * @param ResponseInterface $response
+     * @param Parameter $param
+     * @return ResultInterface
+     */
+    public function visit(
+        ResultInterface $result,
+        ResponseInterface $response,
+        Parameter $param
+    ) {
+        $result[$param->getName()] = $param->filter($response->getStatusCode());
+
+        return $result;
+    }
+}

+ 311 - 0
addons/cos/library/Guzzle/guzzle-services/src/ResponseLocation/XmlLocation.php

@@ -0,0 +1,311 @@
+<?php
+namespace GuzzleHttp\Command\Guzzle\ResponseLocation;
+
+use GuzzleHttp\Command\Guzzle\Parameter;
+use GuzzleHttp\Command\Result;
+use GuzzleHttp\Command\ResultInterface;
+use Psr\Http\Message\ResponseInterface;
+
+/**
+ * Extracts elements from an XML document
+ */
+class XmlLocation extends AbstractLocation
+{
+    /** @var \SimpleXMLElement XML document being visited */
+    private $xml;
+
+    /**
+     * Set the name of the location
+     *
+     * @param string $locationName
+     */
+    public function __construct($locationName = 'xml')
+    {
+        parent::__construct($locationName);
+    }
+
+    /**
+     * @param ResultInterface $result
+     * @param ResponseInterface $response
+     * @param Parameter $model
+     * @return ResultInterface
+     */
+    public function before(
+        ResultInterface $result,
+        ResponseInterface $response,
+        Parameter $model
+    ) {
+        $this->xml = simplexml_load_string((string) $response->getBody());
+
+        return $result;
+    }
+
+    /**
+     * @param ResultInterface $result
+     * @param ResponseInterface $response
+     * @param Parameter $model
+     * @return Result|ResultInterface
+     */
+    public function after(
+        ResultInterface $result,
+        ResponseInterface $response,
+        Parameter $model
+    ) {
+        // Handle additional, undefined properties
+        $additional = $model->getAdditionalProperties();
+        if ($additional instanceof Parameter &&
+            $additional->getLocation() == $this->locationName
+        ) {
+            $result = new Result(array_merge(
+                $result->toArray(),
+                self::xmlToArray($this->xml)
+            ));
+        }
+
+        $this->xml = null;
+
+        return $result;
+    }
+
+    /**
+     * @param ResultInterface $result
+     * @param ResponseInterface $response
+     * @param Parameter $param
+     * @return ResultInterface
+     */
+    public function visit(
+        ResultInterface $result,
+        ResponseInterface $response,
+        Parameter $param
+    ) {
+        $sentAs = $param->getWireName();
+        $ns = null;
+        if (strstr($sentAs, ':')) {
+            list($ns, $sentAs) = explode(':', $sentAs);
+        }
+
+        // Process the primary property
+        if (count($this->xml->children($ns, true)->{$sentAs})) {
+            $result[$param->getName()] = $this->recursiveProcess(
+                $param,
+                $this->xml->children($ns, true)->{$sentAs}
+            );
+        }
+
+        return $result;
+    }
+
+    /**
+     * Recursively process a parameter while applying filters
+     *
+     * @param Parameter         $param API parameter being processed
+     * @param \SimpleXMLElement $node  Node being processed
+     * @return array
+     */
+    private function recursiveProcess(
+        Parameter $param,
+        \SimpleXMLElement $node
+    ) {
+        $result = [];
+        $type = $param->getType();
+
+        if ($type == 'object') {
+            $result = $this->processObject($param, $node);
+        } elseif ($type == 'array') {
+            $result = $this->processArray($param, $node);
+        } else {
+            // We are probably handling a flat data node (i.e. string or
+            // integer), so let's check if it's childless, which indicates a
+            // node containing plain text.
+            if ($node->children()->count() == 0) {
+                // Retrieve text from node
+                $result = (string) $node;
+            }
+        }
+
+        // Filter out the value
+        if (isset($result)) {
+            $result = $param->filter($result);
+        }
+
+        return $result;
+    }
+
+    /**
+     * @param Parameter $param
+     * @param \SimpleXMLElement $node
+     * @return array
+     */
+    private function processArray(Parameter $param, \SimpleXMLElement $node)
+    {
+        // Cast to an array if the value was a string, but should be an array
+        $items = $param->getItems();
+        $sentAs = $items->getWireName();
+        $result = [];
+        $ns = null;
+
+        if (strstr($sentAs, ':')) {
+            // Get namespace from the wire name
+            list($ns, $sentAs) = explode(':', $sentAs);
+        } else {
+            // Get namespace from data
+            $ns = $items->getData('xmlNs');
+        }
+
+        if ($sentAs === null) {
+            // A general collection of nodes
+            foreach ($node as $child) {
+                $result[] = $this->recursiveProcess($items, $child);
+            }
+        } else {
+            // A collection of named, repeating nodes
+            // (i.e. <collection><foo></foo><foo></foo></collection>)
+            $children = $node->children($ns, true)->{$sentAs};
+            foreach ($children as $child) {
+                $result[] = $this->recursiveProcess($items, $child);
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * Process an object
+     *
+     * @param Parameter         $param API parameter being parsed
+     * @param \SimpleXMLElement $node  Value to process
+     * @return array
+     */
+    private function processObject(Parameter $param, \SimpleXMLElement $node)
+    {
+        $result = $knownProps = $knownAttributes = [];
+
+        // Handle known properties
+        if ($properties = $param->getProperties()) {
+            foreach ($properties as $property) {
+                $name = $property->getName();
+                $sentAs = $property->getWireName();
+                $knownProps[$sentAs] = 1;
+                if (strpos($sentAs, ':')) {
+                    list($ns, $sentAs) = explode(':', $sentAs);
+                } else {
+                    $ns = $property->getData('xmlNs');
+                }
+
+                if ($property->getData('xmlAttribute')) {
+                    // Handle XML attributes
+                    $result[$name] = (string) $node->attributes($ns, true)->{$sentAs};
+                    $knownAttributes[$sentAs] = 1;
+                } elseif (count($node->children($ns, true)->{$sentAs})) {
+                    // Found a child node matching wire name
+                    $childNode = $node->children($ns, true)->{$sentAs};
+                    $result[$name] = $this->recursiveProcess(
+                        $property,
+                        $childNode
+                    );
+                }
+            }
+        }
+
+        // Handle additional, undefined properties
+        $additional = $param->getAdditionalProperties();
+        if ($additional instanceof Parameter) {
+            // Process all child elements according to the given schema
+            foreach ($node->children($additional->getData('xmlNs'), true) as $childNode) {
+                $sentAs = $childNode->getName();
+                if (!isset($knownProps[$sentAs])) {
+                    $result[$sentAs] = $this->recursiveProcess(
+                        $additional,
+                        $childNode
+                    );
+                }
+            }
+        } elseif ($additional === null || $additional === true) {
+            // Blindly transform the XML into an array preserving as much data
+            // as possible. Remove processed, aliased properties.
+            $array = array_diff_key(self::xmlToArray($node), $knownProps);
+            // Remove @attributes that were explicitly plucked from the
+            // attributes list.
+            if (isset($array['@attributes']) && $knownAttributes) {
+                $array['@attributes'] = array_diff_key($array['@attributes'], $knownProps);
+                if (!$array['@attributes']) {
+                    unset($array['@attributes']);
+                }
+            }
+
+            // Merge it together with the original result
+            $result = array_merge($array, $result);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Convert an XML document to an array.
+     *
+     * @param \SimpleXMLElement $xml
+     * @param int               $nesting
+     * @param null              $ns
+     *
+     * @return array
+     */
+    private static function xmlToArray(
+        \SimpleXMLElement $xml,
+        $ns = null,
+        $nesting = 0
+    ) {
+        $result = [];
+        $children = $xml->children($ns, true);
+
+        foreach ($children as $name => $child) {
+            $attributes = (array) $child->attributes($ns, true);
+            if (!isset($result[$name])) {
+                $childArray = self::xmlToArray($child, $ns, $nesting + 1);
+                $result[$name] = $attributes
+                    ? array_merge($attributes, $childArray)
+                    : $childArray;
+                continue;
+            }
+            // A child element with this name exists so we're assuming
+            // that the node contains a list of elements
+            if (!is_array($result[$name])) {
+                $result[$name] = [$result[$name]];
+            } elseif (!isset($result[$name][0])) {
+                // Convert the first child into the first element of a numerically indexed array
+                $firstResult = $result[$name];
+                $result[$name] = [];
+                $result[$name][] = $firstResult;
+            }
+            $childArray = self::xmlToArray($child, $ns, $nesting + 1);
+            if ($attributes) {
+                $result[$name][] = array_merge($attributes, $childArray);
+            } else {
+                $result[$name][] = $childArray;
+            }
+        }
+
+        // Extract text from node
+        $text = trim((string) $xml);
+        if ($text === '') {
+            $text = null;
+        }
+
+        // Process attributes
+        $attributes = (array) $xml->attributes($ns, true);
+        if ($attributes) {
+            if ($text !== null) {
+                $result['value'] = $text;
+            }
+            $result = array_merge($attributes, $result);
+        } elseif ($text !== null) {
+            $result = $text;
+        }
+
+        // Make sure we're always returning an array
+        if ($nesting == 0 && !is_array($result)) {
+            $result = [$result];
+        }
+
+        return $result;
+    }
+}

+ 141 - 0
addons/cos/library/Guzzle/guzzle-services/src/SchemaFormatter.php

@@ -0,0 +1,141 @@
+<?php
+namespace GuzzleHttp\Command\Guzzle;
+
+/**
+ * JSON Schema formatter class
+ */
+class SchemaFormatter
+{
+    /**
+     * Format a value by a registered format name
+     *
+     * @param string $format Registered format used to format the value
+     * @param mixed  $value  Value being formatted
+     *
+     * @return mixed
+     */
+    public function format($format, $value)
+    {
+        switch ($format) {
+            case 'date-time':
+                return $this->formatDateTime($value);
+            case 'date-time-http':
+                return $this->formatDateTimeHttp($value);
+            case 'date':
+                return $this->formatDate($value);
+            case 'time':
+                return $this->formatTime($value);
+            case 'timestamp':
+                return $this->formatTimestamp($value);
+            case 'boolean-string':
+                return $this->formatBooleanAsString($value);
+            default:
+                return $value;
+        }
+    }
+
+    /**
+     * Perform the actual DateTime formatting
+     *
+     * @param int|string|\DateTime $dateTime Date time value
+     * @param string               $format   Format of the result
+     *
+     * @return string
+     * @throws \InvalidArgumentException
+     */
+    protected function dateFormatter($dateTime, $format)
+    {
+        if (is_numeric($dateTime)) {
+            return gmdate($format, (int) $dateTime);
+        }
+
+        if (is_string($dateTime)) {
+            $dateTime = new \DateTime($dateTime);
+        }
+
+        if ($dateTime instanceof \DateTimeInterface) {
+            static $utc;
+            if (!$utc) {
+                $utc = new \DateTimeZone('UTC');
+            }
+            return $dateTime->setTimezone($utc)->format($format);
+        }
+
+        throw new \InvalidArgumentException('Date/Time values must be either '
+            . 'be a string, integer, or DateTime object');
+    }
+
+    /**
+     * Create a ISO 8601 (YYYY-MM-DDThh:mm:ssZ) formatted date time value in
+     * UTC time.
+     *
+     * @param string|integer|\DateTime $value Date time value
+     *
+     * @return string
+     */
+    private function formatDateTime($value)
+    {
+        return $this->dateFormatter($value, 'Y-m-d\TH:i:s\Z');
+    }
+
+    /**
+     * Create an HTTP date (RFC 1123 / RFC 822) formatted UTC date-time string
+     *
+     * @param string|integer|\DateTime $value Date time value
+     *
+     * @return string
+     */
+    private function formatDateTimeHttp($value)
+    {
+        return $this->dateFormatter($value, 'D, d M Y H:i:s \G\M\T');
+    }
+
+    /**
+     * Create a YYYY-MM-DD formatted string
+     *
+     * @param string|integer|\DateTime $value Date time value
+     *
+     * @return string
+     */
+    private function formatDate($value)
+    {
+        return $this->dateFormatter($value, 'Y-m-d');
+    }
+
+    /**
+     * Create a hh:mm:ss formatted string
+     *
+     * @param string|integer|\DateTime $value Date time value
+     *
+     * @return string
+     */
+    private function formatTime($value)
+    {
+        return $this->dateFormatter($value, 'H:i:s');
+    }
+
+    /**
+     * Formats a boolean value as a string
+     *
+     * @param string|integer|bool $value Value to convert to a boolean
+     *                                   'true' / 'false' value
+     *
+     * @return string
+     */
+    private function formatBooleanAsString($value)
+    {
+        return filter_var($value, FILTER_VALIDATE_BOOLEAN) ? 'true' : 'false';
+    }
+
+    /**
+     * Return a UNIX timestamp in the UTC timezone
+     *
+     * @param string|integer|\DateTime $value Time value
+     *
+     * @return int
+     */
+    private function formatTimestamp($value)
+    {
+        return (int) $this->dateFormatter($value, 'U');
+    }
+}

+ 297 - 0
addons/cos/library/Guzzle/guzzle-services/src/SchemaValidator.php

@@ -0,0 +1,297 @@
+<?php
+namespace GuzzleHttp\Command\Guzzle;
+
+use GuzzleHttp\Command\ToArrayInterface;
+
+/**
+ * Default parameter validator
+ */
+class SchemaValidator
+{
+    /**
+     * Whether or not integers are converted to strings when an integer is
+     * received for a string input
+     *
+     * @var bool
+     */
+    protected $castIntegerToStringType;
+
+    /** @var array Errors encountered while validating */
+    protected $errors;
+
+    /**
+     * @param bool $castIntegerToStringType Set to true to convert integers
+     *     into strings when a required type is a string and the input value is
+     *     an integer. Defaults to true.
+     */
+    public function __construct($castIntegerToStringType = true)
+    {
+        $this->castIntegerToStringType = $castIntegerToStringType;
+    }
+
+    /**
+     * @param Parameter $param
+     * @param $value
+     * @return bool
+     */
+    public function validate(Parameter $param, &$value)
+    {
+        $this->errors = [];
+        $this->recursiveProcess($param, $value);
+
+        if (empty($this->errors)) {
+            return true;
+        } else {
+            sort($this->errors);
+            return false;
+        }
+    }
+
+    /**
+     * Get the errors encountered while validating
+     *
+     * @return array
+     */
+    public function getErrors()
+    {
+        return $this->errors ?: [];
+    }
+
+    /**
+     * From the allowable types, determine the type that the variable matches
+     *
+     * @param string|array $type Parameter type
+     * @param mixed $value Value to determine the type
+     *
+     * @return string|false Returns the matching type on
+     */
+    protected function determineType($type, $value)
+    {
+        foreach ((array) $type as $t) {
+            if ($t == 'string'
+                && (is_string($value) || (is_object($value) && method_exists($value, '__toString')))
+            ) {
+                return 'string';
+            } elseif ($t == 'object' && (is_array($value) || is_object($value))) {
+                return 'object';
+            } elseif ($t == 'array' && is_array($value)) {
+                return 'array';
+            } elseif ($t == 'integer' && is_integer($value)) {
+                return 'integer';
+            } elseif ($t == 'boolean' && is_bool($value)) {
+                return 'boolean';
+            } elseif ($t == 'number' && is_numeric($value)) {
+                return 'number';
+            } elseif ($t == 'numeric' && is_numeric($value)) {
+                return 'numeric';
+            } elseif ($t == 'null' && !$value) {
+                return 'null';
+            } elseif ($t == 'any') {
+                return 'any';
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Recursively validate a parameter
+     *
+     * @param Parameter $param  API parameter being validated
+     * @param mixed     $value  Value to validate and validate. The value may
+     *                          change during this validate.
+     * @param string    $path   Current validation path (used for error reporting)
+     * @param int       $depth  Current depth in the validation validate
+     *
+     * @return bool Returns true if valid, or false if invalid
+     */
+    protected function recursiveProcess(
+        Parameter $param,
+        &$value,
+        $path = '',
+        $depth = 0
+    ) {
+        // Update the value by adding default or static values
+        $value = $param->getValue($value);
+
+        $required = $param->isRequired();
+        // if the value is null and the parameter is not required or is static,
+        // then skip any further recursion
+        if ((null === $value && !$required) || $param->isStatic()) {
+            return true;
+        }
+
+        $type = $param->getType();
+        // Attempt to limit the number of times is_array is called by tracking
+        // if the value is an array
+        $valueIsArray = is_array($value);
+        // If a name is set then update the path so that validation messages
+        // are more helpful
+        if ($name = $param->getName()) {
+            $path .= "[{$name}]";
+        }
+
+        if ($type == 'object') {
+            // Determine whether or not this "value" has properties and should
+            // be traversed
+            $traverse = $temporaryValue = false;
+
+            // Convert the value to an array
+            if (!$valueIsArray && $value instanceof ToArrayInterface) {
+                $value = $value->toArray();
+            }
+
+            if ($valueIsArray) {
+                // Ensure that the array is associative and not numerically
+                // indexed
+                if (isset($value[0])) {
+                    $this->errors[] = "{$path} must be an array of properties. Got a numerically indexed array.";
+                    return false;
+                }
+                $traverse = true;
+            } elseif ($value === null) {
+                // Attempt to let the contents be built up by default values if
+                // possible
+                $value = [];
+                $temporaryValue = $valueIsArray = $traverse = true;
+            }
+
+            if ($traverse) {
+                if ($properties = $param->getProperties()) {
+                    // if properties were found, validate each property
+                    foreach ($properties as $property) {
+                        $name = $property->getName();
+                        if (isset($value[$name])) {
+                            $this->recursiveProcess($property, $value[$name], $path, $depth + 1);
+                        } else {
+                            $current = null;
+                            $this->recursiveProcess($property, $current, $path, $depth + 1);
+                            // Only set the value if it was populated
+                            if (null !== $current) {
+                                $value[$name] = $current;
+                            }
+                        }
+                    }
+                }
+
+                $additional = $param->getAdditionalProperties();
+                if ($additional !== true) {
+                    // If additional properties were found, then validate each
+                    // against the additionalProperties attr.
+                    $keys = array_keys($value);
+                    // Determine the keys that were specified that were not
+                    // listed in the properties of the schema
+                    $diff = array_diff($keys, array_keys($properties));
+                    if (!empty($diff)) {
+                        // Determine which keys are not in the properties
+                        if ($additional instanceof Parameter) {
+                            foreach ($diff as $key) {
+                                $this->recursiveProcess($additional, $value[$key], "{$path}[{$key}]", $depth);
+                            }
+                        } else {
+                            // if additionalProperties is set to false and there
+                            // are additionalProperties in the values, then fail
+                            foreach ($diff as $prop) {
+                                $this->errors[] = sprintf('%s[%s] is not an allowed property', $path, $prop);
+                            }
+                        }
+                    }
+                }
+
+                // A temporary value will be used to traverse elements that
+                // have no corresponding input value. This allows nested
+                // required parameters with default values to bubble up into the
+                // input. Here we check if we used a temp value and nothing
+                // bubbled up, then we need to remote the value.
+                if ($temporaryValue && empty($value)) {
+                    $value = null;
+                    $valueIsArray = false;
+                }
+            }
+
+        } elseif ($type == 'array' && $valueIsArray && $param->getItems()) {
+            foreach ($value as $i => &$item) {
+                // Validate each item in an array against the items attribute of the schema
+                $this->recursiveProcess($param->getItems(), $item, $path . "[{$i}]", $depth + 1);
+            }
+        }
+
+        // If the value is required and the type is not null, then there is an
+        // error if the value is not set
+        if ($required && $value === null && $type != 'null') {
+            $message = "{$path} is " . ($param->getType()
+                ? ('a required ' . implode(' or ', (array) $param->getType()))
+                : 'required');
+            if ($param->has('description')) {
+                $message .= ': ' . $param->getDescription();
+            }
+            $this->errors[] = $message;
+            return false;
+        }
+
+        // Validate that the type is correct. If the type is string but an
+        // integer was passed, the class can be instructed to cast the integer
+        // to a string to pass validation. This is the default behavior.
+        if ($type && (!$type = $this->determineType($type, $value))) {
+            if ($this->castIntegerToStringType
+                && $param->getType() == 'string'
+                && is_integer($value)
+            ) {
+                $value = (string) $value;
+            } else {
+                $this->errors[] = "{$path} must be of type " . implode(' or ', (array) $param->getType());
+            }
+        }
+
+        // Perform type specific validation for strings, arrays, and integers
+        if ($type == 'string') {
+            // Strings can have enums which are a list of predefined values
+            if (($enum = $param->getEnum()) && !in_array($value, $enum)) {
+                $this->errors[] = "{$path} must be one of " . implode(' or ', array_map(function ($s) {
+                        return '"' . addslashes($s) . '"';
+                }, $enum));
+            }
+            // Strings can have a regex pattern that the value must match
+            if (($pattern  = $param->getPattern()) && !preg_match($pattern, $value)) {
+                $this->errors[] = "{$path} must match the following regular expression: {$pattern}";
+            }
+
+            $strLen = null;
+            if ($min = $param->getMinLength()) {
+                $strLen = strlen($value);
+                if ($strLen < $min) {
+                    $this->errors[] = "{$path} length must be greater than or equal to {$min}";
+                }
+            }
+            if ($max = $param->getMaxLength()) {
+                if (($strLen ?: strlen($value)) > $max) {
+                    $this->errors[] = "{$path} length must be less than or equal to {$max}";
+                }
+            }
+
+        } elseif ($type == 'array') {
+            $size = null;
+            if ($min = $param->getMinItems()) {
+                $size = count($value);
+                if ($size < $min) {
+                    $this->errors[] = "{$path} must contain {$min} or more elements";
+                }
+            }
+            if ($max = $param->getMaxItems()) {
+                if (($size ?: count($value)) > $max) {
+                    $this->errors[] = "{$path} must contain {$max} or fewer elements";
+                }
+            }
+
+        } elseif ($type == 'integer' || $type == 'number' || $type == 'numeric') {
+            if (($min = $param->getMinimum()) && $value < $min) {
+                $this->errors[] = "{$path} must be greater than or equal to {$min}";
+            }
+            if (($max = $param->getMaximum()) && $value > $max) {
+                $this->errors[] = "{$path} must be less than or equal to {$max}";
+            }
+        }
+
+        return empty($this->errors);
+    }
+}

+ 164 - 0
addons/cos/library/Guzzle/guzzle-services/src/Serializer.php

@@ -0,0 +1,164 @@
+<?php
+namespace GuzzleHttp\Command\Guzzle;
+
+use GuzzleHttp\Command\CommandInterface;
+use GuzzleHttp\Command\Guzzle\RequestLocation\BodyLocation;
+use GuzzleHttp\Command\Guzzle\RequestLocation\FormParamLocation;
+use GuzzleHttp\Command\Guzzle\RequestLocation\HeaderLocation;
+use GuzzleHttp\Command\Guzzle\RequestLocation\JsonLocation;
+use GuzzleHttp\Command\Guzzle\RequestLocation\MultiPartLocation;
+use GuzzleHttp\Command\Guzzle\RequestLocation\QueryLocation;
+use GuzzleHttp\Command\Guzzle\RequestLocation\RequestLocationInterface;
+use GuzzleHttp\Command\Guzzle\RequestLocation\XmlLocation;
+use GuzzleHttp\Psr7\Request;
+use GuzzleHttp\Psr7\Uri;
+use Psr\Http\Message\RequestInterface;
+
+/**
+ * Serializes requests for a given command.
+ */
+class Serializer
+{
+    /** @var RequestLocationInterface[] */
+    private $locations;
+
+    /** @var DescriptionInterface */
+    private $description;
+
+    /**
+     * @param DescriptionInterface       $description
+     * @param RequestLocationInterface[] $requestLocations Extra request locations
+     */
+    public function __construct(
+        DescriptionInterface $description,
+        array $requestLocations = []
+    ) {
+        static $defaultRequestLocations;
+        if (!$defaultRequestLocations) {
+            $defaultRequestLocations = [
+                'body'      => new BodyLocation(),
+                'query'     => new QueryLocation(),
+                'header'    => new HeaderLocation(),
+                'json'      => new JsonLocation(),
+                'xml'       => new XmlLocation(),
+                'formParam' => new FormParamLocation(),
+                'multipart' => new MultiPartLocation(),
+            ];
+        }
+
+        $this->locations = $requestLocations + $defaultRequestLocations;
+        $this->description = $description;
+    }
+
+    /**
+     * @param CommandInterface $command
+     * @return RequestInterface
+     */
+    public function __invoke(CommandInterface $command)
+    {
+        $request = $this->createRequest($command);
+        return $this->prepareRequest($command, $request);
+    }
+
+    /**
+     * Prepares a request for sending using location visitors
+     *
+     * @param CommandInterface $command
+     * @param RequestInterface $request Request being created
+     * @return RequestInterface
+     * @throws \RuntimeException If a location cannot be handled
+     */
+    protected function prepareRequest(
+        CommandInterface $command,
+        RequestInterface $request
+    ) {
+        $visitedLocations = [];
+        $operation = $this->description->getOperation($command->getName());
+
+        // Visit each actual parameter
+        foreach ($operation->getParams() as $name => $param) {
+            /* @var Parameter $param */
+            $location = $param->getLocation();
+            // Skip parameters that have not been set or are URI location
+            if ($location == 'uri' || !$command->hasParam($name)) {
+                continue;
+            }
+            if (!isset($this->locations[$location])) {
+                throw new \RuntimeException("No location registered for $name");
+            }
+            $visitedLocations[$location] = true;
+            $request = $this->locations[$location]->visit($command, $request, $param);
+        }
+
+        // Ensure that the after() method is invoked for additionalParameters
+        /** @var Parameter $additional */
+        if ($additional = $operation->getAdditionalParameters()) {
+            $visitedLocations[$additional->getLocation()] = true;
+        }
+
+        // Call the after() method for each visited location
+        foreach (array_keys($visitedLocations) as $location) {
+            $request = $this->locations[$location]->after($command, $request, $operation);
+        }
+
+        return $request;
+    }
+
+    /**
+     * Create a request for the command and operation
+     *
+     * @param CommandInterface $command
+     *
+     * @return RequestInterface
+     * @throws \RuntimeException
+     */
+    protected function createRequest(CommandInterface $command)
+    {
+        $operation = $this->description->getOperation($command->getName());
+
+        // If command does not specify a template, assume the client's base URL.
+        if (null === $operation->getUri()) {
+            return new Request(
+                $operation->getHttpMethod(),
+                $this->description->getBaseUri()
+            );
+        }
+
+        return $this->createCommandWithUri($operation, $command);
+    }
+
+    /**
+     * Create a request for an operation with a uri merged onto a base URI
+     *
+     * @param \GuzzleHttp\Command\Guzzle\Operation $operation
+     * @param \GuzzleHttp\Command\CommandInterface $command
+     *
+     * @return \GuzzleHttp\Psr7\Request
+     */
+    private function createCommandWithUri(
+        Operation $operation,
+        CommandInterface $command
+    ) {
+        // Get the path values and use the client config settings
+        $variables = [];
+        foreach ($operation->getParams() as $name => $arg) {
+            /* @var Parameter $arg */
+            if ($arg->getLocation() == 'uri') {
+                if (isset($command[$name])) {
+                    $variables[$name] = $arg->filter($command[$name]);
+                    if (!is_array($variables[$name])) {
+                        $variables[$name] = (string) $variables[$name];
+                    }
+                }
+            }
+        }
+
+        // Expand the URI template.
+        $uri = \GuzzleHttp\uri_template($operation->getUri(), $variables);
+
+        return new Request(
+            $operation->getHttpMethod(),
+            Uri::resolve($this->description->getBaseUri(), $uri)
+        );
+    }
+}

+ 13 - 0
addons/cos/library/Guzzle/guzzle-services/tests/Asset/Exception/CustomCommandException.php

@@ -0,0 +1,13 @@
+<?php
+namespace GuzzleHttp\Tests\Command\Guzzle\Asset\Exception;
+
+use GuzzleHttp\Command\Exception\CommandException;
+
+/**
+ * Class CustomCommandException
+ *
+ * @package GuzzleHttp\Tests\Command\Guzzle\Asset\Exception
+ */
+class CustomCommandException extends CommandException
+{
+}

+ 13 - 0
addons/cos/library/Guzzle/guzzle-services/tests/Asset/Exception/OtherCustomCommandException.php

@@ -0,0 +1,13 @@
+<?php
+namespace GuzzleHttp\Tests\Command\Guzzle\Asset\Exception;
+
+use GuzzleHttp\Command\Exception\CommandException;
+
+/**
+ * Class OtherCustomCommandException
+ *
+ * @package GuzzleHttp\Tests\Command\Guzzle\Asset\Exception
+ */
+class OtherCustomCommandException extends CommandException
+{
+}

+ 10 - 0
addons/cos/library/Guzzle/guzzle-services/tests/Asset/test.html

@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>Title</title>
+</head>
+<body>
+
+</body>
+</html>

+ 184 - 0
addons/cos/library/Guzzle/guzzle-services/tests/DescriptionTest.php

@@ -0,0 +1,184 @@
+<?php
+namespace GuzzleHttp\Tests\Command\Guzzle;
+
+use GuzzleHttp\Command\Guzzle\Description;
+use GuzzleHttp\Command\Guzzle\Operation;
+use GuzzleHttp\Command\Guzzle\Parameter;
+use GuzzleHttp\Command\Guzzle\SchemaFormatter;
+
+/**
+ * @covers \GuzzleHttp\Command\Guzzle\Description
+ */
+class DescriptionTest extends \PHPUnit_Framework_TestCase
+{
+    protected $operations;
+
+    public function setup()
+    {
+        $this->operations = [
+            'test_command' => [
+                'name'        => 'test_command',
+                'description' => 'documentationForCommand',
+                'httpMethod'  => 'DELETE',
+                'class'       => 'FooModel',
+                'parameters'  => [
+                    'bucket'  => ['required' => true],
+                    'key'     => ['required' => true]
+                ]
+            ]
+        ];
+    }
+
+    public function testConstructor()
+    {
+        $service = new Description(['operations' => $this->operations]);
+        $this->assertEquals(1, count($service->getOperations()));
+        $this->assertFalse($service->hasOperation('foobar'));
+        $this->assertTrue($service->hasOperation('test_command'));
+    }
+
+    public function testContainsModels()
+    {
+        $d = new Description([
+            'operations' => ['foo' => []],
+            'models' => [
+                'Tag'    => ['type' => 'object'],
+                'Person' => ['type' => 'object']
+            ]
+        ]);
+        $this->assertTrue($d->hasModel('Tag'));
+        $this->assertTrue($d->hasModel('Person'));
+        $this->assertFalse($d->hasModel('Foo'));
+        $this->assertInstanceOf(Parameter::class, $d->getModel('Tag'));
+        $this->assertEquals(['Tag', 'Person'], array_keys($d->getModels()));
+    }
+
+    public function testCanUseResponseClass()
+    {
+        $d = new Description([
+            'operations' => [
+                'foo' => ['responseClass' => 'Tag']
+            ],
+            'models' => ['Tag' => ['type' => 'object']]
+        ]);
+        $op = $d->getOperation('foo');
+        $this->assertNotNull($op->getResponseModel());
+    }
+
+    /**
+     * @expectedException \InvalidArgumentException
+     */
+    public function testRetrievingMissingModelThrowsException()
+    {
+        $d = new Description([]);
+        $d->getModel('foo');
+    }
+
+    public function testHasAttributes()
+    {
+        $d = new Description([
+            'operations'  => [],
+            'name'        => 'Name',
+            'description' => 'Description',
+            'apiVersion'  => '1.24'
+        ]);
+
+        $this->assertEquals('Name', $d->getName());
+        $this->assertEquals('Description', $d->getDescription());
+        $this->assertEquals('1.24', $d->getApiVersion());
+    }
+
+    public function testPersistsCustomAttributes()
+    {
+        $data = [
+            'operations'  => ['foo' => ['class' => 'foo', 'parameters' => []]],
+            'name'        => 'Name',
+            'description' => 'Test',
+            'apiVersion'  => '1.24',
+            'auth'        => 'foo',
+            'keyParam'    => 'bar'
+        ];
+        $d = new Description($data);
+        $this->assertEquals('foo', $d->getData('auth'));
+        $this->assertEquals('bar', $d->getData('keyParam'));
+        $this->assertEquals(['auth' => 'foo', 'keyParam' => 'bar'], $d->getData());
+        $this->assertNull($d->getData('missing'));
+    }
+
+    /**
+     * @expectedException \InvalidArgumentException
+     */
+    public function testThrowsExceptionForMissingOperation()
+    {
+        $s = new Description([]);
+        $this->assertNull($s->getOperation('foo'));
+    }
+
+    /**
+     * @expectedException \InvalidArgumentException
+     */
+    public function testValidatesOperationTypes()
+    {
+        new Description([
+            'operations' => ['foo' => new \stdClass()]
+        ]);
+    }
+
+    public function testHasbaseUrl()
+    {
+        $description = new Description(['baseUrl' => 'http://foo.com']);
+        $this->assertEquals('http://foo.com', $description->getBaseUri());
+    }
+
+    public function testHasbaseUri()
+    {
+        $description = new Description(['baseUri' => 'http://foo.com']);
+        $this->assertEquals('http://foo.com', $description->getBaseUri());
+    }
+
+    public function testModelsHaveNames()
+    {
+        $desc = [
+            'models' => [
+                'date' => ['type' => 'string'],
+                'user'=> [
+                    'type' => 'object',
+                    'properties' => [
+                        'dob' => ['$ref' => 'date']
+                    ]
+                ]
+            ]
+        ];
+
+        $s = new Description($desc);
+        $this->assertEquals('string', $s->getModel('date')->getType());
+        $this->assertEquals('dob', $s->getModel('user')->getProperty('dob')->getName());
+    }
+
+    public function testHasOperations()
+    {
+        $desc = ['operations' => ['foo' => ['parameters' => ['foo' => [
+            'name' => 'foo'
+        ]]]]];
+        $s = new Description($desc);
+        $this->assertInstanceOf(Operation::class, $s->getOperation('foo'));
+        $this->assertSame($s->getOperation('foo'), $s->getOperation('foo'));
+    }
+
+    public function testHasFormatter()
+    {
+        $s = new Description([]);
+        $this->assertNotEmpty($s->format('date', 'now'));
+    }
+
+    public function testCanUseCustomFormatter()
+    {
+        $formatter = $this->getMockBuilder(SchemaFormatter::class)
+            ->setMethods(['format'])
+            ->getMock();
+        $formatter->expects($this->once())
+            ->method('format');
+        $s = new Description([], ['formatter' => $formatter]);
+        $s->format('time', 'now');
+    }
+}

+ 386 - 0
addons/cos/library/Guzzle/guzzle-services/tests/DeserializerTest.php

@@ -0,0 +1,386 @@
+<?php
+namespace GuzzleHttp\Tests\Command\Guzzle;
+
+use GuzzleHttp\Client as HttpClient;
+use GuzzleHttp\Command\CommandInterface;
+use GuzzleHttp\Command\Guzzle\Description;
+use GuzzleHttp\Command\Guzzle\DescriptionInterface;
+use GuzzleHttp\Command\Guzzle\GuzzleClient;
+use GuzzleHttp\Command\Guzzle\Operation;
+use GuzzleHttp\Command\ServiceClientInterface;
+use GuzzleHttp\Handler\MockHandler;
+use GuzzleHttp\HandlerStack;
+use GuzzleHttp\Psr7\Response;
+use GuzzleHttp\Tests\Command\Guzzle\Asset\Exception\CustomCommandException;
+use GuzzleHttp\Tests\Command\Guzzle\Asset\Exception\OtherCustomCommandException;
+use Predis\Response\ResponseInterface;
+
+/**
+ * @covers \GuzzleHttp\Command\Guzzle\Deserializer
+ */
+class DeserializerTest extends \PHPUnit_Framework_TestCase
+{
+    /** @var ServiceClientInterface|\PHPUnit_Framework_MockObject_MockObject */
+    private $serviceClient;
+
+    /** @var CommandInterface|\PHPUnit_Framework_MockObject_MockObject */
+    private $command;
+
+    public function setUp()
+    {
+        $this->serviceClient = $this->getMockBuilder(GuzzleClient::class)
+                            ->disableOriginalConstructor()
+                            ->getMock();
+        $this->command = $this->getMockBuilder(CommandInterface::class)->getMock();
+    }
+
+    protected function prepareErrorResponses($commandName, array $errors = [])
+    {
+        $this->command->expects($this->once())->method('getName')->will($this->returnValue($commandName));
+
+        $description = $this->getMockBuilder(DescriptionInterface::class)->getMock();
+        $operation = new Operation(['errorResponses' => $errors], $description);
+
+        $description->expects($this->once())
+            ->method('getOperation')
+            ->with($commandName)
+            ->will($this->returnValue($operation));
+
+        $this->serviceClient->expects($this->once())
+            ->method('getDescription')
+            ->will($this->returnValue($description));
+    }
+
+    public function testDoNothingIfNoException()
+    {
+        $mock = new MockHandler([new Response(200)]);
+        $description = new Description([
+            'operations' => [
+                'foo' => [
+                    'uri' => 'http://httpbin.org/{foo}',
+                    'httpMethod' => 'GET',
+                    'responseModel' => 'j',
+                    'parameters' => [
+                        'bar' => [
+                            'type'     => 'string',
+                            'required' => true,
+                            'location' => 'uri'
+                        ]
+                    ]
+                ]
+            ],
+            'models' => [
+                'j' => [
+                    'type' => 'object'
+                ]
+            ]
+        ]);
+        $httpClient = new HttpClient(['handler' => $mock]);
+        $client = new GuzzleClient($httpClient, $description);
+        $client->foo(['bar' => 'baz']);
+    }
+
+    /**
+     * @expectedException \GuzzleHttp\Tests\Command\Guzzle\Asset\Exception\CustomCommandException
+     */
+    public function testCreateExceptionWithCode()
+    {
+        $response = new Response(404);
+        $mock = new MockHandler([$response]);
+
+        $description = new Description([
+            'name' => 'Test API',
+            'baseUri' => 'http://httpbin.org',
+            'operations' => [
+                'foo' => [
+                    'uri' => '/{foo}',
+                    'httpMethod' => 'GET',
+                    'responseClass' => 'Foo',
+                    'parameters' => [
+                        'bar' => [
+                            'type'     => 'string',
+                            'required' => true,
+                            'description' => 'Unique user name (alphanumeric)',
+                            'location' => 'json'
+                        ],
+                    ],
+                    'errorResponses' => [
+                        ['code' => 404, 'class' => CustomCommandException::class]
+                    ]
+                ]
+            ],
+            'models' => [
+                'Foo' => [
+                    'type' => 'object',
+                    'additionalProperties' => [
+                        'location' => 'json'
+                    ]
+                ]
+            ]
+        ]);
+
+        $httpClient = new HttpClient(['handler' => $mock]);
+        $client = new GuzzleClient($httpClient, $description);
+        $client->foo(['bar' => 'baz']);
+    }
+
+    public function testNotCreateExceptionIfDoesNotMatchCode()
+    {
+        $response = new Response(401);
+        $mock = new MockHandler([$response]);
+
+        $description = new Description([
+            'name' => 'Test API',
+            'baseUri' => 'http://httpbin.org',
+            'operations' => [
+                'foo' => [
+                    'uri' => '/{foo}',
+                    'httpMethod' => 'GET',
+                    'responseClass' => 'Foo',
+                    'parameters' => [
+                        'bar' => [
+                            'type'     => 'string',
+                            'required' => true,
+                            'description' => 'Unique user name (alphanumeric)',
+                            'location' => 'json'
+                        ],
+                    ],
+                    'errorResponses' => [
+                        ['code' => 404, 'class' => CustomCommandException::class]
+                    ]
+                ]
+            ],
+            'models' => [
+                'Foo' => [
+                    'type' => 'object',
+                    'additionalProperties' => [
+                        'location' => 'json'
+                    ]
+                ]
+            ]
+        ]);
+
+        $httpClient = new HttpClient(['handler' => $mock]);
+        $client = new GuzzleClient($httpClient, $description);
+        $client->foo(['bar' => 'baz']);
+    }
+
+    /**
+     * @expectedException \GuzzleHttp\Tests\Command\Guzzle\Asset\Exception\CustomCommandException
+     */
+    public function testCreateExceptionWithExactMatchOfReasonPhrase()
+    {
+        $response = new Response(404, [], null, '1.1', 'Bar');
+        $mock = new MockHandler([$response]);
+
+        $description = new Description([
+            'name' => 'Test API',
+            'baseUri' => 'http://httpbin.org',
+            'operations' => [
+                'foo' => [
+                    'uri' => '/{foo}',
+                    'httpMethod' => 'GET',
+                    'responseClass' => 'Foo',
+                    'parameters' => [
+                        'bar' => [
+                            'type'     => 'string',
+                            'required' => true,
+                            'description' => 'Unique user name (alphanumeric)',
+                            'location' => 'json'
+                        ],
+                    ],
+                    'errorResponses' => [
+                        ['code' => 404, 'phrase' => 'Bar', 'class' => CustomCommandException::class]
+                    ]
+                ]
+            ],
+            'models' => [
+                'Foo' => [
+                    'type' => 'object',
+                    'additionalProperties' => [
+                        'location' => 'json'
+                    ]
+                ]
+            ]
+        ]);
+
+        $httpClient = new HttpClient(['handler' => $mock]);
+        $client = new GuzzleClient($httpClient, $description);
+        $client->foo(['bar' => 'baz']);
+    }
+
+    /**
+     * @expectedException \GuzzleHttp\Tests\Command\Guzzle\Asset\Exception\OtherCustomCommandException
+     */
+    public function testFavourMostPreciseMatch()
+    {
+        $response = new Response(404, [], null, '1.1', 'Bar');
+        $mock = new MockHandler([$response]);
+
+        $description = new Description([
+            'name' => 'Test API',
+            'baseUri' => 'http://httpbin.org',
+            'operations' => [
+                'foo' => [
+                    'uri' => '/{foo}',
+                    'httpMethod' => 'GET',
+                    'responseClass' => 'Foo',
+                    'parameters' => [
+                        'bar' => [
+                            'type'     => 'string',
+                            'required' => true,
+                            'description' => 'Unique user name (alphanumeric)',
+                            'location' => 'json'
+                        ],
+                    ],
+                    'errorResponses' => [
+                        ['code' => 404, 'class' => CustomCommandException::class],
+                        ['code' => 404, 'phrase' => 'Bar', 'class' => OtherCustomCommandException::class],
+                    ]
+                ]
+            ],
+            'models' => [
+                'Foo' => [
+                    'type' => 'object',
+                    'additionalProperties' => [
+                        'location' => 'json'
+                    ]
+                ]
+            ]
+        ]);
+
+        $httpClient = new HttpClient(['handler' => $mock]);
+        $client = new GuzzleClient($httpClient, $description);
+        $client->foo(['bar' => 'baz']);
+    }
+
+    /**
+     * @expectedException \GuzzleHttp\Command\Exception\CommandException
+     * @expectedExceptionMessage 404
+     */
+    public function testDoesNotAddResultWhenExceptionIsPresent()
+    {
+        $description = new Description([
+            'operations' => [
+                'foo' => [
+                    'uri' => 'http://httpbin.org/{foo}',
+                    'httpMethod' => 'GET',
+                    'responseModel' => 'j',
+                    'parameters' => [
+                        'bar' => [
+                            'type'     => 'string',
+                            'required' => true,
+                            'location' => 'uri'
+                        ]
+                    ]
+                ]
+            ],
+            'models' => [
+                'j' => [
+                    'type' => 'object'
+                ]
+            ]
+        ]);
+
+        $mock = new MockHandler([new Response(404)]);
+        $stack = HandlerStack::create($mock);
+        $httpClient = new HttpClient(['handler' => $stack]);
+        $client = new GuzzleClient($httpClient, $description);
+        $client->foo(['bar' => 'baz']);
+    }
+
+    public function testReturnsExpectedResult()
+    {
+        $loginResponse = new Response(
+            200,
+            [],
+            '{
+                "LoginResponse":{
+                    "result":{
+                        "type":4,
+                        "username":{
+                            "uid":38664492,
+                            "content":"skyfillers-api-test"
+                        },
+                        "token":"3FB1F21014D630481D35CBC30CBF4043"
+                    },
+                    "status":{
+                        "code":200,
+                        "content":"OK"
+                    }
+                }
+            }'
+        );
+        $mock = new MockHandler([$loginResponse]);
+
+        $description = new Description([
+            'name' => 'Test API',
+            'baseUri' => 'http://httpbin.org',
+            'operations' => [
+                'Login' => [
+                    'uri' => '/{foo}',
+                    'httpMethod' => 'POST',
+                    'responseClass' => 'LoginResponse',
+                    'parameters' => [
+                        'username' => [
+                            'type'     => 'string',
+                            'required' => true,
+                            'description' => 'Unique user name (alphanumeric)',
+                            'location' => 'json'
+                        ],
+                        'password' => [
+                            'type'     => 'string',
+                            'required' => true,
+                            'description' => 'User\'s password',
+                            'location' => 'json'
+                        ],
+                        'response' => [
+                            'type'     => 'string',
+                            'required' => false,
+                            'description' => 'Determines the response type: xml = result content will be xml formatted (default); plain = result content will be simple text, without structure; json  = result content will be json formatted',
+                            'location' => 'json'
+                        ],
+                        'token' => [
+                            'type'     => 'string',
+                            'required' => false,
+                            'description' => 'Provides the authentication token',
+                            'location' => 'json'
+                        ]
+                    ]
+                ]
+            ],
+            'models' => [
+                'LoginResponse' => [
+                    'type' => 'object',
+                    'additionalProperties' => [
+                        'location' => 'json'
+                    ]
+                ]
+            ]
+        ]);
+
+        $httpClient = new HttpClient(['handler' => $mock]);
+        $client = new GuzzleClient($httpClient, $description);
+        $result = $client->Login([
+            'username' => 'test',
+            'password' => 'test',
+            'response' => 'json',
+        ]);
+
+        $expected = [
+            'result' => [
+                'type' => 4,
+                'username' => [
+                    'uid' => 38664492,
+                    'content' => 'skyfillers-api-test'
+                ],
+                'token' => '3FB1F21014D630481D35CBC30CBF4043'
+            ],
+            'status' => [
+                'code' => 200,
+                'content' => 'OK'
+            ]
+        ];
+        $this->assertArraySubset($expected, $result['LoginResponse']);
+    }
+}

+ 1037 - 0
addons/cos/library/Guzzle/guzzle-services/tests/GuzzleClientTest.php

@@ -0,0 +1,1037 @@
+<?php
+namespace GuzzleHttp\Tests\Command\Guzzle;
+
+use GuzzleHttp\Client as HttpClient;
+use GuzzleHttp\Command\CommandInterface;
+use GuzzleHttp\Command\Guzzle\Description;
+use GuzzleHttp\Command\Guzzle\GuzzleClient;
+use GuzzleHttp\Command\Result;
+use GuzzleHttp\Command\ResultInterface;
+use GuzzleHttp\Handler\MockHandler;
+use GuzzleHttp\Psr7\Request;
+use GuzzleHttp\Psr7\Response;
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
+
+/**
+ * @covers \GuzzleHttp\Command\Guzzle\GuzzleClient
+ */
+class GuzzleClientTest extends \PHPUnit_Framework_TestCase
+{
+    public function testExecuteCommandViaMagicMethod()
+    {
+        $client = $this->getServiceClient(
+            [
+                new Response(200, [], '{"foo":"bar"}'),
+                new Response(200, [], '{"foofoo":"barbar"}'),
+            ],
+            null,
+            $this->commandToRequestTransformer()
+        );
+
+        // Synchronous
+        $result1 = $client->doThatThingYouDo(['fizz' => 'buzz']);
+        $this->assertEquals('bar', $result1['foo']);
+        $this->assertEquals('buzz', $result1['_request']['fizz']);
+        $this->assertEquals('doThatThingYouDo', $result1['_request']['action']);
+
+        // Asynchronous
+        $result2 = $client->doThatThingOtherYouDoAsync(['fizz' => 'buzz'])->wait();
+        $this->assertEquals('barbar', $result2['foofoo']);
+        $this->assertEquals('doThatThingOtherYouDo', $result2['_request']['action']);
+    }
+
+    public function testExecuteWithQueryLocation()
+    {
+        $mock = new MockHandler();
+        $client = $this->getServiceClient(
+            [
+                new Response(200, [], '{"foo":"bar"}'),
+                new Response(200, [], '{"foo":"bar"}')
+            ],
+            $mock
+        );
+
+        $client->doQueryLocation(['foo' => 'Foo']);
+        $this->assertEquals('foo=Foo', $mock->getLastRequest()->getUri()->getQuery());
+
+        $client->doQueryLocation([
+            'foo' => 'Foo',
+            'bar' => 'Bar',
+            'baz' => 'Baz'
+        ]);
+        $last = $mock->getLastRequest();
+        $this->assertEquals('foo=Foo&bar=Bar&baz=Baz', $last->getUri()->getQuery());
+    }
+
+    public function testExecuteWithBodyLocation()
+    {
+        $mock = new MockHandler();
+
+        $client = $this->getServiceClient(
+            [
+                new Response(200, [], '{"foo":"bar"}'),
+                new Response(200, [], '{"foo":"bar"}')
+            ],
+            $mock
+        );
+
+        $client->doBodyLocation(['foo' => 'Foo']);
+        $this->assertEquals('foo=Foo', (string) $mock->getLastRequest()->getBody());
+
+        $client->doBodyLocation([
+            'foo' => 'Foo',
+            'bar' => 'Bar',
+            'baz' => 'Baz'
+        ]);
+        $this->assertEquals('foo=Foo&bar=Bar&baz=Baz', (string) $mock->getLastRequest()->getBody());
+    }
+
+    public function testExecuteWithJsonLocation()
+    {
+        $mock = new MockHandler();
+
+        $client = $this->getServiceClient(
+            [
+                new Response(200, [], '{"foo":"bar"}'),
+                new Response(200, [], '{"foo":"bar"}')
+            ],
+            $mock
+        );
+
+        $client->doJsonLocation(['foo' => 'Foo']);
+        $this->assertEquals('{"foo":"Foo"}', (string) $mock->getLastRequest()->getBody());
+
+        $client->doJsonLocation([
+            'foo' => 'Foo',
+            'bar' => 'Bar',
+            'baz' => 'Baz'
+        ]);
+        $this->assertEquals('{"foo":"Foo","bar":"Bar","baz":"Baz"}', (string) $mock->getLastRequest()->getBody());
+    }
+
+    public function testExecuteWithHeaderLocation()
+    {
+        $mock = new MockHandler();
+
+        $client = $this->getServiceClient(
+            [
+                new Response(200, [], '{"foo":"bar"}'),
+                new Response(200, [], '{"foo":"bar"}')
+            ],
+            $mock
+        );
+
+        $client->doHeaderLocation(['foo' => 'Foo']);
+        $this->assertEquals(['Foo'], $mock->getLastRequest()->getHeader('foo'));
+
+        $client->doHeaderLocation([
+            'foo' => 'Foo',
+            'bar' => 'Bar',
+            'baz' => 'Baz'
+        ]);
+        $this->assertEquals(['Foo'], $mock->getLastRequest()->getHeader('foo'));
+        $this->assertEquals(['Bar'], $mock->getLastRequest()->getHeader('bar'));
+        $this->assertEquals(['Baz'], $mock->getLastRequest()->getHeader('baz'));
+    }
+
+    public function testExecuteWithXmlLocation()
+    {
+        $mock = new MockHandler();
+
+        $client = $this->getServiceClient(
+            [
+                new Response(200, [], '{"foo":"bar"}'),
+                new Response(200, [], '{"foo":"bar"}')
+            ],
+            $mock
+        );
+
+        $client->doXmlLocation(['foo' => 'Foo']);
+        $this->assertEquals(
+            "<?xml version=\"1.0\"?>\n<Request><foo>Foo</foo></Request>\n",
+            (string) $mock->getLastRequest()->getBody()
+        );
+
+        $client->doXmlLocation([
+            'foo' => 'Foo',
+            'bar' => 'Bar',
+            'baz' => 'Baz'
+        ]);
+        $this->assertEquals(
+            "<?xml version=\"1.0\"?>\n<Request><foo>Foo</foo><bar>Bar</bar><baz>Baz</baz></Request>\n",
+            $mock->getLastRequest()->getBody()
+        );
+    }
+    
+    public function testExecuteWithMultiPartLocation()
+    {
+        $mock = new MockHandler();
+
+        $client = $this->getServiceClient(
+            [
+                new Response(200, [], '{"foo":"bar"}'),
+                new Response(200, [], '{"foo":"bar"}'),
+                new Response(200, [], '{"foo":"bar"}')
+            ],
+            $mock
+        );
+
+        $client->doMultiPartLocation(['foo' => 'Foo']);
+        $multiPartRequestBody = (string) $mock->getLastRequest()->getBody();
+        $this->assertContains('name="foo"', $multiPartRequestBody);
+        $this->assertContains('Foo', $multiPartRequestBody);
+
+        $client->doMultiPartLocation([
+            'foo' => 'Foo',
+            'bar' => 'Bar',
+            'baz' => 'Baz'
+        ]);
+
+        $multiPartRequestBody = (string) $mock->getLastRequest()->getBody();
+        $this->assertContains('name="foo"', $multiPartRequestBody);
+        $this->assertContains('Foo', $multiPartRequestBody);
+        $this->assertContains('name="bar"', $multiPartRequestBody);
+        $this->assertContains('Bar', $multiPartRequestBody);
+        $this->assertContains('name="baz"', $multiPartRequestBody);
+        $this->assertContains('Baz', $multiPartRequestBody);
+
+        $client->doMultiPartLocation([
+            'file' => fopen(dirname(__FILE__) . '/Asset/test.html', 'r'),
+        ]);
+        $multiPartRequestBody = (string) $mock->getLastRequest()->getBody();
+        $this->assertContains('name="file"', $multiPartRequestBody);
+        $this->assertContains('filename="test.html"', $multiPartRequestBody);
+        $this->assertContains('<title>Title</title>', $multiPartRequestBody);
+    }
+
+    public function testHasConfig()
+    {
+        $client = new HttpClient();
+        $description = new Description([]);
+        $guzzle = new GuzzleClient(
+            $client,
+            $description,
+            $this->commandToRequestTransformer(),
+            $this->responseToResultTransformer(),
+            null,
+            ['foo' => 'bar']
+        );
+
+        $this->assertSame($client, $guzzle->getHttpClient());
+        $this->assertSame($description, $guzzle->getDescription());
+        $this->assertEquals('bar', $guzzle->getConfig('foo'));
+        $this->assertEquals([], $guzzle->getConfig('defaults'));
+        $guzzle->setConfig('abc', 'listen');
+        $this->assertEquals('listen', $guzzle->getConfig('abc'));
+    }
+
+    public function testAddsValidateHandlerWhenTrue()
+    {
+        $client = new HttpClient();
+        $description = new Description([]);
+        $guzzle = new GuzzleClient(
+            $client,
+            $description,
+            $this->commandToRequestTransformer(),
+            $this->responseToResultTransformer(),
+            null,
+            [
+                'validate' => true,
+                'process' => false
+            ]
+        );
+
+        $handlers = explode("\n", $guzzle->getHandlerStack()->__toString());
+        $handlers = array_filter($handlers);
+        $this->assertCount(3, $handlers);
+    }
+
+    public function testDisablesHandlersWhenFalse()
+    {
+        $client = new HttpClient();
+        $description = new Description([]);
+        $guzzle = new GuzzleClient(
+            $client,
+            $description,
+            $this->commandToRequestTransformer(),
+            $this->responseToResultTransformer(),
+            null,
+            [
+                'validate' => false,
+                'process' => false
+            ]
+        );
+
+        $handlers = explode("\n", $guzzle->getHandlerStack()->__toString());
+        $handlers = array_filter($handlers);
+        $this->assertCount(1, $handlers);
+    }
+
+    public function testValidateDescription()
+    {
+        $client = new HttpClient();
+        $description = new Description(
+            [
+                'name' => 'Testing API ',
+                'baseUri' => 'http://httpbin.org/',
+                'operations' => [
+                    'Foo' => [
+                        'httpMethod' => 'GET',
+                        'uri' => '/get',
+                        'parameters' => [
+                            'bar' => [
+                                'type' => 'string',
+                                'required' => false,
+                                'description' => 'Bar',
+                                'location' => 'query'
+                            ],
+                            'baz' => [
+                                'type' => 'string',
+                                'required' => false,
+                                'description' => 'baz',
+                                'location' => 'query'
+                            ],
+                        ],
+                        'responseModel' => 'Foo'
+                    ],
+                ],
+                'models' => [
+                    'Foo' => [
+                        'type' => 'object',
+                        'properties' => [
+                            'id' => [
+                                'location' => 'json',
+                                'type' => 'string'
+                            ],
+                            'location' => [
+                                'location' => 'header',
+                                'sentAs' => 'Location',
+                                'type' => 'string'
+                            ],
+                            'age' => [
+                                'location' => 'json',
+                                'type' => 'integer'
+                            ],
+                            'statusCode' => [
+                                'location' => 'statusCode',
+                                'type' => 'integer'
+                            ],
+                        ],
+                    ],
+                ],
+            ]
+        );
+
+        $guzzle = new GuzzleClient(
+            $client,
+            $description,
+            null,
+            null,
+            null,
+            [
+                'validate' => true,
+                'process' => false
+            ]
+        );
+
+        $command = $guzzle->getCommand('Foo', ['baz' => 'BAZ']);
+        /** @var ResponseInterface $response */
+        $response = $guzzle->execute($command);
+        $this->assertInstanceOf(Response::class, $response);
+        $this->assertEquals(200, $response->getStatusCode());
+    }
+
+    /**
+     * @expectedException \GuzzleHttp\Command\Exception\CommandException
+     * @expectedExceptionMessage Validation errors: [baz] is a required string: baz
+     */
+    public function testValidateDescriptionFailsDueMissingRequiredParameter()
+    {
+        $client = new HttpClient();
+        $description = new Description(
+            [
+                'name' => 'Testing API ',
+                'baseUri' => 'http://httpbin.org/',
+                'operations' => [
+                    'Foo' => [
+                        'httpMethod' => 'GET',
+                        'uri' => '/get',
+                        'parameters' => [
+                            'bar' => [
+                                'type' => 'string',
+                                'required' => false,
+                                'description' => 'Bar',
+                                'location' => 'query'
+                            ],
+                            'baz' => [
+                                'type' => 'string',
+                                'required' => true,
+                                'description' => 'baz',
+                                'location' => 'query'
+                            ],
+                        ],
+                        'responseModel' => 'Foo'
+                    ],
+                ],
+                'models' => [
+                    'Foo' => [
+                        'type' => 'object',
+                        'properties' => [
+                            'id' => [
+                                'location' => 'json',
+                                'type' => 'string'
+                            ],
+                            'location' => [
+                                'location' => 'header',
+                                'sentAs' => 'Location',
+                                'type' => 'string'
+                            ],
+                            'age' => [
+                                'location' => 'json',
+                                'type' => 'integer'
+                            ],
+                            'statusCode' => [
+                                'location' => 'statusCode',
+                                'type' => 'integer'
+                            ],
+                        ],
+                    ],
+                ],
+            ]
+        );
+
+        $guzzle = new GuzzleClient(
+            $client,
+            $description,
+            null,
+            null,
+            null,
+            [
+                'validate' => true,
+                'process' => false
+            ]
+        );
+
+        $command = $guzzle->getCommand('Foo');
+        /** @var ResultInterface $result */
+        $result = $guzzle->execute($command);
+        $this->assertInstanceOf(Result::class, $result);
+        $result = $result->toArray();
+        $this->assertEquals(200, $result['statusCode']);
+    }
+
+    /**
+     * @expectedException \GuzzleHttp\Command\Exception\CommandException
+     * @expectedExceptionMessage Validation errors: [baz] must be of type integer
+     */
+    public function testValidateDescriptionFailsDueTypeMismatch()
+    {
+        $client = new HttpClient();
+        $description = new Description(
+            [
+                'name' => 'Testing API ',
+                'baseUri' => 'http://httpbin.org/',
+                'operations' => [
+                    'Foo' => [
+                        'httpMethod' => 'GET',
+                        'uri' => '/get',
+                        'parameters' => [
+                            'bar' => [
+                                'type' => 'string',
+                                'required' => false,
+                                'description' => 'Bar',
+                                'location' => 'query'
+                            ],
+                            'baz' => [
+                                'type' => 'integer',
+                                'required' => true,
+                                'description' => 'baz',
+                                'location' => 'query'
+                            ],
+                        ],
+                        'responseModel' => 'Foo'
+                    ],
+                ],
+                'models' => [
+                    'Foo' => [
+                        'type' => 'object',
+                        'properties' => [
+                            'id' => [
+                                'location' => 'json',
+                                'type' => 'string'
+                            ],
+                            'location' => [
+                                'location' => 'header',
+                                'sentAs' => 'Location',
+                                'type' => 'string'
+                            ],
+                            'age' => [
+                                'location' => 'json',
+                                'type' => 'integer'
+                            ],
+                            'statusCode' => [
+                                'location' => 'statusCode',
+                                'type' => 'integer'
+                            ],
+                        ],
+                    ],
+                ],
+            ]
+        );
+
+        $guzzle = new GuzzleClient(
+            $client,
+            $description,
+            null,
+            null,
+            null,
+            [
+                'validate' => true,
+                'process' => false
+            ]
+        );
+
+        $command = $guzzle->getCommand('Foo', ['baz' => 'Hello']);
+        /** @var ResultInterface $result */
+        $result = $guzzle->execute($command);
+        $this->assertInstanceOf(Result::class, $result);
+        $result = $result->toArray();
+        $this->assertEquals(200, $result['statusCode']);
+    }
+
+    public function testValidateDescriptionDoesNotFailWhenSendingIntegerButExpectingString()
+    {
+        $client = new HttpClient();
+        $description = new Description(
+            [
+                'name' => 'Testing API ',
+                'baseUri' => 'http://httpbin.org/',
+                'operations' => [
+                    'Foo' => [
+                        'httpMethod' => 'GET',
+                        'uri' => '/get',
+                        'parameters' => [
+                            'bar' => [
+                                'type' => 'string',
+                                'required' => false,
+                                'description' => 'Bar',
+                                'location' => 'query'
+                            ],
+                            'baz' => [
+                                'type' => 'string',
+                                'required' => true,
+                                'description' => 'baz',
+                                'location' => 'query'
+                            ],
+                        ],
+                        'responseModel' => 'Foo'
+                    ],
+                ],
+                'models' => [
+                    'Foo' => [
+                        'type' => 'object',
+                        'properties' => [
+                            'id' => [
+                                'location' => 'json',
+                                'type' => 'string'
+                            ],
+                            'location' => [
+                                'location' => 'header',
+                                'sentAs' => 'Location',
+                                'type' => 'string'
+                            ],
+                            'age' => [
+                                'location' => 'json',
+                                'type' => 'integer'
+                            ],
+                            'statusCode' => [
+                                'location' => 'statusCode',
+                                'type' => 'integer'
+                            ],
+                        ],
+                    ],
+                ],
+            ]
+        );
+
+        $guzzle = new GuzzleClient($client, $description);
+
+        $command = $guzzle->getCommand('Foo', ['baz' => 42]);
+        /** @var ResultInterface $result */
+        $result = $guzzle->execute($command);
+        $this->assertInstanceOf(Result::class, $result);
+        $result = $result->toArray();
+        $this->assertEquals(200, $result['statusCode']);
+    }
+
+    public function testMagicMethodExecutesCommands()
+    {
+        $client = new HttpClient();
+        $description = new Description(
+            [
+                'name' => 'Testing API ',
+                'baseUri' => 'http://httpbin.org/',
+                'operations' => [
+                    'Foo' => [
+                        'httpMethod' => 'GET',
+                        'uri' => '/get',
+                        'parameters' => [
+                            'bar' => [
+                                'type' => 'string',
+                                'required' => false,
+                                'description' => 'Bar',
+                                'location' => 'query'
+                            ],
+                            'baz' => [
+                                'type' => 'string',
+                                'required' => true,
+                                'description' => 'baz',
+                                'location' => 'query'
+                            ],
+                        ],
+                        'responseModel' => 'Foo'
+                    ],
+                ],
+                'models' => [
+                    'Foo' => [
+                        'type' => 'object',
+                        'properties' => [
+                            'id' => [
+                                'location' => 'json',
+                                'type' => 'string'
+                            ],
+                            'location' => [
+                                'location' => 'header',
+                                'sentAs' => 'Location',
+                                'type' => 'string'
+                            ],
+                            'age' => [
+                                'location' => 'json',
+                                'type' => 'integer'
+                            ],
+                            'statusCode' => [
+                                'location' => 'statusCode',
+                                'type' => 'integer'
+                            ],
+                        ],
+                    ],
+                ],
+            ]
+        );
+
+        $guzzle = $this->getMockBuilder(GuzzleClient::class)
+            ->setConstructorArgs([
+                $client,
+                $description
+            ])
+            ->setMethods(['execute'])
+            ->getMock();
+
+        $guzzle->expects($this->once())
+            ->method('execute')
+            ->will($this->returnValue('foo'));
+
+        $this->assertEquals('foo', $guzzle->foo([]));
+    }
+
+    /**
+     * @expectedException \InvalidArgumentException
+     * @expectedExceptionMessage No operation found named Foo
+     */
+    public function testThrowsWhenOperationNotFoundInDescription()
+    {
+        $client = new HttpClient();
+        $description = new Description([]);
+        $guzzle = new GuzzleClient(
+            $client,
+            $description,
+            $this->commandToRequestTransformer(),
+            $this->responseToResultTransformer()
+        );
+        $guzzle->getCommand('foo');
+    }
+
+    public function testReturnsProcessedResponse()
+    {
+        $client = new HttpClient();
+
+        $description = new Description(
+            [
+                'name' => 'Testing API ',
+                'baseUri' => 'http://httpbin.org/',
+                'operations' => [
+                    'Foo' => [
+                        'httpMethod' => 'GET',
+                        'uri' => '/get',
+                        'parameters' => [
+                            'bar' => [
+                                'type' => 'string',
+                                'required' => false,
+                                'description' => 'Bar',
+                                'location' => 'query'
+                            ],
+                            'baz' => [
+                                'type' => 'string',
+                                'required' => true,
+                                'description' => 'baz',
+                                'location' => 'query'
+                            ],
+                        ],
+                        'responseModel' => 'Foo'
+                    ],
+                ],
+                'models' => [
+                    'Foo' => [
+                        'type' => 'object',
+                        'properties' => [
+                            'id' => [
+                                'location' => 'json',
+                                'type' => 'string'
+                            ],
+                            'location' => [
+                                'location' => 'header',
+                                'sentAs' => 'Location',
+                                'type' => 'string'
+                            ],
+                            'age' => [
+                                'location' => 'json',
+                                'type' => 'integer'
+                            ],
+                            'statusCode' => [
+                                'location' => 'statusCode',
+                                'type' => 'integer'
+                            ],
+                        ],
+                    ],
+                ],
+            ]
+        );
+
+        $guzzle = new GuzzleClient($client, $description, null, null);
+        $command = $guzzle->getCommand('foo', ['baz' => 'BAZ']);
+
+        /** @var ResultInterface $result */
+        $result = $guzzle->execute($command);
+        $this->assertInstanceOf(Result::class, $result);
+        $result = $result->toArray();
+        $this->assertEquals(200, $result['statusCode']);
+    }
+
+    private function getServiceClient(
+        array $responses,
+        MockHandler $mock = null,
+        callable $commandToRequestTransformer = null
+    ) {
+        $mock = $mock ?: new MockHandler();
+
+        foreach ($responses as $response) {
+            $mock->append($response);
+        }
+
+        return new GuzzleClient(
+            new HttpClient([
+                'handler' => $mock
+            ]),
+            $this->getDescription(),
+            $commandToRequestTransformer,
+            $this->responseToResultTransformer(),
+            null,
+            ['foo' => 'bar']
+        );
+    }
+
+    private function commandToRequestTransformer()
+    {
+        return function (CommandInterface $command) {
+            $data           = $command->toArray();
+            $data['action'] = $command->getName();
+
+            return new Request('POST', '/', [], http_build_query($data));
+        };
+    }
+
+    private function responseToResultTransformer()
+    {
+        return function (ResponseInterface $response, RequestInterface $request, CommandInterface $command) {
+            $data = \GuzzleHttp\json_decode($response->getBody(), true);
+            parse_str($request->getBody(), $data['_request']);
+
+            return new Result($data);
+        };
+    }
+
+    private function getDescription()
+    {
+        return new Description(
+            [
+                'name' => 'Testing API ',
+                'baseUri' => 'http://httpbin.org/',
+                'operations' => [
+                    'doThatThingYouDo' => [
+                        'responseModel' => 'Bar'
+                    ],
+                    'doThatThingOtherYouDo' => [
+                        'responseModel' => 'Foo'
+                    ],
+                    'doQueryLocation' => [
+                        'httpMethod' => 'GET',
+                        'uri' => '/queryLocation',
+                        'parameters' => [
+                            'foo' => [
+                                'type' => 'string',
+                                'required' => false,
+                                'description' => 'Testing query request location',
+                                'location' => 'query'
+                            ],
+                            'bar' => [
+                                'type' => 'string',
+                                'required' => false,
+                                'description' => 'Testing query request location',
+                                'location' => 'query'
+                            ],
+                            'baz' => [
+                                'type' => 'string',
+                                'required' => false,
+                                'description' => 'Testing query request location',
+                                'location' => 'query'
+                            ]
+                        ],
+                        'responseModel' => 'QueryResponse'
+                    ],
+                    'doBodyLocation' => [
+                        'httpMethod' => 'GET',
+                        'uri' => '/bodyLocation',
+                        'parameters' => [
+                            'foo' => [
+                                'type' => 'string',
+                                'required' => false,
+                                'description' => 'Testing body request location',
+                                'location' => 'body'
+                            ],
+                            'bar' => [
+                                'type' => 'string',
+                                'required' => false,
+                                'description' => 'Testing body request location',
+                                'location' => 'body'
+                            ],
+                            'baz' => [
+                                'type' => 'string',
+                                'required' => false,
+                                'description' => 'Testing body request location',
+                                'location' => 'body'
+                            ]
+                        ],
+                        'responseModel' => 'BodyResponse'
+                    ],
+                    'doJsonLocation' => [
+                        'httpMethod' => 'GET',
+                        'uri' => '/jsonLocation',
+                        'parameters' => [
+                            'foo' => [
+                                'type' => 'string',
+                                'required' => false,
+                                'description' => 'Testing json request location',
+                                'location' => 'json'
+                            ],
+                            'bar' => [
+                                'type' => 'string',
+                                'required' => false,
+                                'description' => 'Testing json request location',
+                                'location' => 'json'
+                            ],
+                            'baz' => [
+                                'type' => 'string',
+                                'required' => false,
+                                'description' => 'Testing json request location',
+                                'location' => 'json'
+                            ]
+                        ],
+                        'responseModel' => 'JsonResponse'
+                    ],
+                    'doHeaderLocation' => [
+                        'httpMethod' => 'GET',
+                        'uri' => '/headerLocation',
+                        'parameters' => [
+                            'foo' => [
+                                'type' => 'string',
+                                'required' => false,
+                                'description' => 'Testing header request location',
+                                'location' => 'header'
+                            ],
+                            'bar' => [
+                                'type' => 'string',
+                                'required' => false,
+                                'description' => 'Testing header request location',
+                                'location' => 'header'
+                            ],
+                            'baz' => [
+                                'type' => 'string',
+                                'required' => false,
+                                'description' => 'Testing header request location',
+                                'location' => 'header'
+                            ]
+                        ],
+                        'responseModel' => 'HeaderResponse'
+                    ],
+                    'doXmlLocation' => [
+                        'httpMethod' => 'GET',
+                        'uri' => '/xmlLocation',
+                        'parameters' => [
+                            'foo' => [
+                                'type' => 'string',
+                                'required' => false,
+                                'description' => 'Testing xml request location',
+                                'location' => 'xml'
+                            ],
+                            'bar' => [
+                                'type' => 'string',
+                                'required' => false,
+                                'description' => 'Testing xml request location',
+                                'location' => 'xml'
+                            ],
+                            'baz' => [
+                                'type' => 'string',
+                                'required' => false,
+                                'description' => 'Testing xml request location',
+                                'location' => 'xml'
+                            ]
+                        ],
+                        'responseModel' => 'XmlResponse'
+                    ],
+                    'doMultiPartLocation' => [
+                        'httpMethod' => 'POST',
+                        'uri' => '/multipartLocation',
+                        'parameters' => [
+                            'foo' => [
+                                'type' => 'string',
+                                'required' => false,
+                                'description' => 'Testing multipart request location',
+                                'location' => 'multipart'
+                            ],
+                            'bar' => [
+                                'type' => 'string',
+                                'required' => false,
+                                'description' => 'Testing multipart request location',
+                                'location' => 'multipart'
+                            ],
+                            'baz' => [
+                                'type' => 'string',
+                                'required' => false,
+                                'description' => 'Testing multipart request location',
+                                'location' => 'multipart'
+                            ],
+                            'file' => [
+                                'type' => 'any',
+                                'required' => false,
+                                'description' => 'Testing multipart request location',
+                                'location' => 'multipart'
+                            ]
+                        ],
+                        'responseModel' => 'MultipartResponse'
+                    ],
+                ],
+                'models'  => [
+                    'Foo' => [
+                        'type' => 'object',
+                        'properties' => [
+                            'code' => [
+                                'location' => 'statusCode'
+                            ]
+                        ]
+                    ],
+                    'Bar' => [
+                        'type' => 'object',
+                        'properties' => [
+                            'code' => ['
+                                location' => 'statusCode'
+                            ]
+                        ]
+                    ]
+                ]
+            ]
+        );
+    }
+
+    public function testDocumentationExampleFromReadme()
+    {
+        $client = new HttpClient();
+        $description = new Description([
+            'baseUrl' => 'http://httpbin.org/',
+                'operations' => [
+                    'testing' => [
+                        'httpMethod' => 'GET',
+                        'uri' => '/get{?foo}',
+                        'responseModel' => 'getResponse',
+                        'parameters' => [
+                            'foo' => [
+                                'type' => 'string',
+                                'location' => 'uri'
+                            ],
+                            'bar' => [
+                                'type' => 'string',
+                                'location' => 'query'
+                            ]
+                        ]
+                    ]
+                ],
+                'models' => [
+                    'getResponse' => [
+                        'type' => 'object',
+                        'additionalProperties' => [
+                            'location' => 'json'
+                        ]
+                    ]
+                ]
+        ]);
+
+        $guzzle = new GuzzleClient($client, $description);
+
+        $result = $guzzle->testing(['foo' => 'bar']);
+        $this->assertEquals('bar', $result['args']['foo']);
+    }
+
+    public function testDescriptionWithExtends()
+        {
+            $client = new HttpClient();
+            $description = new Description([
+                    'baseUrl' => 'http://httpbin.org/',
+                    'operations' => [
+                        'testing' => [
+                            'httpMethod' => 'GET',
+                            'uri' => '/get',
+                            'responseModel' => 'getResponse',
+                            'parameters' => [
+                                'foo' => [
+                                    'type' => 'string',
+                                    'default' => 'foo',
+                                    'location' => 'query'
+                                ]
+                            ]
+                        ],
+                        'testing_extends' => [
+                            'extends' => 'testing',
+                            'responseModel' => 'getResponse',
+                            'parameters' => [
+                                'bar' => [
+                                    'type' => 'string',
+                                    'location' => 'query'
+                                ]
+                            ]
+                        ],
+                    ],
+                    'models' => [
+                        'getResponse' => [
+                            'type' => 'object',
+                            'additionalProperties' => [
+                                'location' => 'json'
+                            ]
+                        ]
+                    ]
+            ]);
+            $guzzle = new GuzzleClient($client, $description);
+            $result = $guzzle->testing_extends(['bar' => 'bar']);
+            $this->assertEquals('bar', $result['args']['bar']);
+            $this->assertEquals('foo', $result['args']['foo']);
+        }
+}

+ 112 - 0
addons/cos/library/Guzzle/guzzle-services/tests/Handler/ValidatedDescriptionHandlerTest.php

@@ -0,0 +1,112 @@
+<?php
+namespace GuzzleHttp\Tests\Command\Guzzle\Handler;
+
+use GuzzleHttp\Client as HttpClient;
+use GuzzleHttp\Command\Guzzle\Description;
+use GuzzleHttp\Command\Guzzle\GuzzleClient;
+
+/**
+ * @covers \GuzzleHttp\Command\Guzzle\Handler\ValidatedDescriptionHandler
+ */
+class ValidatedDescriptionHandlerTest extends \PHPUnit_Framework_TestCase
+{
+
+    /**
+     * @expectedException \GuzzleHttp\Command\Exception\CommandException
+     * @expectedExceptionMessage Validation errors: [bar] is a required string
+     */
+    public function testValidates()
+    {
+        $description = new Description([
+            'operations' => [
+                'foo' => [
+                    'uri' => 'http://httpbin.org',
+                    'httpMethod' => 'GET',
+                    'responseModel' => 'j',
+                    'parameters' => [
+                        'bar' => [
+                            'type'     => 'string',
+                            'required' => true
+                        ]
+                    ]
+                ]
+            ]
+        ]);
+
+        $client = new GuzzleClient(new HttpClient(), $description);
+        $client->foo([]);
+    }
+
+    public function testSuccessfulValidationDoesNotThrow()
+    {
+        $description = new Description([
+            'operations' => [
+                'foo' => [
+                    'uri' => 'http://httpbin.org',
+                    'httpMethod' => 'GET',
+                    'responseModel' => 'j',
+                    'parameters' => []
+                ]
+            ],
+            'models' => [
+                'j' => [
+                    'type' => 'object'
+                ]
+            ]
+        ]);
+
+        $client = new GuzzleClient(new HttpClient(), $description);
+        $client->foo([]);
+    }
+
+    /**
+     * @expectedException \GuzzleHttp\Command\Exception\CommandException
+     * @expectedExceptionMessage Validation errors: [bar] must be of type string
+     */
+    public function testValidatesAdditionalParameters()
+    {
+        $description = new Description([
+            'operations' => [
+                'foo' => [
+                    'uri' => 'http://httpbin.org',
+                    'httpMethod' => 'GET',
+                    'responseModel' => 'j',
+                    'additionalParameters' => [
+                        'type'     => 'string'
+                    ]
+                ]
+            ],
+            'models' => [
+                'j' => [
+                    'type' => 'object'
+                ]
+            ]
+        ]);
+
+        $client = new GuzzleClient(new HttpClient(), $description);
+        $client->foo(['bar' => new \stdClass()]);
+    }
+
+    public function testFilterBeforeValidate()
+    {
+        $description = new Description([
+            'operations' => [
+                'foo' => [
+                    'uri' => 'http://httpbin.org',
+                    'httpMethod' => 'GET',
+                    'parameters' => [
+                        'bar' => [
+                            'location' => 'uri',
+                            'type'     => 'string',
+                            'format'   => 'date-time',
+                            'required' => true
+                        ]
+                    ]
+                ]
+            ]
+        ]);
+
+        $client = new GuzzleClient(new HttpClient(), $description);
+        $client->foo(['bar' => new \DateTimeImmutable()]); // Should not throw any exception
+    }
+}

+ 227 - 0
addons/cos/library/Guzzle/guzzle-services/tests/OperationTest.php

@@ -0,0 +1,227 @@
+<?php
+namespace Guzzle\Tests\Service\Description;
+
+use GuzzleHttp\Command\Guzzle\Description;
+use GuzzleHttp\Command\Guzzle\Operation;
+
+/**
+ * @covers \GuzzleHttp\Command\Guzzle\Operation
+ */
+class OperationTest extends \PHPUnit_Framework_TestCase
+{
+    public static function strtoupper($string)
+    {
+        return strtoupper($string);
+    }
+
+    public function testOperationIsDataObject()
+    {
+        $c = new Operation([
+            'name'               => 'test',
+            'summary'            => 'doc',
+            'notes'              => 'notes',
+            'documentationUrl'   => 'http://www.example.com',
+            'httpMethod'         => 'POST',
+            'uri'                => '/api/v1',
+            'responseModel'      => 'abc',
+            'deprecated'         => true,
+            'parameters'         => [
+                'key' => [
+                    'required'  => true,
+                    'type'      => 'string',
+                    'maxLength' => 10,
+                    'name'      => 'key'
+                ],
+                'key_2' => [
+                    'required' => true,
+                    'type'     => 'integer',
+                    'default'  => 10,
+                    'name'     => 'key_2'
+                ]
+            ]
+        ]);
+
+        $this->assertEquals('test', $c->getName());
+        $this->assertEquals('doc', $c->getSummary());
+        $this->assertEquals('http://www.example.com', $c->getDocumentationUrl());
+        $this->assertEquals('POST', $c->getHttpMethod());
+        $this->assertEquals('/api/v1', $c->getUri());
+        $this->assertEquals('abc', $c->getResponseModel());
+        $this->assertTrue($c->getDeprecated());
+
+        $params = array_map(function ($c) {
+            return $c->toArray();
+        }, $c->getParams());
+
+        $this->assertEquals([
+            'key' => [
+                'required'  => true,
+                'type'      => 'string',
+                'maxLength' => 10,
+                'name'       => 'key'
+            ],
+            'key_2' => [
+                'required' => true,
+                'type'     => 'integer',
+                'default'  => 10,
+                'name'     => 'key_2'
+            ]
+        ], $params);
+
+        $this->assertEquals([
+            'required' => true,
+            'type'     => 'integer',
+            'default'  => 10,
+            'name'     => 'key_2'
+        ], $c->getParam('key_2')->toArray());
+
+        $this->assertNull($c->getParam('afefwef'));
+        $this->assertArrayNotHasKey('parent', $c->getParam('key_2')->toArray());
+    }
+
+    public function testDeterminesIfHasParam()
+    {
+        $command = $this->getTestCommand();
+        $this->assertTrue($command->hasParam('data'));
+        $this->assertFalse($command->hasParam('baz'));
+    }
+
+    protected function getTestCommand()
+    {
+        return new Operation([
+            'parameters' => [
+                'data' => ['type' => 'string']
+            ]
+        ]);
+    }
+
+    public function testAddsNameToParametersIfNeeded()
+    {
+        $command = new Operation(['parameters' => ['foo' => []]]);
+        $this->assertEquals('foo', $command->getParam('foo')->getName());
+    }
+
+    public function testContainsApiErrorInformation()
+    {
+        $command = $this->getOperation();
+        $this->assertEquals(1, count($command->getErrorResponses()));
+    }
+
+    public function testHasNotes()
+    {
+        $o = new Operation(['notes' => 'foo']);
+        $this->assertEquals('foo', $o->getNotes());
+    }
+
+    public function testHasData()
+    {
+        $o = new Operation(['data' => ['foo' => 'baz', 'bar' => 123]]);
+        $this->assertEquals('baz', $o->getData('foo'));
+        $this->assertEquals(123, $o->getData('bar'));
+        $this->assertNull($o->getData('wfefwe'));
+        $this->assertEquals(['foo' => 'baz', 'bar' => 123], $o->getData());
+    }
+
+    /**
+     * @expectedException \InvalidArgumentException
+     * @expectedExceptionMesssage Parameters must be arrays
+     */
+    public function testEnsuresParametersAreArrays()
+    {
+        new Operation(['parameters' => ['foo' => true]]);
+    }
+
+    public function testHasDescription()
+    {
+        $s = new Description([]);
+        $o = new Operation([], $s);
+        $this->assertSame($s, $o->getServiceDescription());
+    }
+
+    public function testHasAdditionalParameters()
+    {
+        $o = new Operation([
+            'additionalParameters' => [
+                'type' => 'string', 'name' => 'binks',
+            ],
+            'parameters' => [
+                'foo' => ['type' => 'integer'],
+            ],
+        ]);
+        $this->assertEquals('string', $o->getAdditionalParameters()->getType());
+    }
+
+    /**
+     * @return Operation
+     */
+    protected function getOperation()
+    {
+        return new Operation([
+            'name'       => 'OperationTest',
+            'class'      => get_class($this),
+            'parameters' => [
+                'test'          => ['type' => 'object'],
+                'bool_1'        => ['default' => true, 'type' => 'boolean'],
+                'bool_2'        => ['default' => false],
+                'float'         => ['type' => 'numeric'],
+                'int'           => ['type' => 'integer'],
+                'date'          => ['type' => 'string'],
+                'timestamp'     => ['type' => 'string'],
+                'string'        => ['type' => 'string'],
+                'username'      => ['type' => 'string', 'required' => true, 'filters' => 'strtolower'],
+                'test_function' => ['type' => 'string', 'filters' => __CLASS__ . '::strtoupper'],
+            ],
+            'errorResponses' => [
+                [
+                    'code' => 503,
+                    'reason' => 'InsufficientCapacity',
+                    'class' => 'Guzzle\\Exception\\RuntimeException',
+                ],
+            ],
+        ]);
+    }
+
+    public function testCanExtendFromOtherOperations()
+    {
+        $d = new Description([
+            'operations' => [
+                'A' => [
+                    'parameters' => [
+                        'A' => [
+                            'type' => 'object',
+                            'properties' => ['foo' => ['type' => 'string']]
+                        ],
+                        'B' => ['type' => 'string']
+                    ],
+                    'summary' => 'foo'
+                ],
+                'B' => [
+                    'extends' => 'A',
+                    'summary' => 'Bar'
+                ],
+                'C' => [
+                    'extends' => 'B',
+                    'summary' => 'Bar',
+                    'parameters' => [
+                        'B' => ['type' => 'number']
+                    ]
+                ]
+            ]
+        ]);
+
+        $a = $d->getOperation('A');
+        $this->assertEquals('foo', $a->getSummary());
+        $this->assertTrue($a->hasParam('A'));
+        $this->assertEquals('string', $a->getParam('B')->getType());
+
+        $b = $d->getOperation('B');
+        $this->assertTrue($a->hasParam('A'));
+        $this->assertEquals('Bar', $b->getSummary());
+        $this->assertEquals('string', $a->getParam('B')->getType());
+
+        $c = $d->getOperation('C');
+        $this->assertTrue($a->hasParam('A'));
+        $this->assertEquals('Bar', $c->getSummary());
+        $this->assertEquals('number', $c->getParam('B')->getType());
+    }
+}

+ 378 - 0
addons/cos/library/Guzzle/guzzle-services/tests/ParameterTest.php

@@ -0,0 +1,378 @@
+<?php
+namespace Guzzle\Tests\Service\Description;
+
+use GuzzleHttp\Command\Guzzle\Description;
+use GuzzleHttp\Command\Guzzle\Parameter;
+
+/**
+ * @covers \GuzzleHttp\Command\Guzzle\Parameter
+ */
+class ParameterTest extends \PHPUnit_Framework_TestCase
+{
+    protected $data = [
+        'name'            => 'foo',
+        'type'            => 'bar',
+        'required'        => true,
+        'default'         => '123',
+        'description'     => '456',
+        'minLength'       => 2,
+        'maxLength'       => 5,
+        'location'        => 'body',
+        'static'          => true,
+        'filters'         => ['trim', 'json_encode']
+    ];
+
+    public function testCreatesParamFromArray()
+    {
+        $p = new Parameter($this->data);
+        $this->assertEquals('foo', $p->getName());
+        $this->assertEquals('bar', $p->getType());
+        $this->assertTrue($p->isRequired());
+        $this->assertEquals('123', $p->getDefault());
+        $this->assertEquals('456', $p->getDescription());
+        $this->assertEquals(2, $p->getMinLength());
+        $this->assertEquals(5, $p->getMaxLength());
+        $this->assertEquals('body', $p->getLocation());
+        $this->assertTrue($p->isStatic());
+        $this->assertEquals(['trim', 'json_encode'], $p->getFilters());
+        $p->setName('abc');
+        $this->assertEquals('abc', $p->getName());
+    }
+
+    /**
+     * @expectedException \InvalidArgumentException
+     */
+    public function testValidatesDescription()
+    {
+        new Parameter($this->data, ['description' => 'foo']);
+    }
+
+    public function testCanConvertToArray()
+    {
+        $p = new Parameter($this->data);
+        $this->assertEquals($this->data, $p->toArray());
+    }
+
+    public function testUsesStatic()
+    {
+        $d = $this->data;
+        $d['default'] = 'booboo';
+        $d['static'] = true;
+        $p = new Parameter($d);
+        $this->assertEquals('booboo', $p->getValue('bar'));
+    }
+
+    public function testUsesDefault()
+    {
+        $d = $this->data;
+        $d['default'] = 'foo';
+        $d['static'] = null;
+        $p = new Parameter($d);
+        $this->assertEquals('foo', $p->getValue(null));
+    }
+
+    public function testReturnsYourValue()
+    {
+        $d = $this->data;
+        $d['static'] = null;
+        $p = new Parameter($d);
+        $this->assertEquals('foo', $p->getValue('foo'));
+    }
+
+    public function testZeroValueDoesNotCauseDefaultToBeReturned()
+    {
+        $d = $this->data;
+        $d['default'] = '1';
+        $d['static'] = null;
+        $p = new Parameter($d);
+        $this->assertEquals('0', $p->getValue('0'));
+    }
+
+    public function testFiltersValues()
+    {
+        $d = $this->data;
+        $d['static'] = null;
+        $d['filters'] = 'strtoupper';
+        $p = new Parameter($d);
+        $this->assertEquals('FOO', $p->filter('foo'));
+    }
+
+    /**
+     * @expectedException \RuntimeException
+     * @expectedExceptionMessage No service description
+     */
+    public function testRequiresServiceDescriptionForFormatting()
+    {
+        $d = $this->data;
+        $d['format'] = 'foo';
+        $p = new Parameter($d);
+        $p->filter('bar');
+    }
+
+    public function testConvertsBooleans()
+    {
+        $p = new Parameter(['type' => 'boolean']);
+        $this->assertEquals(true, $p->filter('true'));
+        $this->assertEquals(false, $p->filter('false'));
+    }
+
+    public function testUsesArrayByDefaultForFilters()
+    {
+        $d = $this->data;
+        $d['filters'] = null;
+        $p = new Parameter($d);
+        $this->assertEquals([], $p->getFilters());
+    }
+
+    public function testAllowsSimpleLocationValue()
+    {
+        $p = new Parameter(['name' => 'myname', 'location' => 'foo', 'sentAs' => 'Hello']);
+        $this->assertEquals('foo', $p->getLocation());
+        $this->assertEquals('Hello', $p->getSentAs());
+    }
+
+    public function testParsesTypeValues()
+    {
+        $p = new Parameter(['type' => 'foo']);
+        $this->assertEquals('foo', $p->getType());
+    }
+
+    /**
+     * @expectedException \InvalidArgumentException
+     * @expectedExceptionMessage A [method] value must be specified for each complex filter
+     */
+    public function testValidatesComplexFilters()
+    {
+        $p = new Parameter(['filters' => [['args' => 'foo']]]);
+    }
+
+    public function testAllowsComplexFilters()
+    {
+        $that = $this;
+        $param = new Parameter([
+            'filters' => [
+                [
+                    'method' => function ($a, $b, $c, $d) use ($that, &$param) {
+                        $that->assertEquals('test', $a);
+                        $that->assertEquals('my_value!', $b);
+                        $that->assertEquals('bar', $c);
+                        $that->assertSame($param, $d);
+                        return 'abc' . $b;
+                    },
+                    'args' => ['test', '@value', 'bar', '@api']
+                ]
+            ]
+        ]);
+
+        $this->assertEquals('abcmy_value!', $param->filter('my_value!'));
+    }
+
+    public function testAddsAdditionalProperties()
+    {
+        $p = new Parameter([
+            'type' => 'object',
+            'additionalProperties' => ['type' => 'string']
+        ]);
+        $this->assertInstanceOf('GuzzleHttp\Command\Guzzle\Parameter', $p->getAdditionalProperties());
+        $this->assertNull($p->getAdditionalProperties()->getAdditionalProperties());
+        $p = new Parameter(['type' => 'object']);
+        $this->assertTrue($p->getAdditionalProperties());
+    }
+
+    public function testAddsItems()
+    {
+        $p = new Parameter([
+            'type'  => 'array',
+            'items' => ['type' => 'string']
+        ]);
+        $this->assertInstanceOf('GuzzleHttp\Command\Guzzle\Parameter', $p->getItems());
+        $out = $p->toArray();
+        $this->assertEquals('array', $out['type']);
+        $this->assertInternalType('array', $out['items']);
+    }
+
+    public function testCanRetrieveKnownPropertiesUsingDataMethod()
+    {
+        $p = new Parameter(['data' => ['name' => 'test'], 'extra' => 'hi!']);
+        $this->assertEquals('test', $p->getData('name'));
+        $this->assertEquals(['name' => 'test'], $p->getData());
+        $this->assertNull($p->getData('fjnweefe'));
+        $this->assertEquals('hi!', $p->getData('extra'));
+    }
+
+    public function testHasPattern()
+    {
+        $p = new Parameter(['pattern' => '/[0-9]+/']);
+        $this->assertEquals('/[0-9]+/', $p->getPattern());
+    }
+
+    public function testHasEnum()
+    {
+        $p = new Parameter(['enum' => ['foo', 'bar']]);
+        $this->assertEquals(['foo', 'bar'], $p->getEnum());
+    }
+
+    public function testSerializesItems()
+    {
+        $p = new Parameter([
+            'type'  => 'object',
+            'additionalProperties' => ['type' => 'string']
+        ]);
+        $this->assertEquals([
+            'type'  => 'object',
+            'additionalProperties' => ['type' => 'string']
+        ], $p->toArray());
+    }
+
+    public function testResolvesRefKeysRecursively()
+    {
+        $description = new Description([
+            'models' => [
+                'JarJar' => ['type' => 'string', 'default' => 'Mesa address tha senate!'],
+                'Anakin' => ['type' => 'array', 'items' => ['$ref' => 'JarJar']]
+            ],
+        ]);
+        $p = new Parameter(['$ref' => 'Anakin', 'description' => 'added'], ['description' => $description]);
+        $this->assertEquals([
+            'description' => 'added',
+            '$ref' => 'Anakin'
+        ], $p->toArray());
+    }
+
+    public function testResolvesExtendsRecursively()
+    {
+        $jarJar = ['type' => 'string', 'default' => 'Mesa address tha senate!', 'description' => 'a'];
+        $anakin = ['type' => 'array', 'items' => ['extends' => 'JarJar', 'description' => 'b']];
+        $description = new Description([
+            'models' => ['JarJar' => $jarJar, 'Anakin' => $anakin]
+        ]);
+        // Description attribute will be updated, and format added
+        $p = new Parameter(['extends' => 'Anakin', 'format' => 'date'], ['description' => $description]);
+        $this->assertEquals([
+            'format' => 'date',
+            'extends' => 'Anakin'
+        ], $p->toArray());
+    }
+
+    public function testHasKeyMethod()
+    {
+        $p = new Parameter(['name' => 'foo', 'sentAs' => 'bar']);
+        $this->assertEquals('bar', $p->getWireName());
+    }
+
+    public function testIncludesNameInToArrayWhenItemsAttributeHasName()
+    {
+        $p = new Parameter([
+            'type' => 'array',
+            'name' => 'Abc',
+            'items' => [
+                'name' => 'Foo',
+                'type' => 'object'
+            ]
+        ]);
+        $result = $p->toArray();
+        $this->assertEquals([
+            'type' => 'array',
+            'name' => 'Abc',
+            'items' => [
+                'name' => 'Foo',
+                'type' => 'object'
+            ]
+        ], $result);
+    }
+
+    public function dateTimeProvider()
+    {
+        $d = 'October 13, 2012 16:15:46 UTC';
+
+        return [
+            [$d, 'date-time', '2012-10-13T16:15:46Z'],
+            [$d, 'date', '2012-10-13'],
+            [$d, 'timestamp', strtotime($d)],
+            [new \DateTime($d), 'timestamp', strtotime($d)]
+        ];
+    }
+
+    /**
+     * @dataProvider dateTimeProvider
+     */
+    public function testAppliesFormat($d, $format, $result)
+    {
+        $p = new Parameter(['format' => $format], ['description' => new Description([])]);
+        $this->assertEquals($format, $p->getFormat());
+        $this->assertEquals($result, $p->filter($d));
+    }
+
+    public function testHasMinAndMax()
+    {
+        $p = new Parameter([
+            'minimum' => 2,
+            'maximum' => 3,
+            'minItems' => 4,
+            'maxItems' => 5,
+        ]);
+        $this->assertEquals(2, $p->getMinimum());
+        $this->assertEquals(3, $p->getMaximum());
+        $this->assertEquals(4, $p->getMinItems());
+        $this->assertEquals(5, $p->getMaxItems());
+    }
+
+    public function testHasProperties()
+    {
+        $data = [
+            'type' => 'object',
+            'properties' => [
+                'foo' => ['type' => 'string'],
+                'bar' => ['type' => 'string'],
+            ]
+        ];
+        $p = new Parameter($data);
+        $this->assertInstanceOf('GuzzleHttp\\Command\\Guzzle\\Parameter', $p->getProperty('foo'));
+        $this->assertSame($p->getProperty('foo'), $p->getProperty('foo'));
+        $this->assertNull($p->getProperty('wefwe'));
+
+        $properties = $p->getProperties();
+        $this->assertInternalType('array', $properties);
+        foreach ($properties as $prop) {
+            $this->assertInstanceOf('GuzzleHttp\\Command\\Guzzle\\Parameter', $prop);
+        }
+
+        $this->assertEquals($data, $p->toArray());
+    }
+
+    /**
+     * @expectedException \InvalidArgumentException
+     * @expectedExceptionMessage Expected a string. Got: array
+     */
+    public function testThrowsWhenNotPassString()
+    {
+        $emptyParam = new Parameter();
+        $this->assertFalse($emptyParam->has([]));
+        $this->assertFalse($emptyParam->has(new \stdClass()));
+        $this->assertFalse($emptyParam->has('1'));
+        $this->assertFalse($emptyParam->has(1));
+    }
+
+    public function testHasReturnsFalseForWrongOrEmptyValues()
+    {
+        $emptyParam = new Parameter();
+        $this->assertFalse($emptyParam->has(''));
+        $this->assertFalse($emptyParam->has('description'));
+        $this->assertFalse($emptyParam->has('noExisting'));
+    }
+
+    public function testHasReturnsTrueForCorrectValues()
+    {
+        $p = new Parameter([
+            'minimum' => 2,
+            'maximum' => 3,
+            'minItems' => 4,
+            'maxItems' => 5,
+        ]);
+
+        $this->assertTrue($p->has('minimum'));
+        $this->assertTrue($p->has('maximum'));
+        $this->assertTrue($p->has('minItems'));
+        $this->assertTrue($p->has('maxItems'));
+    }
+}

+ 35 - 0
addons/cos/library/Guzzle/guzzle-services/tests/QuerySerializer/Rfc3986SerializerTest.php

@@ -0,0 +1,35 @@
+<?php
+namespace GuzzleHttp\Tests\Command\Guzzle\QuerySerializer;
+
+use GuzzleHttp\Command\Guzzle\QuerySerializer\Rfc3986Serializer;
+
+class Rfc3986SerializerTest extends \PHPUnit_Framework_TestCase
+{
+    public function queryProvider()
+    {
+        return [
+            [['foo' => 'bar'], 'foo=bar'],
+            [['foo' => [1, 2]], 'foo[0]=1&foo[1]=2'],
+            [['foo' => ['bar' => 'baz', 'bim' => [4, 5]]], 'foo[bar]=baz&foo[bim][0]=4&foo[bim][1]=5']
+        ];
+    }
+
+    /**
+     * @dataProvider queryProvider
+     */
+    public function testSerializeQueryParams(array $params, $expectedResult)
+    {
+        $serializer = new Rfc3986Serializer();
+        $result     = $serializer->aggregate($params);
+
+        $this->assertEquals($expectedResult, urldecode($result));
+    }
+
+    public function testCanRemoveNumericIndices()
+    {
+        $serializer = new Rfc3986Serializer(true);
+        $result     = $serializer->aggregate(['foo' => ['bar', 'baz'], 'bar' => ['bim' => [4, 5]]]);
+
+        $this->assertEquals('foo[]=bar&foo[]=baz&bar[bim][]=4&bar[bim][]=5', urldecode($result));
+    }
+}

+ 26 - 0
addons/cos/library/Guzzle/guzzle-services/tests/RequestLocation/BodyLocationTest.php

@@ -0,0 +1,26 @@
+<?php
+namespace GuzzleHttp\Tests\Command\Guzzle\RequestLocation;
+
+use GuzzleHttp\Command\Command;
+use GuzzleHttp\Command\Guzzle\Parameter;
+use GuzzleHttp\Command\Guzzle\RequestLocation\BodyLocation;
+use GuzzleHttp\Psr7\Request;
+
+/**
+ * @covers \GuzzleHttp\Command\Guzzle\RequestLocation\BodyLocation
+ */
+class BodyLocationTest extends \PHPUnit_Framework_TestCase
+{
+    /**
+     * @group RequestLocation
+     */
+    public function testVisitsLocation()
+    {
+        $location = new BodyLocation('body');
+        $command = new Command('foo', ['foo' => 'bar']);
+        $request = new Request('POST', 'http://httbin.org');
+        $param = new Parameter(['name' => 'foo']);
+        $request = $location->visit($command, $request, $param);
+        $this->assertEquals('foo=bar', $request->getBody()->getContents());
+    }
+}

+ 52 - 0
addons/cos/library/Guzzle/guzzle-services/tests/RequestLocation/FormParamLocationTest.php

@@ -0,0 +1,52 @@
+<?php
+namespace GuzzleHttp\Tests\Command\Guzzle\RequestLocation;
+
+use GuzzleHttp\Command\Command;
+use GuzzleHttp\Command\Guzzle\Operation;
+use GuzzleHttp\Command\Guzzle\Parameter;
+use GuzzleHttp\Command\Guzzle\RequestLocation\FormParamLocation;
+use GuzzleHttp\Command\Guzzle\RequestLocation\PostFieldLocation;
+use GuzzleHttp\Psr7\Request;
+
+/**
+ * @covers \GuzzleHttp\Command\Guzzle\RequestLocation\FormParamLocation
+ * @covers \GuzzleHttp\Command\Guzzle\RequestLocation\AbstractLocation
+ */
+class FormParamLocationTest extends \PHPUnit_Framework_TestCase
+{
+    /**
+     * @group RequestLocation
+     */
+    public function testVisitsLocation()
+    {
+        $location = new FormParamLocation();
+        $command = new Command('foo', ['foo' => 'bar']);
+        $request = new Request('POST', 'http://httbin.org');
+        $param = new Parameter(['name' => 'foo']);
+        $request = $location->visit($command, $request, $param);
+        $operation = new Operation();
+        $request = $location->after($command, $request, $operation);
+        $this->assertEquals('foo=bar', $request->getBody()->getContents());
+        $this->assertArraySubset([0 => 'application/x-www-form-urlencoded; charset=utf-8'], $request->getHeader('Content-Type'));
+    }
+
+    /**
+     * @group RequestLocation
+     */
+    public function testAddsAdditionalProperties()
+    {
+        $location = new FormParamLocation();
+        $command = new Command('foo', ['foo' => 'bar']);
+        $command['add'] = 'props';
+        $request = new Request('POST', 'http://httbin.org', []);
+        $param = new Parameter(['name' => 'foo']);
+        $request = $location->visit($command, $request, $param);
+        $operation = new Operation([
+            'additionalParameters' => [
+                'location' => 'formParam'
+            ]
+        ]);
+        $request = $location->after($command, $request, $operation);
+        $this->assertEquals('foo=bar&add=props', $request->getBody()->getContents());
+    }
+}

+ 52 - 0
addons/cos/library/Guzzle/guzzle-services/tests/RequestLocation/HeaderLocationTest.php

@@ -0,0 +1,52 @@
+<?php
+namespace GuzzleHttp\Tests\Command\Guzzle\RequestLocation;
+
+use GuzzleHttp\Command\Command;
+use GuzzleHttp\Command\Guzzle\Operation;
+use GuzzleHttp\Command\Guzzle\Parameter;
+use GuzzleHttp\Command\Guzzle\RequestLocation\HeaderLocation;
+use GuzzleHttp\Psr7\Request;
+
+/**
+ * @covers \GuzzleHttp\Command\Guzzle\RequestLocation\HeaderLocation
+ * @covers \GuzzleHttp\Command\Guzzle\RequestLocation\AbstractLocation
+ */
+class HeaderLocationTest extends \PHPUnit_Framework_TestCase
+{
+    /**
+     * @group RequestLocation
+     */
+    public function testVisitsLocation()
+    {
+        $location = new HeaderLocation('header');
+        $command = new Command('foo', ['foo' => 'bar']);
+        $request = new Request('POST', 'http://httbin.org');
+        $param = new Parameter(['name' => 'foo']);
+        $request = $location->visit($command, $request, $param);
+
+        $header = $request->getHeader('foo');
+        $this->assertTrue(is_array($header));
+        $this->assertArraySubset([0 => 'bar'], $request->getHeader('foo'));
+    }
+
+    /**
+     * @group RequestLocation
+     */
+    public function testAddsAdditionalProperties()
+    {
+        $location = new HeaderLocation('header');
+        $command = new Command('foo', ['foo' => 'bar']);
+        $command['add'] = 'props';
+        $operation = new Operation([
+            'additionalParameters' => [
+                'location' => 'header'
+            ]
+        ]);
+        $request = new Request('POST', 'http://httbin.org');
+        $request = $location->after($command, $request, $operation);
+
+        $header = $request->getHeader('add');
+        $this->assertTrue(is_array($header));
+        $this->assertArraySubset([0 => 'props'], $header);
+    }
+}

+ 91 - 0
addons/cos/library/Guzzle/guzzle-services/tests/RequestLocation/JsonLocationTest.php

@@ -0,0 +1,91 @@
+<?php
+namespace GuzzleHttp\Tests\Command\Guzzle\RequestLocation;
+
+use GuzzleHttp\Command\Command;
+use GuzzleHttp\Command\Guzzle\Operation;
+use GuzzleHttp\Command\Guzzle\Parameter;
+use GuzzleHttp\Command\Guzzle\RequestLocation\JsonLocation;
+use GuzzleHttp\Psr7\Request;
+
+/**
+ * @covers \GuzzleHttp\Command\Guzzle\RequestLocation\JsonLocation
+ * @covers \GuzzleHttp\Command\Guzzle\RequestLocation\AbstractLocation
+ */
+class JsonLocationTest extends \PHPUnit_Framework_TestCase
+{
+    /**
+     * @group RequestLocation
+     */
+    public function testVisitsLocation()
+    {
+        $location = new JsonLocation('json');
+        $command = new Command('foo', ['foo' => 'bar']);
+        $request = new Request('POST', 'http://httbin.org');
+        $param = new Parameter(['name' => 'foo']);
+        $location->visit($command, $request, $param);
+        $operation = new Operation();
+        $request = $location->after($command, $request, $operation);
+        $this->assertEquals('{"foo":"bar"}', $request->getBody()->getContents());
+        $this->assertArraySubset([0 => 'application/json'], $request->getHeader('Content-Type'));
+    }
+
+    /**
+     * @group RequestLocation
+     */
+    public function testVisitsAdditionalProperties()
+    {
+        $location = new JsonLocation('json', 'foo');
+        $command = new Command('foo', ['foo' => 'bar']);
+        $command['baz'] = ['bam' => [1]];
+        $request = new Request('POST', 'http://httbin.org');
+        $param = new Parameter(['name' => 'foo']);
+        $location->visit($command, $request, $param);
+        $operation = new Operation([
+            'additionalParameters' => [
+                'location' => 'json'
+            ]
+        ]);
+        $request = $location->after($command, $request, $operation);
+        $this->assertEquals('{"foo":"bar","baz":{"bam":[1]}}', $request->getBody()->getContents());
+        $this->assertEquals([0 => 'foo'], $request->getHeader('Content-Type'));
+    }
+
+    /**
+     * @group RequestLocation
+     */
+    public function testVisitsNestedLocation()
+    {
+        $location = new JsonLocation('json');
+        $command = new Command('foo', ['foo' => 'bar']);
+        $request = new Request('POST', 'http://httbin.org');
+        $param = new Parameter([
+            'name' => 'foo',
+            'type' => 'object',
+            'properties' => [
+                'baz' => [
+                    'type' => 'array',
+                    'items' => [
+                        'type' => 'string',
+                        'filters' => ['strtoupper']
+                    ]
+                ]
+            ],
+            'additionalProperties' => [
+                'type' => 'array',
+                'items' => [
+                    'type' => 'string',
+                    'filters' => ['strtolower']
+                ]
+            ]
+        ]);
+        $command['foo'] = [
+            'baz' => ['a', 'b'],
+            'bam' => ['A', 'B'],
+        ];
+        $location->visit($command, $request, $param);
+        $operation = new Operation();
+        $request = $location->after($command, $request, $operation);
+        $this->assertEquals('{"foo":{"baz":["A","B"],"bam":["a","b"]}}', (string) $request->getBody()->getContents());
+        $this->assertEquals([0 => 'application/json'], $request->getHeader('Content-Type'));
+    }
+}

+ 33 - 0
addons/cos/library/Guzzle/guzzle-services/tests/RequestLocation/MultiPartLocationTest.php

@@ -0,0 +1,33 @@
+<?php
+namespace GuzzleHttp\Tests\Command\Guzzle\RequestLocation;
+
+use GuzzleHttp\Command\Command;
+use GuzzleHttp\Command\Guzzle\Operation;
+use GuzzleHttp\Command\Guzzle\Parameter;
+use GuzzleHttp\Command\Guzzle\RequestLocation\MultiPartLocation;
+use GuzzleHttp\Command\Guzzle\RequestLocation\PostFileLocation;
+use GuzzleHttp\Psr7\Request;
+
+/**
+ * @covers \GuzzleHttp\Command\Guzzle\RequestLocation\MultiPartLocation
+ */
+class MultiPartLocationTest extends \PHPUnit_Framework_TestCase
+{
+    /**
+     * @group RequestLocation
+     */
+    public function testVisitsLocation()
+    {
+        $location = new MultiPartLocation();
+        $command = new Command('foo', ['foo' => 'bar']);
+        $request = new Request('POST', 'http://httbin.org', []);
+        $param = new Parameter(['name' => 'foo']);
+        $request = $location->visit($command, $request, $param);
+        $operation = new Operation();
+        $request = $location->after($command, $request, $operation);
+        $actual = $request->getBody()->getContents();
+
+        $this->assertNotFalse(strpos($actual, 'name="foo"'));
+        $this->assertNotFalse(strpos($actual, 'bar'));
+    }
+}

+ 77 - 0
addons/cos/library/Guzzle/guzzle-services/tests/RequestLocation/QueryLocationTest.php

@@ -0,0 +1,77 @@
+<?php
+namespace GuzzleHttp\Tests\Command\Guzzle\RequestLocation;
+
+use GuzzleHttp\Command\Command;
+use GuzzleHttp\Command\Guzzle\Operation;
+use GuzzleHttp\Command\Guzzle\Parameter;
+use GuzzleHttp\Command\Guzzle\RequestLocation\QueryLocation;
+use GuzzleHttp\Psr7;
+use GuzzleHttp\Psr7\Request;
+
+/**
+ * @covers \GuzzleHttp\Command\Guzzle\RequestLocation\QueryLocation
+ * @covers \GuzzleHttp\Command\Guzzle\RequestLocation\AbstractLocation
+ */
+class QueryLocationTest extends \PHPUnit_Framework_TestCase
+{
+    public function queryProvider()
+    {
+        return [
+            [['foo' => 'bar'], 'foo=bar'],
+            [['foo' => [1, 2]], 'foo[0]=1&foo[1]=2'],
+            [['foo' => ['bar' => 'baz', 'bim' => [4, 5]]], 'foo[bar]=baz&foo[bim][0]=4&foo[bim][1]=5']
+        ];
+    }
+
+    /**
+     * @group RequestLocation
+     */
+    public function testVisitsLocation()
+    {
+        $location = new QueryLocation();
+        $command = new Command('foo', ['foo' => 'bar']);
+        $request = new Request('POST', 'http://httbin.org');
+        $param = new Parameter(['name' => 'foo']);
+        $request = $location->visit($command, $request, $param);
+
+        $this->assertEquals('foo=bar', urldecode($request->getUri()->getQuery()));
+    }
+
+    public function testVisitsMultipleLocations()
+    {
+        $request = new Request('POST', 'http://httbin.org');
+
+        // First location
+        $location = new QueryLocation();
+        $command = new Command('foo', ['foo' => 'bar']);
+        $param = new Parameter(['name' => 'foo']);
+        $request = $location->visit($command, $request, $param);
+
+        // Second location
+        $location = new QueryLocation();
+        $command = new Command('baz', ['baz' => [6, 7]]);
+        $param = new Parameter(['name' => 'baz']);
+        $request = $location->visit($command, $request, $param);
+
+        $this->assertEquals('foo=bar&baz[0]=6&baz[1]=7', urldecode($request->getUri()->getQuery()));
+    }
+
+    /**
+     * @group RequestLocation
+     */
+    public function testAddsAdditionalProperties()
+    {
+        $location = new QueryLocation();
+        $command = new Command('foo', ['foo' => 'bar']);
+        $command['add'] = 'props';
+        $operation = new Operation([
+            'additionalParameters' => [
+                'location' => 'query'
+            ]
+        ]);
+        $request = new Request('POST', 'http://httbin.org');
+        $request = $location->after($command, $request, $operation);
+
+        $this->assertEquals('props', Psr7\parse_query($request->getUri()->getQuery())['add']);
+    }
+}

+ 525 - 0
addons/cos/library/Guzzle/guzzle-services/tests/RequestLocation/XmlLocationTest.php

@@ -0,0 +1,525 @@
+<?php
+namespace GuzzleHttp\Tests\Command\Guzzle\RequestLocation;
+
+use GuzzleHttp\Client;
+use GuzzleHttp\Command\Command;
+use GuzzleHttp\Command\Guzzle\Description;
+use GuzzleHttp\Command\Guzzle\GuzzleClient;
+use GuzzleHttp\Command\Guzzle\Operation;
+use GuzzleHttp\Command\Guzzle\Parameter;
+use GuzzleHttp\Command\Guzzle\RequestLocation\XmlLocation;
+use GuzzleHttp\Handler\MockHandler;
+use GuzzleHttp\HandlerStack;
+use GuzzleHttp\Middleware;
+use GuzzleHttp\Psr7\Request;
+use GuzzleHttp\Psr7\Response;
+
+/**
+ * @covers \GuzzleHttp\Command\Guzzle\RequestLocation\XmlLocation
+ */
+class XmlLocationTest extends \PHPUnit_Framework_TestCase
+{
+    /**
+     * @group RequestLocation
+     */
+    public function testVisitsLocation()
+    {
+        $location = new XmlLocation();
+        $command = new Command('foo', ['foo' => 'bar']);
+        $command['bar'] = 'test';
+        $request = new Request('POST', 'http://httbin.org');
+        $param = new Parameter(['name' => 'foo']);
+        $location->visit($command, $request, $param);
+        $param = new Parameter(['name' => 'bar']);
+        $location->visit($command, $request, $param);
+        $operation = new Operation();
+        $request = $location->after($command, $request, $operation);
+        $xml = $request->getBody()->getContents();
+
+        $this->assertEquals('<?xml version="1.0"?>' . "\n"
+            . '<Request><foo>bar</foo><bar>test</bar></Request>' . "\n", $xml);
+        $header = $request->getHeader('Content-Type');
+        $this->assertArraySubset([0 => 'application/xml'], $header);
+    }
+
+    /**
+     * @group RequestLocation
+     */
+    public function testCreatesBodyForEmptyDocument()
+    {
+        $location = new XmlLocation();
+        $command = new Command('foo', ['foo' => 'bar']);
+        $request = new Request('POST', 'http://httbin.org');
+        $operation = new Operation([
+            'data' => ['xmlAllowEmpty' => true]
+        ]);
+        $request = $location->after($command, $request, $operation);
+        $xml = $request->getBody()->getContents();
+        $this->assertEquals('<?xml version="1.0"?>' . "\n"
+            . '<Request/>' . "\n", $xml);
+
+        $header = $request->getHeader('Content-Type');
+        $this->assertArraySubset([0 => 'application/xml'], $header);
+    }
+
+    /**
+     * @group RequestLocation
+     */
+    public function testAddsAdditionalParameters()
+    {
+        $location = new XmlLocation('xml', 'test');
+        $command = new Command('foo', ['foo' => 'bar']);
+        $request = new Request('POST', 'http://httbin.org');
+        $param = new Parameter(['name' => 'foo']);
+        $command['foo'] = 'bar';
+        $location->visit($command, $request, $param);
+        $operation = new Operation([
+            'additionalParameters' => [
+                'location' => 'xml'
+            ]
+        ]);
+        $command['bam'] = 'boo';
+        $request = $location->after($command, $request, $operation);
+        $xml = $request->getBody()->getContents();
+        $this->assertEquals('<?xml version="1.0"?>' . "\n"
+            . '<Request><foo>bar</foo><foo>bar</foo><bam>boo</bam></Request>' . "\n", $xml);
+        $header = $request->getHeader('Content-Type');
+        $this->assertArraySubset([0 => 'test'], $header);
+    }
+
+    /**
+     * @group RequestLocation
+     */
+    public function testAllowsXmlEncoding()
+    {
+        $location = new XmlLocation();
+        $operation = new Operation([
+            'data' => ['xmlEncoding' => 'UTF-8']
+        ]);
+        $command = new Command('foo', ['foo' => 'bar']);
+        $request = new Request('POST', 'http://httbin.org');
+        $param = new Parameter(['name' => 'foo']);
+        $command['foo'] = 'bar';
+        $location->visit($command, $request, $param);
+        $request = $location->after($command, $request, $operation);
+        $xml = $request->getBody()->getContents();
+        $this->assertEquals('<?xml version="1.0" encoding="UTF-8"?>' . "\n"
+            . '<Request><foo>bar</foo></Request>' . "\n", $xml);
+    }
+
+    public function xmlProvider()
+    {
+        return [
+            [
+                [
+                    'data' => [
+                        'xmlRoot' => [
+                            'name'       => 'test',
+                            'namespaces' => 'http://foo.com'
+                        ]
+                    ],
+                    'parameters' => [
+                        'Foo' => [
+                            'location' => 'xml',
+                            'type' => 'string'
+                        ],
+                        'Baz' => [
+                            'location' => 'xml',
+                            'type' => 'string'
+                        ]
+                    ]
+                ],
+                [
+                    'Foo' => 'test',
+                    'Baz' => 'bar'
+                ],
+                '<test xmlns="http://foo.com"><Foo>test</Foo><Baz>bar</Baz></test>'
+            ],
+            // Ensure that the content-type is not added
+            [
+                [
+                    'parameters' => [
+                        'Foo' => [
+                            'location' => 'xml',
+                            'type' => 'string'
+                        ]
+                    ]
+                ],
+                [],
+                ''
+            ],
+            // Test with adding attributes and no namespace
+            [
+                [
+                    'data' => [
+                        'xmlRoot' => [
+                            'name' => 'test'
+                        ]
+                    ],
+                    'parameters' => [
+                        'Foo' => [
+                            'location' => 'xml',
+                            'type' => 'string',
+                            'data' => ['xmlAttribute' => true]
+                        ]
+                    ]
+                ],
+                [
+                    'Foo' => 'test',
+                    'Baz' => 'bar'
+                ],
+                '<test Foo="test"/>'
+            ],
+            // Test adding with an array
+            [
+                [
+                    'parameters' => [
+                        'Foo' => [
+                            'location' => 'xml',
+                            'type' => 'string'
+                        ],
+                        'Baz' => [
+                            'type' => 'array',
+                            'location' => 'xml',
+                            'items' => [
+                                'type' => 'numeric',
+                                'sentAs' => 'Bar'
+                            ]
+                        ]
+                    ]
+                ],
+                ['Foo' => 'test', 'Baz' => [1, 2]],
+                '<Request><Foo>test</Foo><Baz><Bar>1</Bar><Bar>2</Bar></Baz></Request>'
+            ],
+            // Test adding an object
+            [
+                [
+                    'parameters' => [
+                        'Foo' => ['location' => 'xml', 'type' => 'string'],
+                        'Baz' => [
+                            'type'     => 'object',
+                            'location' => 'xml',
+                            'properties' => [
+                                'Bar' => ['type' => 'string'],
+                                'Bam' => []
+                            ]
+                        ]
+                    ]
+                ],
+                [
+                    'Foo' => 'test',
+                    'Baz' => [
+                        'Bar' => 'abc',
+                        'Bam' => 'foo'
+                    ]
+                ],
+                '<Request><Foo>test</Foo><Baz><Bar>abc</Bar><Bam>foo</Bam></Baz></Request>'
+            ],
+            // Add an array that contains an object
+            [
+                [
+                    'parameters' => [
+                        'Baz' => [
+                            'type'     => 'array',
+                            'location' => 'xml',
+                            'items' => [
+                                'type'       => 'object',
+                                'sentAs'     => 'Bar',
+                                'properties' => ['A' => [], 'B' => []]
+                            ]
+                        ]
+                    ]
+                ],
+                ['Baz' => [
+                    [
+                        'A' => '1',
+                        'B' => '2'
+                    ],
+                    [
+                        'A' => '3',
+                        'B' => '4'
+                    ]
+                ]],
+                '<Request><Baz><Bar><A>1</A><B>2</B></Bar><Bar><A>3</A><B>4</B></Bar></Baz></Request>'
+            ],
+            // Add an object of attributes
+            [
+                [
+                    'parameters' => [
+                        'Foo' => [
+                            'location' => 'xml',
+                            'type' => 'string'
+                        ],
+                        'Baz' => [
+                            'type'     => 'object',
+                            'location' => 'xml',
+                            'properties' => [
+                                'Bar' => [
+                                    'type' => 'string',
+                                    'data' => [
+                                        'xmlAttribute' => true
+                                    ]
+                                ],
+                                'Bam' => []
+                            ]
+                        ]
+                    ]
+                ],
+                [
+                    'Foo' => 'test',
+                    'Baz' => [
+                        'Bar' => 'abc',
+                        'Bam' => 'foo'
+                    ]
+                ],
+                '<Request><Foo>test</Foo><Baz Bar="abc"><Bam>foo</Bam></Baz></Request>'
+            ],
+            // Check order doesn't matter
+            [
+                [
+                    'parameters' => [
+                        'Foo' => [
+                            'location' => 'xml',
+                            'type' => 'string'
+                        ],
+                        'Baz' => [
+                            'type'     => 'object',
+                            'location' => 'xml',
+                            'properties' => [
+                                'Bar' => [
+                                    'type' => 'string',
+                                    'data' => [
+                                        'xmlAttribute' => true
+                                    ]
+                                ],
+                                'Bam' => []
+                            ]
+                        ]
+                    ]
+                ],
+                [
+                    'Foo' => 'test',
+                    'Baz' => [
+                        'Bam' => 'foo',
+                        'Bar' => 'abc'
+                    ]
+                ],
+                '<Request><Foo>test</Foo><Baz Bar="abc"><Bam>foo</Bam></Baz></Request>'
+            ],
+            // Add values with custom namespaces
+            [
+                [
+                    'parameters' => [
+                        'Foo' => [
+                            'location' => 'xml',
+                            'type' => 'string',
+                            'data' => [
+                                'xmlNamespace' => 'http://foo.com'
+                            ]
+                        ]
+                    ]
+                ],
+                ['Foo' => 'test'],
+                '<Request><Foo xmlns="http://foo.com">test</Foo></Request>'
+            ],
+            // Add attributes with custom namespace prefix
+            [
+                [
+                    'parameters' => [
+                        'Wrap' => [
+                            'type' => 'object',
+                            'location' => 'xml',
+                            'properties' => [
+                                'Foo' => [
+                                    'type' => 'string',
+                                    'sentAs' => 'xsi:baz',
+                                    'data' => [
+                                        'xmlNamespace' => 'http://foo.com',
+                                        'xmlAttribute' => true
+                                    ]
+                                ]
+                            ]
+                        ],
+                    ]
+                ],
+                ['Wrap' => [
+                    'Foo' => 'test'
+                ]],
+                '<Request><Wrap xsi:baz="test" xmlns:xsi="http://foo.com"/></Request>'
+            ],
+            // Add nodes with custom namespace prefix
+            [
+                [
+                    'parameters' => [
+                        'Wrap' => [
+                            'type' => 'object',
+                            'location' => 'xml',
+                            'properties' => [
+                                'Foo' => [
+                                    'type' => 'string',
+                                    'sentAs' => 'xsi:Foo',
+                                    'data' => [
+                                        'xmlNamespace' => 'http://foobar.com'
+                                    ]
+                                ]
+                            ]
+                        ],
+                    ]
+                ],
+                ['Wrap' => [
+                    'Foo' => 'test'
+                ]],
+                '<Request><Wrap><xsi:Foo xmlns:xsi="http://foobar.com">test</xsi:Foo></Wrap></Request>'
+            ],
+            [
+                [
+                    'parameters' => [
+                        'Foo' => [
+                            'location' => 'xml',
+                            'type' => 'string',
+                            'data' => [
+                                'xmlNamespace' => 'http://foo.com'
+                            ]
+                        ]
+                    ]
+                ],
+                ['Foo' => '<h1>This is a title</h1>'],
+                '<Request><Foo xmlns="http://foo.com"><![CDATA[<h1>This is a title</h1>]]></Foo></Request>'
+            ],
+            // Flat array at top level
+            [
+                [
+                    'parameters' => [
+                        'Bars' => [
+                            'type'     => 'array',
+                            'data'     => ['xmlFlattened' => true],
+                            'location' => 'xml',
+                            'items' => [
+                                'type'       => 'object',
+                                'sentAs'     => 'Bar',
+                                'properties' => [
+                                    'A' => [],
+                                    'B' => []
+                                ]
+                            ]
+                        ],
+                        'Boos' => [
+                            'type'     => 'array',
+                            'data'     => ['xmlFlattened' => true],
+                            'location' => 'xml',
+                            'items'  => [
+                                'sentAs' => 'Boo',
+                                'type' => 'string'
+                            ]
+                        ]
+                    ]
+                ],
+                [
+                    'Bars' => [
+                        ['A' => '1', 'B' => '2'],
+                        ['A' => '3', 'B' => '4']
+                    ],
+                    'Boos' => ['test', '123']
+                ],
+                '<Request><Bar><A>1</A><B>2</B></Bar><Bar><A>3</A><B>4</B></Bar><Boo>test</Boo><Boo>123</Boo></Request>'
+            ],
+            // Nested flat arrays
+            [
+                [
+                    'parameters' => [
+                        'Delete' => [
+                            'type'     => 'object',
+                            'location' => 'xml',
+                            'properties' => [
+                                'Items' => [
+                                    'type' => 'array',
+                                    'data' => ['xmlFlattened' => true],
+                                    'items' => [
+                                        'type'       => 'object',
+                                        'sentAs'     => 'Item',
+                                        'properties' => [
+                                            'A' => [],
+                                            'B' => []
+                                        ]
+                                    ]
+                                ]
+                            ]
+                        ]
+                    ]
+                ],
+                [
+                    'Delete' => [
+                        'Items' => [
+                            ['A' => '1', 'B' => '2'],
+                            ['A' => '3', 'B' => '4']
+                        ]
+                    ]
+                ],
+                '<Request><Delete><Item><A>1</A><B>2</B></Item><Item><A>3</A><B>4</B></Item></Delete></Request>'
+            ],
+            // Test adding root node attributes after nodes
+            [
+                [
+                    'data' => [
+                        'xmlRoot' => [
+                            'name' => 'test'
+                        ]
+                    ],
+                    'parameters' => [
+                        'Foo' => ['location' => 'xml', 'type' => 'string'],
+                        'Baz' => ['location' => 'xml', 'type' => 'string', 'data' => ['xmlAttribute' => true]],
+                    ]
+                ],
+                ['Foo' => 'test', 'Baz' => 'bar'],
+                '<test Baz="bar"><Foo>test</Foo></test>'
+            ],
+        ];
+    }
+
+    /**
+     * @param array  $operation
+     * @param array  $input
+     * @param string $xml
+     * @dataProvider xmlProvider
+     * @group RequestLocation
+     */
+    public function testSerializesXml(array $operation, array $input, $xml)
+    {
+        $container = [];
+        $history = Middleware::history($container);
+        $mock = new MockHandler([new Response(200)]);
+
+        $stack = new HandlerStack($mock);
+        $stack->push($history);
+        $operation['uri'] = 'http://httpbin.org';
+        $client = new GuzzleClient(
+            new Client(['handler' => $stack]),
+            new Description([
+                'operations' => [
+                    'foo' => $operation
+                ]
+            ])
+        );
+
+        $command = $client->getCommand('foo', $input);
+
+        $client->execute($command);
+
+        $this->assertCount(1, $container);
+
+        foreach ($container as $transaction) {
+            /** @var Request $request */
+            $request = $transaction['request'];
+            if (empty($input)) {
+                if ($request->hasHeader('Content-Type')) {
+                    $this->assertArraySubset([0 => ''], $request->getHeader('Content-Type'));
+                }
+            } else {
+                $this->assertArraySubset([0 => 'application/xml'], $request->getHeader('Content-Type'));
+            }
+
+            $body = str_replace(["\n", "<?xml version=\"1.0\"?>"], '', (string) $request->getBody());
+            $this->assertEquals($xml, $body);
+        }
+    }
+}

+ 30 - 0
addons/cos/library/Guzzle/guzzle-services/tests/ResponseLocation/BodyLocationTest.php

@@ -0,0 +1,30 @@
+<?php
+namespace GuzzleHttp\Tests\Command\Guzzle\ResponseLocation;
+
+use GuzzleHttp\Command\Guzzle\Parameter;
+use GuzzleHttp\Command\Guzzle\ResponseLocation\BodyLocation;
+use GuzzleHttp\Command\Result;
+use GuzzleHttp\Psr7\Response;
+
+/**
+ * @covers \GuzzleHttp\Command\Guzzle\ResponseLocation\BodyLocation
+ * @covers \GuzzleHttp\Command\Guzzle\ResponseLocation\AbstractLocation
+ */
+class BodyLocationTest extends \PHPUnit_Framework_TestCase
+{
+    /**
+     * @group ResponseLocation
+     */
+    public function testVisitsLocation()
+    {
+        $location = new BodyLocation();
+        $parameter = new Parameter([
+            'name'    => 'val',
+            'filters' => ['strtoupper']
+        ]);
+        $response = new Response(200, [], 'foo');
+        $result = new Result();
+        $result = $location->visit($result, $response, $parameter);
+        $this->assertEquals('FOO', $result['val']);
+    }
+}

+ 31 - 0
addons/cos/library/Guzzle/guzzle-services/tests/ResponseLocation/HeaderLocationTest.php

@@ -0,0 +1,31 @@
+<?php
+namespace GuzzleHttp\Tests\Command\Guzzle\ResponseLocation;
+
+use GuzzleHttp\Command\Guzzle\Parameter;
+use GuzzleHttp\Command\Guzzle\ResponseLocation\HeaderLocation;
+use GuzzleHttp\Command\Result;
+use GuzzleHttp\Psr7\Response;
+
+/**
+ * @covers \GuzzleHttp\Command\Guzzle\ResponseLocation\HeaderLocation
+ * @covers \GuzzleHttp\Command\Guzzle\ResponseLocation\AbstractLocation
+ */
+class HeaderLocationTest extends \PHPUnit_Framework_TestCase
+{
+    /**
+     * @group ResponseLocation
+     */
+    public function testVisitsLocation()
+    {
+        $location = new HeaderLocation();
+        $parameter = new Parameter([
+            'name'    => 'val',
+            'sentAs'  => 'X-Foo',
+            'filters' => ['strtoupper']
+        ]);
+        $response = new Response(200, ['X-Foo' => 'bar']);
+        $result = new Result();
+        $result = $location->visit($result, $response, $parameter);
+        $this->assertEquals('BAR', $result['val']);
+    }
+}

+ 581 - 0
addons/cos/library/Guzzle/guzzle-services/tests/ResponseLocation/JsonLocationTest.php

@@ -0,0 +1,581 @@
+<?php
+namespace GuzzleHttp\Tests\Command\Guzzle\ResponseLocation;
+
+use GuzzleHttp\Client;
+use GuzzleHttp\Command\Guzzle\Description;
+use GuzzleHttp\Command\Guzzle\GuzzleClient;
+use GuzzleHttp\Command\Guzzle\Parameter;
+use GuzzleHttp\Command\Guzzle\ResponseLocation\JsonLocation;
+use GuzzleHttp\Command\Result;
+use GuzzleHttp\Command\ResultInterface;
+use GuzzleHttp\Handler\MockHandler;
+use GuzzleHttp\Psr7\Response;
+
+/**
+ * @covers \GuzzleHttp\Command\Guzzle\ResponseLocation\JsonLocation
+ * @covers \GuzzleHttp\Command\Guzzle\Deserializer
+ */
+class JsonLocationTest extends \PHPUnit_Framework_TestCase
+{
+
+    /**
+     * @group ResponseLocation
+     */
+    public function testVisitsLocation()
+    {
+        $location = new JsonLocation();
+        $parameter = new Parameter([
+            'name'    => 'val',
+            'sentAs'  => 'vim',
+            'filters' => ['strtoupper']
+        ]);
+        $response = new Response(200, [], '{"vim":"bar"}');
+        $result = new Result();
+        $result = $location->before($result, $response, $parameter);
+        $result = $location->visit($result, $response, $parameter);
+        $this->assertEquals('BAR', $result['val']);
+    }
+    /**
+     * @group ResponseLocation
+     * @param $name
+     * @param $expected
+     */
+    public function testVisitsWiredArray()
+    {
+        $json = ['car_models' => ['ferrari', 'aston martin']];
+        $body = \GuzzleHttp\json_encode($json);
+        $response = new Response(200, ['Content-Type' => 'application/json'], $body);
+        $mock = new MockHandler([$response]);
+
+        $guzzle = new Client(['handler' => $mock]);
+
+        $description = new Description([
+            'operations' => [
+                'getCars' => [
+                    'uri' => 'http://httpbin.org',
+                    'httpMethod' => 'GET',
+                    'responseModel' => 'Cars'
+                ]
+            ],
+            'models' => [
+                'Cars' => [
+                    'type' => 'object',
+                    'location' => 'json',
+                    'properties' => [
+                        'cars' => [
+                            'type' => 'array',
+                            'sentAs' => 'car_models',
+                            'items' => [
+                                'type' => 'object',
+                            ]
+                        ]
+                    ],
+                ]
+            ]
+        ]);
+
+        $guzzle = new GuzzleClient($guzzle, $description);
+        $result = $guzzle->getCars();
+
+        $this->assertEquals(['cars' => ['ferrari', 'aston martin']], $result->toArray());
+    }
+
+    /**
+     * @group ResponseLocation
+     */
+    public function testVisitsAdditionalProperties()
+    {
+        $location = new JsonLocation();
+        $parameter = new Parameter();
+        $model = new Parameter(['additionalProperties' => ['location' => 'json']]);
+        $response = new Response(200, [], '{"vim":"bar","qux":[1,2]}');
+        $result = new Result();
+        $result = $location->before($result, $response, $parameter);
+        $result = $location->visit($result, $response, $parameter);
+        $result = $location->after($result, $response, $model);
+        $this->assertEquals('bar', $result['vim']);
+        $this->assertEquals([1, 2], $result['qux']);
+    }
+
+    /**
+     * @group ResponseLocation
+     */
+    public function testVisitsAdditionalPropertiesWithEmptyResponse()
+    {
+        $location = new JsonLocation();
+        $parameter = new Parameter();
+        $model = new Parameter(['additionalProperties' => ['location' => 'json']]);
+        $response = new Response(204);
+        $result = new Result();
+        $result = $location->before($result, $response, $parameter);
+        $result = $location->visit($result, $response, $parameter);
+        $result = $location->after($result, $response, $model);
+        $this->assertEquals([], $result->toArray());
+    }
+
+    public function jsonProvider()
+    {
+        return [
+            [null, [['foo' => 'BAR'], ['baz' => 'BAM']]],
+            ['under_me', ['under_me' => [['foo' => 'BAR'], ['baz' => 'BAM']]]],
+        ];
+    }
+
+    /**
+     * @dataProvider jsonProvider
+     * @group ResponseLocation
+     * @param $name
+     * @param $expected
+     */
+    public function testVisitsTopLevelArrays($name, $expected)
+    {
+        $json = [
+            ['foo' => 'bar'],
+            ['baz' => 'bam'],
+        ];
+        $body = \GuzzleHttp\json_encode($json);
+        $response = new Response(200, ['Content-Type' => 'application/json'], $body);
+        $mock = new MockHandler([$response]);
+
+        $guzzle = new Client(['handler' => $mock]);
+
+        $description = new Description([
+            'operations' => [
+                'foo' => [
+                    'uri' => 'http://httpbin.org',
+                    'httpMethod' => 'GET',
+                    'responseModel' => 'j'
+                ]
+            ],
+            'models' => [
+                'j' => [
+                    'type' => 'array',
+                    'location' => 'json',
+                    'name' => $name,
+                    'items' => [
+                        'type' => 'object',
+                        'additionalProperties' => [
+                            'type' => 'string',
+                            'filters' => ['strtoupper']
+                        ]
+                    ]
+                ]
+            ]
+        ]);
+        $guzzle = new GuzzleClient($guzzle, $description);
+        /** @var ResultInterface $result */
+        $result = $guzzle->foo();
+        $this->assertEquals($expected, $result->toArray());
+    }
+
+    /**
+     * @group ResponseLocation
+     */
+    public function testVisitsNestedArrays()
+    {
+        $json = [
+            'scalar' => 'foo',
+            'nested' => [
+                'bar',
+                'baz'
+            ]
+        ];
+        $body = \GuzzleHttp\json_encode($json);
+        $response = new Response(200, ['Content-Type' => 'application/json'], $body);
+        $mock = new MockHandler([$response]);
+
+        $httpClient = new Client(['handler' => $mock]);
+
+        $description = new Description([
+            'operations' => [
+                'foo' => [
+                    'uri' => 'http://httpbin.org',
+                    'httpMethod' => 'GET',
+                    'responseModel' => 'j'
+                ]
+            ],
+            'models' => [
+                'j' => [
+                    'type' => 'object',
+                    'location' => 'json',
+                    'properties' => [
+                        'scalar' => ['type' => 'string'],
+                        'nested' => [
+                            'type' => 'array',
+                            'items' => ['type' => 'string']
+                        ]
+                    ]
+                ]
+            ]
+        ]);
+        $guzzle = new GuzzleClient($httpClient, $description);
+        /** @var ResultInterface $result */
+        $result = $guzzle->foo();
+        $expected = [
+            'scalar' => 'foo',
+            'nested' => [
+                'bar',
+                'baz'
+            ]
+        ];
+        $this->assertEquals($expected, $result->toArray());
+    }
+
+    public function nestedProvider()
+    {
+        return [
+            [
+                [
+                    'operations' => [
+                        'foo' => [
+                            'uri' => 'http://httpbin.org',
+                            'httpMethod' => 'GET',
+                            'responseModel' => 'j'
+                        ]
+                    ],
+                    'models' => [
+                        'j' => [
+                            'type' => 'object',
+                            'properties' => [
+                                'nested' => [
+                                    'location' => 'json',
+                                    'type' => 'object',
+                                    'properties' => [
+                                        'foo' => ['type' => 'string'],
+                                        'bar' => ['type' => 'number'],
+                                        'bam' => [
+                                            'type' => 'object',
+                                            'properties' => [
+                                                'abc' => [
+                                                    'type' => 'number'
+                                                ]
+                                            ]
+                                        ]
+                                    ]
+                                ]
+                            ],
+                            'additionalProperties' => [
+                                'location' => 'json',
+                                'type' => 'string',
+                                'filters' => ['strtoupper']
+                            ]
+                        ]
+                    ]
+                ]
+            ],
+            [
+                [
+                    'operations' => [
+                        'foo' => [
+                            'uri' => 'http://httpbin.org',
+                            'httpMethod' => 'GET',
+                            'responseModel' => 'j'
+                        ]
+                    ],
+                    'models' => [
+                        'j' => [
+                            'type' => 'object',
+                            'location' => 'json',
+                            'properties' => [
+                                'nested' => [
+                                    'type' => 'object',
+                                    'properties' => [
+                                        'foo' => ['type' => 'string'],
+                                        'bar' => ['type' => 'number'],
+                                        'bam' => [
+                                            'type' => 'object',
+                                            'properties' => [
+                                                'abc' => [
+                                                    'type' => 'number'
+                                                ]
+                                            ]
+                                        ]
+                                    ]
+                                ]
+                            ],
+                            'additionalProperties' => [
+                                'type' => 'string',
+                                'filters' => ['strtoupper']
+                            ]
+                        ]
+                    ]
+                ]
+            ]
+        ];
+    }
+
+    /**
+     * @dataProvider nestedProvider
+     * @group ResponseLocation
+     */
+    public function testVisitsNestedProperties($desc)
+    {
+        $json = [
+            'nested' => [
+                'foo' => 'abc',
+                'bar' => 123,
+                'bam' => [
+                    'abc' => 456
+                ]
+            ],
+            'baz' => 'boo'
+        ];
+        $body = \GuzzleHttp\json_encode($json);
+        $response = new Response(200, ['Content-Type' => 'application/json'], $body);
+        $mock = new MockHandler([$response]);
+
+        $httpClient = new Client(['handler' => $mock]);
+
+        $description = new Description($desc);
+        $guzzle = new GuzzleClient($httpClient, $description);
+        /** @var ResultInterface $result */
+        $result = $guzzle->foo();
+        $expected = [
+            'nested' => [
+                'foo' => 'abc',
+                'bar' => 123,
+                'bam' => [
+                    'abc' => 456
+                ]
+            ],
+            'baz' => 'BOO'
+        ];
+
+        $this->assertEquals($expected, $result->toArray());
+    }
+
+    /**
+     * @group ResponseLocation
+     */
+    public function testVisitsNullResponseProperties()
+    {
+
+        $json = [
+            'data' => [
+                'link' => null
+            ]
+        ];
+
+        $body = \GuzzleHttp\json_encode($json);
+        $response = new Response(200, ['Content-Type' => 'application/json'], $body);
+        $mock = new MockHandler([$response]);
+
+        $httpClient = new Client(['handler' => $mock]);
+
+        $description = new Description(
+            [
+                'operations' => [
+                    'foo' => [
+                        'uri' => 'http://httpbin.org',
+                        'httpMethod' => 'GET',
+                        'responseModel' => 'j'
+                    ]
+                ],
+                'models' => [
+                    'j' => [
+                        'type' => 'object',
+                        'location' => 'json',
+                        'properties' => [
+                            'scalar' => ['type' => 'string'],
+                            'data' => [
+                                'type'          => 'object',
+                                'location'      => 'json',
+                                'properties'    => [
+                                    'link' => [
+                                        'name'    => 'val',
+                                        'type' => 'string',
+                                        'location' => 'json'
+                                    ],
+                                ],
+                                'additionalProperties' => false
+                            ]
+                        ]
+                    ]
+                ]
+            ]
+        );
+        $guzzle = new GuzzleClient($httpClient, $description);
+        /** @var ResultInterface $result */
+        $result = $guzzle->foo();
+
+        $expected = [
+            'data' => [
+                'link' => null
+            ]
+        ];
+
+        $this->assertEquals($expected, $result->toArray());
+    }
+
+    /**
+     * @group ResponseLocation
+     */
+    public function testVisitsNestedArrayOfArrays()
+    {
+        $json = [
+            'scalar' => 'foo',
+            'nested' => [
+                [
+                    'bar' => 123,
+                    'baz' => false,
+                ],
+                [
+                    'bar' => 345,
+                    'baz' => true,
+                ],
+                [
+                    'bar' => 678,
+                    'baz' => true,
+                ],
+            ]
+        ];
+
+        $body = \GuzzleHttp\json_encode($json);
+        $response = new Response(200, ['Content-Type' => 'application/json'], $body);
+        $mock = new MockHandler([$response]);
+
+        $httpClient = new Client(['handler' => $mock]);
+
+        $description = new Description([
+            'operations' => [
+                'foo' => [
+                    'uri' => 'http://httpbin.org',
+                    'httpMethod' => 'GET',
+                    'responseModel' => 'j'
+                ]
+            ],
+            'models' => [
+                'j' => [
+                    'type' => 'object',
+                    'properties' => [
+                        'scalar' => [
+                            // for some reason (probably because location is also set on array of arrays)
+                            // array of arrays sibling elements must have location set to `json`
+                            // otherwise JsonLocation ignores them
+                            'location' => 'json',
+                            'type' => 'string'
+                        ],
+                        'nested' => [
+                            // array of arrays type must be set to `array`
+                            // without that JsonLocation throws an exception
+                            'type' => 'array',
+                            // for array of arrays `location` must be set to `json`
+                            // otherwise JsonLocation returns an empty array
+                            'location' => 'json',
+                            'items' => [
+                                // although this is array of arrays, array items type
+                                // must be set as `object`
+                                'type' => 'object',
+                                'properties' => [
+                                    'bar' => [
+                                        'type' => 'integer',
+                                    ],
+                                    'baz' => [
+                                        'type' => 'boolean',
+                                    ],
+                                ],
+                            ]
+                        ]
+                    ]
+                ]
+            ]
+        ]);
+
+        $guzzle = new GuzzleClient($httpClient, $description);
+        /** @var ResultInterface $result */
+        $result = $guzzle->foo();
+        $expected = [
+            'scalar' => 'foo',
+            'nested' => [
+                [
+                    'bar' => 123,
+                    'baz' => false,
+                ],
+                [
+                    'bar' => 345,
+                    'baz' => true,
+                ],
+                [
+                    'bar' => 678,
+                    'baz' => true,
+                ],
+            ]
+        ];
+
+        $this->assertEquals($expected, $result->toArray());
+    }
+
+    /**
+     * @group ResponseLocation
+     */
+    public function testVisitsNestedArrayOfObjects()
+    {
+        $json = json_decode('{"scalar":"foo","nested":[{"bar":123,"baz":false},{"bar":345,"baz":true},{"bar":678,"baz":true}]}');
+
+        $body = \GuzzleHttp\json_encode($json);
+        $response = new Response(200, ['Content-Type' => 'application/json'], $body);
+        $mock = new MockHandler([$response]);
+
+        $httpClient = new Client(['handler' => $mock]);
+
+        $description = new Description([
+            'operations' => [
+                'foo' => [
+                    'uri' => 'http://httpbin.org',
+                    'httpMethod' => 'GET',
+                    'responseModel' => 'j'
+                ]
+            ],
+            'models' => [
+                'j' => [
+                    'type' => 'object',
+                    'location' => 'json',
+                    'properties' => [
+                        'scalar' => [
+                            'type' => 'string'
+                        ],
+                        'nested' => [
+                            // array of objects type must be set to `array`
+                            // without that JsonLocation throws an exception
+                            'type' => 'array',
+                            'items' => [
+                                // array elements type must be set to `object`
+                                'type' => 'object',
+                                'properties' => [
+                                    'bar' => [
+                                        'type' => 'integer',
+                                    ],
+                                    'baz' => [
+                                        'type' => 'boolean',
+                                    ],
+                                ],
+                            ]
+                        ]
+                    ]
+                ]
+            ]
+        ]);
+
+        $guzzle = new GuzzleClient($httpClient, $description);
+        /** @var ResultInterface $result */
+        $result = $guzzle->foo();
+        $expected = [
+            'scalar' => 'foo',
+            'nested' => [
+                [
+                    'bar' => 123,
+                    'baz' => false,
+                ],
+                [
+                    'bar' => 345,
+                    'baz' => true,
+                ],
+                [
+                    'bar' => 678,
+                    'baz' => true,
+                ],
+            ]
+        ];
+        $this->assertEquals($expected, $result->toArray());
+    }
+}

+ 30 - 0
addons/cos/library/Guzzle/guzzle-services/tests/ResponseLocation/ReasonPhraseLocationTest.php

@@ -0,0 +1,30 @@
+<?php
+namespace GuzzleHttp\Tests\Command\Guzzle\ResponseLocation;
+
+use GuzzleHttp\Command\Guzzle\Parameter;
+use GuzzleHttp\Command\Guzzle\ResponseLocation\ReasonPhraseLocation;
+use GuzzleHttp\Command\Result;
+use GuzzleHttp\Psr7\Response;
+
+/**
+ * @covers \GuzzleHttp\Command\Guzzle\ResponseLocation\ReasonPhraseLocation
+ * @covers \GuzzleHttp\Command\Guzzle\ResponseLocation\AbstractLocation
+ */
+class ReasonPhraseLocationTest extends \PHPUnit_Framework_TestCase
+{
+    /**
+     * @group ResponseLocation
+     */
+    public function testVisitsLocation()
+    {
+        $location = new ReasonPhraseLocation();
+        $parameter = new Parameter([
+            'name' => 'val',
+            'filters' => ['strtolower']
+        ]);
+        $response = new Response(200);
+        $result = new Result();
+        $result = $location->visit($result, $response, $parameter);
+        $this->assertEquals('ok', $result['val']);
+    }
+}

+ 27 - 0
addons/cos/library/Guzzle/guzzle-services/tests/ResponseLocation/StatusCodeLocationTest.php

@@ -0,0 +1,27 @@
+<?php
+namespace GuzzleHttp\Tests\Command\Guzzle\ResponseLocation;
+
+use GuzzleHttp\Command\Guzzle\Parameter;
+use GuzzleHttp\Command\Guzzle\ResponseLocation\StatusCodeLocation;
+use GuzzleHttp\Command\Result;
+use GuzzleHttp\Psr7\Response;
+
+/**
+ * @covers \GuzzleHttp\Command\Guzzle\ResponseLocation\StatusCodeLocation
+ * @covers \GuzzleHttp\Command\Guzzle\ResponseLocation\AbstractLocation
+ */
+class StatusCodeLocationTest extends \PHPUnit_Framework_TestCase
+{
+    /**
+     * @group ResponseLocation
+     */
+    public function testVisitsLocation()
+    {
+        $location = new StatusCodeLocation();
+        $parameter = new Parameter(['name' => 'val']);
+        $response = new Response(200);
+        $result = new Result();
+        $result = $location->visit($result, $response, $parameter);
+        $this->assertEquals(200, $result['val']);
+    }
+}

+ 795 - 0
addons/cos/library/Guzzle/guzzle-services/tests/ResponseLocation/XmlLocationTest.php

@@ -0,0 +1,795 @@
+<?php
+namespace GuzzleHttp\Tests\Command\Guzzle\ResponseLocation;
+
+use GuzzleHttp\Command\Guzzle\Parameter;
+use GuzzleHttp\Command\Guzzle\ResponseLocation\XmlLocation;
+use GuzzleHttp\Command\Result;
+use GuzzleHttp\Psr7\Response;
+
+/**
+ * @covers \GuzzleHttp\Command\Guzzle\ResponseLocation\XmlLocation
+ */
+class XmlLocationTest extends \PHPUnit_Framework_TestCase
+{
+    /**
+     * @group ResponseLocation
+     */
+    public function testVisitsLocation()
+    {
+        $location = new XmlLocation();
+        $parameter = new Parameter([
+            'name'    => 'val',
+            'sentAs'  => 'vim',
+            'filters' => ['strtoupper']
+        ]);
+        $model = new Parameter();
+        $response = new Response(200, [], \GuzzleHttp\Psr7\stream_for('<w><vim>bar</vim></w>'));
+        $result = new Result();
+        $result = $location->before($result, $response, $model);
+        $result = $location->visit($result, $response, $parameter);
+        $result = $location->after($result, $response, $model);
+        $this->assertEquals('BAR', $result['val']);
+    }
+
+    /**
+     * @group ResponseLocation
+     */
+    public function testVisitsAdditionalProperties()
+    {
+        $location = new XmlLocation();
+        $parameter = new Parameter();
+        $model = new Parameter(['additionalProperties' => ['location' => 'xml']]);
+        $response = new Response(200, [], \GuzzleHttp\Psr7\stream_for('<w><vim>bar</vim></w>'));
+        $result = new Result();
+        $result = $location->before($result, $response, $parameter);
+        $result = $location->visit($result, $response, $parameter);
+        $result = $location->after($result, $response, $model);
+        $this->assertEquals('bar', $result['vim']);
+    }
+
+    /**
+     * @group ResponseLocation
+     */
+    public function testEnsuresFlatArraysAreFlat()
+    {
+        $param = new Parameter([
+            'location' => 'xml',
+            'name'     => 'foo',
+            'type'     => 'array',
+            'items'    => ['type' => 'string'],
+        ]);
+
+        $xml = '<xml><foo>bar</foo><foo>baz</foo></xml>';
+        $this->xmlTest($param, $xml, ['foo' => ['bar', 'baz']]);
+        $this->xmlTest($param, '<xml><foo>bar</foo></xml>', ['foo' => ['bar']]);
+    }
+
+    public function xmlDataProvider()
+    {
+        $param = new Parameter([
+            'location' => 'xml',
+            'name'     => 'Items',
+            'type'     => 'array',
+            'items'    => [
+                'type'       => 'object',
+                'name'       => 'Item',
+                'properties' => [
+                    'Bar' => ['type' => 'string'],
+                    'Baz' => ['type' => 'string'],
+                ],
+            ],
+        ]);
+
+        return [
+            [$param, '<Test><Items><Item><Bar>1</Bar></Item><Item><Bar>2</Bar></Item></Items></Test>', [
+                'Items' => [
+                    ['Bar' => 1],
+                    ['Bar' => 2],
+                ],
+            ]],
+            [$param, '<Test><Items><Item><Bar>1</Bar></Item></Items></Test>', [
+                'Items' => [
+                    ['Bar' => 1],
+                ]
+            ]],
+            [$param, '<Test><Items /></Test>', [
+                'Items' => [],
+            ]]
+        ];
+    }
+
+    /**
+     * @dataProvider xmlDataProvider
+     * @group ResponseLocation
+     */
+    public function testEnsuresWrappedArraysAreInCorrectLocations($param, $xml, $expected)
+    {
+        $location = new XmlLocation();
+        $model = new Parameter();
+        $response = new Response(200, [], \GuzzleHttp\Psr7\stream_for($xml));
+        $result = new Result();
+        $result = $location->before($result, $response, $param);
+        $result = $location->visit($result, $response, $param);
+        $result = $location->after($result, $response, $model);
+        $this->assertEquals($expected, $result->toArray());
+    }
+
+    /**
+     * @group ResponseLocation
+     */
+    public function testCanRenameValues()
+    {
+        $param = new Parameter([
+            'name'     => 'TerminatingInstances',
+            'type'     => 'array',
+            'location' => 'xml',
+            'sentAs'   => 'instancesSet',
+            'items'    => [
+                'name'       => 'item',
+                'type'       => 'object',
+                'sentAs'     => 'item',
+                'properties' => [
+                    'InstanceId'    => [
+                        'type'   => 'string',
+                        'sentAs' => 'instanceId',
+                    ],
+                    'CurrentState'  => [
+                        'type'       => 'object',
+                        'sentAs'     => 'currentState',
+                        'properties' => [
+                            'Code' => [
+                                'type'   => 'numeric',
+                                'sentAs' => 'code',
+                            ],
+                            'Name' => [
+                                'type'   => 'string',
+                                'sentAs' => 'name',
+                            ],
+                        ],
+                    ],
+                    'PreviousState' => [
+                        'type'       => 'object',
+                        'sentAs'     => 'previousState',
+                        'properties' => [
+                            'Code' => [
+                                'type'   => 'numeric',
+                                'sentAs' => 'code',
+                            ],
+                            'Name' => [
+                                'type'   => 'string',
+                                'sentAs' => 'name',
+                            ],
+                        ],
+                    ],
+                ],
+            ]
+        ]);
+
+        $xml = '
+            <xml>
+                <instancesSet>
+                    <item>
+                        <instanceId>i-3ea74257</instanceId>
+                        <currentState>
+                            <code>32</code>
+                            <name>shutting-down</name>
+                        </currentState>
+                        <previousState>
+                            <code>16</code>
+                            <name>running</name>
+                        </previousState>
+                    </item>
+                </instancesSet>
+            </xml>
+        ';
+
+        $this->xmlTest($param, $xml, [
+            'TerminatingInstances' => [
+                [
+                    'InstanceId'    => 'i-3ea74257',
+                    'CurrentState'  => [
+                        'Code' => '32',
+                        'Name' => 'shutting-down',
+                    ],
+                    'PreviousState' => [
+                        'Code' => '16',
+                        'Name' => 'running',
+                    ],
+                ],
+            ],
+        ]);
+    }
+
+    /**
+     * @group ResponseLocation
+     */
+    public function testCanRenameAttributes()
+    {
+        $param = new Parameter([
+            'name'     => 'RunningQueues',
+            'type'     => 'array',
+            'location' => 'xml',
+            'items'    => [
+                'type'       => 'object',
+                'sentAs'     => 'item',
+                'properties' => [
+                    'QueueId'       => [
+                        'type'   => 'string',
+                        'sentAs' => 'queue_id',
+                        'data'   => [
+                            'xmlAttribute' => true,
+                        ],
+                    ],
+                    'CurrentState'  => [
+                        'type'       => 'object',
+                        'properties' => [
+                            'Code' => [
+                                'type'   => 'numeric',
+                                'sentAs' => 'code',
+                                'data'   => [
+                                    'xmlAttribute' => true,
+                                ],
+                            ],
+                            'Name' => [
+                                'sentAs' => 'name',
+                                'data'   => [
+                                    'xmlAttribute' => true,
+                                ],
+                            ],
+                        ],
+                    ],
+                    'PreviousState' => [
+                        'type'       => 'object',
+                        'properties' => [
+                            'Code' => [
+                                'type'   => 'numeric',
+                                'sentAs' => 'code',
+                                'data'   => [
+                                    'xmlAttribute' => true,
+                                ],
+                            ],
+                            'Name' => [
+                                'sentAs' => 'name',
+                                'data'   => [
+                                    'xmlAttribute' => true,
+                                ],
+                            ],
+                        ],
+                    ],
+                ],
+            ]
+        ]);
+
+        $xml = '
+            <wrap>
+                <RunningQueues>
+                    <item queue_id="q-3ea74257">
+                        <CurrentState code="32" name="processing" />
+                        <PreviousState code="16" name="wait" />
+                    </item>
+                </RunningQueues>
+            </wrap>';
+
+        $this->xmlTest($param, $xml, [
+            'RunningQueues' => [
+                [
+                    'QueueId'       => 'q-3ea74257',
+                    'CurrentState'  => [
+                        'Code' => '32',
+                        'Name' => 'processing',
+                    ],
+                    'PreviousState' => [
+                        'Code' => '16',
+                        'Name' => 'wait',
+                    ],
+                ],
+            ],
+        ]);
+    }
+
+    /**
+     * @group ResponseLocation
+     */
+    public function testAddsEmptyArraysWhenValueIsMissing()
+    {
+        $param = new Parameter([
+            'name'     => 'Foo',
+            'type'     => 'array',
+            'location' => 'xml',
+            'items'    => [
+                'type'       => 'object',
+                'properties' => [
+                    'Baz' => ['type' => 'array'],
+                    'Bar' => [
+                        'type'       => 'object',
+                        'properties' => [
+                            'Baz' => ['type' => 'array'],
+                        ],
+                    ],
+                ],
+            ],
+        ]);
+
+        $xml = '<xml><Foo><Bar></Bar></Foo></xml>';
+
+        $this->xmlTest($param, $xml, [
+            'Foo' => [
+                [
+                    'Bar' => [],
+                ]
+            ],
+        ]);
+    }
+
+    /**
+     * @group issue-399, ResponseLocation
+     * @link  https://github.com/guzzle/guzzle/issues/399
+     */
+    public function testDiscardingUnknownProperties()
+    {
+        $param = new Parameter([
+            'name'                 => 'foo',
+            'type'                 => 'object',
+            'additionalProperties' => false,
+            'properties'           => [
+                'bar' => [
+                    'type' => 'string',
+                    'name' => 'bar',
+                ],
+            ],
+        ]);
+
+        $xml = '
+            <xml>
+                <foo>
+                    <bar>15</bar>
+                    <unknown>discard me</unknown>
+                </foo>
+            </xml>
+        ';
+
+        $this->xmlTest($param, $xml, [
+            'foo' => [
+                'bar' => 15
+            ]
+        ]);
+    }
+
+    /**
+     * @group issue-399, ResponseLocation
+     * @link  https://github.com/guzzle/guzzle/issues/399
+     */
+    public function testDiscardingUnknownPropertiesWithAliasing()
+    {
+        $param = new Parameter([
+            'name'                 => 'foo',
+            'type'                 => 'object',
+            'additionalProperties' => false,
+            'properties'           => [
+                'bar' => [
+                    'name'   => 'bar',
+                    'sentAs' => 'baz',
+                ],
+            ],
+        ]);
+
+        $xml = '
+            <xml>
+                <foo>
+                    <baz>15</baz>
+                    <unknown>discard me</unknown>
+                </foo>
+            </xml>
+        ';
+
+        $this->xmlTest($param, $xml, [
+            'foo' => [
+                'bar' => 15,
+            ],
+        ]);
+    }
+
+    /**
+     * @group ResponseLocation
+     */
+    public function testProcessingOfNestedAdditionalProperties()
+    {
+        $param = new Parameter([
+            'name'                 => 'foo',
+            'type'                 => 'object',
+            'additionalProperties' => true,
+            'properties'           => [
+                'bar' => [
+                    'name'   => 'bar',
+                    'sentAs' => 'baz',
+                ],
+                'nestedNoAdditional'  => [
+                    'type' => 'object',
+                    'additionalProperties' => false,
+                    'properties' => [
+                        'id' => [
+                            'type' => 'integer',
+                        ],
+                    ],
+                ],
+                'nestedWithAdditional' => [
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                ],
+                'nestedWithAdditionalSchema' => [
+                    'type' => 'object',
+                    'additionalProperties' => [
+                        'type'  => 'array',
+                        'items' => [
+                            'type' => 'string',
+                        ],
+                    ],
+                ],
+            ],
+        ]);
+
+        $xml = '
+            <xml>
+                <foo>
+                    <baz>15</baz>
+                    <additional>include me</additional>
+                    <nestedNoAdditional>
+                        <id>15</id>
+                        <unknown>discard me</unknown>
+                    </nestedNoAdditional>
+                    <nestedWithAdditional>
+                        <id>15</id>
+                        <additional>include me</additional>
+                    </nestedWithAdditional>
+                    <nestedWithAdditionalSchema>
+                        <arrayA>
+                            <item>1</item>
+                            <item>2</item>
+                            <item>3</item>
+                        </arrayA>
+                        <arrayB>
+                            <item>A</item>
+                            <item>B</item>
+                            <item>C</item>
+                        </arrayB>
+                    </nestedWithAdditionalSchema>
+                </foo>
+            </xml>
+        ';
+
+        $this->xmlTest($param, $xml, [
+            'foo' => [
+                'bar' => '15',
+                'additional' => 'include me',
+                'nestedNoAdditional' => [
+                    'id' => '15',
+                ],
+                'nestedWithAdditional' => [
+                    'id'         => '15',
+                    'additional' => 'include me',
+                ],
+                'nestedWithAdditionalSchema' => [
+                    'arrayA' => ['1', '2', '3'],
+                    'arrayB' => ['A', 'B', 'C'],
+                ],
+            ],
+        ]);
+    }
+
+    /**
+     * @group ResponseLocation
+     */
+    public function testConvertsMultipleAssociativeElementsToArray()
+    {
+        $param = new Parameter([
+            'name'                 => 'foo',
+            'type'                 => 'object',
+            'additionalProperties' => true,
+        ]);
+
+        $xml = '
+            <xml>
+                <foo>
+                    <baz>15</baz>
+                    <baz>25</baz>
+                    <bar>hi</bar>
+                    <bam>test</bam>
+                    <bam attr="hi" />
+                </foo>
+            </xml>
+        ';
+
+        $this->xmlTest($param, $xml, [
+            'foo' => [
+                'baz' => ['15', '25'],
+                'bar' => 'hi',
+                'bam' => [
+                    'test',
+                    ['@attributes' => ['attr' => 'hi']]
+                ]
+            ]
+        ]);
+    }
+
+    /**
+     * @group ResponseLocation
+     */
+    public function testUnderstandsNamespaces()
+    {
+        $param = new Parameter([
+            'name'     => 'nstest',
+            'type'     => 'array',
+            'location' => 'xml',
+            'items'    => [
+                'name'       => 'item',
+                'type'       => 'object',
+                'sentAs'     => 'item',
+                'properties' => [
+                    'id'           => [
+                        'type' => 'string',
+                    ],
+                    'isbn:number'  => [
+                        'type' => 'string',
+                    ],
+                    'meta'         => [
+                        'type'       => 'object',
+                        'sentAs'     => 'abstract:meta',
+                        'properties' => [
+                            'foo' => [
+                                'type' => 'numeric',
+                            ],
+                            'bar' => [
+                                'type'       => 'object',
+                                'properties' =>[
+                                    'attribute' => [
+                                        'type' => 'string',
+                                        'data' => [
+                                            'xmlAttribute' => true,
+                                            'xmlNs'        => 'abstract',
+                                        ],
+                                    ],
+                                ],
+                            ],
+                        ],
+                    ],
+                    'gamma'        => [
+                        'type'                 => 'object',
+                        'data'                 => [
+                            'xmlNs' => 'abstract',
+                        ],
+                        'additionalProperties' => true,
+                    ],
+                    'nonExistent'  => [
+                        'type'                 => 'object',
+                        'data'                 => [
+                            'xmlNs' => 'abstract',
+                        ],
+                        'additionalProperties' => true,
+                    ],
+                    'nonExistent2' => [
+                        'type'                 => 'object',
+                        'additionalProperties' => true,
+                    ],
+                ],
+            ],
+        ]);
+
+        $xml = '
+            <xml>
+                <nstest xmlns:isbn="urn:ISBN:0-395-36341-6" xmlns:abstract="urn:my.org:abstract">
+                    <item>
+                        <id>101</id>
+                        <isbn:number>1568491379</isbn:number>
+                        <abstract:meta>
+                            <foo>10</foo>
+                            <bar abstract:attribute="foo"></bar>
+                        </abstract:meta>
+                        <abstract:gamma>
+                            <foo>bar</foo>
+                        </abstract:gamma>
+                    </item>
+                    <item>
+                        <id>102</id>
+                        <isbn:number>1568491999</isbn:number>
+                        <abstract:meta>
+                            <foo>20</foo>
+                            <bar abstract:attribute="bar"></bar>
+                        </abstract:meta>
+                        <abstract:gamma>
+                            <foo>baz</foo>
+                        </abstract:gamma>
+                    </item>
+                </nstest>
+            </xml>
+        ';
+
+        $this->xmlTest($param, $xml, [
+            'nstest' => [
+                [
+                    'id'          => '101',
+                    'isbn:number' => 1568491379,
+                    'meta'        => [
+                        'foo' => 10,
+                        'bar' => [
+                            'attribute' => 'foo',
+                        ],
+                    ],
+                    'gamma'       => [
+                        'foo' => 'bar',
+                    ],
+                ],
+                [
+                    'id'          => '102',
+                    'isbn:number' => 1568491999,
+                    'meta'        => [
+                        'foo' => 20,
+                        'bar' => [
+                            'attribute' => 'bar'
+                        ],
+                    ],
+                    'gamma'       => [
+                        'foo' => 'baz',
+                    ],
+                ],
+            ],
+        ]);
+    }
+
+    /**
+     * @group ResponseLocation
+     */
+    public function testCanWalkUndefinedPropertiesWithNamespace()
+    {
+        $param = new Parameter([
+            'name'     => 'nstest',
+            'type'     => 'array',
+            'location' => 'xml',
+            'items'    => [
+                'name' => 'item',
+                'type' => 'object',
+                'sentAs' => 'item',
+                'additionalProperties' => [
+                    'type' => 'object',
+                    'data' => [
+                        'xmlNs' => 'abstract'
+                    ],
+                ],
+                'properties' => [
+                    'id' => [
+                        'type' => 'string',
+                    ],
+                    'isbn:number' => [
+                        'type' => 'string',
+                    ],
+                ],
+            ],
+        ]);
+
+        $xml = '
+            <xml>
+                <nstest xmlns:isbn="urn:ISBN:0-395-36341-6" xmlns:abstract="urn:my.org:abstract">
+                    <item>
+                        <id>101</id>
+                        <isbn:number>1568491379</isbn:number>
+                        <abstract:meta>
+                            <foo>10</foo>
+                            <bar>baz</bar>
+                        </abstract:meta>
+                    </item>
+                    <item>
+                        <id>102</id>
+                        <isbn:number>1568491999</isbn:number>
+                        <abstract:meta>
+                            <foo>20</foo>
+                            <bar>foo</bar>
+                        </abstract:meta>
+                    </item>
+                </nstest>
+            </xml>
+        ';
+
+        $this->xmlTest($param, $xml, [
+            'nstest' => [
+                [
+                    'id'          => '101',
+                    'isbn:number' => 1568491379,
+                    'meta'        => [
+                        'foo' => 10,
+                        'bar' => 'baz',
+                    ],
+                ],
+                [
+                    'id'          => '102',
+                    'isbn:number' => 1568491999,
+                    'meta'        => [
+                        'foo' => 20,
+                        'bar' => 'foo',
+                    ],
+                ],
+            ]
+        ]);
+    }
+
+    /**
+     * @group ResponseLocation
+     */
+    public function testCanWalkSimpleArrayWithNamespace()
+    {
+        $param = new Parameter([
+            'name'     => 'nstest',
+            'type'     => 'array',
+            'location' => 'xml',
+            'items'    => [
+                'type'   => 'string',
+                'sentAs' => 'number',
+                'data'   => [
+                    'xmlNs' => 'isbn'
+                ],
+            ],
+        ]);
+
+        $xml = '
+            <xml>
+                <nstest xmlns:isbn="urn:ISBN:0-395-36341-6">
+                    <isbn:number>1568491379</isbn:number>
+                    <isbn:number>1568491999</isbn:number>
+                    <isbn:number>1568492999</isbn:number>
+                </nstest>
+            </xml>
+        ';
+
+        $this->xmlTest($param, $xml, [
+            'nstest' => [
+                1568491379,
+                1568491999,
+                1568492999,
+            ],
+        ]);
+    }
+
+    /**
+     * @group ResponseLocation
+     */
+    public function testCanWalkSimpleArrayWithNamespace2()
+    {
+        $param = new Parameter([
+            'name'     => 'nstest',
+            'type'     => 'array',
+            'location' => 'xml',
+            'items'    => [
+                'type'   => 'string',
+                'sentAs' => 'isbn:number',
+            ]
+        ]);
+
+        $xml = '
+            <xml>
+                <nstest xmlns:isbn="urn:ISBN:0-395-36341-6">
+                    <isbn:number>1568491379</isbn:number>
+                    <isbn:number>1568491999</isbn:number>
+                    <isbn:number>1568492999</isbn:number>
+                </nstest>
+            </xml>
+        ';
+
+        $this->xmlTest($param, $xml, [
+            'nstest' => [
+                1568491379,
+                1568491999,
+                1568492999,
+            ],
+        ]);
+    }
+
+    private function xmlTest(Parameter $param, $xml, $expected)
+    {
+        $location = new XmlLocation();
+        $model = new Parameter();
+        $response = new Response(200, [], \GuzzleHttp\Psr7\stream_for($xml));
+        $result = new Result();
+        $result = $location->before($result, $response, $param);
+        $result = $location->visit($result, $response, $param);
+        $result = $location->after($result, $response, $model);
+        $this->assertEquals($expected, $result->toArray());
+    }
+}

+ 60 - 0
addons/cos/library/Guzzle/guzzle-services/tests/SchemaFormatterTest.php

@@ -0,0 +1,60 @@
+<?php
+namespace GuzzleHttp\Tests\Command\Guzzle;
+
+use GuzzleHttp\Command\Guzzle\SchemaFormatter;
+
+/**
+ * @covers \GuzzleHttp\Command\Guzzle\SchemaFormatter
+ */
+class SchemaFormatterTest extends \PHPUnit_Framework_TestCase
+{
+    public function dateTimeProvider()
+    {
+        $dateUtc = 'October 13, 2012 16:15:46 UTC';
+        $dateOffset = 'October 13, 2012 10:15:46 -06:00';
+        $expectedDateTime = '2012-10-13T16:15:46Z';
+
+        return [
+            ['foo', 'does-not-exist', 'foo'],
+            [$dateUtc, 'date-time', $expectedDateTime],
+            [$dateUtc, 'date-time-http', 'Sat, 13 Oct 2012 16:15:46 GMT'],
+            [$dateUtc, 'date', '2012-10-13'],
+            [$dateUtc, 'timestamp', strtotime($dateUtc)],
+            [new \DateTime($dateUtc), 'timestamp', strtotime($dateUtc)],
+            [$dateUtc, 'time', '16:15:46'],
+            [strtotime($dateUtc), 'time', '16:15:46'],
+            [strtotime($dateUtc), 'timestamp', strtotime($dateUtc)],
+            ['true', 'boolean-string', 'true'],
+            [true, 'boolean-string', 'true'],
+            ['false', 'boolean-string', 'false'],
+            [false, 'boolean-string', 'false'],
+            ['1350144946', 'date-time', $expectedDateTime],
+            [1350144946, 'date-time', $expectedDateTime],
+            [$dateOffset, 'date-time', $expectedDateTime],
+        ];
+    }
+
+    /**
+     * @dataProvider dateTimeProvider
+     */
+    public function testFilters($value, $format, $result)
+    {
+        $this->assertEquals($result, (new SchemaFormatter)->format($format, $value));
+    }
+
+    /**
+     * @expectedException \InvalidArgumentException
+     */
+    public function testValidatesDateTimeInput()
+    {
+        (new SchemaFormatter)->format('date-time', false);
+    }
+
+    public function testEnsuresTimestampsAreIntegers()
+    {
+        $t = time();
+        $result = (new SchemaFormatter)->format('timestamp', $t);
+        $this->assertSame($t, $result);
+        $this->assertInternalType('int', $result);
+    }
+}

+ 330 - 0
addons/cos/library/Guzzle/guzzle-services/tests/SchemaValidatorTest.php

@@ -0,0 +1,330 @@
+<?php
+namespace Guzzle\Tests\Service\Description;
+
+use GuzzleHttp\Command\Guzzle\Parameter;
+use GuzzleHttp\Command\Guzzle\SchemaValidator;
+use GuzzleHttp\Command\ToArrayInterface;
+
+/**
+ * @covers \GuzzleHttp\Command\Guzzle\SchemaValidator
+ */
+class SchemaValidatorTest extends \PHPUnit_Framework_TestCase
+{
+    /** @var SchemaValidator */
+    protected $validator;
+
+    public function setUp()
+    {
+        $this->validator = new SchemaValidator();
+    }
+
+    public function testValidatesArrayListsAreNumericallyIndexed()
+    {
+        $value = [[1]];
+        $this->assertFalse($this->validator->validate($this->getComplexParam(), $value));
+        $this->assertEquals(
+            ['[Foo][0] must be an array of properties. Got a numerically indexed array.'],
+            $this->validator->getErrors()
+        );
+    }
+
+    public function testValidatesArrayListsContainProperItems()
+    {
+        $value = [true];
+        $this->assertFalse($this->validator->validate($this->getComplexParam(), $value));
+        $this->assertEquals(
+            ['[Foo][0] must be of type object'],
+            $this->validator->getErrors()
+        );
+    }
+
+    public function testAddsDefaultValuesInLists()
+    {
+        $value = [[]];
+        $this->assertTrue($this->validator->validate($this->getComplexParam(), $value));
+        $this->assertEquals([['Bar' => true]], $value);
+    }
+
+    public function testMergesDefaultValuesInLists()
+    {
+        $value = [
+            ['Baz' => 'hello!'],
+            ['Bar' => false],
+        ];
+        $this->assertTrue($this->validator->validate($this->getComplexParam(), $value));
+        $this->assertEquals([
+            [
+                'Baz' => 'hello!',
+                'Bar' => true,
+            ],
+            ['Bar' => false],
+        ], $value);
+    }
+
+    public function testCorrectlyConvertsParametersToArrayWhenArraysArePresent()
+    {
+        $param = $this->getComplexParam();
+        $result = $param->toArray();
+        $this->assertInternalType('array', $result['items']);
+        $this->assertEquals('array', $result['type']);
+        $this->assertInstanceOf('GuzzleHttp\Command\Guzzle\Parameter', $param->getItems());
+    }
+
+    public function testEnforcesInstanceOfOnlyWhenObject()
+    {
+        $p = new Parameter([
+            'name'       => 'foo',
+            'type'       => ['object', 'string'],
+            'instanceOf' => get_class($this)
+        ]);
+        $this->assertTrue($this->validator->validate($p, $this));
+        $s = 'test';
+        $this->assertTrue($this->validator->validate($p, $s));
+    }
+
+    public function testConvertsObjectsToArraysWhenToArrayInterface()
+    {
+        $o = $this->getMockBuilder(ToArrayInterface::class)
+            ->setMethods(['toArray'])
+            ->getMockForAbstractClass();
+        $o->expects($this->once())
+            ->method('toArray')
+            ->will($this->returnValue(['foo' => 'bar']));
+        $p = new Parameter([
+            'name'       => 'test',
+            'type'       => 'object',
+            'properties' => [
+                'foo' => ['required' => 'true'],
+            ],
+        ]);
+        $this->assertTrue($this->validator->validate($p, $o));
+    }
+
+    public function testMergesValidationErrorsInPropertiesWithParent()
+    {
+        $p = new Parameter([
+            'name'       => 'foo',
+            'type'       => 'object',
+            'properties' => [
+                'bar'   => ['type' => 'string', 'required' => true, 'description' => 'This is what it does'],
+                'test'  => ['type' => 'string', 'minLength' => 2, 'maxLength' => 5],
+                'test2' => ['type' => 'string', 'minLength' => 2, 'maxLength' => 2],
+                'test3' => ['type' => 'integer', 'minimum' => 100],
+                'test4' => ['type' => 'integer', 'maximum' => 10],
+                'test5' => ['type' => 'array', 'maxItems' => 2],
+                'test6' => ['type' => 'string', 'enum' => ['a', 'bc']],
+                'test7' => ['type' => 'string', 'pattern' => '/[0-9]+/'],
+                'test8' => ['type' => 'number'],
+                'baz' => [
+                    'type'     => 'array',
+                    'minItems' => 2,
+                    'required' => true,
+                    "items"    => ["type" => "string"],
+                ],
+            ],
+        ]);
+
+        $value = [
+            'test' => 'a',
+            'test2' => 'abc',
+            'baz' => [false],
+            'test3' => 10,
+            'test4' => 100,
+            'test5' => [1, 3, 4],
+            'test6' => 'Foo',
+            'test7' => 'abc',
+            'test8' => 'abc',
+        ];
+
+        $this->assertFalse($this->validator->validate($p, $value));
+        $this->assertEquals([
+            '[foo][bar] is a required string: This is what it does',
+            '[foo][baz] must contain 2 or more elements',
+            '[foo][baz][0] must be of type string',
+            '[foo][test2] length must be less than or equal to 2',
+            '[foo][test3] must be greater than or equal to 100',
+            '[foo][test4] must be less than or equal to 10',
+            '[foo][test5] must contain 2 or fewer elements',
+            '[foo][test6] must be one of "a" or "bc"',
+            '[foo][test7] must match the following regular expression: /[0-9]+/',
+            '[foo][test8] must be of type number',
+            '[foo][test] length must be greater than or equal to 2',
+        ], $this->validator->getErrors());
+    }
+
+    public function testHandlesNullValuesInArraysWithDefaults()
+    {
+        $p = new Parameter([
+            'name'       => 'foo',
+            'type'       => 'object',
+            'required'   => true,
+            'properties' => [
+                'bar' => [
+                    'type' => 'object',
+                    'required' => true,
+                    'properties' => [
+                        'foo' => ['default' => 'hi'],
+                    ],
+                ],
+            ],
+        ]);
+        $value = [];
+        $this->assertTrue($this->validator->validate($p, $value));
+        $this->assertEquals(['bar' => ['foo' => 'hi']], $value);
+    }
+
+    public function testFailsWhenNullValuesInArraysWithNoDefaults()
+    {
+        $p = new Parameter([
+            'name'       => 'foo',
+            'type'       => 'object',
+            'required'   => true,
+            'properties' => [
+                'bar' => [
+                    'type' => 'object',
+                    'required' => true,
+                    'properties' => [
+                        'foo' => ['type' => 'string'],
+                    ],
+                ],
+            ],
+        ]);
+        $value = [];
+        $this->assertFalse($this->validator->validate($p, $value));
+        $this->assertEquals(['[foo][bar] is a required object'], $this->validator->getErrors());
+    }
+
+    public function testChecksTypes()
+    {
+        $p = new SchemaValidator();
+        $r = new \ReflectionMethod($p, 'determineType');
+        $r->setAccessible(true);
+        $this->assertEquals('any', $r->invoke($p, 'any', 'hello'));
+        $this->assertEquals(false, $r->invoke($p, 'foo', 'foo'));
+        $this->assertEquals('string', $r->invoke($p, 'string', 'hello'));
+        $this->assertEquals(false, $r->invoke($p, 'string', false));
+        $this->assertEquals('integer', $r->invoke($p, 'integer', 1));
+        $this->assertEquals(false, $r->invoke($p, 'integer', 'abc'));
+        $this->assertEquals('numeric', $r->invoke($p, 'numeric', 1));
+        $this->assertEquals('numeric', $r->invoke($p, 'numeric', '1'));
+        $this->assertEquals('number', $r->invoke($p, 'number', 1));
+        $this->assertEquals('number', $r->invoke($p, 'number', '1'));
+        $this->assertEquals(false, $r->invoke($p, 'numeric', 'a'));
+        $this->assertEquals('boolean', $r->invoke($p, 'boolean', true));
+        $this->assertEquals('boolean', $r->invoke($p, 'boolean', false));
+        $this->assertEquals(false, $r->invoke($p, 'boolean', 'false'));
+        $this->assertEquals('null', $r->invoke($p, 'null', null));
+        $this->assertEquals(false, $r->invoke($p, 'null', 'abc'));
+        $this->assertEquals('array', $r->invoke($p, 'array', []));
+        $this->assertEquals(false, $r->invoke($p, 'array', 'foo'));
+    }
+
+    public function testValidatesFalseAdditionalProperties()
+    {
+        $param = new Parameter([
+            'name'      => 'foo',
+            'type'      => 'object',
+            'properties' => [
+                'bar' => ['type' => 'string'],
+            ],
+            'additionalProperties' => false,
+        ]);
+        $value = ['test' => '123'];
+        $this->assertFalse($this->validator->validate($param, $value));
+        $this->assertEquals(['[foo][test] is not an allowed property'], $this->validator->getErrors());
+        $value = ['bar' => '123'];
+        $this->assertTrue($this->validator->validate($param, $value));
+    }
+
+    public function testAllowsUndefinedAdditionalProperties()
+    {
+        $param = new Parameter([
+            'name'      => 'foo',
+            'type'      => 'object',
+            'properties' => [
+                'bar' => ['type' => 'string'],
+            ]
+        ]);
+        $value = ['test' => '123'];
+        $this->assertTrue($this->validator->validate($param, $value));
+    }
+
+    public function testValidatesAdditionalProperties()
+    {
+        $param = new Parameter([
+            'name'      => 'foo',
+            'type'      => 'object',
+            'properties' => [
+                'bar' => ['type' => 'string'],
+            ],
+            'additionalProperties' => ['type' => 'integer'],
+        ]);
+        $value = ['test' => 'foo'];
+        $this->assertFalse($this->validator->validate($param, $value));
+        $this->assertEquals(['[foo][test] must be of type integer'], $this->validator->getErrors());
+    }
+
+    public function testValidatesAdditionalPropertiesThatArrayArrays()
+    {
+        $param = new Parameter([
+            'name' => 'foo',
+            'type' => 'object',
+            'additionalProperties' => [
+                'type'  => 'array',
+                'items' => ['type' => 'string'],
+            ],
+        ]);
+        $value = ['test' => [true]];
+        $this->assertFalse($this->validator->validate($param, $value));
+        $this->assertEquals(['[foo][test][0] must be of type string'], $this->validator->getErrors());
+    }
+
+    public function testIntegersCastToStringWhenTypeMismatch()
+    {
+        $param = new Parameter([
+            'name' => 'test',
+            'type' => 'string',
+        ]);
+        $value = 12;
+        $this->assertTrue($this->validator->validate($param, $value));
+        $this->assertEquals('12', $value);
+    }
+
+    public function testRequiredMessageIncludesType()
+    {
+        $param = new Parameter([
+            'name' => 'test',
+            'type' => [
+                'string',
+                'boolean',
+            ],
+            'required' => true,
+        ]);
+        $value = null;
+        $this->assertFalse($this->validator->validate($param, $value));
+        $this->assertEquals(['[test] is a required string or boolean'], $this->validator->getErrors());
+    }
+
+    protected function getComplexParam()
+    {
+        return new Parameter([
+            'name'     => 'Foo',
+            'type'     => 'array',
+            'required' => true,
+            'min'      => 1,
+            'items'    => [
+                'type'       => 'object',
+                'properties' => [
+                    'Baz' => [
+                        'type'    => 'string',
+                    ],
+                    'Bar' => [
+                        'required' => true,
+                        'type'     => 'boolean',
+                        'default'  => true,
+                    ],
+                ],
+            ],
+        ]);
+    }
+}

+ 39 - 0
addons/cos/library/Guzzle/guzzle-services/tests/SerializerTest.php

@@ -0,0 +1,39 @@
+<?php
+namespace GuzzleHttp\Tests\Command\Guzzle;
+
+use GuzzleHttp\Command\Command;
+use GuzzleHttp\Command\Guzzle\Description;
+use GuzzleHttp\Command\Guzzle\Serializer;
+use GuzzleHttp\Psr7\Request;
+
+/**
+ * @covers \GuzzleHttp\Command\Guzzle\Serializer
+ */
+class SerializerTest extends \PHPUnit_Framework_TestCase
+{
+    public function testAllowsUriTemplates()
+    {
+        $description = new Description([
+            'baseUri' => 'http://test.com',
+            'operations' => [
+                'test' => [
+                    'httpMethod'         => 'GET',
+                    'uri'                => '/api/{key}/foo',
+                    'parameters'         => [
+                        'key' => [
+                            'required'  => true,
+                            'type'      => 'string',
+                            'location'  => 'uri'
+                        ],
+                    ]
+                ]
+            ]
+        ]);
+
+        $command = new Command('test', ['key' => 'bar']);
+        $serializer = new Serializer($description);
+        /** @var Request $request */
+        $request = $serializer($command);
+        $this->assertEquals('http://test.com/api/bar/foo', $request->getUri());
+    }
+}

+ 283 - 0
addons/cos/library/Qcloud/Cos/Client.php

@@ -0,0 +1,283 @@
+<?php
+
+namespace Qcloud\Cos;
+
+include("Common.php");
+
+use Qcloud\Cos\Signature;
+use GuzzleHttp\Client as HttpClient;
+use GuzzleHttp\HandlerStack;
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
+use GuzzleHttp\Command\Guzzle\Description;
+use GuzzleHttp\Command\Guzzle\GuzzleClient;
+use GuzzleHttp\Command\Guzzle\Deserializer;
+use GuzzleHttp\Command\CommandInterface;
+use GuzzleHttp\Command\Exception\CommandException;
+use GuzzleHttp\Exception\RequestException;
+use GuzzleHttp\Middleware;
+use GuzzleHttp\Psr7;
+use GuzzleHttp\Pool;
+
+
+class Client extends GuzzleClient {
+    const VERSION = '2.0.9';
+
+    public $httpClient;
+    
+    private $api;
+    private $desc;
+    private $action;
+    private $operation;
+    private $cosConfig;
+    private $signature;
+    private $rawCosConfig;
+
+    public function __construct($cosConfig) {
+        $this->rawCosConfig = $cosConfig;
+        $this->cosConfig['schema'] = isset($cosConfig['schema']) ? $cosConfig['schema'] : 'http';
+        $this->cosConfig['region'] =  region_map($cosConfig['region']);
+        $this->cosConfig['appId'] = isset($cosConfig['credentials']['appId']) ? $cosConfig['credentials']['appId'] : null;
+        $this->cosConfig['secretId'] = isset($cosConfig['credentials']['secretId']) ? $cosConfig['credentials']['secretId'] : "";
+        $this->cosConfig['secretKey'] = isset($cosConfig['credentials']['secretKey']) ? $cosConfig['credentials']['secretKey'] : "";
+        $this->cosConfig['anonymous'] = isset($cosConfig['credentials']['anonymous']) ? $cosConfig['anonymous']['anonymous'] : false;
+        $this->cosConfig['token'] = isset($cosConfig['credentials']['token']) ? $cosConfig['credentials']['token'] : null;
+        $this->cosConfig['timeout'] = isset($cosConfig['timeout']) ? $cosConfig['timeout'] : 3600;
+        $this->cosConfig['connect_timeout'] = isset($cosConfig['connect_timeout']) ? $cosConfig['connect_timeout'] : 3600;
+        $this->cosConfig['ip'] = isset($cosConfig['ip']) ? $cosConfig['ip'] : null;
+        $this->cosConfig['port'] = isset($cosConfig['port']) ? $cosConfig['port'] : null;
+        $this->cosConfig['endpoint'] = isset($cosConfig['endpoint']) ? $cosConfig['endpoint'] : 'myqcloud.com';
+        $this->cosConfig['domain'] = isset($cosConfig['domain']) ? $cosConfig['domain'] : null;
+        $this->cosConfig['proxy'] = isset($cosConfig['proxy']) ? $cosConfig['proxy'] : null;
+        $this->cosConfig['retry'] = isset($cosConfig['retry']) ? $cosConfig['retry'] : 1;
+        $this->cosConfig['userAgent'] = isset($cosConfig['userAgent']) ? $cosConfig['userAgent'] : 'cos-php-sdk-v5.'. Client::VERSION;
+        $this->cosConfig['pathStyle'] = isset($cosConfig['pathStyle']) ? $cosConfig['pathStyle'] : false;
+        
+        
+        $service = Service::getService();
+        $handler = HandlerStack::create();
+		$handler->push(Middleware::mapRequest(function (RequestInterface $request) {
+			return $request->withHeader('User-Agent', $this->cosConfig['userAgent']);
+        }));
+        if ($this->cosConfig['anonymous'] != true) {
+            $handler->push($this::handleSignature($this->cosConfig['secretId'], $this->cosConfig['secretKey']));
+        }
+        if ($this->cosConfig['token'] != null) {
+            $handler->push(Middleware::mapRequest(function (RequestInterface $request) {
+                return $request->withHeader('x-cos-security-token', $this->cosConfig['token']);
+            }));
+        }
+        $handler->push($this::handleErrors());
+        $this->signature = new Signature($this->cosConfig['secretId'], $this->cosConfig['secretKey'], $this->cosConfig['token']);
+        $this->httpClient = new HttpClient([
+            'base_uri' => $this->cosConfig['schema'].'://cos.' . $this->cosConfig['region'] . '.myqcloud.com/',
+            'timeout' => $this->cosConfig['timeout'],
+            'handler' => $handler,
+            'proxy' => $this->cosConfig['proxy'],
+        ]);
+        $this->desc = new Description($service);
+        $this->api = (array)($this->desc->getOperations());
+        parent::__construct($this->httpClient, $this->desc, [$this,
+        'commandToRequestTransformer'], [$this, 'responseToResultTransformer'],
+        null);
+    }
+
+    public function commandToRequestTransformer(CommandInterface $command)
+    {
+        $this->action = $command->GetName();
+        $this->operation = $this->api[$this->action];
+        $transformer = new CommandToRequestTransformer($this->cosConfig, $this->operation); 
+        $seri = new Serializer($this->desc);
+        $request = $seri($command);
+        $request = $transformer->bucketStyleTransformer($command, $request);
+        $request = $transformer->uploadBodyTransformer($command, $request);
+        $request = $transformer->metadataTransformer($command, $request);
+        $request = $transformer->md5Transformer($command, $request);
+        $request = $transformer->specialParamTransformer($command, $request);
+        return $request;
+    }
+
+    public function responseToResultTransformer(ResponseInterface $response, RequestInterface $request, CommandInterface $command)
+    {
+        $transformer = new ResultTransformer($this->cosConfig, $this->operation); 
+        $transformer->writeDataToLocal($command, $request, $response);
+        $deseri = new Deserializer($this->desc, true);
+        $result = $deseri($response, $request, $command);
+
+        $result = $transformer->metaDataTransformer($command, $response, $result);
+        $result = $transformer->extraHeadersTransformer($command, $request, $result);
+        $result = $transformer->selectContentTransformer($command, $result);
+        return $result;
+    }
+    
+    public function __destruct() {
+    }
+
+    public function __call($method, array $args) {
+        for ($i = 1; $i <= $this->cosConfig['retry']; $i++) {
+            try {
+                return parent::__call(ucfirst($method), $args);
+            } catch (CommandException $e) {
+                if ($i != $this->cosConfig['retry']) {
+                    sleep(1 << ($i-1));
+                    continue;
+                }
+                $previous = $e->getPrevious();
+                if ($previous !== null) {
+                    throw $previous;
+                } else {
+                    throw $e;
+                }
+            }
+        }
+    }
+
+    public function getApi() {
+        return $this->api;
+    }
+
+    private function getCosConfig() {
+        return $this->cosConfig;
+    }
+
+    private function createPresignedUrl(RequestInterface $request, $expires) {
+        return $this->signature->createPresignedUrl($request, $expires);
+    }
+
+    public function getPresignetUrl($method, $args, $expires = "+30 minutes") {
+        $command = $this->getCommand($method, $args);
+        $request = $this->commandToRequestTransformer($command);
+        return $this->createPresignedUrl($request, $expires);
+    }
+
+    public function getObjectUrl($bucket, $key, $expires = "+30 minutes", array $args = array()) {
+        $command = $this->getCommand('GetObject', $args + array('Bucket' => $bucket, 'Key' => $key));
+        $request = $this->commandToRequestTransformer($command);
+        return $this->createPresignedUrl($request, $expires)->__toString();
+    }
+
+    public function upload($bucket, $key, $body, $options = array()) {
+        $body = Psr7\stream_for($body);
+        $options['PartSize'] = isset($options['PartSize']) ? $options['PartSize'] : MultipartUpload::DEFAULT_PART_SIZE;
+        if ($body->getSize() < $options['PartSize']) {
+            $rt = $this->putObject(array(
+                    'Bucket' => $bucket,
+                    'Key'    => $key,
+                    'Body'   => $body,
+                ) + $options);
+        }
+        else {
+            $multipartUpload = new MultipartUpload($this, $body, array(
+                    'Bucket' => $bucket,
+                    'Key' => $key,
+                ) + $options);
+
+            $rt = $multipartUpload->performUploading();
+        }
+        return $rt;
+    }
+
+    public function resumeUpload($bucket, $key, $body, $uploadId, $options = array()) {
+        $body = Psr7\stream_for($body);
+        $options['PartSize'] = isset($options['PartSize']) ? $options['PartSize'] : MultipartUpload::DEFAULT_PART_SIZE;
+        $multipartUpload = new MultipartUpload($this, $body, array(
+                'Bucket' => $bucket,
+                'Key' => $key,
+                'UploadId' => $uploadId,
+            ) + $options);
+
+        $rt = $multipartUpload->resumeUploading();
+        return $rt;
+    }
+
+    public function copy($bucket, $key, $copySource, $options = array()) {
+
+        $options['PartSize'] = isset($options['PartSize']) ? $options['PartSize'] : Copy::DEFAULT_PART_SIZE;
+
+        // set copysource client
+        $sourceConfig = $this->rawCosConfig;
+        $sourceConfig['region'] = $copySource['Region'];
+        $cosSourceClient = new Client($sourceConfig);
+        $copySource['VersionId'] = isset($copySource['VersionId']) ? $copySource['VersionId'] : "";
+        try {
+            $rt = $cosSourceClient->headObject(
+                array('Bucket'=>$copySource['Bucket'],
+                    'Key'=>$copySource['Key'],
+                    'VersionId'=>$copySource['VersionId'],
+                )
+            );
+        } catch (\Exception $e) {
+            throw $e;
+        }
+
+        $contentLength =$rt['ContentLength'];
+        // sample copy
+        if ($contentLength < $options['PartSize']) {
+            $rt = $this->copyObject(array(
+                    'Bucket' => $bucket,
+                    'Key'    => $key,
+                    'CopySource'   => $copySource['Bucket']. '.cos.'. $copySource['Region'].
+                                      ".myqcloud.com/". $copySource['Key']. "?versionId=". $copySource['VersionId'],
+                ) + $options
+            );
+            return $rt;
+        }
+        // multi part copy
+        $copySource['ContentLength'] = $contentLength;
+        $copy = new Copy($this, $copySource, array(
+                'Bucket' => $bucket,
+                'Key'    => $key
+            ) + $options
+        );
+        return $copy->copy();
+    }
+
+    public function doesBucketExist($bucket, array $options = array())
+    {
+        try {
+            $this->HeadBucket(array(
+                'Bucket' => $bucket));
+            return True;
+        } catch (\Exception $e){
+            return False;
+        }
+    }
+
+    public function doesObjectExist($bucket, $key, array $options = array())
+    {
+        try {
+            $this->HeadObject(array(
+                'Bucket' => $bucket,
+                'Key' => $key));
+            return True;
+        } catch (\Exception $e){
+            return False;
+        }
+    }
+    
+    public static function explodeKey($key) {
+        // Remove a leading slash if one is found
+        $split_key = explode('/', $key && $key[0] == '/' ? substr($key, 1) : $key);
+        // Remove empty element
+        $split_key = array_filter($split_key, function($var) {
+            return !($var == '' || $var == null);
+        });
+        $final_key = implode("/", $split_key);
+        if (substr($key, -1)  == '/') {
+            $final_key = $final_key . '/';
+        }
+        return $final_key;
+    }
+
+    public static function handleSignature($secretId, $secretKey) {
+            return function (callable $handler) use ($secretId, $secretKey) {
+                    return new SignatureMiddleware($handler, $secretId, $secretKey);
+            };
+    }
+
+    public static function handleErrors() {
+            return function (callable $handler) {
+                    return new ExceptionMiddleware($handler);
+            };
+    }
+}

+ 163 - 0
addons/cos/library/Qcloud/Cos/CommandToRequestTransformer.php

@@ -0,0 +1,163 @@
+<?php
+
+namespace Qcloud\Cos;
+
+use Guzzle\Service\Description\Parameter;
+use Guzzle\Service\Description\ServiceDescription;
+use GuzzleHttp\HandlerStack;
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
+use Qcloud\Cos\Signature;
+use GuzzleHttp\Command\Guzzle\Description;
+use GuzzleHttp\Command\Guzzle\GuzzleClient;
+use GuzzleHttp\Command\CommandInterface;
+use GuzzleHttp\Exception\RequestException;
+use GuzzleHttp\Middleware;
+use GuzzleHttp\Psr7;
+use GuzzleHttp\Psr7\Uri;
+use InvalidArgumentException;
+
+
+class CommandToRequestTransformer {
+    private $config;
+    private $operation;
+
+    public function __construct($config ,$operation) {
+        $this->config = $config;
+        $this->operation = $operation;
+    }
+
+    // format bucket style
+    public function bucketStyleTransformer(CommandInterface $command, RequestInterface $request) {
+        $action = $command->getName();
+        if ($action == 'ListBuckets') {
+            return $request->withUri(new Uri($this->config['schema']."://service.cos.myqcloud.com/"));
+        }
+        $operation = $this->operation;
+        $bucketname = $command['Bucket'];
+
+        $appId = $this->config['appId'];
+        if ($appId != null && endWith($bucketname, '-'.$appId) == False)
+        {
+            $bucketname = $bucketname.'-'.$appId;
+        }
+        $command['Bucket'] = $bucketname;
+        $path = ''; 
+        $http_method = $operation['httpMethod'];
+        $uri = $operation['uri'];
+        
+        // Hoststyle is used by default
+        // Pathstyle
+        if ($this->config['pathStyle'] != true) {
+            if (isset($operation['parameters']['Bucket']) && $command->hasParam('Bucket')) {
+                $uri = str_replace("{Bucket}", '', $uri);
+            }   
+            if (isset($operation['parameters']['Key']) && $command->hasParam('Key')) {
+                $uri = str_replace("{/Key*}", encodeKey($command['Key']), $uri);
+            }
+        }
+        $origin_host = $bucketname. '.cos.' . $this->config['region'] . '.' . $this->config['endpoint'];
+        // domain
+        if ($this->config['domain'] != null) {
+            $origin_host = $this->config['domain'];
+        }
+        $host = $origin_host;
+        if ($this->config['ip'] != null) {
+            $host = $this->config['ip'];
+            if ($this->config['port'] != null) {
+                $host = $this->config['ip'] . ":" . $this->config['port'];
+            }
+        }
+
+
+        $path = $this->config['schema'].'://'. $host . $uri;
+        $uri = new Uri($path);
+        $query = $request->getUri()->getQuery();
+        if ($uri->getQuery() != $query && $uri->getQuery() != "") {
+            $query =   $uri->getQuery() . "&" . $request->getUri()->getQuery();
+        }
+        $uri = $uri->withQuery($query);
+        $request = $request->withUri($uri);
+        $request = $request->withHeader('Host', $origin_host);
+        return $request;
+    }
+
+    // format upload body
+    public function uploadBodyTransformer(CommandInterface $command, $request, $bodyParameter = 'Body', $sourceParameter = 'SourceFile') {
+        
+        $operation = $this->operation;
+        if (!isset($operation['parameters']['Body'])) {
+            return $request;
+        }
+        $source = isset($command[$sourceParameter]) ? $command[$sourceParameter] : null;
+        $body = isset($command[$bodyParameter]) ? $command[$bodyParameter] : null;
+        // If a file path is passed in then get the file handle
+        if (is_string($source) && file_exists($source)) {
+            $body = fopen($source, 'rb');
+        }
+        // Prepare the body parameter and remove the source file parameter
+        if (null !== $body) {
+            return $request;
+        } else {
+            throw new InvalidArgumentException(
+                "You must specify a non-null value for the {$bodyParameter} or {$sourceParameter} parameters.");
+        }
+    }
+
+    // update md5
+    public function md5Transformer(CommandInterface $command, $request) {
+        $operation = $this->operation;
+        if (isset($operation['data']['contentMd5'])) {
+            $request = $this->addMd5($request);
+        }
+        if (isset($operation['parameters']['ContentMD5']) &&
+            isset($command['ContentMD5'])) {
+            $value = $command['ContentMD5'];
+            if ($value === true) {
+                $request = $this->addMd5($request);
+            }
+        }
+
+        return $request;
+    }
+
+    // add meta
+    public function metadataTransformer(CommandInterface $command, $request) {
+        $operation = $this->operation;
+        if (isset($command['Metadata'])) {
+            $meta = $command['Metadata'];
+            foreach ($meta as $key => $value) {
+                $request = $request->withHeader('x-cos-meta-' . $key, $value);
+            }
+        }
+        $request = headersMap($command, $request);  
+        return $request;
+    }
+
+    // count md5
+    private function addMd5($request) {
+        $body = $request->getBody();
+        if ($body && $body->getSize() > 0) {
+            $md5 = base64_encode(md5($body, true));
+            return $request->withHeader('Content-MD5', $md5);
+        }
+        return $request;
+    }
+
+    // inventoryId
+    public function specialParamTransformer(CommandInterface $command, $request) {
+        $action = $command->getName();
+        if ($action == 'PutBucketInventory') {
+            $id = $command['Id'];
+            $uri = $request->getUri();
+            $query = $uri->getQuery();
+            $uri = $uri->withQuery($query . "&Id=".$id);
+            return $request->withUri($uri);
+        }
+        return $request;
+    }
+
+    public function __destruct() {
+    }
+
+}

+ 48 - 0
addons/cos/library/Qcloud/Cos/Common.php

@@ -0,0 +1,48 @@
+<?php
+
+namespace Qcloud\Cos;
+
+function region_map($region) {
+    $regionmap = array('cn-east'=>'ap-shanghai',
+            'cn-south'=>'ap-guangzhou',
+            'cn-north'=>'ap-beijing-1',
+            'cn-south-2'=>'ap-guangzhou-2',
+            'cn-southwest'=>'ap-chengdu',
+            'sg'=>'ap-singapore',
+            'tj'=>'ap-beijing-1',
+            'bj'=>'ap-beijing',
+            'sh'=>'ap-shanghai',
+            'gz'=>'ap-guangzhou',
+            'cd'=>'ap-chengdu',
+            'sgp'=>'ap-singapore');
+    if (array_key_exists($region, $regionmap)) {
+        return $regionmap[$region];
+    }
+    return $region;
+}
+
+function encodeKey($key) {
+    return str_replace('%2F', '/', rawurlencode($key));
+}
+
+function endWith($haystack, $needle) {
+    $length = strlen($needle);
+    if($length == 0)
+    {
+        return true;
+    }
+    return (substr($haystack, -$length) === $needle);
+}
+
+function headersMap($command, $request) {
+    $headermap = array(
+            'TransferEncoding'=>'Transfer-Encoding',
+            'ChannelId'=>'x-cos-channel-id'
+    );
+    foreach ($headermap as $key => $value) {
+        if (isset($command[$key])) {
+            $request = $request->withHeader($value, $command[$key]);
+        }
+    }
+    return $request;
+}

+ 141 - 0
addons/cos/library/Qcloud/Cos/Copy.php

@@ -0,0 +1,141 @@
+<?php
+
+namespace Qcloud\Cos;
+
+use GuzzleHttp\Psr7\Request;
+use GuzzleHttp\Pool;
+
+class Copy {
+    const MIN_PART_SIZE = 1048576;
+    const MAX_PART_SIZE = 5368709120;
+    const DEFAULT_PART_SIZE = 52428800;
+    const MAX_PARTS     = 10000;
+
+    private $client;
+    private $copySource;
+    private $options;
+    private $partSize;
+    private $parts;
+    private $size;
+    private $commandList = [];
+    private $requestList = [];
+
+    public function __construct($client, $source, $options = array()) {
+        $minPartSize = $options['PartSize'];
+        unset($options['PartSize']);
+        $this->client = $client;
+        $this->copySource = $source;
+        $this->options = $options;
+        $this->size = $source['ContentLength'];
+        unset($source['ContentLength']);
+        $this->partSize = $this->calculatePartSize($minPartSize);
+        $this->concurrency = isset($options['Concurrency']) ? $options['Concurrency'] : 10;
+        $this->retry = isset($options['Retry']) ? $options['Retry'] : 5;
+    }
+    public function copy() {
+        $uploadId= $this->initiateMultipartUpload();
+        for ($i = 0; $i < $this->retry; $i += 1) {
+            $rt = $this->uploadParts($uploadId);
+            if ($rt == 0) {
+                break;
+            }
+            sleep(1 << $i);
+        }
+        foreach ( $this->parts as $key => $row ){
+            $num1[$key] = $row ['PartNumber'];
+            $num2[$key] = $row ['ETag'];
+        }
+        array_multisort($num1, SORT_ASC, $num2, SORT_ASC, $this->parts);
+        return $this->client->completeMultipartUpload(array(
+            'Bucket' => $this->options['Bucket'],
+            'Key' => $this->options['Key'],
+            'UploadId' => $uploadId,
+            'Parts' => $this->parts)
+        );
+
+    }
+    public function uploadParts($uploadId) {
+        $copyRequests = function ($uploadId) {
+            $offset = 0;
+            $partNumber = 1;
+            $partSize = $this->partSize;
+            $finishedNum = 0;
+            $this->parts = array();
+            for ($index = 1; ; $index ++) {
+                if ($offset + $partSize  >= $this->size)
+                {
+                    $partSize = $this->size - $offset;
+                }
+                $copySourcePath = $this->copySource['Bucket']. '.cos.'. $this->copySource['Region'].
+                    ".myqcloud.com/". $this->copySource['Key']. "?versionId=". $this->copySource['VersionId'];
+                $params = array(
+                    'Bucket' => $this->options['Bucket'],
+                    'Key' => $this->options['Key'],
+                    'UploadId' => $uploadId,
+                    'PartNumber' => $partNumber,
+                    'CopySource'=> $copySourcePath,
+                    'CopySourceRange' => 'bytes='.((string)$offset).'-'.(string)($offset+$partSize - 1),
+                );
+                if(!isset($this->parts[$partNumber])) {
+                    $command = $this->client->getCommand('uploadPartCopy', $params);
+                    $request = $this->client->commandToRequestTransformer($command);
+                    $this->commandList[$index] = $command;
+                    $this->requestList[$index] = $request;
+                    yield $request;
+                }
+                ++$partNumber;
+                $offset += $partSize;
+                if ($this->size == $offset) {
+                    break;
+                }
+            }
+        };
+        $pool = new Pool($this->client->httpClient, $copyRequests($uploadId), [
+            'concurrency' => $this->concurrency,
+            'fulfilled' => function ($response, $index) {
+                $index = $index + 1;
+                $response = $this->client->responseToResultTransformer($response, $this->requestList[$index], $this->commandList[$index]);
+                $part = array('PartNumber' => $index, 'ETag' => $response['ETag']);
+                $this->parts[$index] = $part;
+            },
+           
+            'rejected' => function ($reason, $index) { 
+                $index = $index += 1;
+                $retry = 2;
+                for ($i = 1; $i <= $retry; $i++) {
+                    try {
+                        $rt =$this->client->execute($this->commandList[$index]);
+                        $part = array('PartNumber' => $index, 'ETag' => $rt['ETag']);
+                        $this->parts[$index] = $part;
+                    } catch(\Exception $e) {
+                        if ($i == $retry) {
+                            throw($e);
+                        }
+                    }
+                }
+            },
+        ]);
+        
+        // Initiate the transfers and create a promise
+        $promise = $pool->promise();
+        
+        // Force the pool of requests to complete.
+        $promise->wait();
+    }
+
+
+    private function calculatePartSize($minPartSize)
+    {
+        $partSize = intval(ceil(($this->size / self::MAX_PARTS)));
+        $partSize = max($minPartSize, $partSize);
+        $partSize = min($partSize, self::MAX_PART_SIZE);
+        $partSize = max($partSize, self::MIN_PART_SIZE);
+        return $partSize;
+    }
+
+    private function initiateMultipartUpload() {
+        $result = $this->client->createMultipartUpload($this->options);
+        return $result['UploadId'];
+    }
+
+}

+ 162 - 0
addons/cos/library/Qcloud/Cos/CosTransformer.php

@@ -0,0 +1,162 @@
+<?php
+
+namespace Qcloud\Cos;
+
+use Guzzle\Service\Description\Parameter;
+use Guzzle\Service\Description\ServiceDescription;
+use GuzzleHttp\HandlerStack;
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
+use Qcloud\Cos\Signature;
+use GuzzleHttp\Command\Guzzle\Description;
+use GuzzleHttp\Command\Guzzle\GuzzleClient;
+use GuzzleHttp\Command\CommandInterface;
+use GuzzleHttp\Exception\RequestException;
+use GuzzleHttp\Middleware;
+use GuzzleHttp\Psr7;
+use GuzzleHttp\Psr7\Uri;
+use InvalidArgumentException;
+
+
+class CosTransformer {
+    private $config;
+    private $operation;
+
+    public function __construct($config ,$operation) {
+        $this->config = $config;
+        $this->operation = $operation;
+    }
+
+    // format bucket style
+    public function bucketStyleTransformer(CommandInterface $command, RequestInterface $request) {
+        $action = $command->getName();
+        if ($action == 'ListBuckets') {
+            return $request->withUri(new Uri($this->config['schema']."://service.cos.myqcloud.com/"));
+        }
+        $operation = $this->operation;
+        $bucketname = $command['Bucket'];
+
+        $appId = $this->config['appId'];
+        if ($appId != null && endWith($bucketname, '-'.$appId) == False)
+        {
+            $bucketname = $bucketname.'-'.$appId;
+        }
+        $command['Bucket'] = $bucketname;
+        $path = ''; 
+        $http_method = $operation['httpMethod'];
+        $uri = $operation['uri'];
+        
+        // Hoststyle is used by default
+        // Pathstyle
+        if ($this->config['pathStyle'] != true) {
+            if (isset($operation['parameters']['Bucket']) && $command->hasParam('Bucket')) {
+                $uri = str_replace("{Bucket}", '', $uri);
+            }   
+            if (isset($operation['parameters']['Key']) && $command->hasParam('Key')) {
+                $uri = str_replace("{/Key*}", encodeKey($command['Key']), $uri);
+            }
+        }
+        $origin_host = $bucketname. '.cos.' . $this->config['region'] . '.' . $this->config['endpoint'];
+        // domain
+        if ($this->config['domain'] != null) {
+            $origin_host = $this->config['domain'];
+        }
+        $host = $origin_host;
+        if ($this->config['ip'] != null) {
+            $host = $this->config['ip'];
+            if ($this->config['port'] != null) {
+                $host = $this->config['ip'] . ":" . $this->config['port'];
+            }
+        }
+
+
+        $path = $this->config['schema'].'://'. $host . $uri;
+        $uri = new Uri($path);
+        $query = $request->getUri()->getQuery();
+        if ($uri->getQuery() != $query && $uri->getQuery() != "") {
+            $query =   $uri->getQuery() . "&" . $request->getUri()->getQuery();
+        }
+        $uri = $uri->withQuery($query);
+        $request = $request->withUri($uri);
+        $request = $request->withHeader('Host', $origin_host);
+        return $request;
+    }
+
+    // format upload body
+    public function uploadBodyTransformer(CommandInterface $command, $request, $bodyParameter = 'Body', $sourceParameter = 'SourceFile') {
+        
+        $operation = $this->operation;
+        if (!isset($operation['parameters']['Body'])) {
+            return $request;
+        }
+        $source = isset($command[$sourceParameter]) ? $command[$sourceParameter] : null;
+        $body = isset($command[$bodyParameter]) ? $command[$bodyParameter] : null;
+        // If a file path is passed in then get the file handle
+        if (is_string($source) && file_exists($source)) {
+            $body = fopen($source, 'rb');
+        }
+        // Prepare the body parameter and remove the source file parameter
+        if (null !== $body) {
+            return $request;
+        } else {
+            throw new InvalidArgumentException(
+                "You must specify a non-null value for the {$bodyParameter} or {$sourceParameter} parameters.");
+        }
+    }
+
+    // update md5
+    public function md5Transformer(CommandInterface $command, $request) {
+        $operation = $this->operation;
+        if (isset($operation['data']['contentMd5'])) {
+            $request = $this->addMd5($request);
+        }
+        if (isset($operation['parameters']['ContentMD5']) &&
+            isset($command['ContentMD5'])) {
+            $value = $command['ContentMD5'];
+            if ($value === true) {
+                $request = $this->addMd5($request);
+            }
+        }
+
+        return $request;
+    }
+
+    // add meta
+    public function metadataTransformer(CommandInterface $command, $request) {
+        $operation = $this->operation;
+        if (isset($command['Metadata'])) {
+            $meta = $command['Metadata'];
+            foreach ($meta as $key => $value) {
+                $request = $request->withHeader('x-cos-meta-' . $key, $value);
+            }
+        }
+        return $request;
+    }
+
+    // count md5
+    private function addMd5($request) {
+        $body = $request->getBody();
+        if ($body && $body->getSize() > 0) {
+            $md5 = base64_encode(md5($body, true));
+            return $request->withHeader('Content-MD5', $md5);
+        }
+        return $request;
+    }
+
+    // inventoryId
+    public function specialParamTransformer(CommandInterface $command, $request) {
+        $action = $command->getName();
+        if ($action == 'PutBucketInventory') {
+            $id = $command['Id'];
+            $uri = $request->getUri();
+            $query = $uri->getQuery();
+            $uri = $uri->withQuery($query . "&Id=".$id);
+            return $request->withUri($uri);
+        }
+        return $request;
+    }
+
+    public function __destruct() {
+    }
+
+}

+ 7 - 0
addons/cos/library/Qcloud/Cos/Exception/CosException.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace Qcloud\Cos\Exception;
+
+use Qcloud\Cos\Exception\ServiceResponseException;
+
+class CosException extends ServiceResponseException {}

+ 189 - 0
addons/cos/library/Qcloud/Cos/Exception/ServiceResponseException.php

@@ -0,0 +1,189 @@
+<?php
+
+namespace Qcloud\Cos\Exception;
+
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
+
+class ServiceResponseException extends \RuntimeException {
+
+    /**
+     * @var Response Response
+     */
+    protected $response;
+
+    /**
+     * @var RequestInterface Request
+     */
+    protected $request;
+
+    /**
+     * @var string Request ID
+     */
+    protected $requestId;
+
+    /**
+     * @var string Exception type (client / server)
+     */
+    protected $exceptionType;
+
+    /**
+     * @var string Exception code
+     */
+    protected $exceptionCode;
+
+    /**
+     * Set the exception code
+     *
+     * @param string $code Exception code
+     */
+    public function setExceptionCode($code) {
+        $this->exceptionCode = $code;
+    }
+
+    /**
+     * Get the exception code
+     *
+     * @return string|null
+     */
+    public function getExceptionCode() {
+        return $this->exceptionCode;
+    }
+
+    /**
+     * Set the exception type
+     *
+     * @param string $type Exception type
+     */
+    public function setExceptionType($type) {
+        $this->exceptionType = $type;
+    }
+
+    /**
+     * Get the exception type (one of client or server)
+     *
+     * @return string|null
+     */
+    public function getExceptionType() {
+        return $this->exceptionType;
+    }
+
+    /**
+     * Set the request ID
+     *
+     * @param string $id Request ID
+     */
+    public function setRequestId($id) {
+        $this->requestId = $id;
+    }
+
+    /**
+     * Get the Request ID
+     *
+     * @return string|null
+     */
+    public function getRequestId() {
+        return $this->requestId;
+    }
+
+    /**
+     * Set the associated response
+     *
+     * @param Response $response Response
+     */
+    public function setResponse(ResponseInterface $response) {
+        $this->response = $response;
+    }
+
+    /**
+     * Get the associated response object
+     *
+     * @return Response|null
+     */
+    public function getResponse() {
+        return $this->response;
+    }
+
+    /**
+     * Set the associated request
+     *
+     * @param RequestInterface $request
+     */
+    public function setRequest(RequestInterface $request) {
+        $this->request = $request;
+    }
+
+    /**
+     * Get the associated request object
+     *
+     * @return RequestInterface|null
+     */
+    public function getRequest() {
+        return $this->request;
+    }
+
+    /**
+     * Get the status code of the response
+     *
+     * @return int|null
+     */
+    public function getStatusCode() {
+        return $this->response ? $this->response->getStatusCode() : null;
+    }
+
+    /**
+     * Cast to a string
+     *
+     * @return string
+     */
+    public function __toString() {
+        $message = get_class($this) . ': '
+            . 'Cos Error Code: ' . $this->getExceptionCode() . ', '
+            . 'Status Code: ' . $this->getStatusCode() . ', '
+            . 'Cos Request ID: ' . $this->getRequestId() . ', '
+            . 'Cos Error Type: ' . $this->getExceptionType() . ', '
+            . 'Cos Error Message: ' . $this->getMessage();
+
+        // Add the User-Agent if available
+        if ($this->request) {
+            $message .= ', ' . 'User-Agent: ' . $this->request->getHeader('User-Agent')[0];
+        }
+
+        return $message;
+    }
+
+    /**
+     * Get the request ID of the error. This value is only present if a
+     * response was received, and is not present in the event of a networking
+     * error.
+     *
+     * Same as `getRequestId()` method, but matches the interface for SDKv3.
+     *
+     * @return string|null Returns null if no response was received
+     */
+    public function getCosRequestId() {
+        return $this->requestId;
+    }
+
+    /**
+     * Get the Cos error type.
+     *
+     * Same as `getExceptionType()` method, but matches the interface for SDKv3.
+     *
+     * @return string|null Returns null if no response was received
+     */
+    public function getCosErrorType() {
+        return $this->exceptionType;
+    }
+
+    /**
+     * Get the Cos error code.
+     *
+     * Same as `getExceptionCode()` method, but matches the interface for SDKv3.
+     *
+     * @return string|null Returns null if no response was received
+     */
+    public function getCosErrorCode() {
+        return $this->exceptionCode;
+    }
+}

+ 72 - 0
addons/cos/library/Qcloud/Cos/ExceptionMiddleware.php

@@ -0,0 +1,72 @@
+<?php
+
+namespace Qcloud\Cos;
+
+use Qcloud\Cos\Exception\ServiceResponseException;
+use GuzzleHttp\Promise\PromiseInterface;
+use GuzzleHttp\Psr7;
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
+use GuzzleHttp\Exception\RequestException;
+
+class ExceptionMiddleware {
+    private $nextHandler;
+    protected $parser;
+    protected $defaultException;
+
+    /**
+     * @param callable $nextHandler Next handler to invoke.
+     */
+    public function __construct(callable $nextHandler) {
+        $this->nextHandler = $nextHandler;
+        $this->parser = new ExceptionParser();
+        $this->defaultException = 'Qcloud\Cos\Exception\ServiceResponseException';
+    }
+
+    /**
+     * @param RequestInterface $request
+     * @param array            $options
+     *
+     * @return PromiseInterface
+     */
+    public function __invoke(RequestInterface $request, array $options) {
+        $fn = $this->nextHandler;
+        return $fn($request, $options)->then(
+                    function (ResponseInterface $response) use ($request) {
+						return $this->handle($request, $response);
+                    }
+		);
+	}
+
+	public function handle(RequestInterface $request, ResponseInterface $response) {
+		$code = $response->getStatusCode();
+		if ($code < 400) {
+			return $response;
+		}
+
+		//throw RequestException::create($request, $response);
+        $parts = $this->parser->parse($request, $response);
+
+        $className = 'Qcloud\\Cos\\Exception\\' . $parts['code'];
+        if (substr($className, -9) !== 'Exception') {
+            $className .= 'Exception';
+        }
+
+        $className = class_exists($className) ? $className : $this->defaultException;
+
+        throw $this->createException($className, $request, $response, $parts);
+	}
+
+    protected function createException($className, RequestInterface $request, ResponseInterface $response, array $parts) {
+        $class = new $className($parts['message']);
+
+        if ($class instanceof ServiceResponseException) {
+            $class->setExceptionCode($parts['code']);
+            $class->setExceptionType($parts['type']);
+            $class->setResponse($response);
+            $class->setRequest($request);
+            $class->setRequestId($parts['request_id']);
+        }
+        return $class;
+    }
+}

+ 112 - 0
addons/cos/library/Qcloud/Cos/ExceptionParser.php

@@ -0,0 +1,112 @@
+<?php
+
+namespace Qcloud\Cos;
+
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
+
+/**
+ * Parses default XML exception responses
+ */
+class ExceptionParser {
+
+    public function parse(RequestInterface $request, ResponseInterface $response) {
+        $data = array(
+            'code'       => null,
+            'message'    => null,
+            //'type'       => $response->isClientError() ? 'client' : 'server',
+            'type'       => 'client',
+            'request_id' => null,
+            'parsed'     => null
+        );
+
+		$body = strval($response->getBody());
+
+        if (empty($body)) {
+            $this->parseHeaders($request, $response, $data);
+            return $data;
+        }
+
+        try {
+            $xml = new \SimpleXMLElement(utf8_encode($body));
+            $this->parseBody($xml, $data);
+            return $data;
+        } catch (\Exception $e) {
+            $data['code'] = 'PhpInternalXmlParseError';
+            $data['message'] = 'A non-XML response was received';
+            return $data;
+        }
+    }
+
+    /**
+     * Parses additional exception information from the response headers
+     *
+     * @param RequestInterface $request  Request that was issued
+     * @param Response         $response The response from the request
+     * @param array            $data     The current set of exception data
+     */
+    protected function parseHeaders(RequestInterface $request, ResponseInterface $response, array &$data) {
+        $data['message'] = $response->getStatusCode() . ' ' . $response->getReasonPhrase();
+        $requestId = $response->getHeader('x-cos-request-id');
+        if (isset($requestId[0])) {
+            $requestId = $requestId[0];
+            $data['request_id'] = $requestId;
+            $data['message'] .= " (Request-ID: $requestId)";
+        }
+
+        // Get the request
+        $status  = $response->getStatusCode();
+        $method  = $request->getMethod();
+
+        // Attempt to determine code for 403s and 404s
+        if ($status === 403) {
+            $data['code'] = 'AccessDenied';
+        } elseif ($method === 'HEAD' && $status === 404) {
+            $path   = explode('/', trim($request->getUri()->getPath(), '/'));
+            $host   = explode('.', $request->getUri()->getHost());
+            $bucket = (count($host) >= 4) ? $host[0] : array_shift($path);
+            $object = array_shift($path);
+
+            if ($bucket && $object) {
+                $data['code'] = 'NoSuchKey';
+            } elseif ($bucket) {
+                $data['code'] = 'NoSuchBucket';
+            }
+        }
+    }
+
+    /**
+     * Parses additional exception information from the response body
+     *
+     * @param \SimpleXMLElement $body The response body as XML
+     * @param array             $data The current set of exception data
+     */
+    protected function parseBody(\SimpleXMLElement $body, array &$data) {
+        $data['parsed'] = $body;
+
+        $namespaces = $body->getDocNamespaces();
+        if (isset($namespaces[''])) {
+            // Account for the default namespace being defined and PHP not being able to handle it :(
+            $body->registerXPathNamespace('ns', $namespaces['']);
+            $prefix = 'ns:';
+        } else {
+            $prefix = '';
+        }
+
+        if ($tempXml = $body->xpath("//{$prefix}Code[1]")) {
+            $data['code'] = (string) $tempXml[0];
+        }
+
+        if ($tempXml = $body->xpath("//{$prefix}Message[1]")) {
+            $data['message'] = (string) $tempXml[0];
+        }
+
+        $tempXml = $body->xpath("//{$prefix}RequestId[1]");
+        if (empty($tempXml)) {
+            $tempXml = $body->xpath("//{$prefix}RequestID[1]");
+        }
+        if (isset($tempXml[0])) {
+            $data['request_id'] = (string) $tempXml[0];
+        }
+    }
+}

+ 136 - 0
addons/cos/library/Qcloud/Cos/MultipartUpload.php

@@ -0,0 +1,136 @@
+<?php
+
+namespace Qcloud\Cos;
+
+use Qcloud\Cos\Exception\CosException;
+use GuzzleHttp\Pool;
+
+class MultipartUpload {
+    const MIN_PART_SIZE = 1048576;
+    const MAX_PART_SIZE = 5368709120;
+    const DEFAULT_PART_SIZE = 52428800;
+    const MAX_PARTS     = 10000;
+
+    private $client;
+    private $options;
+    private $partSize;
+    private $parts;
+    private $body;
+
+    public function __construct($client, $body, $options = array()) {
+        $minPartSize = $options['PartSize'];
+        unset($options['PartSize']);
+        $this->body = $body;
+        $this->client = $client;
+        $this->options = $options;
+        $this->partSize = $this->calculatePartSize($minPartSize);
+        $this->concurrency = isset($options['Concurrency']) ? $options['Concurrency'] : 10;
+        $this->parts = [];
+        $this->partNumberList = [];
+    }
+    public function performUploading() {
+        $uploadId= $this->initiateMultipartUpload();
+        $this->uploadParts($uploadId);
+        foreach ( $this->parts as $key => $row ){
+            $num1[$key] = $row ['PartNumber'];
+            $num2[$key] = $row ['ETag'];
+        }
+        array_multisort($num1, SORT_ASC, $num2, SORT_ASC, $this->parts);
+        return $this->client->completeMultipartUpload(array(
+            'Bucket' => $this->options['Bucket'],
+            'Key' => $this->options['Key'],
+            'UploadId' => $uploadId,
+            'Parts' => $this->parts)
+        );
+
+    }
+    public function uploadParts($uploadId) {
+        $uploadRequests = function ($uploadId) {
+            $partNumber = 1;
+            $index = 1;
+            for ( ; ; $partNumber ++) {
+                if ($this->body->eof()) {
+                    break;
+                }
+                $body = $this->body->read($this->partSize);
+                if (empty($body)) {
+                    break;
+                }
+                if (isset($this->parts[$partNumber])) {
+                    continue;
+                }
+                $this->partNumberList[$index] = $partNumber;
+                $params = array(
+                    'Bucket' => $this->options['Bucket'],
+                    'Key' => $this->options['Key'],
+                    'UploadId' => $uploadId,
+                    'PartNumber' => $partNumber,
+                    'Body' => $body
+                );
+                if(!isset($this->parts[$partNumber])) {
+                    $command = $this->client->getCommand('uploadPart', $params);
+                    $request = $this->client->commandToRequestTransformer($command);
+                    $index ++;
+                    yield $request;
+                }
+            }
+        };
+        $pool = new Pool($this->client->httpClient, $uploadRequests($uploadId), [
+            'concurrency' => $this->concurrency,
+            'fulfilled' => function ($response, $index) {
+                $index = $index + 1;
+                $partNumber = $this->partNumberList[$index];
+                $etag = $response->getHeaders()["ETag"][0];
+                $part = array('PartNumber' => $partNumber, 'ETag' => $etag);
+                $this->parts[$partNumber] = $part;
+            },
+
+            'rejected' => function ($reason, $index) {
+                throw($reason);
+            }
+        ]);
+        $promise = $pool->promise();
+        $promise->wait();
+    }
+
+    public function resumeUploading() {
+        $uploadId = $this->options['UploadId'];
+        $rt = $this->client->ListParts(
+            array('UploadId' => $uploadId,
+                'Bucket'=>$this->options['Bucket'],
+                'Key'=>$this->options['Key']));
+                $parts = array();
+        if (count($rt['Parts']) > 0) {
+            foreach ($rt['Parts'] as $part) {
+                $this->parts[$part['PartNumber']] = array('PartNumber' => $part['PartNumber'], 'ETag' => $part['ETag']);
+            }
+        }
+        $this->uploadParts($uploadId);
+        foreach ( $this->parts as $key => $row ){
+            $num1[$key] = $row ['PartNumber'];
+            $num2[$key] = $row ['ETag'];
+        }
+        array_multisort($num1, SORT_ASC, $num2, SORT_ASC, $this->parts);
+        return $this->client->completeMultipartUpload(array(
+            'Bucket' => $this->options['Bucket'],
+            'Key' => $this->options['Key'],
+            'UploadId' => $uploadId,
+            'Parts' => $this->parts)
+        );
+    }
+
+    private function calculatePartSize($minPartSize)
+    {
+        $partSize = intval(ceil(($this->body->getSize() / self::MAX_PARTS)));
+        $partSize = max($minPartSize, $partSize);
+        $partSize = min($partSize, self::MAX_PART_SIZE);
+        $partSize = max($partSize, self::MIN_PART_SIZE);
+        return $partSize;
+    }
+
+    private function initiateMultipartUpload() {
+        $result = $this->client->createMultipartUpload($this->options);
+        return $result['UploadId'];
+    }
+
+}

+ 49 - 0
addons/cos/library/Qcloud/Cos/Request/BodyLocation.php

@@ -0,0 +1,49 @@
+<?php
+
+namespace Qcloud\Cos\Request;
+
+use GuzzleHttp\Command\Guzzle\RequestLocation\AbstractLocation;
+use GuzzleHttp\Command\CommandInterface;
+use GuzzleHttp\Command\Guzzle\Parameter;
+use GuzzleHttp\Psr7;
+use Psr\Http\Message\MessageInterface;
+use Psr\Http\Message\RequestInterface;
+
+/**
+ * Adds a raw/binary body to a request.
+ * This is here because: https://github.com/guzzle/guzzle-services/issues/160
+ */
+class BodyLocation extends AbstractLocation
+{
+
+    /**
+     * Set the name of the location
+     *
+     * @param string $locationName
+     */
+    public function __construct($locationName = 'body')
+    {
+        parent::__construct($locationName);
+    }
+
+    /**
+     * @param CommandInterface $command
+     * @param RequestInterface $request
+     * @param Parameter        $param
+     *
+     * @return MessageInterface
+     */
+    public function visit(
+        CommandInterface $command,
+        RequestInterface $request,
+        Parameter $param
+    ) {
+        $value = $request->getBody()->getContents();
+        if ('' !== $value) {
+            throw new \RuntimeException('Only one "body" location may exist per operation');
+        }
+        // binary string data from bound parameter
+        $value = $command[$param->getName()];
+        return $request->withBody(Psr7\stream_for($value));
+    }
+}

+ 127 - 0
addons/cos/library/Qcloud/Cos/ResultTransformer.php

@@ -0,0 +1,127 @@
+<?php
+
+namespace Qcloud\Cos;
+
+use Guzzle\Service\Description\Parameter;
+use Guzzle\Service\Description\ServiceDescription;
+use GuzzleHttp\HandlerStack;
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
+use Qcloud\Cos\Signature;
+use GuzzleHttp\Command\Guzzle\Description;
+use GuzzleHttp\Command\Guzzle\GuzzleClient;
+use GuzzleHttp\Command\CommandInterface;
+use GuzzleHttp\Exception\RequestException;
+use GuzzleHttp\Middleware;
+use GuzzleHttp\Psr7;
+use GuzzleHttp\Psr7\Uri;
+use GuzzleHttp\Command\Result;
+use InvalidArgumentException;
+
+
+class ResultTransformer {
+    private $config;
+    private $operation;
+
+    public function __construct($config, $operation) {
+        $this->config = $config;
+        $this->operation = $operation;
+    }
+
+    public function writeDataToLocal(CommandInterface $command, RequestInterface $request, ResponseInterface $response) {
+        $action = $command->getName();
+        if ($action == "GetObject") {
+            if (isset($command['SaveAs'])) {
+                $fp = fopen($command['SaveAs'], "wb");
+                $stream = $response->getBody();
+                $offset = 0;
+                $partsize = 8192;
+                while (!$stream->eof()) {
+                    $output = $stream->read($partsize);
+                    fseek($fp, $offset);
+                    fwrite($fp, $output);
+                    $offset += $partsize;
+                }
+                fclose($fp);
+            }
+        }
+    }
+
+    public function metaDataTransformer(CommandInterface $command, ResponseInterface $response, Result $result) {
+        $headers = $response->getHeaders();
+        $metadata = array();
+        foreach ($headers as $key => $value) {
+            if (strpos($key, "x-cos-meta-") === 0) {
+                $metadata[substr($key, 11)] = $value[0];
+            }
+        }
+        if (!empty($metadata)) {
+            $result['Metadata'] = $metadata;
+        }
+        return $result;
+    }
+
+    public function extraHeadersTransformer(CommandInterface $command, RequestInterface $request, Result $result) {
+        if ($command['Key'] != null && $result['Key'] == null) {
+            $result['Key'] = $command['Key'];
+        }
+        if ($command['Bucket'] != null && $result['Bucket'] == null) {
+            $result['Bucket'] = $command['Bucket'];
+        }
+        $result['Location'] = $request->getHeader("Host")[0] .  $request->getUri()->getPath();
+        return $result;
+    }
+
+    public function selectContentTransformer(CommandInterface $command, Result $result) {
+        $action = $command->getName();
+        if ($action == "SelectObjectContent") {
+            $result['Data'] = $this->getSelectContents($result);
+        }
+        return $result;
+    }
+
+    public function getSelectContents($result) {
+        $f = $result['RawData'];
+        while (!$f->eof()) {
+            $data = array();
+            $tmp = $f->read(4);
+            if (empty($tmp)) {
+                break;
+            }
+            $totol_length = (int)(unpack("N", $tmp)[1]);
+            $headers_length = (int)(unpack("N", $f->read(4))[1]);
+            $body_length = $totol_length - $headers_length - 16;
+            $predule_crc = (int)(unpack("N", $f->read(4))[1]);
+            $headers = array();
+            for ($offset = 0; $offset < $headers_length;) {
+                $key_length = (int)(unpack("C", $f->read(1))[1]);
+                $key = $f->read($key_length);
+    
+                $head_value_type = (int)(unpack("C", $f->read(1))[1]);
+    
+                $value_length = (int)(unpack("n", $f->read(2))[1]);
+                $value = $f->read($value_length);
+                $offset += 4 + $key_length + $value_length;
+                if ($key == ":message-type") {
+                    $data['MessageType'] = $value;
+                }
+                if ($key == ":event-type") {
+                    $data['EventType'] = $value;
+                }
+                if ($key == ":error-code") {
+                    $data['ErrorCode'] = $value;
+                }
+                if ($key == ":error-message") {
+                    $data['ErrorMessage'] = $value;
+                }
+            }
+            $body = $f->read($body_length);
+            $message_crc = (int)(unpack("N", $f->read(4))[1]);
+            $data['Body'] = $body;
+            yield $data;
+        }
+    }
+    public function __destruct() {
+    }
+
+}

+ 79 - 0
addons/cos/library/Qcloud/Cos/Serializer.php

@@ -0,0 +1,79 @@
+<?php
+namespace Qcloud\Cos;
+use GuzzleHttp\Command\CommandInterface;
+use GuzzleHttp\Command\Guzzle\SchemaValidator;
+use GuzzleHttp\Command\Guzzle\DescriptionInterface;
+use GuzzleHttp\Command\Guzzle\Serializer as DefaultSerializer;
+use Psr\Http\Message\RequestInterface;
+/**
+ * Override Request serializer to modify authentication mechanism
+ */
+class Serializer extends DefaultSerializer
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function __construct(
+        DescriptionInterface $description,
+        array $requestLocations = []
+    ) {
+        // Override Guzzle's body location as it isn't raw binary data
+        $requestLocations['body'] = new Request\BodyLocation;
+        parent::__construct($description, $requestLocations);
+    }
+    /**
+     * Authorization header is Loco's preferred authorization method.
+     * Add Authorization header to request if API key is set, unless query is explicitly configured as auth method.
+     * Unset key from command to avoid sending it as a query param.
+     *
+     * @override
+     *
+     * @param CommandInterface $command
+     * @param RequestInterface $request
+     *
+     * @return RequestInterface
+     *
+     * @throws \InvalidArgumentException
+     */
+    protected function prepareRequest(
+        CommandInterface $command,
+        RequestInterface $request
+    ) {
+		/*
+        if ($command->offsetExists('key') === true) {
+            $mode = empty($command->offsetGet('auth')) === false
+                    ? $command->offsetGet('auth')
+                    : 'loco';
+            if ($mode !== 'query') {
+                // else use Authorization header of various types
+                if ($mode === 'loco') {
+                    $value = 'Loco '.$command->offsetGet('key');
+                    $request = $request->withHeader('Authorization', $value);
+                } elseif ($mode === 'basic') {
+                    $value = 'Basic '.base64_encode($command->offsetGet('key').':');
+                    $request = $request->withHeader('Authorization', $value);
+                } else {
+                    throw new \InvalidArgumentException("Invalid auth type: {$mode}");
+                }
+                // avoid request sending key parameter in query string
+                $command->offsetUnset('key');
+            }
+        }
+        // Remap legacy parameters to common `data` binding on request body
+        static $remap = [
+            'import' => ['src'=>'data'],
+            'translate' => ['translation'=>'data'],
+        ];
+        $name = $command->getName();
+        if (isset($remap[$name])) {
+            foreach ($remap[$name] as $old => $new) {
+                if ($command->offsetExists($old)) {
+                    $command->offsetSet($new, $command->offsetGet($old));
+                    $command->offsetUnset($old);
+                }
+            }
+        }
+		*/
+        return parent::prepareRequest($command, $request);
+    }
+}

+ 4843 - 0
addons/cos/library/Qcloud/Cos/Service.php

@@ -0,0 +1,4843 @@
+<?php
+namespace Qcloud\Cos;
+// http://guzzle3.readthedocs.io/webservice-client/guzzle-service-descriptions.html
+class Service {
+    public static function getService() {
+        return array(
+            'name' => 'Cos Service',
+            'apiVersion' => 'V5',
+            'description' => 'Cos V5 API Service',
+            'operations' => array(
+                // 舍弃一个分块上传且删除已上传的分片块的方法.
+                'AbortMultipartUpload' => array(
+                    'httpMethod' => 'DELETE',
+                    'uri' => '/{Bucket}{/Key*}',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'AbortMultipartUploadOutput',
+                    'responseType' => 'model',
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri'),
+                        'Key' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                            'minLength' => 1,
+                            'filters' => array(
+                                'Qcloud\\Cos\\Client::explodeKey')),
+                        'UploadId' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'query',
+                            'sentAs' => 'uploadId'
+                        )
+                    )
+                ),
+                // 创建存储桶(Bucket)的方法.
+                'CreateBucket' => array(
+                    'httpMethod' => 'PUT',
+                    'uri' => '/{Bucket}',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'CreateBucketOutput',
+                    'responseType' => 'model',
+                    'data' => array(
+                        'xmlRoot' => array(
+                            'name' => 'CreateBucketConfiguration')),
+                    'parameters' => array(
+                        'ACL' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-acl'),
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri'
+                        )
+                    )
+                ),
+                // 完成整个分块上传的方法.
+                'CompleteMultipartUpload' => array(
+                    'httpMethod' => 'POST',
+                    'uri' => '/{Bucket}{/Key*}',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'CompleteMultipartUploadOutput',
+                    'responseType' => 'model',
+                    'data' => array(
+                        'xmlRoot' => array(
+                            'name' => 'CompleteMultipartUpload'
+                        )
+                    ),
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri'),
+                        'Key' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                            'minLength' => 1,
+                            'filters' => array(
+                                'Qcloud\\Cos\\Client::explodeKey'
+                            )
+                        ),
+                        'Parts' => array(
+                            'type' => 'array',
+                            'location' => 'xml',
+                            'data' => array(
+                                'xmlFlattened' => true),
+                            'items' => array(
+                                'name' => 'CompletedPart',
+                                'type' => 'object',
+                                'sentAs' => 'Part',
+                                'properties' => array(
+                                    'ETag' => array(
+                                        'type' => 'string'
+                                    ),
+                                    'PartNumber' => array(
+                                        'type' => 'numeric'
+                                    )
+                                )
+                            )
+                        ),
+                        'UploadId' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'query',
+                            'sentAs' => 'uploadId',
+                        ),
+                        'PicOperations' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'Pic-Operations',
+                        )
+                    )
+                ),
+                // 初始化分块上传的方法.
+                'CreateMultipartUpload' => array(
+                    'httpMethod' => 'POST',
+                    'uri' => '/{Bucket}{/Key*}?uploads',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'CreateMultipartUploadOutput',
+                    'responseType' => 'model',
+                    'data' => array(
+                        'xmlRoot' => array(
+                            'name' => 'CreateMultipartUploadRequest'
+                        )
+                    ),
+                    'parameters' => array(
+                        'ACL' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-acl',
+                        ),
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        ),
+                        'CacheControl' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'Cache-Control',
+                        ),
+                        'ContentDisposition' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'Content-Disposition',
+                        ),
+                        'ContentEncoding' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'Content-Encoding',
+                        ),
+                        'ContentLanguage' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'Content-Language',
+                        ),
+                        'ContentType' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'Content-Type',
+                        ),
+                        'Expires' => array(
+                            'type' => array(
+                                'object',
+                                'string',
+                                'integer',
+                            ),
+                            'format' => 'date-time-http',
+                            'location' => 'header',
+                        ),
+                        'GrantFullControl' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-grant-full-control',
+                        ),
+                        'GrantRead' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-grant-read',
+                        ),
+                        'GrantReadACP' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-grant-read-acp',
+                        ),
+                        'GrantWriteACP' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-grant-write-acp',
+                        ),
+                        'Key' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                            'minLength' => 1,
+                            'filters' => array(
+                                'Qcloud\\Cos\\Client::explodeKey'
+                            )
+                        ),
+                        'ServerSideEncryption' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption',
+                        ),
+                        'StorageClass' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-storage-class',
+                        ),
+                        'WebsiteRedirectLocation' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-website-redirect-location',
+                        ),
+                        'SSECustomerAlgorithm' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-customer-algorithm',
+                        ),
+                        'SSECustomerKey' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-customer-key',
+                        ),
+                        'SSECustomerKeyMD5' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-customer-key-MD5',
+                        ),
+                        'SSEKMSKeyId' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-aws-kms-key-id',
+                        ),
+                        'RequestPayer' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-payer',
+                        ),
+                        'ACP' => array(
+                            'type' => 'object',
+                            'additionalProperties' => true,
+                        ),
+                        'PicOperations' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'Pic-Operations',
+                        )
+                    )
+                ),
+                // 复制对象的方法.
+                'CopyObject' => array(
+                    'httpMethod' => 'PUT',
+                    'uri' => '/{Bucket}{/Key*}',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'CopyObjectOutput',
+                    'responseType' => 'model',
+                    'data' => array(
+                        'xmlRoot' => array(
+                            'name' => 'CopyObjectRequest',
+                        ),
+                    ),
+                    'parameters' => array(
+                        'ACL' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-acl',
+                        ),
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        ),
+                        'CacheControl' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'Cache-Control',
+                        ),
+                        'ContentDisposition' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'Content-Disposition',
+                        ),
+                        'ContentEncoding' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'Content-Encoding',
+                        ),
+                        'ContentLanguage' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'Content-Language',
+                        ),
+                        'ContentType' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'Content-Type',
+                        ),
+                        'CopySource' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-copy-source',
+                        ),
+                        'CopySourceIfMatch' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-copy-source-if-match',
+                        ),
+                        'CopySourceIfModifiedSince' => array(
+                            'type' => array(
+                                'object',
+                                'string',
+                                'integer',
+                            ),
+                            'format' => 'date-time-http',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-copy-source-if-modified-since',
+                        ),
+                        'CopySourceIfNoneMatch' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-copy-source-if-none-match',
+                        ),
+                        'CopySourceIfUnmodifiedSince' => array(
+                            'type' => array(
+                                'object',
+                                'string',
+                                'integer',
+                            ),
+                            'format' => 'date-time-http',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-copy-source-if-unmodified-since',
+                        ),
+                        'Expires' => array(
+                            'type' => array(
+                                'object',
+                                'string',
+                                'integer',
+                            ),
+                            'format' => 'date-time-http',
+                            'location' => 'header',
+                        ),
+                        'GrantFullControl' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-grant-full-control',
+                        ),
+                        'GrantRead' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-grant-read',
+                        ),
+                        'GrantReadACP' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-grant-read-acp',
+                        ),
+                        'GrantWriteACP' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-grant-write-acp',
+                        ),
+                        'Key' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                            'minLength' => 1,
+                            'filters' => array(
+                                'Qcloud\\Cos\\Client::explodeKey')
+                        ),
+                        'MetadataDirective' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-metadata-directive',
+                        ),
+                        'ServerSideEncryption' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption',
+                        ),
+                        'StorageClass' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-storage-class',
+                        ),
+                        'WebsiteRedirectLocation' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-website-redirect-location',
+                        ),
+                        'SSECustomerAlgorithm' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-customer-algorithm',
+                        ),
+                        'SSECustomerKey' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-customer-key',
+                        ),
+                        'CopySourceSSECustomerAlgorithm' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-copy-source-server-side-encryption-customer-algorithm',
+                        ),
+                        'CopySourceSSECustomerKey' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-copy-source-server-side-encryption-customer-key',
+                        ),
+                        'CopySourceSSECustomerKeyMD5' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-copy-source-server-side-encryption-customer-key-MD5',
+                        ),
+                        'RequestPayer' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-payer',
+                        ),
+                        'ACP' => array(
+                            'type' => 'object',
+                            'additionalProperties' => true,
+                        )
+                    ),
+                ),
+                // 删除存储桶 (Bucket)的方法.
+                'DeleteBucket' => array(
+                    'httpMethod' => 'DELETE',
+                    'uri' => '/{Bucket}',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'DeleteBucketOutput',
+                    'responseType' => 'model',
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri'
+                        )
+                    )
+                ),
+                // 删除跨域访问配置信息的方法
+                'DeleteBucketCors' => array(
+                    'httpMethod' => 'DELETE',
+                    'uri' => '/{Bucket}?cors',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'DeleteBucketCorsOutput',
+                    'responseType' => 'model',
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        ),
+                    ),
+                ),
+                // 删除存储桶标签信息的方法
+                'DeleteBucketTagging' => array(
+                    'httpMethod' => 'DELETE',
+                    'uri' => '/{Bucket}?tagging',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'DeleteBucketTaggingOutput',
+                    'responseType' => 'model',
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        ),
+                    ),
+                ),
+                // 删除存储桶标清单任务的方法
+                'DeleteBucketInventory' => array(
+                    'httpMethod' => 'Delete',
+                    'uri' => '/{Bucket}?inventory',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'DeleteBucketInventoryOutput',
+                    'responseType' => 'model',
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        ),
+                        'Id' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'query',
+                            'sentAs' => 'id',
+                        )
+                    ),
+                ),
+                // 删除 COS 上单个对象的方法.
+                'DeleteObject' => array(
+                    'httpMethod' => 'DELETE',
+                    'uri' => '/{Bucket}{/Key*}',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'DeleteObjectOutput',
+                    'responseType' => 'model',
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri'
+                        ),
+                        'Key' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                            'minLength' => 1,
+                            'filters' => array(
+                                'Qcloud\\Cos\\Client::explodeKey'
+                            )
+                        ),
+                        'MFA' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-mfa',
+                        ),
+                        'VersionId' => array(
+                            'type' => 'string',
+                            'location' => 'query',
+                            'sentAs' => 'versionId',
+                        ),
+                        'RequestPayer' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-payer',
+                        )
+                    )
+                ),
+                // 批量删除 COS 对象的方法.
+                'DeleteObjects' => array(
+                    'httpMethod' => 'POST',
+                    'uri' => '/{Bucket}?delete',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'DeleteObjectsOutput',
+                    'responseType' => 'model',
+                    'data' => array(
+                        'xmlRoot' => array(
+                            'name' => 'Delete',
+                        ),
+                        'contentMd5' => true,
+                    ),
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        ),
+                        'Objects' => array(
+                            'required' => true,
+                            'type' => 'array',
+                            'location' => 'xml',
+                            'data' => array(
+                                'xmlFlattened' => true,
+                            ),
+                            'items' => array(
+                                'type' => 'object',
+                                'sentAs' => 'Object',
+                                'properties' => array(
+                                    'Key' => array(
+                                        'required' => true,
+                                        'type' => 'string',
+                                        'minLength' => 1,
+                                    ),
+                                    'VersionId' => array(
+                                        'type' => 'string',
+                                    ),
+                                ),
+                            ),
+                        ),
+                        'Quiet' => array(
+                            'type' => 'boolean',
+                            'format' => 'boolean-string',
+                            'location' => 'xml',
+                        ),
+                        'MFA' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-mfa',
+                        ),
+                        'RequestPayer' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-payer',
+                        )
+                    ),
+                ),
+                // 删除存储桶(Bucket) 的website的方法.
+                'DeleteBucketWebsite' => array(
+                    'httpMethod' => 'DELETE',
+                    'uri' => '/{Bucket}?website',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'DeleteBucketWebsiteOutput',
+                    'responseType' => 'model',
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        ),
+                    ),
+                ),
+                // 删除存储桶(Bucket) 的生命周期配置的方法.
+                'DeleteBucketLifecycle' => array(
+                    'httpMethod' => 'DELETE',
+                    'uri' => '/{Bucket}?lifecycle',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'DeleteBucketLifecycleOutput',
+                    'responseType' => 'model',
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        ),
+                    ),
+                ),
+                // 删除跨区域复制配置的方法.
+                'DeleteBucketReplication' => array(
+                    'httpMethod' => 'DELETE',
+                    'uri' => '/{Bucket}?replication',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'DeleteBucketReplicationOutput',
+                    'responseType' => 'model',
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        ),
+                    ),
+                ),
+                // 下载对象的方法.
+                'GetObject' => array(
+                    'httpMethod' => 'GET',
+                    'uri' => '/{Bucket}{/Key*}',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'GetObjectOutput',
+                    'responseType' => 'model',
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri'
+                        ),
+                        'IfMatch' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'If-Match'
+                        ),
+                        'IfModifiedSince' => array(
+                            'type' => array(
+                                'object',
+                                'string',
+                                'integer'
+                            ),
+                            'format' => 'date-time-http',
+                            'location' => 'header',
+                            'sentAs' => 'If-Modified-Since'
+                        ),
+                        'IfNoneMatch' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'If-None-Match'
+                        ),
+                        'IfUnmodifiedSince' => array(
+                            'type' => array(
+                                'object',
+                                'string',
+                                'integer'
+                            ),
+                            'format' => 'date-time-http',
+                            'location' => 'header',
+                            'sentAs' => 'If-Unmodified-Since'
+                        ),
+                        'Key' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                            'minLength' => 1,
+                            'filters' => array(
+                                'Qcloud\\Cos\\Client::explodeKey'
+                            )
+                        ),
+                        'Range' => array(
+                            'type' => 'string',
+                            'location' => 'header'),
+                        'ResponseCacheControl' => array(
+                            'type' => 'string',
+                            'location' => 'query',
+                            'sentAs' => 'response-cache-control'
+                        ),
+                        'ResponseContentDisposition' => array(
+                            'type' => 'string',
+                            'location' => 'query',
+                            'sentAs' => 'response-content-disposition'
+                        ),
+                        'ResponseContentEncoding' => array(
+                            'type' => 'string',
+                            'location' => 'query',
+                            'sentAs' => 'response-content-encoding'
+                        ),
+                        'ResponseContentLanguage' => array(
+                            'type' => 'string',
+                            'location' => 'query',
+                            'sentAs' => 'response-content-language'
+                        ),
+                        'ResponseContentType' => array(
+                            'type' => 'string',
+                            'location' => 'query',
+                            'sentAs' => 'response-content-type'
+                        ),
+                        'ResponseExpires' => array(
+                            'type' => array(
+                                'object',
+                                'string',
+                                'integer'
+                            ),
+                            'format' => 'date-time-http',
+                            'location' => 'query',
+                            'sentAs' => 'response-expires'
+                        ),
+                        'VersionId' => array(
+                            'type' => 'string',
+                            'location' => 'query',
+                            'sentAs' => 'versionId',
+                        ),
+                        'SSECustomerAlgorithm' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-customer-algorithm',
+                        ),
+                        'SSECustomerKey' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-customer-key',
+                        ),
+                        'SSECustomerKeyMD5' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-customer-key-MD5',
+                        ),
+                        'TrafficLimit' => array(
+                            'type' => 'integer',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-traffic-limit',
+                        )
+                    )
+                ),
+                // 获取 COS 对象的访问权限信息(Access Control List, ACL)的方法.
+                'GetObjectAcl' => array(
+                    'httpMethod' => 'GET',
+                    'uri' => '/{Bucket}{/Key*}?acl',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'GetObjectAclOutput',
+                    'responseType' => 'model',
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        ),
+                        'Key' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                            'minLength' => 1,
+                            'filters' => array(
+                                'Qcloud\\Cos\\Client::explodeKey')
+                        ),
+                        'VersionId' => array(
+                            'type' => 'string',
+                            'location' => 'query',
+                            'sentAs' => 'versionId',
+                        ),
+                        'RequestPayer' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-payer',
+                        )
+                    )
+                ),
+                // 获取存储桶(Bucket) 的访问权限信息(Access Control List, ACL)的方法.
+                'GetBucketAcl' => array(
+                    'httpMethod' => 'GET',
+                    'uri' => '/{Bucket}?acl',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'GetBucketAclOutput',
+                    'responseType' => 'model',
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri'
+                        )
+                    )
+                ),
+                // 查询存储桶(Bucket) 跨域访问配置信息的方法.
+                'GetBucketCors' => array(
+                    'httpMethod' => 'GET',
+                    'uri' => '/{Bucket}?cors',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'GetBucketCorsOutput',
+                    'responseType' => 'model',
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        )
+                    ),
+                ),
+                // 查询存储桶(Bucket) Domain配置信息的方法.
+                'GetBucketDomain' => array(
+                    'httpMethod' => 'GET',
+                    'uri' => '/{Bucket}?domain',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'GetBucketDomainOutput',
+                    'responseType' => 'model',
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        )
+                    ),
+                ),
+                // 查询存储桶(Bucket) Accelerate配置信息的方法.
+                'GetBucketAccelerate' => array(
+                    'httpMethod' => 'GET',
+                    'uri' => '/{Bucket}?accelerate',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'GetBucketAccelerateOutput',
+                    'responseType' => 'model',
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        )
+                    ),
+                ),
+                // 查询存储桶(Bucket) Website配置信息的方法.
+                'GetBucketWebsite' => array(
+                    'httpMethod' => 'GET',
+                    'uri' => '/{Bucket}?website',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'GetBucketWebsiteOutput',
+                    'responseType' => 'model',
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        )
+                    ),
+                ),
+                // 查询存储桶(Bucket) 的生命周期配置的方法.
+                'GetBucketLifecycle' => array(
+                    'httpMethod' => 'GET',
+                    'uri' => '/{Bucket}?lifecycle',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'GetBucketLifecycleOutput',
+                    'responseType' => 'model',
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        )
+                    ),
+                ),
+                // 获取存储桶(Bucket)版本控制信息的方法.
+                'GetBucketVersioning' => array(
+                    'httpMethod' => 'GET',
+                    'uri' => '/{Bucket}?versioning',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'GetBucketVersioningOutput',
+                    'responseType' => 'model',
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        )
+                    ),
+                ),
+                // 获取存储桶(Bucket) 跨区域复制配置信息的方法.
+                'GetBucketReplication' => array(
+                    'httpMethod' => 'GET',
+                    'uri' => '/{Bucket}?replication',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'GetBucketReplicationOutput',
+                    'responseType' => 'model',
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        )
+                    ),
+                ),
+                // 获取存储桶(Bucket) 所在的地域信息的方法.
+                'GetBucketLocation' => array(
+                    'httpMethod' => 'GET',
+                    'uri' => '/{Bucket}?location',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'GetBucketLocationOutput',
+                    'responseType' => 'model',
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        ),
+                    ),
+                ),
+                // 获取存储桶(Bucket) Notification信息的方法.
+                'GetBucketNotification' => array(
+                    'httpMethod' => 'GET',
+                    'uri' => '/{Bucket}?notification',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'GetBucketNotificationOutput',
+                    'responseType' => 'model',
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        )
+                    ),
+                ),
+                // 获取存储桶(Bucket) 日志信息的方法.
+                'GetBucketLogging' => array(
+                    'httpMethod' => 'GET',
+                    'uri' => '/{Bucket}?logging',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'GetBucketLoggingOutput',
+                    'responseType' => 'model',
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        )
+                    ),
+                ),
+                // 获取存储桶(Bucket) 清单信息的方法.
+                'GetBucketInventory' => array(
+                    'httpMethod' => 'GET',
+                    'uri' => '/{Bucket}?inventory',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'GetBucketInventoryOutput',
+                    'responseType' => 'model',
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        ),
+                        'Id' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'query',
+                            'sentAs' => 'id',
+                        )
+                    ),
+                ),
+                // 获取存储桶(Bucket) 标签信息的方法.
+                'GetBucketTagging' => array(
+                    'httpMethod' => 'GET',
+                    'uri' => '/{Bucket}?tagging',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'GetBucketTaggingOutput',
+                    'responseType' => 'model',
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        )
+                    ),
+                ),
+                // 分块上传的方法.
+                'UploadPart' => array(
+                    'httpMethod' => 'PUT',
+                    'uri' => '/{Bucket}{/Key*}',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'UploadPartOutput',
+                    'responseType' => 'model',
+                    'data' => array(
+                        'xmlRoot' => array(
+                            'name' => 'UploadPartRequest'
+                        )
+                    ),
+                    'parameters' => array(
+                        'Body' => array(
+                            'type' => array(
+                                'any'),
+                            'location' => 'body'
+                        ),
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri'
+                        ),
+                        'ContentLength' => array(
+                            'type' => 'numeric',
+                            'minimum'=> 0,
+                            'location' => 'header',
+                            'sentAs' => 'Content-Length'
+                        ),
+                        'ContentMD5' => array(
+                            'type' => array(
+                                'string',
+                                'boolean'
+                            ),
+                            'location' => 'header',
+                            'sentAs' => 'Content-MD5'
+                        ),
+                        'Key' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                            'minLength' => 1,
+                            'filters' => array(
+                                'Qcloud\\Cos\\Client::explodeKey'
+                            )
+                        ),
+                        'PartNumber' => array(
+                            'required' => true,
+                            'type' => 'numeric',
+                            'location' => 'query',
+                            'sentAs' => 'partNumber'),
+                        'UploadId' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'query',
+                            'sentAs' => 'uploadId'),
+                        'ServerSideEncryption' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption',
+                        ),
+                        'SSECustomerAlgorithm' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-customer-algorithm',
+                        ),
+                        'SSECustomerKey' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-customer-key',
+                        ),
+                        'SSECustomerKeyMD5' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-customer-key-MD5',
+                        ),
+                        'RequestPayer' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-payer',
+                        ),
+                        'TrafficLimit' => array(
+                            'type' => 'integer',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-traffic-limit',
+                        )
+                    )
+                ),
+                // 上传对象的方法.
+                'PutObject' => array(
+                    'httpMethod' => 'PUT',
+                    'uri' => '/{Bucket}{/Key*}',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'PutObjectOutput',
+                    'responseType' => 'model',
+                    'data' => array(
+                        'xmlRoot' => array(
+                            'name' => 'PutObjectRequest'
+                        )
+                    ),
+                    'parameters' => array(
+                        'ACL' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-acl'
+                        ),
+                        'Body' => array(
+                            'required' => true,
+                            'type' => array(
+                                'any'
+                            ),
+                            'location' => 'body'
+                        ),
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri'
+                        ),
+                        'CacheControl' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'Cache-Control'
+                        ),
+                        'ContentDisposition' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'Content-Disposition'
+                        ),
+                        'ContentEncoding' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'Content-Encoding'
+                        ),
+                        'ContentLanguage' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'Content-Language'
+                        ),
+                        'ContentLength' => array(
+                            'type' => 'numeric',
+                            'minimum'=> 0,
+                            'location' => 'header',
+                            'sentAs' => 'Content-Length'
+                        ),
+                        'ContentMD5' => array(
+                            'type' => array(
+                                'string',
+                                'boolean'
+                            ),
+                            'location' => 'header',
+                            'sentAs' => 'Content-MD5'
+                        ),
+                        'ContentType' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'Content-Type'
+                        ),
+                        'Key' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                            'minLength' => 1,
+                            'filters' => array(
+                                'Qcloud\\Cos\\Client::explodeKey'
+                            )
+                        ),
+                        'ServerSideEncryption' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption',
+                        ),
+                        'StorageClass' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-storage-class',
+                        ),
+                        'WebsiteRedirectLocation' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-website-redirect-location',
+                        ),
+                        'SSECustomerAlgorithm' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-customer-algorithm',
+                        ),
+                        'SSECustomerKey' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-customer-key',
+                        ),
+                        'SSECustomerKeyMD5' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-customer-key-MD5',
+                        ),
+                        'SSEKMSKeyId' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-cos-kms-key-id',
+                        ),
+                        'RequestPayer' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-payer',
+                        ),
+                        'ACP' => array(
+                            'type' => 'object',
+                            'additionalProperties' => true,
+                        ),
+                        'PicOperations' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'Pic-Operations',
+                        ),
+                        'TrafficLimit' => array(
+                            'type' => 'integer',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-traffic-limit',
+                        )
+                    )
+                ),
+                // 设置 COS 对象的访问权限信息(Access Control List, ACL)的方法.
+                'PutObjectAcl' => array(
+                    'httpMethod' => 'PUT',
+                    'uri' => '/{Bucket}{/Key*}?acl',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'PutObjectAclOutput',
+                    'responseType' => 'model',
+                    'data' => array(
+                        'xmlRoot' => array(
+                            'name' => 'AccessControlPolicy',
+                        ),
+                    ),
+                    'parameters' => array(
+                        'ACL' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-acl',
+                        ),
+                        'Grants' => array(
+                            'type' => 'array',
+                            'location' => 'xml',
+                            'sentAs' => 'AccessControlList',
+                            'items' => array(
+                                'name' => 'Grant',
+                                'type' => 'object',
+                                'properties' => array(
+                                    'Grantee' => array(
+                                        'type' => 'object',
+                                        'properties' => array(
+                                            'DisplayName' => array(
+                                                'type' => 'string'),
+                                            'ID' => array(
+                                                'type' => 'string'),
+                                            'Type' => array(
+                                                'type' => 'string',
+                                                'sentAs' => 'xsi:type',
+                                                'data' => array(
+                                                    'xmlAttribute' => true,
+                                                    'xmlNamespace' => 'http://www.w3.org/2001/XMLSchema-instance')),
+                                            'URI' => array(
+                                                'type' => 'string') )),
+                                    'Permission' => array(
+                                        'type' => 'string',
+                                    ),
+                                ),
+                            ),
+                        ),
+                        'Owner' => array(
+                            'type' => 'object',
+                            'location' => 'xml',
+                            'properties' => array(
+                                'DisplayName' => array(
+                                    'type' => 'string',
+                                ),
+                                'ID' => array(
+                                    'type' => 'string',
+                                ),
+                            ),
+                        ),
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        ),
+                        'GrantFullControl' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-grant-full-control',
+                        ),
+                        'GrantRead' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-grant-read',
+                        ),
+                        'GrantReadACP' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-grant-read-acp',
+                        ),
+                        'GrantWrite' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-grant-write',
+                        ),
+                        'GrantWriteACP' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-grant-write-acp',
+                        ),
+                        'Key' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                            'minLength' => 1,
+                            'filters' => array(
+                                'Qcloud\\Cos\\Client::explodeKey')
+                        ),
+                        'RequestPayer' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-payer',
+                        ),
+                        'ACP' => array(
+                            'type' => 'object',
+                            'additionalProperties' => true,
+                        ),
+                    )
+                ),
+                // 设置存储桶(Bucket) 的访问权限(Access Control List, ACL)的方法.
+                'PutBucketAcl' => array(
+                    'httpMethod' => 'PUT',
+                    'uri' => '/{Bucket}?acl',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'PutBucketAclOutput',
+                    'responseType' => 'model',
+                    'data' => array(
+                        'xmlRoot' => array(
+                            'name' => 'AccessControlPolicy',
+                        ),
+                    ),
+                    'parameters' => array(
+                        'ACL' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-acl',
+                        ),
+                        'Grants' => array(
+                            'type' => 'array',
+                            'location' => 'xml',
+                            'sentAs' => 'AccessControlList',
+                            'items' => array(
+                                'name' => 'Grant',
+                                'type' => 'object',
+                                'properties' => array(
+                                    'Grantee' => array(
+                                        'type' => 'object',
+                                        'properties' => array(
+                                            'DisplayName' => array(
+                                                'type' => 'string',
+                                            ),
+                                            'EmailAddress' => array(
+                                                'type' => 'string',
+                                            ),
+                                            'ID' => array(
+                                                'type' => 'string',
+                                            ),
+                                            'Type' => array(
+                                                'required' => true,
+                                                'type' => 'string',
+                                                'sentAs' => 'xsi:type',
+                                                'data' => array(
+                                                    'xmlAttribute' => true,
+                                                    'xmlNamespace' => 'http://www.w3.org/2001/XMLSchema-instance',
+                                                ),
+                                            ),
+                                            'URI' => array(
+                                                'type' => 'string',
+                                            ),
+                                        ),
+                                    ),
+                                    'Permission' => array(
+                                        'type' => 'string',
+                                    ),
+                                ),
+                            ),
+                        ),
+                        'Owner' => array(
+                            'type' => 'object',
+                            'location' => 'xml',
+                            'properties' => array(
+                                'DisplayName' => array(
+                                    'type' => 'string',
+                                ),
+                                'ID' => array(
+                                    'type' => 'string',
+                                ),
+                            ),
+                        ),
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        ),
+                        'GrantFullControl' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-grant-full-control',
+                        ),
+                        'GrantRead' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-grant-read',
+                        ),
+                        'GrantReadACP' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-grant-read-acp',
+                        ),
+                        'GrantWrite' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-grant-write',
+                        ),
+                        'GrantWriteACP' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-grant-write-acp',
+                        ),
+                        'ACP' => array(
+                            'type' => 'object',
+                            'additionalProperties' => true,
+                        ),
+                    ),
+                ),
+                // 设置存储桶(Bucket) 的跨域配置信息的方法.
+                'PutBucketCors' => array(
+                    'httpMethod' => 'PUT',
+                    'uri' => '/{Bucket}?cors',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'PutBucketCorsOutput',
+                    'responseType' => 'model',
+                    'data' => array(
+                        'xmlRoot' => array(
+                            'name' => 'CORSConfiguration',
+                        ),
+                        'contentMd5' => true,
+                    ),
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        ),
+                        'CORSRules' => array(
+                            'required' => true,
+                            'type' => 'array',
+                            'location' => 'xml',
+                            'data' => array(
+                                'xmlFlattened' => true,
+                            ),
+                            'items' => array(
+                                'name' => 'CORSRule',
+                                'type' => 'object',
+                                'sentAs' => 'CORSRule',
+                                'properties' => array(
+                                    'ID' => array(
+                                        'type' => 'string',
+                                    ),
+                                    'AllowedHeaders' => array(
+                                        'type' => 'array',
+                                        'data' => array(
+                                            'xmlFlattened' => true,
+                                        ),
+                                        'items' => array(
+                                            'name' => 'AllowedHeader',
+                                            'type' => 'string',
+                                            'sentAs' => 'AllowedHeader',
+                                        ),
+                                    ),
+                                    'AllowedMethods' => array(
+                                        'required' => true,
+                                        'type' => 'array',
+                                        'data' => array(
+                                            'xmlFlattened' => true,
+                                        ),
+                                        'items' => array(
+                                            'name' => 'AllowedMethod',
+                                            'type' => 'string',
+                                            'sentAs' => 'AllowedMethod',
+                                        ),
+                                    ),
+                                    'AllowedOrigins' => array(
+                                        'required' => true,
+                                        'type' => 'array',
+                                        'data' => array(
+                                            'xmlFlattened' => true,
+                                        ),
+                                        'items' => array(
+                                            'name' => 'AllowedOrigin',
+                                            'type' => 'string',
+                                            'sentAs' => 'AllowedOrigin',
+                                        ),
+                                    ),
+                                    'ExposeHeaders' => array(
+                                        'type' => 'array',
+                                        'data' => array(
+                                            'xmlFlattened' => true,
+                                        ),
+                                        'items' => array(
+                                            'name' => 'ExposeHeader',
+                                            'type' => 'string',
+                                            'sentAs' => 'ExposeHeader',
+                                        ),
+                                    ),
+                                    'MaxAgeSeconds' => array(
+                                        'type' => 'numeric',
+                                    ),
+                                ),
+                            ),
+                        ),
+                    ),
+                ),
+                // 设置存储桶(Bucket) 的Domain信息的方法.
+                'PutBucketDomain' => array(
+                    'httpMethod' => 'PUT',
+                    'uri' => '/{Bucket}?domain',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'PutBucketDomainOutput',
+                    'responseType' => 'model',
+                    'data' => array(
+                        'xmlRoot' => array(
+                            'name' => 'DomainConfiguration',
+                        ),
+                        'contentMd5' => true,
+                    ),
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        ),
+                        'DomainRules' => array(
+                            'required' => true,
+                            'type' => 'array',
+                            'location' => 'xml',
+                            'data' => array(
+                                'xmlFlattened' => true,
+                            ),
+                            'items' => array(
+                                'name' => 'DomainRule',
+                                'type' => 'object',
+                                'sentAs' => 'DomainRule',
+                                'properties' => array(
+                                    'Status' => array(
+                                        'required' => true,
+                                        'type' => 'string',
+                                    ),
+                                    'Name' => array(
+                                        'required' => true,
+                                        'type' => 'string',
+                                    ),
+                                    'Type' => array(
+                                        'required' => true,
+                                        'type' => 'string',
+                                    ),
+                                    'ForcedReplacement' => array(
+                                        'type' => 'string',
+                                    ),
+                                ),
+                            ),
+                        ),
+                    ),
+                ),
+                // 设置存储桶(Bucket) 生命周期配置的方法.
+                'PutBucketLifecycle' => array(
+                    'httpMethod' => 'PUT',
+                    'uri' => '/{Bucket}?lifecycle',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'PutBucketLifecycleOutput',
+                    'responseType' => 'model',
+                    'data' => array(
+                        'xmlRoot' => array(
+                            'name' => 'LifecycleConfiguration',
+                        ),
+                        'contentMd5' => true,
+                    ),
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        ),
+                        'Rules' => array(
+                            'required' => true,
+                            'type' => 'array',
+                            'location' => 'xml',
+                            'data' => array(
+                                'xmlFlattened' => true,
+                            ),
+                            'items' => array(
+                                'name' => 'Rule',
+                                'type' => 'object',
+                                'sentAs' => 'Rule',
+                                'properties' => array(
+                                    'Expiration' => array(
+                                        'type' => 'object',
+                                        'properties' => array(
+                                            'Date' => array(
+                                                'type' => array(
+                                                    'object',
+                                                    'string',
+                                                    'integer',
+                                                ),
+                                                'format' => 'date-time',
+                                            ),
+                                            'Days' => array(
+                                                'type' => 'numeric',
+                                            ),
+                                        ),
+                                    ),
+                                    'ID' => array(
+                                        'type' => 'string',
+                                    ),
+                                    'Filter' => array(
+                                        'type' => 'object',
+                                        'require' => true,
+                                        'properties' => array(
+                                            'Prefix' => array(
+                                                'type' => 'string',
+                                                'require' => true,
+                                            ),
+                                            'Tag' => array(
+                                                'type' => 'object',
+                                                'require' => true,
+                                                'properties' => array(
+                                                    'Key' => array(
+                                                        'type' => 'string'
+                                                    ),
+                                                    'filters' => array(
+                                                        'Qcloud\\Cos\\Client::explodeKey'),
+                                                    'Value' => array(
+                                                        'type' => 'string'
+                                                    ),
+                                                )
+                                            )
+                                        ),
+                                    ),
+                                    'Status' => array(
+                                        'required' => true,
+                                        'type' => 'string',
+                                    ),
+                                    'Transitions' => array(
+                                        'type' => 'array',
+                                        'location' => 'xml',
+                                        'data' => array(
+                                            'xmlFlattened' => true,
+                                        ),
+                                        'items' => array(
+                                            'name' => 'Transition',
+                                            'type' => 'object',
+                                            'sentAs' => 'Transition',
+                                            'properties' => array(
+                                                'Date' => array(
+                                                    'type' => array(
+                                                        'object',
+                                                        'string',
+                                                        'integer',
+                                                    ),
+                                                    'format' => 'date-time',
+                                                ),
+                                                'Days' => array(
+                                                    'type' => 'numeric',
+                                                ),
+                                                'StorageClass' => array(
+                                                    'type' => 'string',
+                                                )))),
+                                    'NoncurrentVersionTransition' => array(
+                                        'type' => 'object',
+                                        'properties' => array(
+                                            'NoncurrentDays' => array(
+                                                'type' => 'numeric',
+                                            ),
+                                            'StorageClass' => array(
+                                                'type' => 'string',
+                                            ),
+                                        ),
+                                    ),
+                                    'NoncurrentVersionExpiration' => array(
+                                        'type' => 'object',
+                                        'properties' => array(
+                                            'NoncurrentDays' => array(
+                                                'type' => 'numeric',
+                                            ),
+                                        ),
+                                    ),
+                                ),
+                            ),
+                        ),
+                    ),
+                ),
+                // 存储桶(Bucket)版本控制的方法.
+                'PutBucketVersioning' => array(
+                    'httpMethod' => 'PUT',
+                    'uri' => '/{Bucket}?versioning',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'PutBucketVersioningOutput',
+                    'responseType' => 'model',
+                    'data' => array(
+                        'xmlRoot' => array(
+                            'name' => 'VersioningConfiguration',
+                        ),
+                    ),
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        ),
+                        'MFA' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-mfa',
+                        ),
+                        'MFADelete' => array(
+                            'type' => 'string',
+                            'location' => 'xml',
+                            'sentAs' => 'MfaDelete',
+                        ),
+                        'Status' => array(
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                    ),
+                ),
+                // 配置存储桶(Bucket) Accelerate的方法.
+                'PutBucketAccelerate' => array(
+                    'httpMethod' => 'PUT',
+                    'uri' => '/{Bucket}?accelerate',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'PutBucketAccelerateOutput',
+                    'responseType' => 'model',
+                    'data' => array(
+                        'xmlRoot' => array(
+                            'name' => 'AccelerateConfiguration',
+                        ),
+                        'xmlAllowEmpty' => true,
+                    ),
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        ),
+                        'Status' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                        'Type' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                    ),
+                ),
+                // 配置存储桶(Bucket) website的方法.
+                'PutBucketWebsite' => array(
+                    'httpMethod' => 'PUT',
+                    'uri' => '/{Bucket}?website',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'PutBucketWebsiteOutput',
+                    'responseType' => 'model',
+                    'data' => array(
+                        'xmlRoot' => array(
+                            'name' => 'WebsiteConfiguration',
+                        ),
+                        'xmlAllowEmpty' => true,
+                    ),
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        ),
+                        'ErrorDocument' => array(
+                            'type' => 'object',
+                            'location' => 'xml',
+                            'properties' => array(
+                                'Key' => array(
+                                    'type' => 'string',
+                                    'minLength' => 1,
+                                ),
+                            ),
+                        ),
+                        'IndexDocument' => array(
+                            'required' => true,
+                            'type' => 'object',
+                            'location' => 'xml',
+                            'properties' => array(
+                                'Suffix' => array(
+                                    'required' => true,
+                                    'type' => 'string',
+                                ),
+                            ),
+                        ),
+                        'RedirectAllRequestsTo' => array(
+                            'type' => 'object',
+                            'location' => 'xml',
+                            'properties' => array(
+                                'HostName' => array(
+                                    'type' => 'string',
+                                ),
+                                'Protocol' => array(
+                                    'type' => 'string',
+                                ),
+                            ),
+                        ),
+                        'RoutingRules' => array(
+                            'type' => 'array',
+                            'location' => 'xml',
+                            'items' => array(
+                                'name' => 'RoutingRule',
+                                'type' => 'object',
+                                'properties' => array(
+                                    'Condition' => array(
+                                        'type' => 'object',
+                                        'properties' => array(
+                                            'HttpErrorCodeReturnedEquals' => array(
+                                                'type' => 'string',
+                                            ),
+                                            'KeyPrefixEquals' => array(
+                                                'type' => 'string',
+                                            ),
+                                        ),
+                                    ),
+                                    'Redirect' => array(
+                                        'type' => 'object',
+                                        'properties' => array(
+                                            'HostName' => array(
+                                                'type' => 'string',
+                                            ),
+                                            'HttpRedirectCode' => array(
+                                                'type' => 'string',
+                                            ),
+                                            'Protocol' => array(
+                                                'type' => 'string',
+                                            ),
+                                            'ReplaceKeyPrefixWith' => array(
+                                                'type' => 'string',
+                                            ),
+                                            'ReplaceKeyWith' => array(
+                                                'type' => 'string',
+                                            ),
+                                        ),
+                                    ),
+                                ),
+                            ),
+                        ),
+                    ),
+                ),
+                // 配置存储桶(Bucket) 跨区域复制的方法.
+                'PutBucketReplication' => array(
+                    'httpMethod' => 'PUT',
+                    'uri' => '/{Bucket}?replication',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'PutBucketReplicationOutput',
+                    'responseType' => 'model',
+                    'data' => array(
+                        'xmlRoot' => array(
+                            'name' => 'ReplicationConfiguration',
+                        ),
+                        'contentMd5' => true,
+                    ),
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        ),
+                        'Role' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                        'Rules' => array(
+                            'required' => true,
+                            'type' => 'array',
+                            'location' => 'xml',
+                            'data' => array(
+                                'xmlFlattened' => true,
+                            ),
+                            'items' => array(
+                                'name' => 'ReplicationRule',
+                                'type' => 'object',
+                                'sentAs' => 'Rule',
+                                'properties' => array(
+                                    'ID' => array(
+                                        'type' => 'string',
+                                    ),
+                                    'Prefix' => array(
+                                        'required' => true,
+                                        'type' => 'string',
+                                    ),
+                                    'Status' => array(
+                                        'required' => true,
+                                        'type' => 'string',
+                                    ),
+                                    'Destination' => array(
+                                        'required' => true,
+                                        'type' => 'object',
+                                        'properties' => array(
+                                            'Bucket' => array(
+                                                'required' => true,
+                                                'type' => 'string',
+                                            ),
+                                            'StorageClass' => array(
+                                                'type' => 'string',
+                                            ),
+                                        ),
+                                    ),
+                                ),
+                            ),
+                        ),
+                    ),
+                ),
+                // 设置存储桶(Bucket) 的回调设置的方法.
+                'PutBucketNotification' => array(
+                    'httpMethod' => 'PUT',
+                    'uri' => '/{Bucket}?notification',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'PutBucketNotificationOutput',
+                    'responseType' => 'model',
+                    'data' => array(
+                        'xmlRoot' => array(
+                            'name' => 'NotificationConfiguration',
+                        ),
+                    ),
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        ),
+                        'CloudFunctionConfigurations' => array(
+                            'type' => 'array',
+                            'location' => 'xml',
+                            'data' => array(
+                                'xmlFlattened' => true,
+                            ),
+                            'items' => array(
+                                'name' => 'CloudFunctionConfiguration',
+                                'type' => 'object',
+                                'sentAs' => 'CloudFunctionConfiguration',
+                                'properties' => array(
+                                    'Id' => array(
+                                        'type' => 'string',
+                                    ),
+                                    'CloudFunction' => array(
+                                        'required' => true,
+                                        'type' => 'string',
+                                        'sentAs' => 'CloudFunction',
+                                    ),
+                                    'Events' => array(
+                                        'required' => true,
+                                        'type' => 'array',
+                                        'data' => array(
+                                            'xmlFlattened' => true,
+                                        ),
+                                        'items' => array(
+                                            'name' => 'Event',
+                                            'type' => 'string',
+                                            'sentAs' => 'Event',
+                                        ),
+                                    ),
+                                    'Filter' => array(
+                                        'type' => 'object',
+                                        'properties' => array(
+                                            'Key' => array(
+                                                'type' => 'object',
+                                                'sentAs' => 'Key',
+                                                'properties' => array(
+                                                    'FilterRules' => array(
+                                                        'type' => 'array',
+                                                        'data' => array(
+                                                            'xmlFlattened' => true,
+                                                        ),
+                                                        'items' => array(
+                                                            'name' => 'FilterRule',
+                                                            'type' => 'object',
+                                                            'sentAs' => 'FilterRule',
+                                                            'properties' => array(
+                                                                'Name' => array(
+                                                                    'type' => 'string',
+                                                                ),
+                                                                'Value' => array(
+                                                                    'type' => 'string',
+                                                                ),
+                                                            ),
+                                                        ),
+                                                    ),
+                                                ),
+                                            ),
+                                            'filters' => array(
+                                                'Qcloud\\Cos\\Client::explodeKey')
+                                        ),
+                                    ),
+                                ),
+                            ),
+                        ),
+                    ),
+                ),
+                // 配置存储桶(Bucket) 标签的方法.
+                'PutBucketTagging' => array(
+                    'httpMethod' => 'PUT',
+                    'uri' => '/{Bucket}?tagging',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'PutBucketTaggingOutput',
+                    'responseType' => 'model',
+                    'data' => array(
+                        'xmlRoot' => array(
+                            'name' => 'Tagging',
+                        ),
+                        'contentMd5' => true,
+                    ),
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        ),
+                        'TagSet' => array(
+                            'required' => true,
+                            'type' => 'array',
+                            'location' => 'xml',
+                            'items' => array(
+                                'name' => 'TagRule',
+                                'type' => 'object',
+                                'sentAs' => 'Tag',
+                                'properties' => array(
+                                    'Key' => array(
+                                        'required' => true,
+                                        'type' => 'string',
+                                    ),
+                                    'Value' => array(
+                                        'required' => true,
+                                        'type' => 'string',
+                                    ),
+                                ),
+                            ),
+                        ),
+                    ),
+                ),
+                //开启存储桶(Bucket) 日志服务的方法.
+                'PutBucketLogging' => array(
+                    'httpMethod' => 'PUT',
+                    'uri' => '/{Bucket}?logging',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'PutBucketLoggingOutput',
+                    'responseType' => 'model',
+                    'data' => array(
+                        'xmlRoot' => array(
+                            'name' => 'BucketLoggingStatus',
+                        ),
+                        'contentMd5' => true,
+                    ),
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        ),
+                        'LoggingEnabled' => array(
+                            'location' => 'xml',
+                            'type' => 'object',
+                            'properties' => array(
+                                'TargetBucket' => array(
+                                    'type' => 'string',
+                                    'location' => 'xml',
+                                ),
+                                'TargetPrefix' => array(
+                                    'type' => 'string',
+                                    'location' => 'xml',
+                                ),
+                            )
+                        ),
+                    ),
+                ),
+                // 配置存储桶(Bucket) 清单的方法.
+                'PutBucketInventory' => array(
+                    'httpMethod' => 'PUT',
+                    'uri' => '/{Bucket}?inventory',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'PutBucketInventoryOutput',
+                    'responseType' => 'model',
+                    'data' => array(
+                        'xmlRoot' => array(
+                            'name' => 'InventoryConfiguration',
+                        ),
+                        'contentMd5' => true,
+                    ),
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        ),
+                        'Id' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                        'IsEnabled' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                        'Destination' => array(
+                            'type' => 'object',
+                            'location' => 'xml',
+                            'properties' => array(
+                                'COSBucketDestination'=> array(
+                                    'type' => 'object',
+                                    'properties' => array(
+                                        'Format' => array(
+                                            'type' => 'string',
+                                            'require' => true,
+                                        ),
+                                        'AccountId' => array(
+                                            'type' => 'string',
+                                            'require' => true,
+                                        ),
+                                        'Bucket' => array(
+                                            'type' => 'string',
+                                            'require' => true,
+                                        ),
+                                        'Prefix' => array(
+                                            'type' => 'string',
+                                        ),
+                                        'Encryption' => array(
+                                            'type' => 'object',
+                                            'properties' => array(
+                                                'SSE-COS' => array(
+                                                    'type' => 'string',
+                                                ),
+                                            ),
+                                        ),
+                                    ),
+                                ),
+                            ),
+                        ),
+                        'Schedule' => array(
+                            'required' => true,
+                            'type' => 'object',
+                            'location' => 'xml',
+                            'properties' => array(
+                                'Frequency' => array(
+                                    'type' => 'string',
+                                    'require' => true,
+                                ),
+                            )
+                        ),
+                        'Filter' => array(
+                            'type' => 'object',
+                            'location' => 'xml',
+                            'properties' => array(
+                                'Prefix' => array(
+                                    'type' => 'string',
+                                ),
+                            )
+                        ),
+                        'IncludedObjectVersions' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                        'OptionalFields' => array(
+                            'type' => 'array',
+                            'location' => 'xml',
+                            'items' => array(
+                                'name' => 'Fields',
+                                'type' => 'string',
+                                'sentAs' => 'Field',
+                            ),
+                        ),
+                    ),
+                ),
+                // 回热归档对象的方法.
+                'RestoreObject' => array(
+                    'httpMethod' => 'POST',
+                    'uri' => '/{Bucket}{/Key*}?restore',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'RestoreObjectOutput',
+                    'responseType' => 'model',
+                    'data' => array(
+                        'xmlRoot' => array(
+                            'name' => 'RestoreRequest',
+                        ),
+                    ),
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        ),
+                        'Key' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                            'minLength' => 1,
+                            'filters' => array(
+                                'Qcloud\\Cos\\Client::explodeKey')
+                        ),
+                        'VersionId' => array(
+                            'type' => 'string',
+                            'location' => 'query',
+                            'sentAs' => 'versionId',
+                        ),
+                        'Days' => array(
+                            'required' => true,
+                            'type' => 'numeric',
+                            'location' => 'xml',
+                        ),
+                        'CASJobParameters' => array(
+                            'type' => 'object',
+                            'location' => 'xml',
+                            'properties' => array(
+                                'Tier' => array(
+                                    'type' => 'string',
+                                    'required' => true,
+                                ),
+                            ),
+                        ),
+                        'RequestPayer' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-payer',
+                        ),
+                    ),
+                ),
+                // 查询存储桶(Bucket)中正在进行中的分块上传对象的方法.
+                'ListParts' => array(
+                    'httpMethod' => 'GET',
+                    'uri' => '/{Bucket}{/Key*}',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'ListPartsOutput',
+                    'responseType' => 'model',
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri'
+                        ),
+                        'Key' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                            'minLength' => 1,
+                            'filters' => array(
+                                'Qcloud\\Cos\\Client::explodeKey'
+                            )
+                        ),
+                        'MaxParts' => array(
+                            'type' => 'numeric',
+                            'location' => 'query',
+                            'sentAs' => 'max-parts'),
+                        'PartNumberMarker' => array(
+                            'type' => 'numeric',
+                            'location' => 'query',
+                            'sentAs' => 'part-number-marker'
+                        ),
+                        'UploadId' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'query',
+                            'sentAs' => 'uploadId'
+                        )
+                    )
+                ),
+                // 查询存储桶(Bucket) 下的部分或者全部对象的方法.
+                'ListObjects' => array(
+                    'httpMethod' => 'GET',
+                    'uri' => '/{Bucket}',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'ListObjectsOutput',
+                    'responseType' => 'model',
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri'
+                        ),
+                        'Delimiter' => array(
+                            'type' => 'string',
+                            'location' => 'query',
+                            'sentAs' => 'delimiter'
+                        ),
+                        'EncodingType' => array(
+                            'type' => 'string',
+                            'location' => 'query',
+                            'sentAs' => 'encoding-type'
+                        ),
+                        'Marker' => array(
+                            'type' => 'string',
+                            'location' => 'query',
+                            'sentAs' => 'marker'
+                        ),
+                        'MaxKeys' => array(
+                            'type' => 'numeric',
+                            'location' => 'query',
+                            'sentAs' => 'max-keys'
+                        ),
+                        'Prefix' => array(
+                            'type' => 'string',
+                            'location' => 'query',
+                            'sentAs' => 'prefix'
+                        )
+                    )
+                ),
+                // 获取所属账户的所有存储空间列表的方法.
+                'ListBuckets' => array(
+                    'httpMethod' => 'GET',
+                    'uri' => '/',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'ListBucketsOutput',
+                    'responseType' => 'model',
+                    'parameters' => array(
+                    ),
+                ),
+                // 获取多版本对象的方法.
+                'ListObjectVersions' => array(
+                    'httpMethod' => 'GET',
+                    'uri' => '/{Bucket}?versions',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'ListObjectVersionsOutput',
+                    'responseType' => 'model',
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        ),
+                        'Delimiter' => array(
+                            'type' => 'string',
+                            'location' => 'query',
+                            'sentAs' => 'delimiter',
+                        ),
+                        'EncodingType' => array(
+                            'type' => 'string',
+                            'location' => 'query',
+                            'sentAs' => 'encoding-type',
+                        ),
+                        'KeyMarker' => array(
+                            'type' => 'string',
+                            'location' => 'query',
+                            'sentAs' => 'key-marker',
+                        ),
+                        'MaxKeys' => array(
+                            'type' => 'numeric',
+                            'location' => 'query',
+                            'sentAs' => 'max-keys',
+                        ),
+                        'Prefix' => array(
+                            'type' => 'string',
+                            'location' => 'query',
+                            'sentAs' => 'prefix',
+                        ),
+                        'VersionIdMarker' => array(
+                            'type' => 'string',
+                            'location' => 'query',
+                            'sentAs' => 'version-id-marker',
+                        )
+                    ),
+                ),
+                // 获取已上传分块列表的方法
+                'ListMultipartUploads' => array(
+                    'httpMethod' => 'GET',
+                    'uri' => '/{Bucket}?uploads',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'ListMultipartUploadsOutput',
+                    'responseType' => 'model',
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        ),
+                        'Delimiter' => array(
+                            'type' => 'string',
+                            'location' => 'query',
+                            'sentAs' => 'delimiter',
+                        ),
+                        'EncodingType' => array(
+                            'type' => 'string',
+                            'location' => 'query',
+                            'sentAs' => 'encoding-type',
+                        ),
+                        'KeyMarker' => array(
+                            'type' => 'string',
+                            'location' => 'query',
+                            'sentAs' => 'key-marker',
+                        ),
+                        'MaxUploads' => array(
+                            'type' => 'numeric',
+                            'location' => 'query',
+                            'sentAs' => 'max-uploads',
+                        ),
+                        'Prefix' => array(
+                            'type' => 'string',
+                            'location' => 'query',
+                            'sentAs' => 'prefix',
+                        ),
+                        'UploadIdMarker' => array(
+                            'type' => 'string',
+                            'location' => 'query',
+                            'sentAs' => 'upload-id-marker',
+                        )
+                    ),
+                ),
+                // 获取清单列表的方法.
+                'ListBucketInventoryConfigurations' => array(
+                    'httpMethod' => 'GET',
+                    'uri' => '/{Bucket}?inventory',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'ListBucketInventoryConfigurationsOutput',
+                    'responseType' => 'model',
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri'
+                        ),
+                        'ContinuationToken' => array(
+                            'type' => 'string',
+                            'location' => 'query',
+                            'sentAs' => 'continuation-token',
+                        ),
+                    ),
+                ),
+                // 获取对象的meta信息的方法
+                'HeadObject' => array(
+                    'httpMethod' => 'HEAD',
+                    'uri' => '/{Bucket}{/Key*}',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'HeadObjectOutput',
+                    'responseType' => 'model',
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        ),
+                        'IfMatch' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'If-Match',
+                        ),
+                        'IfModifiedSince' => array(
+                            'type' => array(
+                                'object',
+                                'string',
+                                'integer',
+                            ),
+                            'format' => 'date-time-http',
+                            'location' => 'header',
+                            'sentAs' => 'If-Modified-Since',
+                        ),
+                        'IfNoneMatch' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'If-None-Match',
+                        ),
+                        'IfUnmodifiedSince' => array(
+                            'type' => array(
+                                'object',
+                                'string',
+                                'integer',
+                            ),
+                            'format' => 'date-time-http',
+                            'location' => 'header',
+                            'sentAs' => 'If-Unmodified-Since',
+                        ),
+                        'Key' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                            'minLength' => 1,
+                            'filters' => array(
+                                'Qcloud\\Cos\\Client::explodeKey')
+                        ),
+                        'Range' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                        ),
+                        'VersionId' => array(
+                            'type' => 'string',
+                            'location' => 'query',
+                            'sentAs' => 'versionId',
+                        ),
+                        'SSECustomerAlgorithm' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-customer-algorithm',
+                        ),
+                        'SSECustomerKey' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-customer-key',
+                        ),
+                        'SSECustomerKeyMD5' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-customer-key-MD5',
+                        ),
+                        'RequestPayer' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-payer',
+                        ),
+                    )
+                ),
+                // 存储桶(Bucket) 是否存在的方法.
+                'HeadBucket' => array(
+                    'httpMethod' => 'HEAD',
+                    'uri' => '/{Bucket}',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'HeadBucketOutput',
+                    'responseType' => 'model',
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        ),
+                    )
+                ),
+                // 分块copy的方法.
+                'UploadPartCopy' => array(
+                    'httpMethod' => 'PUT',
+                    'uri' => '/{Bucket}{/Key*}',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'UploadPartCopyOutput',
+                    'responseType' => 'model',
+                    'data' => array(
+                        'xmlRoot' => array(
+                            'name' => 'UploadPartCopyRequest',
+                        ),
+                    ),
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        ),
+                        'CopySource' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-copy-source',
+                        ),
+                        'CopySourceIfMatch' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-copy-source-if-match',
+                        ),
+                        'CopySourceIfModifiedSince' => array(
+                            'type' => array(
+                                'object',
+                                'string',
+                                'integer',
+                            ),
+                            'format' => 'date-time-http',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-copy-source-if-modified-since',
+                        ),
+                        'CopySourceIfNoneMatch' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-copy-source-if-none-match',
+                        ),
+                        'CopySourceIfUnmodifiedSince' => array(
+                            'type' => array(
+                                'object',
+                                'string',
+                                'integer',
+                            ),
+                            'format' => 'date-time-http',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-copy-source-if-unmodified-since',
+                        ),
+                        'CopySourceRange' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-copy-source-range',
+                        ),
+                        'Key' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                            'minLength' => 1,
+                            'filters' => array(
+                                'Qcloud\\Cos\\Client::explodeKey')
+                        ),
+                        'PartNumber' => array(
+                            'required' => true,
+                            'type' => 'numeric',
+                            'location' => 'query',
+                            'sentAs' => 'partNumber',
+                        ),
+                        'UploadId' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'query',
+                            'sentAs' => 'uploadId',
+                        ),
+                        'SSECustomerAlgorithm' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-customer-algorithm',
+                        ),
+                        'SSECustomerKey' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-customer-key',
+                        ),
+                        'SSECustomerKeyMD5' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-customer-key-MD5',
+                        ),
+                        'CopySourceSSECustomerAlgorithm' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-copy-source-server-side-encryption-customer-algorithm',
+                        ),
+                        'CopySourceSSECustomerKey' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-copy-source-server-side-encryption-customer-key',
+                        ),
+                        'CopySourceSSECustomerKeyMD5' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-copy-source-server-side-encryption-customer-key-MD5',
+                        ),
+                        'RequestPayer' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-payer',
+                        )
+                    ),
+                ),
+                'SelectObjectContent' => array(
+                    'httpMethod' => 'Post',
+                    'uri' => '/{/Key*}?select&select-type=2',
+                    'class' => 'Qcloud\\Cos\\Command',
+                    'responseClass' => 'SelectObjectContentOutput',
+                    'responseType' => 'model',
+                    'data' => array(
+                        'xmlRoot' => array(
+                            'name' => 'SelectRequest',
+                        ),
+                        'contentMd5' => true,
+                    ),
+                    'parameters' => array(
+                        'Bucket' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                        ),
+                        'Key' => array(
+                            'required' => true,
+                            'type' => 'string',
+                            'location' => 'uri',
+                            'minLength' => 1,
+                            'filters' => array(
+                                'Qcloud\\Cos\\Client::explodeKey')
+                        ),
+                        'Expression' => array(
+                            'type' => 'string',
+                            'location' => 'xml'
+                        ),
+                        'ExpressionType' => array(
+                            'type' => 'string',
+                            'location' => 'xml'
+                        ),
+                        'InputSerialization' => array(
+                            'location' => 'xml',
+                            'type' => 'object',
+                            'properties' => array(
+                                'CompressionType' => array(
+                                    'type' => 'string',
+                                    'location' => 'xml',
+                                ),
+                                'CSV' => array(
+                                    'type' => 'object',
+                                    'location' => 'xml',
+                                    'properties' => array(
+                                        'FileHeaderInfo' => array(
+                                            'type' => 'string',
+                                            'location' => 'xml',
+                                        ),
+                                        'RecordDelimiter' => array(
+                                            'type' => 'string',
+                                            'location' => 'xml',
+                                        ),
+                                        'FieldDelimiter' => array(
+                                            'type' => 'string',
+                                            'location' => 'xml',
+                                        ),
+                                        'QuoteCharacter' => array(
+                                            'type' => 'string',
+                                            'location' => 'xml',
+                                        ),
+                                        'QuoteEscapeCharacter' => array(
+                                            'type' => 'string',
+                                            'location' => 'xml',
+                                        ),
+                                        'Comments' => array(
+                                            'type' => 'string',
+                                            'location' => 'xml',
+                                        ),
+                                        'AllowQuotedRecordDelimiter' => array(
+                                            'type' => 'string',
+                                            'location' => 'xml',
+                                        ),
+                                    )
+                                ),
+                                'JSON' => array(
+                                    'type' => 'string',
+                                    'location' => 'object',
+                                    'properties' => array(
+                                        'Type' => array(
+                                            'type' => 'string',
+                                            'location' => 'xml',
+                                        )
+                                    )
+                                ),
+                            )
+                        ),
+                        'OutputSerialization' => array(
+                            'location' => 'xml',
+                            'type' => 'object',
+                            'properties' => array(
+                                'CompressionType' => array(
+                                    'type' => 'string',
+                                    'location' => 'xml',
+                                ),
+                                'CSV' => array(
+                                    'type' => 'object',
+                                    'location' => 'xml',
+                                    'properties' => array(
+                                        'QuoteFields' => array(
+                                            'type' => 'string',
+                                            'location' => 'xml',
+                                        ),
+                                        'RecordDelimiter' => array(
+                                            'type' => 'string',
+                                            'location' => 'xml',
+                                        ),
+                                        'FieldDelimiter' => array(
+                                            'type' => 'string',
+                                            'location' => 'xml',
+                                        ),
+                                        'QuoteCharacter' => array(
+                                            'type' => 'string',
+                                            'location' => 'xml',
+                                        ),
+                                        'QuoteEscapeCharacter' => array(
+                                            'type' => 'string',
+                                            'location' => 'xml',
+                                        ),
+                                    )
+                                ),
+                                'JSON' => array(
+                                    'type' => 'string',
+                                    'location' => 'object',
+                                    'properties' => array(
+                                        'RecordDelimiter' => array(
+                                            'type' => 'string',
+                                            'location' => 'xml',
+                                        )
+                                    )
+                                ),
+                            )
+                        ),
+                        'RequestProgress' => array(
+                            'location' => 'xml',
+                            'type' => 'object',
+                            'properties' => array(
+                                'Enabled' => array(
+                                    'type' => 'string',
+                                    'location' => 'xml',
+                                ),
+                            )
+                        ),
+                    ),
+                ),
+            ),
+            'models' => array(
+                'AbortMultipartUploadOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id'
+                        )
+                    )
+                ),
+                'CreateBucketOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'Location' => array(
+                            'type' => 'string',
+                            'location' => 'header'
+                        ),
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id'
+                        )
+                    )
+                ),
+                'CompleteMultipartUploadOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'Location' => array(
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                        'Bucket' => array(
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                        'Key' => array(
+                            'type' => 'string',
+                            'location' => 'xml'
+                        ),
+                        'Expiration' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-expiration',
+                        ),
+                        'ETag' => array(
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                        'ServerSideEncryption' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption',
+                        ),
+                        'VersionId' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-version-id',
+                        ),
+                        'SSEKMSKeyId' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-aws-kms-key-id',
+                        ),
+                        'RequestCharged' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-charged',
+                        ),
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'CreateMultipartUploadOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'Bucket' => array(
+                            'type' => 'string',
+                            'location' => 'xml',
+                            'sentAs' => 'Bucket'
+                        ),
+                        'Key' => array(
+                            'type' => 'string',
+                            'location' => 'xml'
+                        ),
+                        'UploadId' => array(
+                            'type' => 'string',
+                            'location' => 'xml'
+                        ),
+                        'ServerSideEncryption' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption',
+                        ),
+                        'SSECustomerAlgorithm' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-customer-algorithm',
+                        ),
+                        'SSECustomerKeyMD5' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-customer-key-MD5',
+                        ),
+                        'SSEKMSKeyId' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-aws-kms-key-id',
+                        ),
+                        'RequestCharged' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-charged',
+                        ),
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        )
+                    )
+                ),
+                'CopyObjectOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'ETag' => array(
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                        'LastModified' => array(
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                        'Expiration' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-expiration',
+                        ),
+                        'CopySourceVersionId' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-copy-source-version-id',
+                        ),
+                        'VersionId' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-version-id',
+                        ),
+                        'ServerSideEncryption' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption',
+                        ),
+                        'SSECustomerAlgorithm' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-customer-algorithm',
+                        ),
+                        'RequestCharged' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-charged',
+                        ),
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'DeleteBucketOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id'
+                        )
+                    )
+                ),
+                'DeleteBucketCorsOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'DeleteBucketTaggingOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'DeleteBucketInventoryOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'DeleteObjectOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'DeleteMarker' => array(
+                            'type' => 'boolean',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-delete-marker',
+                        ),
+                        'VersionId' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-version-id',
+                        ),
+                        'RequestCharged' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-charged',
+                        ),
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'DeleteObjectsOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'Deleted' => array(
+                            'type' => 'array',
+                            'location' => 'xml',
+                            'sentAs' => 'Deleted',
+                            'data' => array(
+                                'xmlFlattened' => true,
+                            ),
+                            'items' => array(
+                                'type' => 'object',
+                                'properties' => array(
+                                    'Key' => array(
+                                        'type' => 'string',
+                                    ),
+                                    'VersionId' => array(
+                                        'type' => 'string',
+                                    ),
+                                    'DeleteMarker' => array(
+                                        'type' => 'boolean',
+                                    ),
+                                    'DeleteMarkerVersionId' => array(
+                                        'type' => 'string',
+                                    ),
+                                ),
+                            ),
+                        ),
+                        'RequestCharged' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-charged',
+                        ),
+                        'Errors' => array(
+                            'type' => 'array',
+                            'location' => 'xml',
+                            'sentAs' => 'Error',
+                            'data' => array(
+                                'xmlFlattened' => true,
+                            ),
+                            'items' => array(
+                                'type' => 'object',
+                                'properties' => array(
+                                    'Key' => array(
+                                        'type' => 'string',
+                                    ),
+                                    'VersionId' => array(
+                                        'type' => 'string',
+                                    ),
+                                    'Code' => array(
+                                        'type' => 'string',
+                                    ),
+                                    'Message' => array(
+                                        'type' => 'string',
+                                    ),
+                                ),
+                            ),
+                        ),
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'DeleteBucketLifecycleOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'DeleteBucketReplicationOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'DeleteBucketWebsiteOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'GetObjectOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'Body' => array(
+                            'type' => 'string',
+                            'instanceOf' => 'GuzzleHttp\\Psr7\\Stream',
+                            'location' => 'body',
+                        ),
+                        'DeleteMarker' => array(
+                            'type' => 'boolean',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-delete-marker',
+                        ),
+                        'AcceptRanges' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'accept-ranges',
+                        ),
+                        'Expiration' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-expiration',
+                        ),
+                        'Restore' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-restore',
+                        ),
+                        'LastModified' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'Last-Modified',
+                        ),
+                        'ContentLength' => array(
+                            'type' => 'numeric',
+                            'minimum'=> 0,
+                            'location' => 'header',
+                            'sentAs' => 'Content-Length',
+                        ),
+                        'ETag' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                        ),
+                        'MissingMeta' => array(
+                            'type' => 'numeric',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-missing-meta',
+                        ),
+                        'VersionId' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-version-id',
+                        ),
+                        'CacheControl' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'Cache-Control',
+                        ),
+                        'ContentDisposition' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'Content-Disposition',
+                        ),
+                        'ContentEncoding' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'Content-Encoding',
+                        ),
+                        'ContentLanguage' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'Content-Language',
+                        ),
+                        'ContentRange' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'Content-Range',
+                        ),
+                        'ContentType' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'Content-Type',
+                        ),
+                        'Expires' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                        ),
+                        'WebsiteRedirectLocation' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-website-redirect-location',
+                        ),
+                        'ServerSideEncryption' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption',
+                        ),
+                        'SSECustomerAlgorithm' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-customer-algorithm',
+                        ),
+                        'SSECustomerKeyMD5' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-customer-key-MD5',
+                        ),
+                        'SSEKMSKeyId' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-aws-kms-key-id',
+                        ),
+                        'StorageClass' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-storage-class',
+                        ),
+                        'RequestCharged' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-charged',
+                        ),
+                        'ReplicationStatus' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-replication-status',
+                        ),
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'GetObjectAclOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'Owner' => array(
+                            'type' => 'object',
+                            'location' => 'xml',
+                            'properties' => array(
+                                'DisplayName' => array(
+                                    'type' => 'string',
+                                ),
+                                'ID' => array(
+                                    'type' => 'string',
+                                ),
+                            ),
+                        ),
+                        'Grants' => array(
+                            'type' => 'array',
+                            'location' => 'xml',
+                            'sentAs' => 'AccessControlList',
+                            'items' => array(
+                                'type' => 'object',
+                                'properties' => array(
+                                    'Grantee' => array(
+                                        'type' => 'object',
+                                        'properties' => array(
+                                            'DisplayName' => array(
+                                                'type' => 'string'),
+                                            'ID' => array(
+                                                'type' => 'string'))),
+                                    'Permission' => array(
+                                        'type' => 'string',
+                                    ),
+                                ),
+                            ),
+                        ),
+                        'RequestCharged' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-charged',
+                        ),
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'GetBucketAclOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'Owner' => array(
+                            'type' => 'object',
+                            'location' => 'xml',
+                            'properties' => array(
+                                'DisplayName' => array(
+                                    'type' => 'string'
+                                ),
+                                'ID' => array(
+                                    'type' => 'string'
+                                )
+                            )
+                        ),
+                        'Grants' => array(
+                            'type' => 'array',
+                            'location' => 'xml',
+                            'sentAs' => 'AccessControlList',
+                            'items' => array(
+                                'type' => 'object',
+                                'properties' => array(
+                                    'Grantee' => array(
+                                        'type' => 'object',
+                                        'properties' => array(
+                                            'DisplayName' => array(
+                                                'type' => 'string'
+                                            ),
+                                            'ID' => array(
+                                                'type' => 'string'
+                                            )
+                                        )
+                                    ),
+                                    'Permission' => array(
+                                        'type' => 'string'
+                                    )
+                                )
+                            )
+                        ),
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id'
+                        )
+                    )
+                ),
+                'GetBucketCorsOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'CORSRules' => array(
+                            'type' => 'array',
+                            'location' => 'xml',
+                            'sentAs' => 'CORSRule',
+                            'data' => array(
+                                'xmlFlattened' => true,
+                            ),
+                            'items' => array(
+                                'type' => 'object',
+                                'properties' => array(
+                                    'ID' => array(
+                                        'type' => 'string'),
+                                    'AllowedHeaders' => array(
+                                        'type' => 'array',
+                                        'sentAs' => 'AllowedHeader',
+                                        'data' => array(
+                                            'xmlFlattened' => true,
+                                        ),
+                                        'items' => [
+                                            'type' => 'string',
+                                        ]
+                                    ),
+                                    'AllowedMethods' => array(
+                                        'type' => 'array',
+                                        'sentAs' => 'AllowedMethod',
+                                        'data' => array(
+                                            'xmlFlattened' => true,
+                                        ),
+                                        'items' => array(
+                                            'type' => 'string',
+                                        ),
+                                    ),
+                                    'AllowedOrigins' => array(
+                                        'type' => 'array',
+                                        'sentAs' => 'AllowedOrigin',
+                                        'data' => array(
+                                            'xmlFlattened' => true,
+                                        ),
+                                        'items' => array(
+                                            'type' => 'string',
+                                        ),
+                                    ),
+                                    'ExposeHeaders' => array(
+                                        'type' => 'array',
+                                        'sentAs' => 'ExposeHeader',
+                                        'data' => array(
+                                            'xmlFlattened' => true,
+                                        ),
+                                        'items' => array(
+                                            'type' => 'string',
+                                        ),
+                                    ),
+                                    'MaxAgeSeconds' => array(
+                                        'type' => 'numeric',
+                                    ),
+                                ),
+                            ),
+                        ),
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'GetBucketDomainOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'DomainRules' => array(
+                            'type' => 'array',
+                            'location' => 'xml',
+                            'sentAs' => 'DomainRule',
+                            'data' => array(
+                                'xmlFlattened' => true,
+                            ),
+                            'items' => array(
+                                'type' => 'object',
+                                'properties' => array(
+                                    'Status' => array(
+                                        'type' => 'string'
+                                    ),
+                                    'Name' => array(
+                                        'type' => 'string'
+                                    ),
+                                    'Type' => array(
+                                        'type' => 'string'
+                                    ),
+                                    'ForcedReplacement' => array(
+                                        'type' => 'string'
+                                    ),
+                                ),
+                            ),
+                        ),
+                        'DomainTxtVerification' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-domain-txt-verification',
+                        ),
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'GetBucketLifecycleOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'Rules' => array(
+                            'type' => 'array',
+                            'location' => 'xml',
+                            'sentAs' => 'Rule',
+                            'data' => array(
+                                'xmlFlattened' => true,
+                            ),
+                            'items' => array(
+                                'type' => 'object',
+                                'properties' => array(
+                                    'Expiration' => array(
+                                        'type' => 'object',
+                                        'properties' => array(
+                                            'Date' => array(
+                                                'type' => 'string',
+                                            ),
+                                            'Days' => array(
+                                                'type' => 'numeric',
+                                            ),
+                                        ),
+                                    ),
+                                    'ID' => array(
+                                        'type' => 'string',
+                                    ),
+                                    'Filter' => array(
+                                        'type' => 'object',
+                                        'properties' => array(
+                                            'Prefix' => array(
+                                                'type' => 'string',
+                                            ),
+                                            'Tag' => array(
+                                                'type' => 'object',
+                                                'properties' => array(
+                                                    'Key' => array(
+                                                        'type' => 'string'
+                                                    ),
+                                                    'Value' => array(
+                                                        'type' => 'string'
+                                                    ),
+                                                )
+                                            )
+                                        ),
+                                    ),
+                                    'Status' => array(
+                                        'type' => 'string',
+                                    ),
+                                    'Transition' => array(
+                                        'type' => 'object',
+                                        'properties' => array(
+                                            'Date' => array(
+                                                'type' => 'string',
+                                            ),
+                                            'Days' => array(
+                                                'type' => 'numeric',
+                                            ),
+                                            'StorageClass' => array(
+                                                'type' => 'string',
+                                            ),
+                                        ),
+                                    ),
+                                    'NoncurrentVersionTransition' => array(
+                                        'type' => 'object',
+                                        'properties' => array(
+                                            'NoncurrentDays' => array(
+                                                'type' => 'numeric',
+                                            ),
+                                            'StorageClass' => array(
+                                                'type' => 'string',
+                                            ),
+                                        ),
+                                    ),
+                                    'NoncurrentVersionExpiration' => array(
+                                        'type' => 'object',
+                                        'properties' => array(
+                                            'NoncurrentDays' => array(
+                                                'type' => 'numeric',
+                                            ),
+                                        ),
+                                    ),
+                                ),
+                            ),
+                        ),
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'GetBucketVersioningOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'Status' => array(
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                        'MFADelete' => array(
+                            'type' => 'string',
+                            'location' => 'xml',
+                            'sentAs' => 'MfaDelete',
+                        ),
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'GetBucketReplicationOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'Role' => array(
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                        'Rules' => array(
+                            'type' => 'array',
+                            'location' => 'xml',
+                            'sentAs' => 'Rule',
+                            'data' => array(
+                                'xmlFlattened' => true,
+                            ),
+                            'items' => array(
+                                'type' => 'object',
+                                'properties' => array(
+                                    'ID' => array(
+                                        'type' => 'string',
+                                    ),
+                                    'Prefix' => array(
+                                        'type' => 'string',
+                                    ),
+                                    'Status' => array(
+                                        'type' => 'string',
+                                    ),
+                                    'Destination' => array(
+                                        'type' => 'object',
+                                        'properties' => array(
+                                            'Bucket' => array(
+                                                'type' => 'string',
+                                            ),
+                                            'StorageClass' => array(
+                                                'type' => 'string',
+                                            ),
+                                        ),
+                                    ),
+                                ),
+                            ),
+                        ),
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'GetBucketLocationOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'Location' => array(
+                            'type' => 'string',
+                            'location' => 'body',
+                            'filters' => array(
+                                'strval',
+                                'strip_tags',
+                                'trim',
+                            ),
+                        ),
+                    ),
+                ),
+                'GetBucketAccelerateOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'Status' => array(
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                        'Type' => array(
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'GetBucketWebsiteOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'RedirectAllRequestsTo' => array(
+                            'type' => 'object',
+                            'location' => 'xml',
+                            'properties' => array(
+                                'HostName' => array(
+                                    'type' => 'string',
+                                ),
+                                'Protocol' => array(
+                                    'type' => 'string',
+                                ),
+                            ),
+                        ),
+                        'IndexDocument' => array(
+                            'type' => 'object',
+                            'location' => 'xml',
+                            'properties' => array(
+                                'Suffix' => array(
+                                    'type' => 'string',
+                                ),
+                            ),
+                        ),
+                        'ErrorDocument' => array(
+                            'type' => 'object',
+                            'location' => 'xml',
+                            'properties' => array(
+                                'Key' => array(
+                                    'type' => 'string',
+                                ),
+                            ),
+                        ),
+                        'RoutingRules' => array(
+                            'type' => 'array',
+                            'location' => 'xml',
+                            'items' => array(
+                                'name' => 'RoutingRule',
+                                'type' => 'object',
+                                'sentAs' => 'RoutingRule',
+                                'properties' => array(
+                                    'Condition' => array(
+                                        'type' => 'object',
+                                        'properties' => array(
+                                            'HttpErrorCodeReturnedEquals' => array(
+                                                'type' => 'string',
+                                            ),
+                                            'KeyPrefixEquals' => array(
+                                                'type' => 'string',
+                                            ),
+                                        ),
+                                    ),
+                                    'Redirect' => array(
+                                        'type' => 'object',
+                                        'properties' => array(
+                                            'HostName' => array(
+                                                'type' => 'string',
+                                            ),
+                                            'HttpRedirectCode' => array(
+                                                'type' => 'string',
+                                            ),
+                                            'Protocol' => array(
+                                                'type' => 'string',
+                                            ),
+                                            'ReplaceKeyPrefixWith' => array(
+                                                'type' => 'string',
+                                            ),
+                                            'ReplaceKeyWith' => array(
+                                                'type' => 'string',
+                                            ),
+                                        ),
+                                    ),
+                                ),
+                            ),
+                        ),
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'GetBucketInventoryOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'Destination' => array(
+                            'type' => 'object',
+                            'location' => 'xml',
+                            'properties' => array(
+                                'COSBucketDestination' => array(
+                                    'type' => 'object',
+                                    'properties' => array(
+                                        'Format' => array(
+                                            'type' => 'string',
+                                        ),
+                                        'AccountId' => array(
+                                            'type' => 'string',
+                                        ),
+                                        'Bucket' => array(
+                                            'type' => 'string',
+                                        ),
+                                        'Prefix' => array(
+                                            'type' => 'string',
+                                        ),
+                                        'Encryption' => array(
+                                            'type' => 'object',
+                                            'properties' => array(
+                                                'SSE-COS' => array(
+                                                    'type' => 'string',
+                                                )
+                                            )
+                                        ),
+                                        
+                                    ),
+                                ),
+                            ),
+                        ),
+                        'Schedule' => array(
+                            'type' => 'object',
+                            'location' => 'xml',
+                            'properties' => array(
+                                'Frequency' => array(
+                                    'type' => 'string',
+                                ),
+                            ),
+                        ),
+                        'OptionalFields' => array(
+                            'type' => 'array',
+                            'location' => 'xml',
+                            'properties' => array(
+                                'Key' => array(
+                                    'type' => 'string',
+                                ),
+                            ),
+                        ),
+                        'OptionalFields' => array(
+                            'type' => 'array',
+                            'location' => 'xml',
+                            'items' => array(
+                                'name' => 'Field',
+                                'type' => 'string',
+                                'sentAs' => 'Field',
+                            ),
+                        ),
+                        'IsEnabled' => array(
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                        'Id' => array(
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                        'IncludedObjectVersions' => array(
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'GetBucketTaggingOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'TagSet' => array(
+                            'type' => 'array',
+                            'location' => 'xml',
+                            'items' => array(
+                                'sentAs' => 'Tag',
+                                'type' => 'object',
+                                'properties' => array(
+                                    'Key' => array(
+                                        'type' => 'string',
+                                    ),
+                                    'Value' => array(
+                                        'type' => 'string',
+                                    ),
+                                ),
+                            ),
+                        ),
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'GetBucketNotificationOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'CloudFunctionConfigurations' => array(
+                            'type' => 'array',
+                            'location' => 'xml',
+                            'sentAs' => 'CloudFunctionConfiguration',
+                            'data' => array(
+                                'xmlFlattened' => true,
+                            ),
+                            'items' => array(
+                                'type' => 'object',
+                                'properties' => array(
+                                    'Id' => array(
+                                        'type' => 'string',
+                                    ),
+                                    'CloudFunction' => array(
+                                        'type' => 'string',
+                                        'sentAs' => 'CloudFunction',
+                                    ),
+                                    'Events' => array(
+                                        'type' => 'array',
+                                        'sentAs' => 'Event',
+                                        'data' => array(
+                                            'xmlFlattened' => true,
+                                        ),
+                                        'items' => array(
+                                            'type' => 'string',
+                                        ),
+                                    ),
+                                    'Filter' => array(
+                                        'type' => 'object',
+                                        'properties' => array(
+                                            'Key' => array(
+                                                'type' => 'object',
+                                                'sentAs' => 'Key',
+                                                'properties' => array(
+                                                    'FilterRules' => array(
+                                                        'type' => 'array',
+                                                        'sentAs' => 'FilterRule',
+                                                        'data' => array(
+                                                            'xmlFlattened' => true,
+                                                        ),
+                                                        'items' => array(
+                                                            'type' => 'object',
+                                                            'properties' => array(
+                                                                'Name' => array(
+                                                                    'type' => 'string',
+                                                                ),
+                                                                'Value' => array(
+                                                                    'type' => 'string',
+                                                                ),
+                                                            ),
+                                                        ),
+                                                    ),
+                                                ),
+                                            ),
+                                        ),
+                                    ),
+                                ),
+                            ),
+                        ),
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'GetBucketLoggingOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'LoggingEnabled' => array(
+                            'location' => 'xml',
+                            'type' => 'object',
+                            'properties' => array(
+                                'TargetBucket' => array(
+                                    'type' => 'string',
+                                    'location' => 'xml',
+                                ),
+                                'TargetPrefix' => array(
+                                    'type' => 'string',
+                                ),
+                            ),
+                        ),
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'UploadPartOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'ServerSideEncryption' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption',
+                        ),
+                        'ETag' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                        ),
+                        'SSECustomerAlgorithm' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-customer-algorithm',
+                        ),
+                        'SSECustomerKeyMD5' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-customer-key-MD5',
+                        ),
+                        'SSEKMSKeyId' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-aws-kms-key-id',
+                        ),
+                        'RequestCharged' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-charged',
+                        ),
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'UploadPartCopyOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'CopySourceVersionId' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-copy-source-version-id',
+                        ),
+                        'ETag' => array(
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                        'LastModified' => array(
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                        'ServerSideEncryption' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption',
+                        ),
+                        'SSECustomerAlgorithm' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-customer-algorithm',
+                        ),
+                        'SSECustomerKeyMD5' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-customer-key-MD5',
+                        ),
+                        'SSEKMSKeyId' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-aws-kms-key-id',
+                        ),
+                        'RequestCharged' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-charged',
+                        ),
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'PutBucketAclOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id'
+                        )
+                    )
+                ),
+                'PutObjectOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'Expiration' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-expiration',
+                        ),
+                        'ETag' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                        ),
+                        'ServerSideEncryption' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption',
+                        ),
+                        'VersionId' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-version-id',
+                        ),
+                        'SSECustomerAlgorithm' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-customer-algorithm',
+                        ),
+                        'SSECustomerKeyMD5' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-customer-key-MD5',
+                        ),
+                        'SSEKMSKeyId' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-aws-kms-key-id',
+                        ),
+                        'RequestCharged' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-charged',
+                        ),
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'PutObjectAclOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'RequestCharged' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-charged',
+                        ),
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'PutBucketCorsOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'PutBucketDomainOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'PutBucketLifecycleOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'PutBucketVersioningOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'PutBucketReplicationOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'PutBucketNotificationOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'PutBucketWebsiteOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'RequestId' => array(
+                            'location' => 'header', 
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'PutBucketAccelerateOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'PutBucketLoggingOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'PutBucketInventoryOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'PutBucketTaggingOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'RestoreObjectOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'RequestCharged' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-charged',
+                        ),
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'ListPartsOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'Bucket' => array(
+                            'type' => 'string',
+                            'location' => 'xml'
+                        ),
+                        'Key' => array(
+                            'type' => 'string',
+                            'location' => 'xml'
+                        ),
+                        'UploadId' => array(
+                            'type' => 'string',
+                            'location' => 'xml'
+                        ),
+                        'PartNumberMarker' => array(
+                            'type' => 'numeric',
+                            'location' => 'xml'
+                        ),
+                        'NextPartNumberMarker' => array(
+                            'type' => 'numeric',
+                            'location' => 'xml'
+                        ),
+                        'MaxParts' => array(
+                            'type' => 'numeric',
+                            'location' => 'xml'
+                        ),
+                        'IsTruncated' => array(
+                            'type' => 'boolean',
+                            'location' => 'xml'
+                        ),
+                        'Parts' => array(
+                            'type' => 'array',
+                            'location' => 'xml',
+                            'sentAs' => 'Part',
+                            'data' => array(
+                                'xmlFlattened' => true
+                            ),
+                            'items' => array(
+                                'type' => 'object',
+                                'properties' => array(
+                                    'PartNumber' => array(
+                                        'type' => 'numeric'
+                                    ),
+                                    'LastModified' => array(
+                                        'type' => 'string'
+                                    ),
+                                    'ETag' => array(
+                                        'type' => 'string'
+                                    ),
+                                    'Size' => array(
+                                        'type' => 'numeric'
+                                    )
+                                )
+                            )
+                        ),
+                        'Initiator' => array(
+                            'type' => 'object',
+                            'location' => 'xml',
+                            'properties' => array(
+                                'ID' => array(
+                                    'type' => 'string'
+                                ),
+                                'DisplayName' => array(
+                                    'type' => 'string'
+                                )
+                            )
+                        ),
+                        'Owner' => array(
+                            'type' => 'object',
+                            'location' => 'xml',
+                            'properties' => array(
+                                'DisplayName' => array(
+                                    'type' => 'string'
+                                ),
+                                'ID' => array(
+                                    'type' => 'string'
+                                )
+                            )
+                        ),
+                        'StorageClass' => array(
+                            'type' => 'string',
+                            'location' => 'xml'
+                        ),
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id'
+                        )
+                    )
+                ),
+                'ListObjectsOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'IsTruncated' => array(
+                            'type' => 'boolean',
+                            'location' => 'xml'
+                        ),
+                        'Marker' => array(
+                            'type' => 'string',
+                            'location' => 'xml'
+                        ),
+                        'NextMarker' => array(
+                            'type' => 'string',
+                            'location' => 'xml'
+                        ),
+                        'Contents' => array(
+                            'type' => 'array',
+                            'location' => 'xml',
+                            'data' => array(
+                                'xmlFlattened' => true
+                            ),
+                            'items' => array(
+                                'type' => 'object',
+                                'properties' => array(
+                                    'Key' => array(
+                                        'type' => 'string'
+                                    ),
+                                    'LastModified' => array(
+                                        'type' => 'string'
+                                    ),
+                                    'ETag' => array(
+                                        'type' => 'string'
+                                    ),
+                                    'Size' => array(
+                                        'type' => 'numeric'
+                                    ),
+                                    'StorageClass' => array(
+                                        'type' => 'string'
+                                    ),
+                                    'Owner' => array(
+                                        'type' => 'object',
+                                        'properties' => array(
+                                            'DisplayName' => array(
+                                                'type' => 'string'
+                                            ),
+                                            'ID' => array(
+                                                'type' => 'string'
+                                            )
+                                        )
+                                    )
+                                )
+                            )
+                        ),
+                        'Name' => array(
+                            'type' => 'string',
+                            'location' => 'xml'
+                        ),
+                        'Prefix' => array(
+                            'type' => 'string',
+                            'location' => 'xml'
+                        ),
+                        'Delimiter' => array(
+                            'type' => 'string',
+                            'location' => 'xml'
+                        ),
+                        'MaxKeys' => array(
+                            'type' => 'numeric',
+                            'location' => 'xml'
+                        ),
+                        'CommonPrefixes' => array(
+                            'type' => 'array',
+                            'location' => 'xml',
+                            'data' => array(
+                                'xmlFlattened' => true
+                            ),
+                            'items' => array(
+                                'type' => 'object',
+                                'properties' => array(
+                                    'Prefix' => array(
+                                        'type' => 'string'
+                                    )
+                                )
+                            )
+                        ),
+                        'EncodingType' => array(
+                            'type' => 'string',
+                            'location' => 'xml'),
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id'
+                        )
+                    )
+                ),
+                'ListBucketsOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'Buckets' => array(
+                            'type' => 'array',
+                            'location' => 'xml',
+                            'items' => array(
+                                'type' => 'object',
+                                'properties' => array(
+                                    'Name' => array(
+                                        'type' => 'string',
+                                    ),
+                                    'CreationDate' => array(
+                                        'type' => 'string',
+                                    ),
+                                ),
+                            ),
+                        ),
+                        'Owner' => array(
+                            'type' => 'object',
+                            'location' => 'xml',
+                            'properties' => array(
+                                'DisplayName' => array(
+                                    'type' => 'string',
+                                ),
+                                'ID' => array(
+                                    'type' => 'string',
+                                ),
+                            ),
+                        ),
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'ListObjectVersionsOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'IsTruncated' => array(
+                            'type' => 'boolean',
+                            'location' => 'xml',
+                        ),
+                        'KeyMarker' => array(
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                        'VersionIdMarker' => array(
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                        'NextKeyMarker' => array(
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                        'NextVersionIdMarker' => array(
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                        'Version' => array(
+                            'type' => 'array',
+                            'location' => 'xml',
+                            'data' => array(
+                                'xmlFlattened' => true,
+                            ),
+                            'items' => array(
+                                'type' => 'object',
+                                'properties' => array(
+                                    'ETag' => array(
+                                        'type' => 'string',
+                                    ),
+                                    'Size' => array(
+                                        'type' => 'numeric',
+                                    ),
+                                    'StorageClass' => array(
+                                        'type' => 'string',
+                                    ),
+                                    'Key' => array(
+                                        'type' => 'string',
+                                    ),
+                                    'VersionId' => array(
+                                        'type' => 'string',
+                                    ),
+                                    'IsLatest' => array(
+                                        'type' => 'boolean',
+                                    ),
+                                    'LastModified' => array(
+                                        'type' => 'string',
+                                    ),
+                                    'Owner' => array(
+                                        'type' => 'object',
+                                        'properties' => array(
+                                            'DisplayName' => array(
+                                                'type' => 'string',
+                                            ),
+                                            'ID' => array(
+                                                'type' => 'string',
+                                            ),
+                                        ),
+                                    ),
+                                ),
+                            ),
+                        ),
+                        'DeleteMarkers' => array(
+                            'type' => 'array',
+                            'location' => 'xml',
+                            'sentAs' => 'DeleteMarker',
+                            'data' => array(
+                                'xmlFlattened' => true,
+                            ),
+                            'items' => array(
+                                'type' => 'object',
+                                'properties' => array(
+                                    'Owner' => array(
+                                        'type' => 'object',
+                                        'properties' => array(
+                                            'DisplayName' => array(
+                                                'type' => 'string',
+                                            ),
+                                            'ID' => array(
+                                                'type' => 'string',
+                                            ),
+                                        ),
+                                    ),
+                                    'Key' => array(
+                                        'type' => 'string',
+                                    ),
+                                    'VersionId' => array(
+                                        'type' => 'string',
+                                    ),
+                                    'IsLatest' => array(
+                                        'type' => 'boolean',
+                                    ),
+                                    'LastModified' => array(
+                                        'type' => 'string',
+                                    ),
+                                ),
+                            ),
+                        ),
+                        'Name' => array(
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                        'Prefix' => array(
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                        'Delimiter' => array(
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                        'MaxKeys' => array(
+                            'type' => 'numeric',
+                            'location' => 'xml',
+                        ),
+                        'CommonPrefixes' => array(
+                            'type' => 'array',
+                            'location' => 'xml',
+                            'data' => array(
+                                'xmlFlattened' => true,
+                            ),
+                            'items' => array(
+                                'type' => 'object',
+                                'properties' => array(
+                                    'Prefix' => array(
+                                        'type' => 'string',
+                                    ),
+                                ),
+                            ),
+                        ),
+                        'EncodingType' => array(
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'ListMultipartUploadsOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'Bucket' => array(
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                        'KeyMarker' => array(
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                        'UploadIdMarker' => array(
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                        'NextKeyMarker' => array(
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                        'Prefix' => array(
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                        'Delimiter' => array(
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                        'NextUploadIdMarker' => array(
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                        'MaxUploads' => array(
+                            'type' => 'numeric',
+                            'location' => 'xml',
+                        ),
+                        'IsTruncated' => array(
+                            'type' => 'boolean',
+                            'location' => 'xml',
+                        ),
+                        'Uploads' => array(
+                            'type' => 'array',
+                            'location' => 'xml',
+                            'sentAs' => 'Upload',
+                            'data' => array(
+                                'xmlFlattened' => true,
+                            ),
+                            'items' => array(
+                                'type' => 'object',
+                                'properties' => array(
+                                    'UploadId' => array(
+                                        'type' => 'string',
+                                    ),
+                                    'Key' => array(
+                                        'type' => 'string',
+                                    ),
+                                    'Initiated' => array(
+                                        'type' => 'string',
+                                    ),
+                                    'StorageClass' => array(
+                                        'type' => 'string',
+                                    ),
+                                    'Owner' => array(
+                                        'type' => 'object',
+                                        'properties' => array(
+                                            'DisplayName' => array(
+                                                'type' => 'string',
+                                            ),
+                                            'ID' => array(
+                                                'type' => 'string',
+                                            ),
+                                        ),
+                                    ),
+                                    'Initiator' => array(
+                                        'type' => 'object',
+                                        'properties' => array(
+                                            'ID' => array(
+                                                'type' => 'string',
+                                            ),
+                                            'DisplayName' => array(
+                                                'type' => 'string',
+                                            ),
+                                        ),
+                                    ),
+                                ),
+                            ),
+                        ),
+                        'CommonPrefixes' => array(
+                            'type' => 'array',
+                            'location' => 'xml',
+                            'data' => array(
+                                'xmlFlattened' => true,
+                            ),
+                            'items' => array(
+                                'type' => 'object',
+                                'properties' => array(
+                                    'Prefix' => array(
+                                        'type' => 'string',
+                                    ),
+                                ),
+                            ),
+                        ),
+                        'EncodingType' => array(
+                            'type' => 'string',
+                            'location' => 'xml',
+                        ),
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                'HeadObjectOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'DeleteMarker' => array(
+                            'type' => 'boolean',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-delete-marker',
+                        ),
+                        'AcceptRanges' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'accept-ranges',
+                        ),
+                        'Expiration' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-expiration',
+                        ),
+                        'Restore' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-restore',
+                        ),
+                        'LastModified' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'Last-Modified',
+                        ),
+                        'ContentLength' => array(
+                            'type' => 'numeric',
+                            'minimum'=> 0,
+                            'location' => 'header',
+                            'sentAs' => 'Content-Length',
+                        ),
+                        'ETag' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                        ),
+                        'MissingMeta' => array(
+                            'type' => 'numeric',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-missing-meta',
+                        ),
+                        'VersionId' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-version-id',
+                        ),
+                        'CacheControl' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'Cache-Control',
+                        ),
+                        'ContentDisposition' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'Content-Disposition',
+                        ),
+                        'ContentEncoding' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'Content-Encoding',
+                        ),
+                        'ContentLanguage' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'Content-Language',
+                        ),
+                        'ContentType' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'Content-Type',
+                        ),
+                        'Expires' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                        ),
+                        'WebsiteRedirectLocation' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-website-redirect-location',
+                        ),
+                        'ServerSideEncryption' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption',
+                        ),
+                        'SSECustomerAlgorithm' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-customer-algorithm',
+                        ),
+                        'SSECustomerKeyMD5' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-customer-key-MD5',
+                        ),
+                        'SSEKMSKeyId' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-server-side-encryption-aws-kms-key-id',
+                        ),
+                        'StorageClass' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-storage-class',
+                        ),
+                        'RequestCharged' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-charged',
+                        ),
+                        'ReplicationStatus' => array(
+                            'type' => 'string',
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-replication-status',
+                        ),
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        )
+                    )
+                ),
+                'HeadBucketOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'RequestId' => array(
+                            'location' => 'header',
+                            'sentAs' => 'x-cos-request-id',
+                        ),
+                    ),
+                ),
+                
+                'SelectObjectContentOutput' => array(
+                    'type' => 'object',
+                    'additionalProperties' => true,
+                    'properties' => array(
+                        'RawData' => array(
+                            'type' => 'string',
+                            'instanceOf' => 'GuzzleHttp\\Psr7\\Stream',
+                            'location' => 'body',
+                        ),
+                    ),
+                ),
+            )
+        );
+    }
+}

+ 48 - 0
addons/cos/library/Qcloud/Cos/Signature.php

@@ -0,0 +1,48 @@
+<?php
+
+namespace Qcloud\Cos;
+
+use Psr\Http\Message\RequestInterface;
+
+class Signature {
+    private $accessKey;           // string: access key.
+    private $secretKey;           // string: secret key.
+    public function __construct($accessKey, $secretKey, $token=null) {
+        $this->accessKey = $accessKey;
+        $this->secretKey = $secretKey;
+        $this->token = $token;
+        date_default_timezone_set("PRC");
+    }
+    public function __destruct() {
+    }
+    public function signRequest(RequestInterface $request) {
+        $authorization = $this->createAuthorization($request);
+        return $request->withHeader('Authorization', $authorization);
+    }
+    public function createAuthorization(RequestInterface $request, $expires = "+30 minutes") {
+        if (is_null($expires)) {
+            $expires = "+30 minutes";
+        }
+        $signTime = (string)(time() - 60) . ';' . (string)(strtotime($expires));
+        $httpString = strtolower($request->getMethod()) . "\n" . urldecode($request->getUri()->getPath()) .
+            "\n\nhost=" . $request->getHeader("Host")[0]. "\n";
+        $sha1edHttpString = sha1($httpString);
+        $stringToSign = "sha1\n$signTime\n$sha1edHttpString\n";
+        $signKey = hash_hmac('sha1', $signTime, $this->secretKey);
+        $signature = hash_hmac('sha1', $stringToSign, $signKey);
+        $authorization = 'q-sign-algorithm=sha1&q-ak='. $this->accessKey .
+            "&q-sign-time=$signTime&q-key-time=$signTime&q-header-list=host&q-url-param-list=&" .
+            "q-signature=$signature";
+        return $authorization;
+    }
+    public function createPresignedUrl(RequestInterface $request, $expires = "+30 minutes") {
+        $authorization = $this->createAuthorization($request, $expires);
+        $uri = $request->getUri();
+        $query = "sign=".urlencode($authorization);
+        if ($this->token != null) {
+            $query = $query."&x-cos-security-token=".$this->token;
+        }
+        $uri = $uri->withQuery($query);
+        return $uri;
+    }
+}

+ 28 - 0
addons/cos/library/Qcloud/Cos/SignatureMiddleware.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace Qcloud\Cos;
+
+use Qcloud\Cos\Exception\ServiceResponseException;
+use GuzzleHttp\Promise\PromiseInterface;
+use GuzzleHttp\Psr7;
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
+use GuzzleHttp\Exception\RequestException;
+
+class SignatureMiddleware {
+    private $nextHandler;
+    protected $signature;
+
+    /**
+     * @param callable $nextHandler Next handler to invoke.
+     */
+    public function __construct(callable $nextHandler, $accessKey, $secretKey) {
+        $this->nextHandler = $nextHandler;
+        $this->signature = new Signature($accessKey, $secretKey);
+    }
+
+    public function __invoke(RequestInterface $request, array $options) {
+        $fn = $this->nextHandler;
+        return $fn($this->signature->signRequest($request), $options);
+	}
+}

+ 1682 - 0
addons/cos/library/Qcloud/Cos/Tests/Test.php

@@ -0,0 +1,1682 @@
+<?php
+
+namespace Qcloud\Cos\Tests;
+
+use Qcloud\Cos\Client;
+use Qcloud\Cos\Exception\ServiceResponseException;
+class COSTest extends \PHPUnit_Framework_TestCase
+{
+    const SYNC_TIME = 5;
+    private $cosClient;
+    private $bucket;
+    private $region;
+    protected function setUp()
+    {
+        $this->bucket = getenv('COS_BUCKET');
+        $this->region = getenv('COS_REGION');
+        $this->bucket2 = "tmp".$this->bucket;
+        $this->cosClient = new Client(array('region' => $this->region,
+            'credentials' => array(
+                'secretId' => getenv('COS_KEY'),
+                'secretKey' => getenv('COS_SECRET'))));
+        try {
+            $this->cosClient->createBucket(['Bucket' => $this->bucket]);
+        } catch(\Exception $e) {
+        }
+    }
+
+    protected function tearDown() {
+    }
+
+    function generateRandomString($length = 10) { 
+        $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; 
+        $randomString = ''; 
+        for ($i = 0; $i < $length; $i++) { 
+            $randomString .= $characters[rand(0, strlen($characters) - 1)]; 
+        } 
+        return $randomString; 
+    }
+
+    function generateRandomFile($size = 10, $filename = 'random-file') { 
+        exec("dd if=/dev/urandom of=". $filename. " bs=1 count=". (string)$size);
+    }
+    
+    /**********************************
+     * TestBucket
+     **********************************/
+    
+     /*
+     * put bucket,bucket已经存在
+     * BucketAlreadyOwnedByYou
+     * 409
+     */
+    public function testCreateExistingBucket()
+    {
+        try {
+            $this->cosClient->createBucket(['Bucket' => $this->bucket]);
+        } catch (ServiceResponseException $e) {
+            $this->assertTrue($e->getExceptionCode() === 'BucketAlreadyOwnedByYou' && $e->getStatusCode() === 409);
+        }
+    }
+
+    /*
+     * put bucket, 创建所有region的bucket
+     * 409
+     */
+    public function testValidRegionBucket()
+    {
+        $regionlist = array('cn-east','ap-shanghai',
+        'cn-south','ap-guangzhou',
+        'cn-north','ap-beijing-1',
+        'cn-southwest','ap-chengdu',
+        'sg','ap-singapore',
+        'tj','ap-beijing-1',
+        'bj','ap-beijing',
+        'sh','ap-shanghai',
+        'gz','ap-guangzhou',
+        'cd','ap-chengdu',
+        'sgp','ap-singapore');
+        foreach ($regionlist as$region) {
+            try {
+
+                $this->cosClient = new Client(array('region' => $region,
+                    'credentials' => array(
+                        'appId' => getenv('COS_APPID'),
+                        'secretId' => getenv('COS_KEY'),
+                        'secretKey' => getenv('COS_SECRET'))));
+                $this->cosClient->createBucket(['Bucket' => $this->bucket]);
+            } catch (ServiceResponseException $e) {
+                $this->assertEquals([$e->getStatusCode()], [409]);
+            }
+        }
+    }
+
+    /*
+     * put bucket, 不合法的region名
+     * 409
+     */
+    public function testInvalidRegionBucket()
+    {
+        $regionlist = array('cn-east-2','ap-shanghai-3');
+        foreach ($regionlist as$region) {
+            try {
+                $this->cosClient = new Client(array('region' => $region,
+                    'credentials' => array(
+                        'appId' => getenv('COS_APPID'),
+                        'secretId' => getenv('COS_KEY'),
+                        'secretKey' => getenv('COS_SECRET'))));
+                $this->cosClient->createBucket(['Bucket' => $this->bucket]);
+            } catch (ServiceResponseException $e) {
+                $this->assertFalse(TRUE);
+            } catch (\GuzzleHttp\Exception\ConnectException $e) {
+                $this->assertTrue(TRUE);
+            }
+        }
+    }
+
+    /*
+     * get Service
+     * 200
+     */
+    public function testGetService()
+    {
+        try {
+            $this->cosClient->ListBuckets();
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * put bucket,bucket名称非法
+     * InvalidBucketName
+     * 400
+     */
+    public function testCreateInvalidBucket()
+    {
+        try {
+            $this->cosClient->createBucket(array('Bucket' => 'qwe_123' . $this->bucket));
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertTrue($e->getExceptionCode() === 'InvalidBucketName' && $e->getStatusCode() === 400);
+        }
+    }
+
+    /*
+     * put bucket,设置bucket公公权限为private
+     * 200
+     */
+    public function testCreatePrivateBucket()
+    {
+        try {
+            $this->cosClient->createBucket(
+                array(
+                    'Bucket' => $this->bucket2,
+                    'ACL'=>'private'
+                ));
+            sleep(COSTest::SYNC_TIME);
+            TestHelper::nuke($this->bucket2);
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * put bucket,设置bucket公公权限为public-read
+     * 200
+     */
+    public function testCreatePublicReadBucket()
+    {
+        try {
+            TestHelper::nuke($this->bucket2);
+            sleep(COSTest::SYNC_TIME);
+            $this->cosClient->createBucket(
+                array(
+                    'Bucket' => $this->bucket2,
+                    'ACL'=>'public-read'
+                )
+            );
+            sleep(COSTest::SYNC_TIME);
+            TestHelper::nuke($this->bucket2);
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * put bucket,公共权限非法
+     * InvalidArgument
+     * 400
+     */
+    public function testCreateInvalidACLBucket()
+    {
+        try {
+            TestHelper::nuke($this->bucket2);
+            sleep(COSTest::SYNC_TIME);
+            $this->cosClient->createBucket(
+                array(
+                    'Bucket' => $this->bucket2,
+                    'ACL'=>'public'
+                )
+            );
+            sleep(COSTest::SYNC_TIME);
+            TestHelper::nuke($this->bucket2);
+        } catch (ServiceResponseException $e) {
+            $this->assertTrue($e->getExceptionCode() === 'InvalidArgument' && $e->getStatusCode() === 400);
+        }
+    }
+
+    /*
+     * put bucket acl,设置bucket公共权限为private
+     * 200
+     */
+    public function testPutBucketAclPrivate()
+    {
+        try {
+            $this->cosClient->PutBucketAcl(
+                array(
+                    'Bucket' => $this->bucket,
+                    'ACL'=>'private'
+                )
+            );
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * put bucket acl,设置bucket公共权限为public-read
+     * 200
+     */
+    public function testPutBucketAclPublicRead()
+    {
+        try {
+            $this->cosClient->PutBucketAcl(
+                array(
+                    'Bucket' => $this->bucket,
+                    'ACL'=>'public-read'
+                )
+            );
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * put bucket acl,公共权限非法
+     * InvalidArgument
+     * 400
+     */
+    public function testPutBucketAclInvalid()
+    {
+        try {
+            $this->cosClient->PutBucketAcl(
+                array(
+                    'Bucket' => $this->bucket,
+                    'ACL'=>'public'
+                )
+            );
+        } catch (ServiceResponseException $e) {
+            $this->assertTrue($e->getExceptionCode() === 'InvalidArgument' && $e->getStatusCode() === 400);
+        }
+    }
+
+    /*
+     * put bucket acl,设置bucket账号权限为grant-read
+     * 200
+     */
+    public function testPutBucketAclReadToUser()
+    {
+        try {
+            $this->cosClient->PutBucketAcl(
+                array(
+                    'Bucket' =>  $this->bucket,
+                    'GrantRead' => 'id="qcs::cam::uin/2779643970:uin/2779643970"'
+                )
+            );
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * put bucket acl,设置bucket账号权限为grant-write
+     * 200
+     */
+    public function testPutBucketAclWriteToUser()
+    {
+        try {
+            $this->cosClient->PutBucketAcl(
+                array(
+                    'Bucket' =>  $this->bucket,
+                    'GrantWrite' => 'id="qcs::cam::uin/2779643970:uin/2779643970"'
+                )
+            );
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * put bucket acl,设置bucket账号权限为grant-full-control
+     * 200
+     */
+    public function testPutBucketAclFullToUser()
+    {
+        try {
+            $this->cosClient->PutBucketAcl(
+                array(
+                    'Bucket' =>  $this->bucket,
+                    'GrantFullControl' => 'id="qcs::cam::uin/2779643970:uin/2779643970"'
+                )
+            );
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * put bucket acl,设置bucket账号权限,同时授权给多个账户
+     * 200
+     */
+    public function testPutBucketAclToUsers()
+    {
+        try {
+            $this->cosClient->PutBucketAcl(
+                array(
+                    'Bucket' =>  $this->bucket,
+                    'GrantFullControl' => 'id="qcs::cam::uin/2779643970:uin/2779643970",id="qcs::cam::uin/2779643970:uin/2779643970",id="qcs::cam::uin/2779643970:uin/2779643970"'
+                )
+            );
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * put bucket acl,设置bucket账号权限,授权给子账号
+     * 200
+     */
+    public function testPutBucketAclToSubuser()
+    {
+        try {
+            $this->cosClient->PutBucketAcl(
+                array(
+                    'Bucket' =>  $this->bucket,
+                    'GrantFullControl' => 'id="qcs::cam::uin/2779643970:uin/2779643970"'
+                )
+            );
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * put bucket acl,设置bucket账号权限,同时指定read、write和fullcontrol
+     * 200
+     */
+    public function testPutBucketAclReadWriteFull()
+    {
+        try {
+            $this->cosClient->PutBucketAcl(
+                array(
+                    'Bucket' =>  $this->bucket,
+                    'GrantRead' => 'id="qcs::cam::uin/123:uin/123"',
+                    'GrantWrite' => 'id="qcs::cam::uin/2779643970:uin/2779643970"',
+                    'GrantFullControl' => 'id="qcs::cam::uin/2779643970:uin/2779643970"'
+                )
+            );
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * put bucket acl,设置bucket账号权限,grant值非法
+     * InvalidArgument
+     * 400
+     */
+    public function testPutBucketAclInvalidGrant()
+    {
+        try {
+            $this->cosClient->PutBucketAcl(
+                array(
+                    'Bucket' =>  $this->bucket,
+                    'GrantFullControl' => 'id="qcs::camuin/321023:uin/2779643970"'
+                )
+            );
+        } catch (ServiceResponseException $e) {
+            $this->assertTrue($e->getExceptionCode() === 'InvalidArgument' && $e->getStatusCode() === 400);
+        }
+    }
+
+    /*
+     * put bucket acl,设置bucket账号权限,通过body方式授权
+     * 200
+     */
+    public function testPutBucketAclByBody()
+    {
+        try {
+            $this->cosClient->PutBucketAcl(
+                array(
+                    'Bucket' => $this->bucket,
+                    'Grants' => array(
+                        array(
+                            'Grantee' => array(
+                                'DisplayName' => 'qcs::cam::uin/2779643970:uin/2779643970',
+                                'ID' => 'qcs::cam::uin/2779643970:uin/2779643970',
+                                'Type' => 'CanonicalUser',
+                            ),
+                            'Permission' => 'FULL_CONTROL',
+                        ),
+                    ),
+                    'Owner' => array(
+                        'DisplayName' => 'qcs::cam::uin/2779643970:uin/2779643970',
+                        'ID' => 'qcs::cam::uin/2779643970:uin/2779643970',
+                    )
+                )
+            );
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * put bucket acl,设置bucket账号权限,通过body方式授权给anyone
+     * 200
+     */
+    public function testPutBucketAclByBodyToAnyone()
+    {
+        try {
+            $this->cosClient->PutBucketAcl(
+                array(
+                    'Bucket' => $this->bucket,
+                    'Grants' => array(
+                        array(
+                            'Grantee' => array(
+                                'DisplayName' => 'qcs::cam::anyone:anyone',
+                                'ID' => 'qcs::cam::anyone:anyone',
+                                'Type' => 'CanonicalUser',
+                            ),
+                            'Permission' => 'FULL_CONTROL',
+                        ),
+                    ),
+                    'Owner' => array(
+                        'DisplayName' => 'qcs::cam::uin/2779643970:uin/2779643970',
+                        'ID' => 'qcs::cam::uin/2779643970:uin/2779643970',
+                    )
+                )
+            );
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * put bucket acl,bucket不存在
+     * NoSuchBucket
+     * 404
+     */
+    public function testPutBucketAclBucketNonexisted()
+    {
+        try {
+            TestHelper::nuke($this->bucket2);
+            sleep(COSTest::SYNC_TIME);
+            $this->cosClient->PutBucketAcl(
+                array(
+                    'Bucket' =>  $this->bucket2,
+                    'GrantFullControl' => 'id="qcs::cam::uin/321023:uin/2779643970"'
+                )
+            );
+        } catch (ServiceResponseException $e) {
+            $this->assertTrue($e->getExceptionCode() === 'NoSuchBucket' && $e->getStatusCode() === 404);
+        }
+    }
+
+    /*
+     * put bucket acl,覆盖设置
+     * x200
+     */
+    public function testPutBucketAclCover()
+    {
+        try {
+            $this->cosClient->PutBucketAcl(array(
+                'Bucket' =>  $this->bucket,
+                'GrantFullControl' => 'id="qcs::cam::uin/2779643970:uin/2779643970"',
+                'GrantRead' => 'id="qcs::cam::uin/2779643970:uin/2779643970"',
+                'GrantWrite' => 'id="qcs::cam::uin/2779643970:uin/2779643970"'));
+            $this->cosClient->PutBucketAcl(array(
+                'Bucket' =>  $this->bucket,
+                'GrantWrite' => 'id="qcs::cam::uin/2779643970:uin/2779643970"'));
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * 正常head bucket
+     * 200
+     */
+    public function testHeadBucket()
+    {
+        try {
+            $this->cosClient->HeadBucket(array(
+                'Bucket' =>  $this->bucket));
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * head bucket,bucket不存在
+     * NoSuchBucket
+     * 404
+     */
+    public function testHeadBucketNonexisted()
+    {
+        try {
+            TestHelper::nuke($this->bucket2);
+            sleep(COSTest::SYNC_TIME);
+            $this->cosClient->HeadBucket(array(
+                'Bucket' =>  $this->bucket2));
+        } catch (ServiceResponseException $e) {
+            $this->assertTrue($e->getExceptionCode() === 'NoSuchBucket' && $e->getStatusCode() === 404);
+        }
+    }
+
+    /*
+     * get bucket,bucket为空
+     * 200
+     */
+    public function testGetBucketEmpty()
+    {
+        try {
+            $this->cosClient->ListObjects(array(
+                'Bucket' =>  $this->bucket));
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * get bucket,bucket不存在
+     * NoSuchBucket
+     * 404
+     */
+    public function testGetBucketNonexisted()
+    {
+        try {
+            TestHelper::nuke($this->bucket2);
+            sleep(COSTest::SYNC_TIME);
+            $this->cosClient->ListObjects(
+                array(
+                    'Bucket' =>  $this->bucket2
+                )
+            );
+        } catch (ServiceResponseException $e) {
+            $this->assertTrue($e->getExceptionCode() === 'NoSuchBucket' && $e->getStatusCode() === 404);
+        }
+    }
+
+
+    /*
+     * put bucket cors,cors规则包含多条
+     * 200
+     */
+    public function testPutBucketCors()
+    {
+        try {
+            $this->cosClient->putBucketCors(
+                array(
+                    'Bucket' => $this->bucket,
+                    'CORSRules' => array(
+                        array(
+                            'ID' => '1234',
+                            'AllowedHeaders' => array('*',),
+                            'AllowedMethods' => array('PUT',),
+                            'AllowedOrigins' => array('*',),
+                            'ExposeHeaders' => array('*',),
+                            'MaxAgeSeconds' => 1,
+                        ),
+                        array(
+                            'ID' => '12345',
+                            'AllowedHeaders' => array('*',),
+                            'AllowedMethods' => array('GET',),
+                            'AllowedOrigins' => array('*',),
+                            'ExposeHeaders' => array('*',),
+                            'MaxAgeSeconds' => 1,
+                        ),
+                    ),
+                )
+            );
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+
+    /*
+     * 正常get bucket cors
+     * 200
+     */
+    public function testGetBucketCors()
+    {
+        try {
+            $this->cosClient->putBucketCors(
+                array(
+                    'Bucket' => $this->bucket,
+                    'CORSRules' => array(
+                        array(
+                            'ID' => '1234',
+                            'AllowedHeaders' => array('*',),
+                            'AllowedMethods' => array('PUT',),
+                            'AllowedOrigins' => array('*',),
+                            'ExposeHeaders' => array('*',),
+                            'MaxAgeSeconds' => 1,
+                        ),
+                        array(
+                            'ID' => '12345',
+                            'AllowedHeaders' => array('*',),
+                            'AllowedMethods' => array('GET',),
+                            'AllowedOrigins' => array('*',),
+                            'ExposeHeaders' => array('*',),
+                            'MaxAgeSeconds' => 1,
+                        ),
+                    ),
+                )
+            );
+            $this->cosClient->getBucketCors(
+                array(
+                    'Bucket' => $this->bucket
+                )
+            );
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * bucket未设置cors规则,发送get bucket cors
+     * NoSuchCORSConfiguration
+     * 404
+     */
+    public function testGetBucketCorsNull()
+    {
+        try {
+            $this->cosClient->getBucketCors(
+                array(
+                    'Bucket' => $this->bucket
+                )
+            );
+        } catch (ServiceResponseException $e) {
+            $this->assertTrue($e->getExceptionCode() === 'NoSuchCORSConfiguration' && $e->getStatusCode() === 404);
+        }
+    }
+
+    /*
+     * 正常get bucket lifecycle
+     * 200
+     */
+    public function testGetBucketLifecycle()
+    {
+        try {
+            $result = $this->cosClient->putBucketLifecycle(
+                array(
+                    'Bucket' => $this->bucket,
+                    'Rules' => array(
+                        array(
+                            'Status' => 'Enabled',
+                            'Filter' => array(
+                                'Tag' => array(
+                                    'Key' => 'datalevel',
+                                    'Value' => 'backup'
+                                )
+                            ),
+                            'Transitions' => array(
+                                array(
+                                    # 30天后转换为Standard_IA
+                                    'Days' => 30,
+                                    'StorageClass' => 'Standard_IA'),
+                                array(
+                                    # 365天后转换为Archive
+                                    'Days' => 365,
+                                    'StorageClass' => 'Archive')
+                            ),
+                            'Expiration' => array(
+                                # 3650天后过期删除
+                                'Days' => 3650,
+                            )
+                        )
+                    )
+                )
+            );
+            $result = $this->cosClient->getBucketLifecycle(array(
+                'Bucket' => $this->bucket,
+            ));
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * 正常delete bucket lifecycle
+     * 200
+     */
+    public function testDeleteBucketLifecycle()
+    {
+        try {
+            $result = $this->cosClient->putBucketLifecycle(
+                array(
+                    'Bucket' => $this->bucket,
+                    'Rules' => array(
+                        array(
+                            'Status' => 'Enabled',
+                            'Filter' => array(
+                                'Tag' => array(
+                                    'Key' => 'datalevel',
+                                    'Value' => 'backup'
+                                )
+                            ),
+                            'Transitions' => array(
+                                array(
+                                    # 30天后转换为Standard_IA
+                                    'Days' => 30,
+                                    'StorageClass' => 'Standard_IA'),
+                                array(
+                                    # 365天后转换为Archive
+                                    'Days' => 365,
+                                    'StorageClass' => 'Archive')
+                            ),
+                            'Expiration' => array(
+                                # 3650天后过期删除
+                                'Days' => 3650,
+                            )
+                        )
+                    )
+                )
+            );
+            $result = $this->cosClient->deleteBucketLifecycle(array(
+                // Bucket is required
+                'Bucket' => $this->bucket,
+            ));
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * put bucket lifecycle,请求body中不指定filter
+     * 200
+     */
+    public function testPutBucketLifecycleNonFilter()
+    {
+        try {
+            $result = $this->cosClient->putBucketLifecycle(
+                array(
+                    'Bucket' => $this->bucket,
+                    'Rules' => array(
+                        array(
+                            'Expiration' => array(
+                                'Days' => 1000,
+                            ),
+                            'ID' => 'id1',
+                            'Status' => 'Enabled',
+                            'Transitions' => array(
+                                array(
+                                    'Days' => 100,
+                                    'StorageClass' => 'Standard_IA'),
+                            ),
+                        ),
+                    )
+                )
+            );
+        } catch (ServiceResponseException $e) {
+            $this->assertTrue($e->getExceptionCode() === 'NoSuchBucket' && $e->getStatusCode() === 404);
+
+        }
+    }
+
+    /*
+     * put bucket,bucket名称带有-
+     * 200
+     */
+    public function testPutBucket2()
+    {
+        try {
+            try{
+                $this->cosClient->deleteBucket(array('Bucket' => '12345-'.$this->bucket));
+            } catch (\Exception $e) {
+            }
+            $this->cosClient->createBucket(array('Bucket' => '12345-'.$this->bucket));
+            sleep(COSTest::SYNC_TIME);
+            $this->cosClient->deleteBucket(array('Bucket' => '12345-'.$this->bucket));
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * put bucket,bucket名称带有两个-
+     * 200
+     */
+    public function testPutBucket3()
+    {
+        try {
+            $this->cosClient->createBucket(array('Bucket' => '12-333-4445' . $this->bucket));
+            $this->cosClient->deleteBucket(array('Bucket' => '12-333-4445' . $this->bucket));
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * 正常get bucket location
+     * 200
+     */
+        public function testGetBucketLocation()
+    {
+        try {
+            $this->cosClient->getBucketLocation(array('Bucket' => $this->bucket));
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * bucket不存在,发送get bucket location请求
+     * NoSuchBucket
+     * 404
+     */
+    public function testGetBucketLocationNonExisted()
+    {
+        try {
+            TestHelper::nuke($this->bucket2);
+            sleep(COSTest::SYNC_TIME);
+            $this->cosClient->getBucketLocation(array('Bucket' => $this->bucket2));
+        } catch (ServiceResponseException $e) {
+            //            echo($e->getExceptionCode());
+            //            echo($e->getStatusCode());
+            $this->assertTrue($e->getExceptionCode() === 'NoSuchBucket' && $e->getStatusCode() === 404);
+        }
+    }
+
+    /**********************************
+     * TestObject
+     **********************************/
+
+    /*
+     * put object, 从本地上传文件
+     * 200
+     */
+    public function testPutObjectLocalObject() {
+        try {
+            $key = '你好.txt';
+            $body = $this->generateRandomString(1024+1023);
+            $md5 = base64_encode(md5($body, true));
+            $local_test_key = "local_test_file";
+            $f = fopen($local_test_key, "wb");
+            fwrite($f, $body);
+            fclose($f);
+            $this->cosClient->putObject(['Bucket' => $this->bucket,
+                                         'Key' => $key,
+                                         'Body' => fopen($local_test_key, "rb")]);
+            $rt = $this->cosClient->getObject(['Bucket'=>$this->bucket, 'Key'=>$key]);
+            $download_md5 = base64_encode(md5($rt['Body'], true));
+            $this->assertEquals($md5, $download_md5);
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * upload, 从本地上传
+     * 200
+     */
+    public function testUploadLocalObject() {
+        try {
+            $key = '你好.txt';
+            $body = $this->generateRandomString(1024+1023);
+            $md5 = base64_encode(md5($body, true));
+            $local_test_key = "local_test_file";
+            $f = fopen($local_test_key, "wb");
+            fwrite($f, $body);
+            fclose($f);
+            $this->cosClient->upload($bucket=$this->bucket,
+                                     $key=$key,
+                                     $body=fopen($local_test_key, "rb"),
+                                     $options=['PartSize'=>1024 * 1024 + 1]);
+            $rt = $this->cosClient->getObject(['Bucket'=>$this->bucket, 'Key'=>$key]);
+            $download_md5 = base64_encode(md5($rt['Body'], true));
+            $this->assertEquals($md5, $download_md5);
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * put object,请求头部携带服务端加密参数
+     * 200
+     */
+    public function testPutObjectEncryption()
+    {
+        try {
+            $this->cosClient->putObject(
+                array(
+                    'Bucket' => $this->bucket,
+                    'Key' => '11//32//43',
+                    'Body' => 'Hello World!',
+                    'ServerSideEncryption' => 'AES256'
+                )
+            );
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * 上传文件Bucket不存在
+     * NoSuchBucket
+     * 404
+     */
+    public function testPutObjectIntoNonexistedBucket() {
+        try {
+            TestHelper::nuke($this->bucket2);
+            sleep(COSTest::SYNC_TIME);
+            $this->cosClient->putObject(
+                array(
+                    'Bucket' => $this->bucket, 'Key' => 'hello.txt', 'Body' => 'Hello World'
+                )
+            );
+        } catch (ServiceResponseException $e) {
+            $this->assertTrue($e->getExceptionCode() === 'NoSuchBucket');
+            $this->assertTrue($e->getStatusCode() === 404);
+        }
+    }
+
+
+    /*
+     * 上传小文件
+     * 200
+     */
+    public function testUploadSmallObject() {
+        try {
+            $this->cosClient->upload($this->bucket, '你好.txt', 'Hello World');
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * 上传空文件
+     * 200
+     */
+    public function testPutObjectEmpty() {
+        try {
+            $this->cosClient->upload($this->bucket, '你好.txt', '');
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * 上传已存在的文件
+     * 200
+     */
+    public function testPutObjectExisted() {
+        try {
+            $this->cosClient->upload($this->bucket, '你好.txt', '1234124');
+            $this->cosClient->upload($this->bucket, '你好.txt', '请二位qwe');
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * put object,请求头部携带自定义头部x-cos-meta-
+     * 200
+     */
+    public function testPutObjectMeta() {
+        try {
+            $key = '你好.txt';
+            $meta = array(
+                'test' => str_repeat('a', 1 * 1024),
+                'test-meta' => 'qwe-23ds-ad-xcz.asd.*qweqw'
+            );
+            $this->cosClient->putObject(array(
+                'Bucket' => $this->bucket,
+                'Key' => '你好.txt',
+                'Body' => '1234124',
+                'Metadata' => $meta
+                     
+            ));
+            $rt = $this->cosClient->headObject(['Bucket'=>$this->bucket, 'Key'=>$key]);
+            $this->assertEquals($rt['Metadata'], $meta);
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * upload large object,请求头部携带自定义头部x-cos-meta-
+     * 200
+     */
+    public function testUploadLargeObjectMeta() {
+        try {
+            $key = '你好.txt';
+            $meta = array(
+                'test' => str_repeat('a', 1 * 1024),
+                'test-meta' => 'qwe-23ds-ad-xcz.asd.*qweqw'
+            );
+            $body = $this->generateRandomString(2*1024*1024+1023);
+            $this->cosClient->upload($bucket=$this->bucket,
+                                     $key=$key,
+                                     $body=$body,
+                                     $options=['PartSize'=>1024 * 1024 + 1, 'Metadata'=>$meta]);
+            $rt = $this->cosClient->headObject(['Bucket'=>$this->bucket, 'Key'=>$key]);
+            $this->assertEquals($rt['Metadata'], $meta);
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * put object,请求头部携带自定义头部x-cos-meta-
+     * KeyTooLong
+     * 400
+     */
+    public function testPutObjectMeta2K() {
+        try {
+            $this->cosClient->putObject(array(
+                'Bucket' => $this->bucket,
+                'Key' => '你好.txt',
+                'Body' => '1234124',
+                'Metadata' => array(
+                    'lew' => str_repeat('a', 3 * 1024),
+                )));
+        } catch (ServiceResponseException $e) {
+            $this->assertEquals(
+                [$e->getStatusCode(), $e->getExceptionCode()],
+                [400, 'KeyTooLong']
+            );
+            print $e;
+        }
+    }
+
+    /*
+     * 上传复杂文件名的文件
+     * 200
+     */
+    public function testUploadComplexObject() {
+        try {
+            $key = '→↓←→↖↗↙↘! \"#$%&\'()*+,-./0123456789:;<=>@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~';
+            $this->cosClient->upload($this->bucket, $key, 'Hello World');
+            $this->cosClient->headObject(array(
+                'Bucket' => $this->bucket,
+                'Key' => $key
+            ));
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * 上传大文件
+     * 200
+     */
+    public function testUploadLargeObject() {
+        try {
+            $key = '你好.txt';
+            $body = $this->generateRandomString(2*1024*1024+1023);
+            $md5 = base64_encode(md5($body, true));
+            $this->cosClient->upload($bucket=$this->bucket,
+                                     $key=$key,
+                                     $body=$body,
+                                     $options=['PartSize'=>1024 * 1024 + 1]);
+            $rt = $this->cosClient->getObject(['Bucket'=>$this->bucket, 'Key'=>$key]);
+            $download_md5 = base64_encode(md5($rt['Body'], true));
+            $this->assertEquals($md5, $download_md5);
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * 断点重传
+     * 200
+     */
+    public function testResumeUpload() {
+        try {
+            $key = '你好.txt';
+            $body = $this->generateRandomString(3*1024*1024+1023);
+            $partSize = 1024 * 1024 + 1;
+            $md5 = base64_encode(md5($body, true));
+            $rt = $this->cosClient->CreateMultipartUpload(['Bucket' => $this->bucket,
+                                                           'Key' => $key]);
+            $uploadId = $rt['UploadId'];
+            $this->cosClient->uploadPart(['Bucket' => $this->bucket,
+                                          'Key' => $key,
+                                          'Body' => substr($body, 0, $partSize),
+                                          'UploadId' => $uploadId,
+                                          'PartNumber' => 1]);
+            $rt = $this->cosClient->ListParts(['Bucket' => $this->bucket,
+                                          'Key' => $key,
+                                          'UploadId' => $uploadId]);
+            $this->assertEquals(count($rt['Parts']), 1);
+            $this->cosClient->resumeUpload($bucket=$this->bucket,
+                                           $key=$key,
+                                           $body=$body,
+                                           $uploadId=$uploadId,
+                                           $options=['PartSize'=>$partSize]);
+            $rt = $this->cosClient->getObject(['Bucket'=>$this->bucket, 'Key'=>$key]);
+            $download_md5 = base64_encode(md5($rt['Body'], true));
+            $this->assertEquals($md5, $download_md5);
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * 下载文件
+     * 200
+     */
+    public function testGetObject() {
+        try {
+            $this->cosClient->upload($this->bucket, '你好.txt', 'Hello World');
+            $this->cosClient->getObject(array(
+                'Bucket' => $this->bucket,
+                'Key' => '你好.txt',));
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * get object,object名称包含特殊字符
+     * 200
+     */
+    public function testGetObjectSpecialName() {
+        try {
+            $this->cosClient->upload($this->bucket, '你好<>!@#^%^&*&(&^!@#@!.txt', 'Hello World');
+            $this->cosClient->getObject(array(
+                'Bucket' => $this->bucket,
+                'Key' => '你好<>!@#^%^&*&(&^!@#@!.txt',));
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * get object,请求头部带if-match,参数值为true
+     * 200
+     */
+    public function testGetObjectIfMatchTrue() {
+        try {
+            $this->cosClient->upload($this->bucket, '你好.txt', 'Hello World');
+            $this->cosClient->getObject(array(
+                'Bucket' => $this->bucket,
+                'Key' => '你好.txt',
+                'IfMatch' => '"b10a8db164e0754105b7a99be72e3fe5"'));
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+
+    /*
+     * get object,请求头部带if-match,参数值为false
+     * PreconditionFailed
+     * 412
+     */
+    public function testGetObjectIfMatchFalse() {
+        try {
+            $this->cosClient->upload($this->bucket, '你好.txt', 'Hello World');
+            $this->cosClient->getObject(array(
+                'Bucket' => $this->bucket,
+                'Key' => '你好.txt',
+                'IfMatch' => '""'));
+        } catch (ServiceResponseException $e) {
+            $this->assertEquals(
+                [$e->getStatusCode(), $e->getExceptionCode()],
+                [412, 'PreconditionFailed']
+            );
+            print $e;
+        }
+    }
+
+    /*
+     * get object,请求头部带if-none-match,参数值为true
+     * 200
+     */
+    public function testGetObjectIfNoneMatchTrue() {
+        try {
+            $this->cosClient->upload($this->bucket, '你好.txt', 'Hello World');
+            $rt = $this->cosClient->getObject(array(
+                'Bucket' => $this->bucket,
+                'Key' => '你好.txt',
+                'IfNoneMatch' => '"b10a8db164e0754105b7a99be72e3fe5"'));
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+
+    /*
+     * get object,请求头部带if-none-match,参数值为false
+     * PreconditionFailed
+     * 412
+     */
+    public function testGetObjectIfNoneMatchFalse() {
+        try {
+            $this->cosClient->upload($this->bucket, '你好.txt', 'Hello World');
+            $this->cosClient->getObject(array(
+                'Bucket' => $this->bucket,
+                'Key' => '你好.txt',
+                'IfNoneMatch' => '""'));
+
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * 获取文件url
+     * 200
+     */
+    public function testGetObjectUrl() {
+        try{
+            $this->cosClient->getObjectUrl($this->bucket, 'hello.txt', '+10 minutes');
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * 复制小文件
+     * 200
+     */
+    public function testCopySmallObject() {
+        try{
+            $this->cosClient->upload($this->bucket, '你好.txt', 'Hello World');
+            $this->cosClient->copy($bucket=$this->bucket,
+                                   $key='hi.txt', 
+                                   $copySource = ['Bucket'=>$this->bucket,
+                                                  'Region'=>$this->region,
+                                                  'Key'=>'你好.txt']);
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * 复制大文件
+     * 200
+     */
+    public function testCopyLargeObject() {
+        try{
+            $src_key = '你好.txt';
+            $dst_key = 'hi.txt';
+            $body = $this->generateRandomString(2*1024*1024+333);
+            $md5 = base64_encode(md5($body, true));
+            $this->cosClient->upload($bucket=$this->bucket,
+                                     $key=$src_key,
+                                     $body=$body,
+                                     $options=['PartSize'=>1024 * 1024 + 1]);
+            $this->cosClient->copy($bucket=$this->bucket,
+                                   $key=$dst_key, 
+                                   $copySource = ['Bucket'=>$this->bucket,
+                                                  'Region'=>$this->region,
+                                                  'Key'=>$src_key],
+                                   $options=['PartSize'=>1024 * 1024 + 1]);
+            
+            $rt = $this->cosClient->getObject(['Bucket'=>$this->bucket, 'Key'=>$dst_key]);
+            $download_md5 = base64_encode(md5($rt['Body'], true));
+            $this->assertEquals($md5, $download_md5);
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * 设置objectacl
+     * 200
+     */
+    public function testPutObjectACL() {
+        try {
+            $this->cosClient->upload($this->bucket, '11', 'hello.txt');
+            $this->cosClient->PutObjectAcl(
+                    array(
+                    'Bucket' => $this->bucket,
+                    'Key' => '11',
+                    'Grants' => array(
+                        array(
+                            'Grantee' => array(
+                                'DisplayName' => 'qcs::cam::uin/2779643970:uin/2779643970',
+                                'ID' => 'qcs::cam::uin/2779643970:uin/2779643970',
+                                'Type' => 'CanonicalUser',
+                            ),
+                            'Permission' => 'FULL_CONTROL',
+                        ),
+                        // ... repeated
+                    ),
+                    'Owner' => array(
+                        'DisplayName' => 'qcs::cam::uin/2779643970:uin/2779643970',
+                        'ID' => 'qcs::cam::uin/2779643970:uin/2779643970',
+                    )
+                )
+            );
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+
+    }
+
+
+    /*
+     * 获取objectacl
+     * 200
+     */
+    public function testGetObjectACL()
+    {
+        try {
+            $this->cosClient->upload($this->bucket, '11', 'hello.txt');
+            $this->cosClient->PutObjectAcl(
+                array(
+                    'Bucket' => $this->bucket,
+                    'Key' => '11',
+                    'Grants' => array(
+                        array(
+                            'Grantee' => array(
+                                'DisplayName' => 'qcs::cam::uin/2779643970:uin/2779643970',
+                                'ID' => 'qcs::cam::uin/2779643970:uin/2779643970',
+                                'Type' => 'CanonicalUser',
+                            ),
+                            'Permission' => 'FULL_CONTROL',
+                        ),
+                    ),
+                    'Owner' => array(
+                        'DisplayName' => 'qcs::cam::uin/2779643970:uin/2779643970',
+                        'ID' => 'qcs::cam::uin/2779643970:uin/2779643970',
+                    )
+                )
+            );
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+        * put object acl,设置object公共权限为private
+        * 200
+        */
+    public function testPutObjectAclPrivate()
+    {
+        try {
+            $this->cosClient->putObject(array('Bucket' => $this->bucket,'Key' => '你好.txt', 'Body' => '123'));
+            $this->cosClient->PutObjectAcl(
+                array(
+                    'Bucket' => $this->bucket,
+                    'Key' => '你好.txt',
+                    'ACL'=>'private'
+                )
+            );
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * put object acl,设置object公共权限为public-read
+     * 200
+     */
+    public function testPutObjectAclPublicRead()
+    {
+        try {
+            $this->cosClient->putObject(array('Bucket' => $this->bucket,'Key' => '你好.txt', 'Body' => '123'));
+            $this->cosClient->PutObjectAcl(
+                array(
+                    'Bucket' => $this->bucket,
+                    'Key' => '你好.txt',
+                    'ACL'=>'public-read'
+                )
+            );
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * put object acl,公共权限非法
+     * InvalidArgument
+     * 400
+     */
+    public function testPutObjectAclInvalid()
+    {
+        try {
+            $this->cosClient->putObject(array('Bucket' => $this->bucket,'Key' => '你好.txt', 'Body' => '123'));
+            $this->cosClient->PutObjectAcl(
+                array(
+                    'Bucket' => $this->bucket,
+                    'Key' => '你好.txt',
+                    'ACL'=>'public'
+                )
+            );
+        } catch (ServiceResponseException $e) {
+            $this->assertTrue($e->getExceptionCode() === 'InvalidArgument' && $e->getStatusCode() === 400);
+        }
+    }
+
+    /*
+     * put object acl,设置object账号权限为grant-read
+     * 200
+     */
+    public function testPutObjectAclReadToUser()
+    {
+        try {
+            $this->cosClient->putObject(array('Bucket' => $this->bucket,'Key' => '你好.txt', 'Body' => '123'));
+            $this->cosClient->PutObjectAcl(
+                array(
+                    'Bucket' =>  $this->bucket,
+                    'Key' => '你好.txt',
+                    'GrantRead' => 'id="qcs::cam::uin/2779643970:uin/2779643970"'
+                )
+            );
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * put object acl,设置object账号权限为grant-full-control
+     * 200
+     */
+    public function testPutObjectAclFullToUser()
+    {
+        try {
+            $this->cosClient->putObject(array('Bucket' => $this->bucket,'Key' => '你好.txt', 'Body' => '123'));
+            $this->cosClient->PutObjectAcl(
+                array(
+                    'Bucket' =>  $this->bucket,
+                    'Key' => '你好.txt',
+                    'GrantFullControl' => 'id="qcs::cam::uin/2779643970:uin/2779643970"'
+                )
+            );
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * put object acl,设置object账号权限,同时授权给多个账户
+     * 200
+     */
+    public function testPutObjectAclToUsers()
+    {
+        try {
+            $this->cosClient->putObject(array('Bucket' => $this->bucket,'Key' => '你好.txt', 'Body' => '123'));
+            $this->cosClient->PutObjectAcl(
+                array(
+                    'Bucket' =>  $this->bucket,
+                    'Key' => '你好.txt',
+                    'GrantFullControl' => 'id="qcs::cam::uin/2779643970:uin/2779643970",id="qcs::cam::uin/2779643970:uin/2779643970",id="qcs::cam::uin/2779643970:uin/2779643970"'
+                )
+            );
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * put object acl,设置object账号权限,授权给子账号
+     * 200
+     */
+    public function testPutObjectAclToSubuser()
+    {
+        try {
+            $this->cosClient->putObject(array('Bucket' => $this->bucket,'Key' => '你好.txt', 'Body' => '123'));
+            $this->cosClient->PutObjectAcl(
+                array(
+                    'Bucket' =>  $this->bucket,
+                    'Key' => '你好.txt',
+                    'GrantFullControl' => 'id="qcs::cam::uin/2779643970:uin/2779643970"'
+                )
+            );
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * put object acl,设置object账号权限,grant值非法
+     * InvalidArgument
+     * 400
+     */
+    public function testPutObjectAclInvalidGrant()
+    {
+        try {
+            $this->cosClient->putObject(array('Bucket' => $this->bucket,'Key' => '你好.txt', 'Body' => '123'));
+            $this->cosClient->PutObjectAcl(
+                array(
+                    'Bucket' =>  $this->bucket,
+                    'Key' => '你好.txt',
+                    'GrantFullControl' => 'id="qcs::camuin/321023:uin/2779643970"'
+                )
+            );
+        } catch (ServiceResponseException $e) {
+            $this->assertTrue($e->getExceptionCode() === 'InvalidArgument' && $e->getStatusCode() === 400);
+        }
+    }
+
+    /*
+     * put object acl,设置object账号权限,通过body方式授权
+     * 200
+     */
+    public function testPutObjectAclByBody()
+    {
+        try {
+            $this->cosClient->putObject(array('Bucket' => $this->bucket,'Key' => '你好.txt', 'Body' => '123'));
+            $this->cosClient->PutObjectAcl(
+                array(
+                    'Bucket' => $this->bucket,
+                    'Key' => '你好.txt',
+                    'Grants' => array(
+                        array(
+                            'Grantee' => array(
+                                'DisplayName' => 'qcs::cam::uin/2779643970:uin/2779643970',
+                                'ID' => 'qcs::cam::uin/2779643970:uin/2779643970',
+                                'Type' => 'CanonicalUser',
+                            ),
+                            'Permission' => 'FULL_CONTROL',
+                        ),
+                        // ... repeated
+                    ),
+                    'Owner' => array(
+                        'DisplayName' => 'qcs::cam::uin/2779643970:uin/2779643970',
+                        'ID' => 'qcs::cam::uin/2779643970:uin/2779643970',
+                    )
+                )
+            );
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+     * put object acl,设置object账号权限,通过body方式授权给anyone
+     * 200
+     */
+    public function testPutObjectAclByBodyToAnyone()
+    {
+        try {
+            $this->cosClient->putObject(array('Bucket' => $this->bucket,'Key' => '你好.txt', 'Body' => '123'));
+            $this->cosClient->putObjectAcl(
+                array(
+                    'Bucket' => $this->bucket,
+                    'Key' => '你好.txt',
+                    'Grants' => array(
+                        array(
+                            'Grantee' => array(
+                                'DisplayName' => 'qcs::cam::anyone:anyone',
+                                'ID' => 'qcs::cam::anyone:anyone',
+                                'Type' => 'CanonicalUser',
+                            ),
+                            'Permission' => 'FULL_CONTROL',
+                        ),
+                        // ... repeated
+                    ),
+                    'Owner' => array(
+                        'DisplayName' => 'qcs::cam::uin/2779643970:uin/2779643970',
+                        'ID' => 'qcs::cam::uin/2779643970:uin/2779643970',
+                    )
+                )
+            );
+        } catch (ServiceResponseException $e) {
+            print $e;
+            $this->assertFalse(TRUE);
+        }
+    }
+
+    /*
+    * selectobject,select检索数据
+    * 200
+    */
+    public function testSelectObjectContent()
+    {
+        $key = '你好.txt';
+        try {
+            $body = "appid,bucket,region
+12500001,22weqwe,sh
+12500002,we2qwe,sh
+12500003,weq3we,sh
+12500004,weqw4e,sh
+3278522,azxc,gz
+4343,ewqew,tj";
+            $this->cosClient->putObject(array('Bucket' => $this->bucket,'Key' => $key, 'Body' => $body));
+            $result = $this->cosClient->selectObjectContent(array(
+                        'Bucket' => $this->bucket, //格式:BucketName-APPID
+                        'Key' => $key,
+                        'Expression' => 'Select * from COSObject s',
+                        'ExpressionType' => 'SQL',
+                        'InputSerialization' => array(
+                            'CompressionType' => 'None',
+                            'CSV' => array(
+                                'FileHeaderInfo' => 'USE',
+                                'RecordDelimiter' => '\n',
+                                'FieldDelimiter' => ',',
+                                'QuoteEscapeCharacter' => '"',
+                                'Comments' => '#',
+                                'AllowQuotedRecordDelimiter' => 'FALSE'
+                                )   
+                            ),  
+                        'OutputSerialization' => array(
+                            'CSV' => array(
+                                'QuoteField' => 'ASNEEDED',
+                                'RecordDelimiter' => '\n',
+                                'FieldDelimiter' => ',',
+                                'QuoteCharacter' => '"',
+                                'QuoteEscapeCharacter' => '"' 
+                                )   
+                            ),  
+                        'RequestProgress' => array(
+                                'Enabled' => 'FALSE'
+                                )   
+                            )); 
+            foreach ($result['Data'] as $data) {
+            }
+        } catch (\Exception $e) {
+            print ($e);
+            $this->assertFalse(TRUE);
+        }
+    }
+
+}

Some files were not shown because too many files changed in this diff