소스 검색

来自知音正式服务器

lizhen_gitee 10 달 전
커밋
f1537bb090
100개의 변경된 파일12713개의 추가작업 그리고 0개의 파일을 삭제
  1. 10 0
      .bowerrc
  2. 11 0
      .env.sample
  3. 26 0
      .gitignore
  4. 1 0
      .htaccess
  5. 191 0
      LICENSE
  6. 97 0
      README.md
  7. 1 0
      addons/.gitkeep
  8. 1 0
      addons/.htaccess
  9. 1 0
      addons/command/.addonrc
  10. 69 0
      addons/command/Command.php
  11. 4 0
      addons/command/config.php
  12. 15 0
      addons/command/controller/Index.php
  13. 10 0
      addons/command/info.ini
  14. 12 0
      addons/command/install.sql
  15. 28 0
      addons/command/library/Output.php
  16. 1 0
      addons/cos/.addonrc
  17. 124 0
      addons/cos/Cos.php
  18. 250 0
      addons/cos/bootstrap.js
  19. 229 0
      addons/cos/config.php
  20. 339 0
      addons/cos/controller/Index.php
  21. 10 0
      addons/cos/info.ini
  22. 19 0
      addons/cos/library/Guzzle/command/LICENSE
  23. 134 0
      addons/cos/library/Guzzle/command/README.md
  24. 36 0
      addons/cos/library/Guzzle/command/composer.json
  25. 55 0
      addons/cos/library/Guzzle/command/src/Command.php
  26. 39 0
      addons/cos/library/Guzzle/command/src/CommandInterface.php
  27. 7 0
      addons/cos/library/Guzzle/command/src/Exception/CommandClientException.php
  28. 109 0
      addons/cos/library/Guzzle/command/src/Exception/CommandException.php
  29. 7 0
      addons/cos/library/Guzzle/command/src/Exception/CommandServerException.php
  30. 60 0
      addons/cos/library/Guzzle/command/src/HasDataTrait.php
  31. 18 0
      addons/cos/library/Guzzle/command/src/Result.php
  32. 9 0
      addons/cos/library/Guzzle/command/src/ResultInterface.php
  33. 217 0
      addons/cos/library/Guzzle/command/src/ServiceClient.php
  34. 92 0
      addons/cos/library/Guzzle/command/src/ServiceClientInterface.php
  35. 16 0
      addons/cos/library/Guzzle/command/src/ToArrayInterface.php
  36. 4 0
      addons/cos/library/Guzzle/guzzle-services/.gitignore
  37. 23 0
      addons/cos/library/Guzzle/guzzle-services/.travis.yml
  38. 351 0
      addons/cos/library/Guzzle/guzzle-services/CHANGELOG.md
  39. 19 0
      addons/cos/library/Guzzle/guzzle-services/LICENSE
  40. 15 0
      addons/cos/library/Guzzle/guzzle-services/Makefile
  41. 129 0
      addons/cos/library/Guzzle/guzzle-services/README.md
  42. 49 0
      addons/cos/library/Guzzle/guzzle-services/composer.json
  43. 14 0
      addons/cos/library/Guzzle/guzzle-services/phpunit.xml.dist
  44. 265 0
      addons/cos/library/Guzzle/guzzle-services/src/Description.php
  45. 107 0
      addons/cos/library/Guzzle/guzzle-services/src/DescriptionInterface.php
  46. 294 0
      addons/cos/library/Guzzle/guzzle-services/src/Deserializer.php
  47. 169 0
      addons/cos/library/Guzzle/guzzle-services/src/GuzzleClient.php
  48. 82 0
      addons/cos/library/Guzzle/guzzle-services/src/Handler/ValidatedDescriptionHandler.php
  49. 312 0
      addons/cos/library/Guzzle/guzzle-services/src/Operation.php
  50. 655 0
      addons/cos/library/Guzzle/guzzle-services/src/Parameter.php
  51. 13 0
      addons/cos/library/Guzzle/guzzle-services/src/QuerySerializer/QuerySerializerInterface.php
  52. 33 0
      addons/cos/library/Guzzle/guzzle-services/src/QuerySerializer/Rfc3986Serializer.php
  53. 101 0
      addons/cos/library/Guzzle/guzzle-services/src/RequestLocation/AbstractLocation.php
  54. 49 0
      addons/cos/library/Guzzle/guzzle-services/src/RequestLocation/BodyLocation.php
  55. 84 0
      addons/cos/library/Guzzle/guzzle-services/src/RequestLocation/FormParamLocation.php
  56. 67 0
      addons/cos/library/Guzzle/guzzle-services/src/RequestLocation/HeaderLocation.php
  57. 85 0
      addons/cos/library/Guzzle/guzzle-services/src/RequestLocation/JsonLocation.php
  58. 76 0
      addons/cos/library/Guzzle/guzzle-services/src/RequestLocation/MultiPartLocation.php
  59. 92 0
      addons/cos/library/Guzzle/guzzle-services/src/RequestLocation/QueryLocation.php
  60. 44 0
      addons/cos/library/Guzzle/guzzle-services/src/RequestLocation/RequestLocationInterface.php
  61. 328 0
      addons/cos/library/Guzzle/guzzle-services/src/RequestLocation/XmlLocation.php
  62. 69 0
      addons/cos/library/Guzzle/guzzle-services/src/ResponseLocation/AbstractLocation.php
  63. 39 0
      addons/cos/library/Guzzle/guzzle-services/src/ResponseLocation/BodyLocation.php
  64. 47 0
      addons/cos/library/Guzzle/guzzle-services/src/ResponseLocation/HeaderLocation.php
  65. 176 0
      addons/cos/library/Guzzle/guzzle-services/src/ResponseLocation/JsonLocation.php
  66. 41 0
      addons/cos/library/Guzzle/guzzle-services/src/ResponseLocation/ReasonPhraseLocation.php
  67. 61 0
      addons/cos/library/Guzzle/guzzle-services/src/ResponseLocation/ResponseLocationInterface.php
  68. 39 0
      addons/cos/library/Guzzle/guzzle-services/src/ResponseLocation/StatusCodeLocation.php
  69. 311 0
      addons/cos/library/Guzzle/guzzle-services/src/ResponseLocation/XmlLocation.php
  70. 141 0
      addons/cos/library/Guzzle/guzzle-services/src/SchemaFormatter.php
  71. 297 0
      addons/cos/library/Guzzle/guzzle-services/src/SchemaValidator.php
  72. 164 0
      addons/cos/library/Guzzle/guzzle-services/src/Serializer.php
  73. 13 0
      addons/cos/library/Guzzle/guzzle-services/tests/Asset/Exception/CustomCommandException.php
  74. 13 0
      addons/cos/library/Guzzle/guzzle-services/tests/Asset/Exception/OtherCustomCommandException.php
  75. 10 0
      addons/cos/library/Guzzle/guzzle-services/tests/Asset/test.html
  76. 184 0
      addons/cos/library/Guzzle/guzzle-services/tests/DescriptionTest.php
  77. 386 0
      addons/cos/library/Guzzle/guzzle-services/tests/DeserializerTest.php
  78. 1037 0
      addons/cos/library/Guzzle/guzzle-services/tests/GuzzleClientTest.php
  79. 112 0
      addons/cos/library/Guzzle/guzzle-services/tests/Handler/ValidatedDescriptionHandlerTest.php
  80. 227 0
      addons/cos/library/Guzzle/guzzle-services/tests/OperationTest.php
  81. 378 0
      addons/cos/library/Guzzle/guzzle-services/tests/ParameterTest.php
  82. 35 0
      addons/cos/library/Guzzle/guzzle-services/tests/QuerySerializer/Rfc3986SerializerTest.php
  83. 26 0
      addons/cos/library/Guzzle/guzzle-services/tests/RequestLocation/BodyLocationTest.php
  84. 52 0
      addons/cos/library/Guzzle/guzzle-services/tests/RequestLocation/FormParamLocationTest.php
  85. 52 0
      addons/cos/library/Guzzle/guzzle-services/tests/RequestLocation/HeaderLocationTest.php
  86. 91 0
      addons/cos/library/Guzzle/guzzle-services/tests/RequestLocation/JsonLocationTest.php
  87. 33 0
      addons/cos/library/Guzzle/guzzle-services/tests/RequestLocation/MultiPartLocationTest.php
  88. 77 0
      addons/cos/library/Guzzle/guzzle-services/tests/RequestLocation/QueryLocationTest.php
  89. 525 0
      addons/cos/library/Guzzle/guzzle-services/tests/RequestLocation/XmlLocationTest.php
  90. 30 0
      addons/cos/library/Guzzle/guzzle-services/tests/ResponseLocation/BodyLocationTest.php
  91. 31 0
      addons/cos/library/Guzzle/guzzle-services/tests/ResponseLocation/HeaderLocationTest.php
  92. 581 0
      addons/cos/library/Guzzle/guzzle-services/tests/ResponseLocation/JsonLocationTest.php
  93. 30 0
      addons/cos/library/Guzzle/guzzle-services/tests/ResponseLocation/ReasonPhraseLocationTest.php
  94. 27 0
      addons/cos/library/Guzzle/guzzle-services/tests/ResponseLocation/StatusCodeLocationTest.php
  95. 795 0
      addons/cos/library/Guzzle/guzzle-services/tests/ResponseLocation/XmlLocationTest.php
  96. 60 0
      addons/cos/library/Guzzle/guzzle-services/tests/SchemaFormatterTest.php
  97. 330 0
      addons/cos/library/Guzzle/guzzle-services/tests/SchemaValidatorTest.php
  98. 39 0
      addons/cos/library/Guzzle/guzzle-services/tests/SerializerTest.php
  99. 283 0
      addons/cos/library/Qcloud/Cos/Client.php
  100. 163 0
      addons/cos/library/Qcloud/Cos/CommandToRequestTransformer.php

+ 10 - 0
.bowerrc

@@ -0,0 +1,10 @@
+{
+  "directory": "public/assets/libs",
+  "ignoredDependencies": [
+    "es6-promise",
+    "file-saver",
+    "html2canvas",
+    "jspdf",
+    "jspdf-autotable"
+  ]
+}

+ 11 - 0
.env.sample

@@ -0,0 +1,11 @@
+[app]
+debug = false
+trace = false
+
+[database]
+hostname = 127.0.0.1
+database = fastadmin
+username = root
+password = root
+hostport = 3306
+prefix = fa_

+ 26 - 0
.gitignore

@@ -0,0 +1,26 @@
+.idea
+资料
+/application/extra/site.php
+/nbproject/
+/paylog/*
+/runtime/*
+/vendor/
+/public/uploads/*
+/public/nginx.htaccess
+.git/
+.well-known
+robots.txt
+composer.lock
+*.log
+*.css.map
+!.gitkeep
+.env
+.svn
+.vscode
+node_modules
+.user.ini
+/public/*.sql
+/*.sql
+*.zip
+*.tar.gz
+*.rar

+ 1 - 0
.htaccess

@@ -0,0 +1 @@
+ 

+ 191 - 0
LICENSE

@@ -0,0 +1,191 @@
+Apache License
+Version 2.0, January 2004
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction, and
+distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by the copyright
+owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all other entities
+that control, are controlled by, or are under common control with that entity.
+For the purposes of this definition, "control" means (i) the power, direct or
+indirect, to cause the direction or management of such entity, whether by
+contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
+outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity exercising
+permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications, including
+but not limited to software source code, documentation source, and configuration
+files.
+
+"Object" form shall mean any form resulting from mechanical transformation or
+translation of a Source form, including but not limited to compiled object code,
+generated documentation, and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or Object form, made
+available under the License, as indicated by a copyright notice that is included
+in or attached to the work (an example is provided in the Appendix below).
+
+"Derivative Works" shall mean any work, whether in Source or Object form, that
+is based on (or derived from) the Work and for which the editorial revisions,
+annotations, elaborations, or other modifications represent, as a whole, an
+original work of authorship. For the purposes of this License, Derivative Works
+shall not include works that remain separable from, or merely link (or bind by
+name) to the interfaces of, the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including the original version
+of the Work and any modifications or additions to that Work or Derivative Works
+thereof, that is intentionally submitted to Licensor for inclusion in the Work
+by the copyright owner or by an individual or Legal Entity authorized to submit
+on behalf of the copyright owner. For the purposes of this definition,
+"submitted" means any form of electronic, verbal, or written communication sent
+to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control systems, and
+issue tracking systems that are managed by, or on behalf of, the Licensor for
+the purpose of discussing and improving the Work, but excluding communication
+that is conspicuously marked or otherwise designated in writing by the copyright
+owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
+of whom a Contribution has been received by Licensor and subsequently
+incorporated within the Work.
+
+2. Grant of Copyright License.
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable copyright license to reproduce, prepare Derivative Works of,
+publicly display, publicly perform, sublicense, and distribute the Work and such
+Derivative Works in Source or Object form.
+
+3. Grant of Patent License.
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable (except as stated in this section) patent license to make, have
+made, use, offer to sell, sell, import, and otherwise transfer the Work, where
+such license applies only to those patent claims licensable by such Contributor
+that are necessarily infringed by their Contribution(s) alone or by combination
+of their Contribution(s) with the Work to which such Contribution(s) was
+submitted. If You institute patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Work or a
+Contribution incorporated within the Work constitutes direct or contributory
+patent infringement, then any patent licenses granted to You under this License
+for that Work shall terminate as of the date such litigation is filed.
+
+4. Redistribution.
+
+You may reproduce and distribute copies of the Work or Derivative Works thereof
+in any medium, with or without modifications, and in Source or Object form,
+provided that You meet the following conditions:
+
+You must give any other recipients of the Work or Derivative Works a copy of
+this License; and
+You must cause any modified files to carry prominent notices stating that You
+changed the files; and
+You must retain, in the Source form of any Derivative Works that You distribute,
+all copyright, patent, trademark, and attribution notices from the Source form
+of the Work, excluding those notices that do not pertain to any part of the
+Derivative Works; and
+If the Work includes a "NOTICE" text file as part of its distribution, then any
+Derivative Works that You distribute must include a readable copy of the
+attribution notices contained within such NOTICE file, excluding those notices
+that do not pertain to any part of the Derivative Works, in at least one of the
+following places: within a NOTICE text file distributed as part of the
+Derivative Works; within the Source form or documentation, if provided along
+with the Derivative Works; or, within a display generated by the Derivative
+Works, if and wherever such third-party notices normally appear. The contents of
+the NOTICE file are for informational purposes only and do not modify the
+License. You may add Your own attribution notices within Derivative Works that
+You distribute, alongside or as an addendum to the NOTICE text from the Work,
+provided that such additional attribution notices cannot be construed as
+modifying the License.
+You may add Your own copyright statement to Your modifications and may provide
+additional or different license terms and conditions for use, reproduction, or
+distribution of Your modifications, or for any such Derivative Works as a whole,
+provided Your use, reproduction, and distribution of the Work otherwise complies
+with the conditions stated in this License.
+
+5. Submission of Contributions.
+
+Unless You explicitly state otherwise, any Contribution intentionally submitted
+for inclusion in the Work by You to the Licensor shall be under the terms and
+conditions of this License, without any additional terms or conditions.
+Notwithstanding the above, nothing herein shall supersede or modify the terms of
+any separate license agreement you may have executed with Licensor regarding
+such Contributions.
+
+6. Trademarks.
+
+This License does not grant permission to use the trade names, trademarks,
+service marks, or product names of the Licensor, except as required for
+reasonable and customary use in describing the origin of the Work and
+reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty.
+
+Unless required by applicable law or agreed to in writing, Licensor provides the
+Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
+including, without limitation, any warranties or conditions of TITLE,
+NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
+solely responsible for determining the appropriateness of using or
+redistributing the Work and assume any risks associated with Your exercise of
+permissions under this License.
+
+8. Limitation of Liability.
+
+In no event and under no legal theory, whether in tort (including negligence),
+contract, or otherwise, unless required by applicable law (such as deliberate
+and grossly negligent acts) or agreed to in writing, shall any Contributor be
+liable to You for damages, including any direct, indirect, special, incidental,
+or consequential damages of any character arising as a result of this License or
+out of the use or inability to use the Work (including but not limited to
+damages for loss of goodwill, work stoppage, computer failure or malfunction, or
+any and all other commercial damages or losses), even if such Contributor has
+been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability.
+
+While redistributing the Work or Derivative Works thereof, You may choose to
+offer, and charge a fee for, acceptance of support, warranty, indemnity, or
+other liability obligations and/or rights consistent with this License. However,
+in accepting such obligations, You may act only on Your own behalf and on Your
+sole responsibility, not on behalf of any other Contributor, and only if You
+agree to indemnify, defend, and hold each Contributor harmless for any liability
+incurred by, or claims asserted against, such Contributor by reason of your
+accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work
+
+To apply the Apache License to your work, attach the following boilerplate
+notice, with the fields enclosed by brackets "{}" replaced with your own
+identifying information. (Don't include the brackets!) The text should be
+enclosed in the appropriate comment syntax for the file format. We also
+recommend that a file or class name and description of purpose be included on
+the same "printed page" as the copyright notice for easier identification within
+third-party archives.
+
+   Copyright 2017 Karson
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 97 - 0
README.md

@@ -0,0 +1,97 @@
+FastAdmin是一款基于ThinkPHP+Bootstrap的极速后台开发框架。
+
+
+## 主要特性
+
+* 基于`Auth`验证的权限管理系统
+    * 支持无限级父子级权限继承,父级的管理员可任意增删改子级管理员及权限设置
+    * 支持单管理员多角色
+    * 支持管理子级数据或个人数据
+* 强大的一键生成功能
+    * 一键生成CRUD,包括控制器、模型、视图、JS、语言包、菜单、回收站等
+    * 一键压缩打包JS和CSS文件,一键CDN静态资源部署
+    * 一键生成控制器菜单和规则
+    * 一键生成API接口文档
+* 完善的前端功能组件开发
+    * 基于`AdminLTE`二次开发
+    * 基于`Bootstrap`开发,自适应手机、平板、PC
+    * 基于`RequireJS`进行JS模块管理,按需加载
+    * 基于`Less`进行样式开发
+* 强大的插件扩展功能,在线安装卸载升级插件
+* 通用的会员模块和API模块
+* 共用同一账号体系的Web端会员中心权限验证和API接口会员权限验证
+* 二级域名部署支持,同时域名支持绑定到应用插件
+* 多语言支持,服务端及客户端支持
+* 支持大文件分片上传、剪切板粘贴上传、拖拽上传,进度条显示,图片上传前压缩
+* 支持表格固定列、固定表头、跨页选择、Excel导出、模板渲染等功能
+* 强大的第三方应用模块支持([CMS](https://www.fastadmin.net/store/cms.html)、[博客](https://www.fastadmin.net/store/blog.html)、[知识付费问答](https://www.fastadmin.net/store/ask.html)、[在线投票系统](https://www.fastadmin.net/store/vote.html)、[B2C商城](https://www.fastadmin.net/store/shopro.html)、[B2B2C商城](https://www.fastadmin.net/store/wanlshop.html))
+* 支持CMS、博客、知识付费问答无缝整合[Xunsearch全文搜索](https://www.fastadmin.net/store/xunsearch.html)
+* 第三方小程序支持([CMS小程序](https://www.fastadmin.net/store/cms.html)、[预订小程序](https://www.fastadmin.net/store/ball.html)、[问答小程序](https://www.fastadmin.net/store/ask.html)、[点餐小程序](https://www.fastadmin.net/store/unidrink.html)、[B2C小程序](https://www.fastadmin.net/store/shopro.html)、[B2B2C小程序](https://www.fastadmin.net/store/wanlshop.html)、[博客小程序](https://www.fastadmin.net/store/blog.html))
+* 整合第三方短信接口(阿里云、腾讯云短信)
+* 无缝整合第三方云存储(七牛云、阿里云OSS、又拍云)功能,支持云储存分片上传
+* 第三方富文本编辑器支持(Summernote、Kindeditor、百度编辑器)
+* 第三方登录(QQ、微信、微博)整合
+* 第三方支付(微信、支付宝)无缝整合,微信支持PC端扫码支付
+* 丰富的插件应用市场
+
+## 安装使用
+
+https://doc.fastadmin.net
+
+## 在线演示
+
+https://demo.fastadmin.net
+
+用户名:admin
+
+密 码:123456
+
+提 示:演示站数据无法进行修改,请下载源码安装体验全部功能
+
+## 界面截图
+![控制台](https://images.gitee.com/uploads/images/2020/0929/202947_8db2d281_10933.gif "控制台")
+
+## 问题反馈
+
+在使用中有任何问题,请使用以下联系方式联系我们
+
+交流社区: https://ask.fastadmin.net
+
+QQ群: [636393962](https://jq.qq.com/?_wv=1027&k=487PNBb)(满) [708784003](https://jq.qq.com/?_wv=1027&k=5ObjtwM)(满) [964776039](https://jq.qq.com/?_wv=1027&k=59qjU2P)(3群) [749803490](https://jq.qq.com/?_wv=1027&k=5tczi88)(满) [767103006](https://jq.qq.com/?_wv=1027&k=5Z1U751)(满) [675115483](https://jq.qq.com/?_wv=1027&k=54I6mts)(6群)
+
+Github: https://github.com/karsonzhang/fastadmin
+
+Gitee: https://gitee.com/karson/fastadmin
+
+## 特别鸣谢
+
+感谢以下的项目,排名不分先后
+
+ThinkPHP:http://www.thinkphp.cn
+
+AdminLTE:https://adminlte.io
+
+Bootstrap:http://getbootstrap.com
+
+jQuery:http://jquery.com
+
+Bootstrap-table:https://github.com/wenzhixin/bootstrap-table
+
+Nice-validator: https://validator.niceue.com
+
+SelectPage: https://github.com/TerryZ/SelectPage
+
+Layer: https://layer.layui.com
+
+DropzoneJS: https://www.dropzonejs.com
+
+
+## 版权信息
+
+FastAdmin遵循Apache2开源协议发布,并提供免费使用。
+
+本项目包含的第三方源码和二进制文件之版权信息另行标注。
+
+版权所有Copyright © 2017-2020 by FastAdmin (https://www.fastadmin.net)
+
+All rights reserved。

+ 1 - 0
addons/.gitkeep

@@ -0,0 +1 @@
+

+ 1 - 0
addons/.htaccess

@@ -0,0 +1 @@
+deny from all

+ 1 - 0
addons/command/.addonrc

@@ -0,0 +1 @@
+{"license":"regular","licenseto":"38239","licensekey":"XZ9eNi3Dd0SLvqlY o4rL8AssI1AAPGnP4uL\/ow==","menus":["command","command\/index","command\/add","command\/detail","command\/execute","command\/del","command\/multi"],"files":["application\\admin\\controller\\Command.php","application\\admin\\lang\\zh-cn\\command.php","application\\admin\\model\\Command.php","application\\admin\\validate\\Command.php","application\\admin\\view\\command\\add.html","application\\admin\\view\\command\\detail.html","application\\admin\\view\\command\\index.html","public\\assets\\js\\backend\\command.js"]}

+ 69 - 0
addons/command/Command.php

@@ -0,0 +1,69 @@
+<?php
+
+namespace addons\command;
+
+use app\common\library\Menu;
+use think\Addons;
+
+/**
+ * 在线命令插件
+ */
+class Command extends Addons
+{
+
+    /**
+     * 插件安装方法
+     * @return bool
+     */
+    public function install()
+    {
+        $menu = [
+            [
+                'name'    => 'command',
+                'title'   => '在线命令管理',
+                'icon'    => 'fa fa-terminal',
+                'sublist' => [
+                    ['name' => 'command/index', 'title' => '查看'],
+                    ['name' => 'command/add', 'title' => '添加'],
+                    ['name' => 'command/detail', 'title' => '详情'],
+                    ['name' => 'command/execute', 'title' => '运行'],
+                    ['name' => 'command/del', 'title' => '删除'],
+                    ['name' => 'command/multi', 'title' => '批量更新'],
+                ]
+            ]
+        ];
+        Menu::create($menu);
+        return true;
+    }
+
+    /**
+     * 插件卸载方法
+     * @return bool
+     */
+    public function uninstall()
+    {
+        Menu::delete('command');
+        return true;
+    }
+
+    /**
+     * 插件启用方法
+     * @return bool
+     */
+    public function enable()
+    {
+        Menu::enable('command');
+        return true;
+    }
+
+    /**
+     * 插件禁用方法
+     * @return bool
+     */
+    public function disable()
+    {
+        Menu::disable('command');
+        return true;
+    }
+
+}

+ 4 - 0
addons/command/config.php

@@ -0,0 +1,4 @@
+<?php
+
+return [
+];

+ 15 - 0
addons/command/controller/Index.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace addons\command\controller;
+
+use think\addons\Controller;
+
+class Index extends Controller
+{
+
+    public function index()
+    {
+        $this->error("当前插件暂无前台页面");
+    }
+
+}

+ 10 - 0
addons/command/info.ini

@@ -0,0 +1,10 @@
+name = command
+title = 在线命令
+intro = 可在线执行FastAdmin的命令行相关命令
+author = Karson
+website = https://www.fastadmin.net
+version = 1.0.6
+state = 1
+url = /addons/command
+license = regular
+licenseto = 38239

+ 12 - 0
addons/command/install.sql

@@ -0,0 +1,12 @@
+CREATE TABLE IF NOT EXISTS `__PREFIX__command`  (
+  `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `type` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '类型',
+  `params` varchar(1500) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '参数',
+  `command` varchar(1500) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '命令',
+  `content` text COMMENT '返回结果',
+  `executetime` int(10) UNSIGNED DEFAULT NULL COMMENT '执行时间',
+  `createtime` int(10) UNSIGNED DEFAULT NULL COMMENT '创建时间',
+  `updatetime` int(10) UNSIGNED DEFAULT NULL COMMENT '更新时间',
+  `status` enum('successed','failured') CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'failured' COMMENT '状态',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '在线命令表';

+ 28 - 0
addons/command/library/Output.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace addons\command\library;
+
+/**
+ * Class Output
+ */
+class Output extends \think\console\Output
+{
+
+    protected $message = [];
+
+    public function __construct($driver = 'console')
+    {
+        parent::__construct($driver);
+    }
+
+    protected function block($style, $message)
+    {
+        $this->message[] = $message;
+    }
+
+    public function getMessage()
+    {
+        return $this->message;
+    }
+
+}

+ 1 - 0
addons/cos/.addonrc

@@ -0,0 +1 @@
+{"license":"regular","licenseto":"19079","licensekey":"jKiCUakTslI42Nvt ncJw7gDBSvU+FdBxTGtIsA==","files":["public\\assets\\addons\\cos\\js\\spark.js"]}

+ 124 - 0
addons/cos/Cos.php

@@ -0,0 +1,124 @@
+<?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\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/'));
+        }
+
+    }
+
+    /**
+     *
+     */
+    public function uploadConfigInit(&$upload)
+    {
+        $config = $this->getConfig();
+        $data = ['deadline' => time() + $config['expire']];
+        $signature = hash_hmac('sha1', json_encode($data), $config['secretKey'], true);
+
+        $multipart = [
+            'costoken' => $config['appId'] . ':' . base64_encode($signature) . ':' . base64_encode(json_encode($data))
+        ];
+
+        $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;
+    }
+}

+ 250 - 0
addons/cos/bootstrap.js

@@ -0,0 +1,250 @@
+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();
+            });
+        };
+
+        //初始化中完成判断
+        Upload.events.onInit = function () {
+            //如果上传接口不是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) {
+                        var url = '/' + file.key;
+                        ret = {code: 1, data: {url: 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;
+                        });
+                    }
+                } 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") || '');
+                        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;
+                        });
+                    });
+                }, 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 {
+
+                    }
+                });
+            }
+        };
+    });
+}

+ 229 - 0
addons/cos/config.php

@@ -0,0 +1,229 @@
+<?php
+
+return [
+    [
+        'name' => 'appId',
+        'title' => 'AppID',
+        'type' => 'string',
+        'content' => [],
+        'value' => '1317709175',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '请前往腾讯控制台 > 访问管理 > API密钥',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'secretId',
+        'title' => 'SecretId',
+        'type' => 'string',
+        'content' => [],
+        'value' => 'AKID42mMlxveBXgaFhz63nNipjPr7B1g7KOE',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '请前往腾讯控制台 > 访问管理 > API密钥',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'secretKey',
+        'title' => 'SecretKey',
+        'type' => 'string',
+        'content' => [],
+        'value' => 'h9OOFrHTRBj5DH6h3B4jARWO2PbaR8ct',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '请前往腾讯控制台 > 访问管理 > API密钥',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'bucket',
+        'title' => '存储桶名称',
+        'type' => 'string',
+        'content' => [],
+        'value' => 'zhiyin-1317709175',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '存储空间名称',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'region',
+        'title' => '地域名称',
+        'type' => 'string',
+        'content' => [],
+        'value' => 'ap-shanghai',
+        '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://zhiyin-1317709175.cos.ap-shanghai.myqcloud.com',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '请输入你的上传接口地址',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'cdnurl',
+        'title' => 'CDN地址',
+        'type' => 'string',
+        'content' => [],
+        'value' => 'https://zhiyin-1317709175.cos.ap-shanghai.myqcloud.com',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '请配置你的CDN地址或在存储桶基础配置中获取',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        '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' => '30M',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'mimetype',
+        'title' => '可上传后缀格式',
+        'type' => 'string',
+        'content' => [],
+        'value' => 'jpg,png,bmp,jpeg,gif,zip,rar,xls,xlsx,mp4',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'multiple',
+        'title' => '多文件上传',
+        'type' => 'bool',
+        'content' => [],
+        'value' => '1',
+        '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' => '',
+    ],
+];

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

@@ -0,0 +1,339 @@
+<?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;
+    }
+
+    /**
+     * 服务器中转上传文件
+     * 上传分片
+     * 合并分片
+     */
+    public function upload()
+    {
+        $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 = substr($name, stripos($name, '.') + 1);
+        $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);
+        }
+        $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.1.5
+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() {
+    }
+
+}

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.