浏览代码

redis++ binaries for mac

Grant Limberg 5 年之前
父节点
当前提交
8f3a0b17ad
共有 96 个文件被更改,包括 35078 次插入0 次删除
  1. 32 0
      ext/redis-plus-plus-1.1.1/.gitignore
  2. 51 0
      ext/redis-plus-plus-1.1.1/CMakeLists.txt
  3. 201 0
      ext/redis-plus-plus-1.1.1/LICENSE
  4. 1776 0
      ext/redis-plus-plus-1.1.1/README.md
  5. 2233 0
      ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/command.h
  6. 180 0
      ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/command_args.h
  7. 211 0
      ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/command_options.h
  8. 194 0
      ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/connection.h
  9. 115 0
      ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/connection_pool.h
  10. 159 0
      ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/errors.h
  11. 49 0
      ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/pipeline.h
  12. 1844 0
      ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/queued_redis.h
  13. 208 0
      ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/queued_redis.hpp
  14. 25 0
      ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/redis++.h
  15. 1523 0
      ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/redis.h
  16. 1365 0
      ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/redis.hpp
  17. 1395 0
      ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/redis_cluster.h
  18. 1415 0
      ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/redis_cluster.hpp
  19. 363 0
      ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/reply.h
  20. 138 0
      ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/sentinel.h
  21. 115 0
      ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/shards.h
  22. 137 0
      ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/shards_pool.h
  23. 231 0
      ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/subscriber.h
  24. 77 0
      ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/transaction.h
  25. 269 0
      ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/utils.h
  26. 376 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/command.cpp
  27. 2233 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/command.h
  28. 180 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/command_args.h
  29. 201 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/command_options.cpp
  30. 211 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/command_options.h
  31. 305 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/connection.cpp
  32. 194 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/connection.h
  33. 249 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/connection_pool.cpp
  34. 115 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/connection_pool.h
  35. 96 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/crc16.cpp
  36. 136 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/errors.cpp
  37. 159 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/errors.h
  38. 35 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/pipeline.cpp
  39. 49 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/pipeline.h
  40. 1844 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/queued_redis.h
  41. 208 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/queued_redis.hpp
  42. 25 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/redis++.h
  43. 882 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/redis.cpp
  44. 1523 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/redis.h
  45. 1365 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/redis.hpp
  46. 769 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/redis_cluster.cpp
  47. 1395 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/redis_cluster.h
  48. 1415 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/redis_cluster.hpp
  49. 150 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/reply.cpp
  50. 363 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/reply.h
  51. 361 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/sentinel.cpp
  52. 138 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/sentinel.h
  53. 50 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/shards.cpp
  54. 115 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/shards.h
  55. 319 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/shards_pool.cpp
  56. 137 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/shards_pool.h
  57. 222 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/subscriber.cpp
  58. 231 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/subscriber.h
  59. 123 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/transaction.cpp
  60. 77 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/transaction.h
  61. 269 0
      ext/redis-plus-plus-1.1.1/src/sw/redis++/utils.h
  62. 33 0
      ext/redis-plus-plus-1.1.1/test/CMakeLists.txt
  63. 83 0
      ext/redis-plus-plus-1.1.1/test/src/sw/redis++/benchmark_test.h
  64. 178 0
      ext/redis-plus-plus-1.1.1/test/src/sw/redis++/benchmark_test.hpp
  65. 49 0
      ext/redis-plus-plus-1.1.1/test/src/sw/redis++/connection_cmds_test.h
  66. 50 0
      ext/redis-plus-plus-1.1.1/test/src/sw/redis++/connection_cmds_test.hpp
  67. 47 0
      ext/redis-plus-plus-1.1.1/test/src/sw/redis++/geo_cmds_test.h
  68. 149 0
      ext/redis-plus-plus-1.1.1/test/src/sw/redis++/geo_cmds_test.hpp
  69. 55 0
      ext/redis-plus-plus-1.1.1/test/src/sw/redis++/hash_cmds_test.h
  70. 177 0
      ext/redis-plus-plus-1.1.1/test/src/sw/redis++/hash_cmds_test.hpp
  71. 47 0
      ext/redis-plus-plus-1.1.1/test/src/sw/redis++/hyperloglog_cmds_test.h
  72. 67 0
      ext/redis-plus-plus-1.1.1/test/src/sw/redis++/hyperloglog_cmds_test.hpp
  73. 55 0
      ext/redis-plus-plus-1.1.1/test/src/sw/redis++/keys_cmds_test.h
  74. 166 0
      ext/redis-plus-plus-1.1.1/test/src/sw/redis++/keys_cmds_test.hpp
  75. 55 0
      ext/redis-plus-plus-1.1.1/test/src/sw/redis++/list_cmds_test.h
  76. 154 0
      ext/redis-plus-plus-1.1.1/test/src/sw/redis++/list_cmds_test.hpp
  77. 57 0
      ext/redis-plus-plus-1.1.1/test/src/sw/redis++/pipeline_transaction_test.h
  78. 184 0
      ext/redis-plus-plus-1.1.1/test/src/sw/redis++/pipeline_transaction_test.hpp
  79. 53 0
      ext/redis-plus-plus-1.1.1/test/src/sw/redis++/pubsub_test.h
  80. 244 0
      ext/redis-plus-plus-1.1.1/test/src/sw/redis++/pubsub_test.hpp
  81. 76 0
      ext/redis-plus-plus-1.1.1/test/src/sw/redis++/sanity_test.h
  82. 299 0
      ext/redis-plus-plus-1.1.1/test/src/sw/redis++/sanity_test.hpp
  83. 49 0
      ext/redis-plus-plus-1.1.1/test/src/sw/redis++/script_cmds_test.h
  84. 97 0
      ext/redis-plus-plus-1.1.1/test/src/sw/redis++/script_cmds_test.hpp
  85. 53 0
      ext/redis-plus-plus-1.1.1/test/src/sw/redis++/set_cmds_test.h
  86. 184 0
      ext/redis-plus-plus-1.1.1/test/src/sw/redis++/set_cmds_test.hpp
  87. 54 0
      ext/redis-plus-plus-1.1.1/test/src/sw/redis++/stream_cmds_test.h
  88. 225 0
      ext/redis-plus-plus-1.1.1/test/src/sw/redis++/stream_cmds_test.hpp
  89. 57 0
      ext/redis-plus-plus-1.1.1/test/src/sw/redis++/string_cmds_test.h
  90. 247 0
      ext/redis-plus-plus-1.1.1/test/src/sw/redis++/string_cmds_test.hpp
  91. 303 0
      ext/redis-plus-plus-1.1.1/test/src/sw/redis++/test_main.cpp
  92. 51 0
      ext/redis-plus-plus-1.1.1/test/src/sw/redis++/threads_test.h
  93. 147 0
      ext/redis-plus-plus-1.1.1/test/src/sw/redis++/threads_test.hpp
  94. 96 0
      ext/redis-plus-plus-1.1.1/test/src/sw/redis++/utils.h
  95. 61 0
      ext/redis-plus-plus-1.1.1/test/src/sw/redis++/zset_cmds_test.h
  96. 350 0
      ext/redis-plus-plus-1.1.1/test/src/sw/redis++/zset_cmds_test.hpp

+ 32 - 0
ext/redis-plus-plus-1.1.1/.gitignore

@@ -0,0 +1,32 @@
+# Prerequisites
+*.d
+
+# Compiled Object files
+*.slo
+*.lo
+*.o
+*.obj
+
+# Precompiled Headers
+*.gch
+*.pch
+
+# Compiled Dynamic libraries
+*.so
+*.dylib
+*.dll
+
+# Fortran module files
+*.mod
+*.smod
+
+# Compiled Static libraries
+*.lai
+*.la
+*.a
+*.lib
+
+# Executables
+*.exe
+*.out
+*.app

+ 51 - 0
ext/redis-plus-plus-1.1.1/CMakeLists.txt

@@ -0,0 +1,51 @@
+project(redis++)
+
+if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
+    cmake_minimum_required(VERSION 3.0.0)
+else()
+    cmake_minimum_required(VERSION 2.8.0)
+endif()
+
+set(CMAKE_CXX_FLAGS "-std=c++11 -Wall -W -Werror -fPIC")
+
+set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
+
+set(PROJECT_SOURCE_DIR ${PROJECT_SOURCE_DIR}/src/sw/redis++)
+
+file(GLOB PROJECT_SOURCE_FILES "${PROJECT_SOURCE_DIR}/*.cpp")
+
+set(STATIC_LIB static)
+#set(SHARED_LIB shared)
+
+add_library(${STATIC_LIB} STATIC ${PROJECT_SOURCE_FILES})
+# add_library(${SHARED_LIB} SHARED ${PROJECT_SOURCE_FILES})
+
+# hiredis dependency
+find_path(HIREDIS_HEADER hiredis)
+target_include_directories(${STATIC_LIB} PUBLIC ${HIREDIS_HEADER})
+# target_include_directories(${SHARED_LIB} PUBLIC ${HIREDIS_HEADER})
+
+#find_library(HIREDIS_LIB hiredis)
+#target_link_libraries(${SHARED_LIB} ${HIREDIS_LIB})
+
+set_target_properties(${STATIC_LIB} PROPERTIES OUTPUT_NAME ${PROJECT_NAME})
+#set_target_properties(${SHARED_LIB} PROPERTIES OUTPUT_NAME ${PROJECT_NAME})
+
+set_target_properties(${STATIC_LIB} PROPERTIES CLEAN_DIRECT_OUTPUT 1)
+#set_target_properties(${SHARED_LIB} PROPERTIES CLEAN_DIRECT_OUTPUT 1)
+
+# add_subdirectory(test)
+
+
+# Install static lib.
+install(TARGETS ${STATIC_LIB}
+        ARCHIVE DESTINATION lib)
+
+# Install shared lib.
+#install(TARGETS ${SHARED_LIB}
+#        LIBRARY DESTINATION lib)
+
+#Install headers.
+set(HEADER_PATH "sw/redis++")
+file(GLOB HEADERS "${PROJECT_SOURCE_DIR}/*.h*")
+install(FILES ${HEADERS} DESTINATION ${CMAKE_INSTALL_PREFIX}/include/${HEADER_PATH})

+ 201 - 0
ext/redis-plus-plus-1.1.1/LICENSE

@@ -0,0 +1,201 @@
+                                 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:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) 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
+
+      (d) 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 [yyyy] [name of copyright owner]
+
+   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.

+ 1776 - 0
ext/redis-plus-plus-1.1.1/README.md

@@ -0,0 +1,1776 @@
+# redis-plus-plus
+
+- [Overview](#overview)
+    - [Features](#features)
+- [Installation](#installation)
+    - [Install hiredis](#install-hiredis)
+    - [Install redis-plus-plus](#install-redis-plus-plus)
+    - [Run Tests (Optional)](#run-tests-optional)
+    - [Use redis-plus-plus In Your Project](#use-redis-plus-plus-in-your-project)
+- [Getting Started](#getting-started)
+- [API Reference](#api-reference)
+    - [Connection](#connection)
+    - [Send Command to Redis Server](#send-command-to-redis-server)
+    - [Generic Command Interface](#generic-command-interface)
+    - [Publish/Subscribe](#publishsubscribe)
+    - [Pipeline](#pipeline)
+    - [Transaction](#transaction)
+    - [Redis Cluster](#redis-cluster)
+    - [Redis Sentinel](#redis-sentinel)
+    - [Redis Stream](#redis-stream)
+- [Author](#author)
+
+## Overview
+
+This is a C++ client for Redis. It's based on [hiredis](https://github.com/redis/hiredis), and written in C++ 11.
+
+**NOTE**: I'm not a native speaker. So if the documentation is unclear, please feel free to open an issue or pull request. I'll response ASAP.
+
+### Features
+- Most commands for Redis.
+- Connection pool.
+- Redis scripting.
+- Thread safe unless otherwise stated.
+- Redis publish/subscribe.
+- Redis pipeline.
+- Redis transaction.
+- Redis Cluster.
+- Redis Sentinel.
+- STL-like interfaces.
+- Generic command interface.
+
+## Installation
+
+### Install hiredis
+
+Since *redis-plus-plus* is based on *hiredis*, you should install *hiredis* first. The minimum version requirement for *hiredis* is **v0.12.1**, and you'd better use the latest release of *hiredis*.
+
+```
+git clone https://github.com/redis/hiredis.git
+
+cd hiredis
+
+make
+
+make install
+```
+
+By default, *hiredis* is installed at */usr/local*. If you want to install *hiredis* at non-default location, use the following commands to specify the installation path.
+
+```
+make PREFIX=/non/default/path
+
+make PREFIX=/non/default/path install
+```
+
+### Install redis-plus-plus
+
+*redis-plus-plus* is built with [CMAKE](https://cmake.org).
+
+```
+git clone https://github.com/sewenew/redis-plus-plus.git
+
+cd redis-plus-plus
+
+mkdir compile
+
+cd compile
+
+cmake -DCMAKE_BUILD_TYPE=Release ..
+
+make
+
+make install
+
+cd ..
+```
+
+If *hiredis* is installed at non-default location, you should use `CMAKE_PREFIX_PATH` to specify the installation path of *hiredis*. By default, *redis-plus-plus* is installed at */usr/local*. However, you can use `CMAKE_INSTALL_PREFIX` to install *redis-plus-plus* at non-default location.
+
+```
+cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=/path/to/hiredis -DCMAKE_INSTALL_PREFIX=/path/to/install/redis-plus-plus ..
+```
+
+### Run Tests (Optional)
+
+*redis-plus-plus* has been fully tested with the following compilers:
+
+```
+gcc version 4.8.5 20150623 (Red Hat 4.8.5-39) (GCC)
+gcc version 5.5.0 20171010 (Ubuntu 5.5.0-12ubuntu1)
+gcc version 6.5.0 20181026 (Ubuntu 6.5.0-2ubuntu1~18.04)
+gcc version 7.4.0 (Ubuntu 7.4.0-1ubuntu1~18.04.1)
+gcc version 8.3.0 (Ubuntu 8.3.0-6ubuntu1~18.04.1)
+clang version 3.9.1-19ubuntu1 (tags/RELEASE_391/rc2)
+clang version 4.0.1-10 (tags/RELEASE_401/final)
+clang version 5.0.1-4 (tags/RELEASE_501/final)
+clang version 6.0.0-1ubuntu2 (tags/RELEASE_600/final)
+clang version 7.0.0-3~ubuntu0.18.04.1 (tags/RELEASE_700/final)
+Apple clang version 11.0.0 (clang-1100.0.33.8)
+```
+
+After compiling with cmake, you'll get a test program in *compile/test* directory: *compile/test/test_redis++*.
+
+In order to run the tests, you need to set up a Redis instance, and a Redis Cluster. Since the test program will send most of Redis commands to the server and cluster, you need to set up Redis of the latest version (by now, it's 5.0). Otherwise, the tests might fail. For example, if you set up Redis 4.0 for testing, the test program will fail when it tries to send the `ZPOPMAX` command (a Redis 5.0 command) to the server. If you want to run the tests with other Redis versions, you have to comment out commands that haven't been supported by your Redis, from test source files in *redis-plus-plus/test/src/sw/redis++/* directory. Sorry for the inconvenience, and I'll fix this problem to make the test program work with any version of Redis in the future.
+
+**NOTE**: The latest version of Redis is only a requirement for running the tests. In fact, you can use *redis-plus-plus* with Redis of any version, e.g. Redis 2.0, Redis 3.0, Redis 4.0, Redis 5.0.
+
+**NEVER** run the test program in production envronment, since the keys, which the test program reads or writes, might conflict with your application.
+
+In order to run tests with both Redis and Redis Cluster, you can run the test program with the following command:
+
+```
+./compile/test/test_redis++ -h host -p port -a auth -n cluster_node -c cluster_port
+```
+
+- *host* and *port* are the host and port number of the Redis instance.
+- *cluster_node* and *cluster_port* are the host and port number of Redis Cluster. You only need to set the host and port number of a single node in the cluster, *redis-plus-plus* will find other nodes automatically.
+- *auth* is the password of the Redis instance and Redis Cluster. The Redis instance and Redis Cluster must be configured with the same password. If there's no password configured, don't set this option.
+
+If you only want to run tests with Redis, you only need to specify *host*, *port* and *auth* options:
+
+```
+./compile/test/test_redis++ -h host -p port -a auth
+```
+
+Similarly, if you only want to run tests with Redis Cluster, just specify *cluster_node*, *cluster_port* and *auth* options:
+
+```
+./compile/test/test_redis++ -a auth -n cluster_node -c cluster_port
+```
+
+The test program will test running *redis-plus-plus* in multi-threads environment, and this test will cost a long time. If you want to skip it (not recommended), just comment out the following lines in *test/src/sw/redis++/test_main.cpp* file.
+
+```C++
+sw::redis::test::ThreadsTest threads_test(opts, cluster_node_opts);
+threads_test.run();
+```
+
+If all tests have been passed, the test program will print the following message:
+
+```
+Pass all tests
+```
+
+Otherwise, it prints the error message.
+
+#### Performance
+
+*redis-plus-plus* runs as fast as *hiredis*, since it's a wrapper of *hiredis*. You can run *test_redis++* in benchmark mode to check the performance in your environment.
+
+```
+./compile/test/test_redis++ -h host -p port -a auth -n cluster_node -c cluster_port -b -t thread_num -s connection_pool_size -r request_num -k key_len -v val_len
+```
+
+- *-b* option turns the test program into benchmark mode.
+- *thread_num* specifies the number of worker threads. `10` by default.
+- *connection_pool_size* specifies the size of the connection pool. `5` by default.
+- *request_num* specifies the total number of requests sent to server for each test. `100000` by default.
+- *key_len* specifies the length of the key for each operation. `10` by default.
+- *val_len* specifies the length of the value. `10` by default.
+
+The bechmark will generate `100` random binary keys for testing, and the size of these keys is specified by *key_len*. When the benchmark runs, it will read/write with these keys. So **NEVER** run the test program in your production environment, otherwise, it might inaccidently delete your data.
+
+### Use redis-plus-plus In Your Project
+
+After compiling the code, you'll get both shared library and static library. Since *redis-plus-plus* depends on *hiredis*, you need to link both libraries to your Application. Also don't forget to specify the `-std=c++11` and thread-related option.
+
+#### Use Static Libraries
+
+Take gcc as an example.
+
+```
+g++ -std=c++11 -o app app.cpp /path/to/libredis++.a /path/to/libhiredis.a -pthread
+```
+
+If *hiredis* and *redis-plus-plus* are installed at non-default location, you should use `-I` option to specify the header path.
+
+```
+g++ -std=c++11 -I/non-default/install/include/path -o app app.cpp /path/to/libredis++.a /path/to/libhiredis.a -pthread
+```
+
+#### Use Shared Libraries
+
+```
+g++ -std=c++11 -o app app.cpp -lredis++ -lhiredis -pthread
+```
+
+If *hiredis* and *redis-plus-plus* are installed at non-default location, you should use `-I` and `-L` options to specify the header and library paths.
+
+```
+g++ -std=c++11 -I/non-default/install/include/path -L/non-default/install/lib/path -o app app.cpp -lredis++ -lhiredis -pthread
+```
+
+When linking with shared libraries, and running your application, you might get the following error message:
+
+```
+error while loading shared libraries: xxx: cannot open shared object file: No such file or directory.
+```
+
+That's because the linker cannot find the shared libraries. In order to solve the problem, you can add the path where you installed *hiredis* and *redis-plus-plus* libraries, to `LD_LIBRARY_PATH` environment variable. For example:
+
+```
+export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib
+```
+
+Check [this StackOverflow question](https://stackoverflow.com/questions/480764) for details on how to solve the problem.
+
+#### Build With Cmake
+
+If you're using cmake to build your application, you need to add *hiredis* and *redis-plus-plus* dependencies in your *CMakeLists.txt*:
+
+```CMake
+# <------------ add hiredis dependency --------------->
+find_path(HIREDIS_HEADER hiredis)
+target_include_directories(target PUBLIC ${HIREDIS_HEADER})
+
+find_library(HIREDIS_LIB hiredis)
+target_link_libraries(target ${HIREDIS_LIB})
+
+# <------------ add redis-plus-plus dependency -------------->
+# NOTE: this should be *sw* NOT *redis++*
+find_path(REDIS_PLUS_PLUS_HEADER sw)
+target_include_directories(target PUBLIC ${REDIS_PLUS_PLUS_HEADER})
+
+find_library(REDIS_PLUS_PLUS_LIB redis++)
+target_link_libraries(target ${REDIS_PLUS_PLUS_LIB})
+```
+
+See [this issue](https://github.com/sewenew/redis-plus-plus/issues/5) for a complete example of *CMakeLists.txt*.
+
+Also, if you installed *hiredis* and *redis-plus-plus* at non-default location, you need to run cmake with `CMAKE_PREFIX_PATH` option to specify the installation path of these two libraries.
+
+```
+cmake -DCMAKE_PREFIX_PATH=/installation/path/to/the/two/libs ..
+```
+
+## Getting Started
+
+```C++
+#include <sw/redis++/redis++.h>
+
+using namespace sw::redis;
+
+try {
+    // Create an Redis object, which is movable but NOT copyable.
+    auto redis = Redis("tcp://127.0.0.1:6379");
+
+    // ***** STRING commands *****
+
+    redis.set("key", "val");
+    auto val = redis.get("key");    // val is of type OptionalString. See 'API Reference' section for details.
+    if (val) {
+        // Dereference val to get the returned value of std::string type.
+        std::cout << *val << std::endl;
+    }   // else key doesn't exist.
+
+    // ***** LIST commands *****
+
+    // std::vector<std::string> to Redis LIST.
+    std::vector<std::string> vec = {"a", "b", "c"};
+    redis.rpush("list", vec.begin(), vec.end());
+
+    // std::initializer_list to Redis LIST.
+    redis.rpush("list", {"a", "b", "c"});
+
+    // Redis LIST to std::vector<std::string>.
+    vec.clear();
+    redis.lrange("list", 0, -1, std::back_inserter(vec));
+
+    // ***** HASH commands *****
+
+    redis.hset("hash", "field", "val");
+
+    // Another way to do the same job.
+    redis.hset("hash", std::make_pair("field", "val"));
+
+    // std::unordered_map<std::string, std::string> to Redis HASH.
+    std::unordered_map<std::string, std::string> m = {
+        {"field1", "val1"},
+        {"field2", "val2"}
+    };
+    redis.hmset("hash", m.begin(), m.end());
+
+    // Redis HASH to std::unordered_map<std::string, std::string>.
+    m.clear();
+    redis.hgetall("hash", std::inserter(m, m.begin()));
+
+    // Get value only.
+    // NOTE: since field might NOT exist, so we need to parse it to OptionalString.
+    std::vector<OptionalString> vals;
+    redis.hmget("hash", {"field1", "field2"}, std::back_inserter(vals));
+
+    // ***** SET commands *****
+
+    redis.sadd("set", "m1");
+
+    // std::unordered_set<std::string> to Redis SET.
+    std::unordered_set<std::string> set = {"m2", "m3"};
+    redis.sadd("set", set.begin(), set.end());
+
+    // std::initializer_list to Redis SET.
+    redis.sadd("set", {"m2", "m3"});
+
+    // Redis SET to std::unordered_set<std::string>.
+    set.clear();
+    redis.smembers("set", std::inserter(set, set.begin()));
+
+    if (redis.sismember("set", "m1")) {
+        std::cout << "m1 exists" << std::endl;
+    }   // else NOT exist.
+
+    // ***** SORTED SET commands *****
+
+    redis.zadd("sorted_set", "m1", 1.3);
+
+    // std::unordered_map<std::string, double> to Redis SORTED SET.
+    std::unordered_map<std::string, double> scores = {
+        {"m2", 2.3},
+        {"m3", 4.5}
+    };
+    redis.zadd("sorted_set", scores.begin(), scores.end());
+
+    // Redis SORTED SET to std::unordered_map<std::string, double>.
+    scores.clear();
+    redis.zrangebyscore("sorted_set",
+            UnboundedInterval<double>{},            // (-inf, +inf)
+            std::inserter(scores, scores.begin()));
+
+    // Only get member names:
+    // pass an inserter of std::vector<std::string> type as output parameter.
+    std::vector<std::string> without_score;
+    redis.zrangebyscore("sorted_set",
+            BoundedInterval<double>(1.5, 3.4, BoundType::CLOSED),   // [1.5, 3.4]
+            std::back_inserter(without_score));
+
+    // Get both member names and scores:
+    // pass an inserter of std::unordered_map<std::string, double> as output parameter.
+    std::unordered_map<std::string, double> with_score;
+    redis.zrangebyscore("sorted_set",
+            BoundedInterval<double>(1.5, 3.4, BoundType::LEFT_OPEN),    // (1.5, 3.4]
+            std::inserter(with_score, with_score.end()));
+
+    // ***** SCRIPTING commands *****
+
+    // Script returns a single element.
+    auto num = redis.eval<long long>("return 1", {}, {});
+
+    // Script returns an array of elements.
+    std::vector<long long> nums;
+    redis.eval("return {ARGV[1], ARGV[2]}", {}, {"1", "2"}, std::back_inserter(nums));
+
+    // ***** Pipeline *****
+
+    // Create a pipeline.
+    auto pipe = redis.pipeline();
+
+    // Send mulitple commands and get all replies.
+    auto pipe_replies = pipe.set("key", "value")
+                            .get("key")
+                            .rename("key", "new-key")
+                            .rpush("list", {"a", "b", "c"})
+                            .lrange("list", 0, -1)
+                            .exec();
+
+    // Parse reply with reply type and index.
+    auto set_cmd_result = pipe_replies.get<bool>(0);
+
+    auto get_cmd_result = pipe_replies.get<OptionalString>(1);
+
+    // rename command result
+    pipe_replies.get<void>(2);
+
+    auto rpush_cmd_result = pipe_replies.get<long long>(3);
+
+    std::vector<std::string> lrange_cmd_result;
+    pipe_replies.get(4, back_inserter(lrange_cmd_result));
+
+    // ***** Transaction *****
+
+    // Create a transaction.
+    auto tx = redis.transaction();
+
+    // Run multiple commands in a transaction, and get all replies.
+    auto tx_replies = tx.incr("num0")
+                        .incr("num1")
+                        .mget({"num0", "num1"})
+                        .exec();
+
+    // Parse reply with reply type and index.
+    auto incr_result0 = tx_replies.get<long long>(0);
+
+    auto incr_result1 = tx_replies.get<long long>(1);
+
+    std::vector<OptionalString> mget_cmd_result;
+    tx_replies.get(2, back_inserter(mget_cmd_result));
+
+    // ***** Generic Command Interface *****
+
+    // There's no *Redis::client_getname* interface.
+    // But you can use *Redis::command* to get the client name.
+    val = redis.command<OptionalString>("client", "getname");
+    if (val) {
+        std::cout << *val << std::endl;
+    }
+
+    // Same as above.
+    auto getname_cmd_str = {"client", "getname"};
+    val = redis.command<OptionalString>(getname_cmd_str.begin(), getname_cmd_str.end());
+
+    // There's no *Redis::sort* interface.
+    // But you can use *Redis::command* to send sort the list.
+    std::vector<std::string> sorted_list;
+    redis.command("sort", "list", "ALPHA", std::back_inserter(sorted_list));
+
+    // Another *Redis::command* to do the same work.
+    auto sort_cmd_str = {"sort", "list", "ALPHA"};
+    redis.command(sort_cmd_str.begin(), sort_cmd_str.end(), std::back_inserter(sorted_list));
+
+    // ***** Redis Cluster *****
+
+    // Create a RedisCluster object, which is movable but NOT copyable.
+    auto redis_cluster = RedisCluster("tcp://127.0.0.1:7000");
+
+    // RedisCluster has similar interfaces as Redis.
+    redis_cluster.set("key", "value");
+    val = redis_cluster.get("key");
+    if (val) {
+        std::cout << *val << std::endl;
+    }   // else key doesn't exist.
+
+    // Keys with hash-tag.
+    redis_cluster.set("key{tag}1", "val1");
+    redis_cluster.set("key{tag}2", "val2");
+    redis_cluster.set("key{tag}3", "val3");
+
+    std::vector<OptionalString> hash_tag_res;
+    redis_cluster.mget({"key{tag}1", "key{tag}2", "key{tag}3"},
+            std::back_inserter(hash_tag_res));
+
+} catch (const Error &e) {
+    // Error handling.
+}
+```
+
+## API Reference
+
+### Connection
+
+`Redis` class maintains a connection pool to Redis server. If the connection is broken, `Redis` reconnects to Redis server automatically.
+
+You can initialize a `Redis` instance with `ConnectionOptions` and `ConnectionPoolOptions`. `ConnectionOptions` specifies options for connection to Redis server, and `ConnectionPoolOptions` specifies options for conneciton pool. `ConnectionPoolOptions` is optional. If not specified, `Redis` maintains a single connection to Redis server.
+
+```C++
+ConnectionOptions connection_options;
+connection_options.host = "127.0.0.1";  // Required.
+connection_options.port = 6666; // Optional. The default port is 6379.
+connection_options.password = "auth";   // Optional. No password by default.
+connection_options.db = 1;  // Optional. Use the 0th database by default.
+
+// Optional. Timeout before we successfully send request to or receive response from redis.
+// By default, the timeout is 0ms, i.e. never timeout and block until we send or receive successfuly.
+// NOTE: if any command is timed out, we throw a TimeoutError exception.
+connection_options.socket_timeout = std::chrono::milliseconds(200);
+
+// Connect to Redis server with a single connection.
+Redis redis1(connection_options);
+
+ConnectionPoolOptions pool_options;
+pool_options.size = 3;  // Pool size, i.e. max number of connections.
+
+// Connect to Redis server with a connection pool.
+Redis redis2(connection_options, pool_options);
+```
+
+See [ConnectionOptions](https://github.com/sewenew/redis-plus-plus/blob/master/src/sw/redis%2B%2B/connection.h#L40) and [ConnectionPoolOptions](https://github.com/sewenew/redis-plus-plus/blob/master/src/sw/redis%2B%2B/connection_pool.h#L30) for more options.
+
+**NOTE**: `Redis` class is movable but NOT copyable.
+
+```C++
+// auto redis3 = redis1;    // this won't compile.
+
+// But it's movable.
+auto redis3 = std::move(redis1);
+```
+
+*redis-plus-plus* also supports connecting to Redis server with Unix Domain Socket.
+
+```C++
+ConnectionOptions options;
+options.type = ConnectionType::UNIX;
+options.path = "/path/to/socket";
+Redis redis(options);
+```
+
+You can also connect to Redis server with a URI. However, in this case, you can only specify *host* and *port*, or *Unix Domain Socket path*. In order to specify other options, you need to use `ConnectionOptions` and `ConnectionPoolOptions`.
+
+```C++
+// Single connection to the given host and port.
+Redis redis1("tcp://127.0.0.1:6666");
+
+// Use default port, i.e. 6379.
+Redis redis2("tcp://127.0.0.1");
+
+// Connect to Unix Domain Socket.
+Redis redis3("unix://path/to/socket");
+```
+
+#### Lazily Create Connection
+
+Connections in the pool are lazily created. When the connection pool is initialized, i.e. the constructor of `Redis`, `Redis` does NOT connect to the server. Instead, it connects to the server only when you try to send command. In this way, we can avoid unnecessary connections. So if the pool size is 5, but the number of max concurrent connections is 3, there will be only 3 connections in the pool.
+
+#### Connection Failure
+
+You don't need to check whether `Redis` object connects to server successfully. If `Redis` fails to create a connection to Redis server, or the connection is broken at some time, it throws an exception of type `Error` when you try to send command with `Redis`. Even when you get an exception, i.e. the connection is broken, you don't need to create a new `Redis` object. You can reuse the `Redis` object to send commands, and the `Redis` object will try to reconnect to server automatically. If it reconnects successfully, it sends command to server. Otherwise, it throws an exception again.
+
+See the [Exception section](#exception) for details on exceptions.
+
+#### Reuse Redis object As Much As Possible
+
+It's NOT cheap to create a `Redis` object, since it will create new connections to Redis server. So you'd better reuse `Redis` object as much as possible. Also, it's safe to call `Redis`' member functions in multi-thread environment, and you can share `Redis` object in multiple threads.
+
+```C++
+// This is GOOD practice.
+auto redis = Redis("tcp://127.0.0.1");
+for (auto idx = 0; idx < 100; ++idx) {
+    // Reuse the Redis object in the loop.
+    redis.set("key", "val");
+}
+
+// This is VERY BAD! It's very inefficient.
+// NEVER DO IT!!!
+for (auto idx = 0; idx < 100; ++idx) {
+    // Create a new Redis object for each iteration.
+    auto redis = Redis("tcp://127.0.0.1");
+    redis.set("key", "val");
+}
+```
+
+### Send Command to Redis Server
+
+You can send [Redis commands](https://redis.io/commands) through `Redis` object. `Redis` has one or more (overloaded) methods for each Redis command. The method has the same (lowercased) name as the corresponding command. For example, we have 3 overload methods for the `DEL key [key ...]` command:
+
+```C++
+// Delete a single key.
+long long Redis::del(const StringView &key);
+
+// Delete a batch of keys: [first, last).
+template <typename Input>
+long long Redis::del(Input first, Input last);
+
+// Delete keys in the initializer_list.
+template <typename T>
+long long Redis::del(std::initializer_list<T> il);
+```
+
+With input parameters, these methods build a Redis command based on [Redis protocol](https://redis.io/topics/protocol), and send the command to Redis server. Then synchronously receive the reply, parse it, and return to the caller.
+
+Let's take a closer look at these methods' parameters and return values.
+
+#### Parameter Type
+
+Most of these methods have the same parameters as the corresponding commands. The following is a list of parameter types:
+
+| Parameter Type | Explaination | Example | Note |
+| :------------: | ------------ | ------- | ---- |
+| **StringView** | Parameters of string type. Normally used for key, value, member name, field name and so on | ***bool Redis::hset(const StringView &key, const StringView &field, const StringView &val)*** | See the [StringView section](#stringview) for details on `StringView` |
+| **long long** | Parameters of integer type. Normally used for index (e.g. list commands) or integer | ***void ltrim(const StringView &key, long long start, long long stop)*** <br> ***long long decrby(const StringView &key, long long decrement)*** | |
+| **double** | Parameters of floating-point type. Normally used for score (e.g. sorted set commands) or number of floating-point type | ***double incrbyfloat(const StringView &key, double increment)*** | |
+| **std::chrono::duration** <br> **std::chrono::time_point** | Time-related parameters | ***bool expire(const StringView &key, const std::chrono::seconds &timeout)*** <br> ***bool expireat(const StringView &key, const std::chrono::time_point<std::chrono::system_clock, std::chrono::seconds> &tp)*** | |
+| **std::pair<StringView, StringView>** | Used for Redis hash's (field, value) pair | ***bool hset(const StringView &key, const std::pair<StringView, StringView> &item)*** | |
+| **std::pair<double, double>** | Used for Redis geo's (longitude, latitude) pair | ***OptionalLongLong georadius(const StringView &key, const std::pair<double, double> &location, double radius, GeoUnit unit, const StringView &destination, bool store_dist, long long count)*** | |
+| **pair of iterators** | Use a pair of iterators to specify a range of input, so that we can pass the data in a STL container to these methods | ***template < typename Input >*** <br> ***long long del(Input first, Input last)*** | Throw an exception, if it's an empty range, i.e. *first == last* |
+| **std::initializer_list< T >** | Use an initializer list to specify a batch of input | ***template < typename T >*** <br> ***long long del(std::initializer_list< T > il)*** | |
+| **some options** | Options for some commands | ***UpdateType***, ***template < typename T > class BoundedInterval*** | See [command_options.h](https://github.com/sewenew/redis-plus-plus/blob/master/src/sw/redis%2B%2B/command_options.h) for details |
+
+##### StringView
+
+[std::string_view](http://en.cppreference.com/w/cpp/string/basic_string_view) is a good option for the type of string parameters. However, by now, not all compilers support `std::string_view`. So we wrote a [simple version](https://github.com/sewenew/redis-plus-plus/blob/master/src/sw/redis%2B%2B/utils.h#L48), i.e. `StringView`. Since there are conversions from `std::string` and c-style string to `StringView`, you can just pass `std::string` or c-style string to methods that need a `StringView` parameter.
+
+```C++
+// bool Redis::hset(const StringView &key, const StringView &field, const StringView &val)
+
+// Pass c-style string to StringView.
+redis.hset("key", "field", "value");
+
+// Pass std::string to StringView.
+std::string key = "key";
+std::string field = "field";
+std::string val = "val";
+redis.hset(key, field, val);
+
+// Mix std::string and c-style string.
+redis.hset(key, field, "value");
+```
+
+#### Return Type
+
+[Redis protocol](https://redis.io/topics/protocol) defines 5 kinds of replies:
+- *Status Reply*: Also known as *Simple String Reply*. It's a non-binary string reply.
+- *Bulk String Reply*: Binary safe string reply.
+- *Integer Reply*: Signed integer reply. Large enough to hold `long long`.
+- *Array Reply*: (Nested) Array reply.
+- *Error Reply*: Non-binary string reply that gives error info.
+
+Also these replies might be *NULL*. For instance, when you try to `GET` the value of a nonexistent key, Redis returns a *NULL Bulk String Reply*.
+
+As we mentioned above, replies are parsed into return values of these methods. The following is a list of return types:
+
+| Return Type | Explaination | Example | Note |
+| :---------: | ------------ | ------- | ---- |
+| **void** | *Status Reply* that should always return a string of "OK" | *RENAME*, *SETEX* | |
+| **std::string** | *Status Reply* that NOT always return "OK", and *Bulk String Reply* | *PING*, *INFO* | |
+| **bool** | *Integer Reply* that always returns 0 or 1 | *EXPIRE*, *HSET* | See the [Boolean Return Value section](#boolean-return-value) for the meaning of a boolean return value |
+| **long long** | *Integer Reply* that not always return 0 or 1 | *DEL*, *APPEND* | |
+| **double** | *Bulk String Reply* that represents a double | *INCRBYFLOAT*, *ZINCRBY* | |
+| **std::pair** | *Array Reply* with exactly 2 elements. Since the return value is always an array of 2 elements, we return the 2 elements as a `std::pair`'s first and second elements | *BLPOP* | |
+| **std::tuple** | *Array Reply* with fixed length and has more than 2 elements. Since length of the returned array is fixed, we return the array as a `std::tuple` | *BZPOPMAX* | |
+| **output iterator** | General *Array Reply* with non-fixed/dynamic length. We use STL-like interface to return this kind of array replies, so that you can insert the return value into a STL container easily | *MGET*, *LRANGE* | Also, sometimes the type of output iterator decides which options to send with the command. See the [Examples section](#command-overloads) for details |
+| **Optional< T >** | For any reply of type `T` that might be *NULL* | *GET*, *LPOP*, *BLPOP*, *BZPOPMAX* | See the [Optional section](#optional) for details on `Optional<T>` |
+
+##### Boolean Return Value
+
+The return type of some methods, e.g. `EXPIRE`, `HSET`, is `bool`. If the method returns `false`, it DOES NOT mean that `Redis` failed to send the command to Redis server. Instead, it means that Redis server returns an *Integer Reply*, and the value of the reply is `0`. Accordingly, if the method returns `true`, it means that Redis server returns an *Integer Reply*, and the value of the reply is `1`. You can 
+check [Redis commands manual](http://redis.io/commands) for what do `0` and `1` stand for.
+
+For example, when we send `EXPIRE` command to Redis server, it returns `1` if the timeout was set, and it returns `0` if the key doesn't exist. Accordingly, if the timeout was set, `Redis::expire` returns `true`, and if the key doesn't exist, `Redis::expire` returns `false`.
+
+So, never use the return value to check if the command has been successfully sent to Redis server. Instead, if `Redis` failed to send command to server, it throws an exception of type `Error`. See the [Exception section](#exception) for details on exceptions.
+
+##### Optional
+
+[std::optional](http://en.cppreference.com/w/cpp/utility/optional) is a good option for return type, if Redis might return *NULL REPLY*. Again, since not all compilers support `std::optional` so far, we implement our own [simple version](https://github.com/sewenew/redis-plus-plus/blob/master/src/sw/redis%2B%2B/utils.h#L85), i.e. `Optional<T>`.
+
+Take the [GET](https://redis.io/commands/get) and [MGET](https://redis.io/commands/mget) commands for example:
+
+```C++
+// Or just: auto val = redis.get("key");
+Optional<std::string> val = redis.get("key");
+
+// Optional<T> has a conversion to bool.
+// If it's NOT a null Optional<T> object, it's converted to true.
+// Otherwise, it's converted to false.
+if (val) {
+    // Key exists. Dereference val to get the string result.
+    std::cout << *val << std::endl;
+} else {
+    // Redis server returns a NULL Bulk String Reply.
+    // It's invalid to dereference a null Optional<T> object.
+    std::cout << "key doesn't exist." << std::endl;
+}
+
+std::vector<Optional<std::string>> values;
+redis.mget({"key1", "key2", "key3"}, std::back_inserter(values));
+for (const auto &val : values) {
+    if (val) {
+        // Key exist, process the value.
+    }
+}
+```
+
+We also have some typedefs for some commonly used `Optional<T>`:
+
+```C++
+using OptionalString = Optional<std::string>;
+
+using OptionalLongLong = Optional<long long>;
+
+using OptionalDouble = Optional<double>;
+
+using OptionalStringPair = Optional<std::pair<std::string, std::string>>;
+```
+
+#### Exception
+
+`Redis` throws exceptions if it receives an *Error Reply* or something bad happens, e.g. failed to create a connection to server, or connection to server is broken. All exceptions derived from `Error` class. See [errors.h](https://github.com/sewenew/redis-plus-plus/blob/master/src/sw/redis%2B%2B/errors.h) for details.
+
+- `Error`: Generic error. It's also the base class of other exceptions.
+- `IoError`: There's some IO error with the connection.
+- `TimeoutError`: Read or write operation was timed out. It's a derived class of `IoError`.
+- `ClosedError`: Redis server closed the connection.
+- `ProtoError`: The command or reply is invalid, and we cannot process it with Redis protocol.
+- `OomError`: *hiredis* library got an out-of-memory error.
+- `ReplyError`: Redis server returned an error reply, e.g. we try to call `redis::lrange` on a Redis hash.
+- `WatchError`: Watched key has been modified. See [Watch section](#watch) for details.
+
+**NOTE**: *NULL REPLY*` is not taken as an exception. For example, if we try to `GET` a non-existent key, we'll get a *NULL Bulk String Reply*. Instead of throwing an exception, we return the *NULL REPLY* as a null `Optional<T>` object. Also see [Optional section](#optional).
+
+#### Examples
+
+Let's see some examples on how to send commands to Redis server.
+
+##### Various Parameter Types
+
+```C++
+// ***** Parameters of StringView type *****
+
+// Implicitly construct StringView with c-style string.
+redis.set("key", "value");
+
+// Implicitly construct StringView with std::string.
+std::string key("key");
+std::string val("value");
+redis.set(key, val);
+
+// Explicitly pass StringView as parameter.
+std::vector<char> large_data;
+// Avoid copying.
+redis.set("key", StringView(large_data.data(), large_data.size()));
+
+// ***** Parameters of long long type *****
+
+// For index.
+redis.bitcount(key, 1, 3);
+
+// For number.
+redis.incrby("num", 100);
+
+// ***** Parameters of double type *****
+
+// For score.
+redis.zadd("zset", "m1", 2.5);
+redis.zadd("zset", "m2", 3.5);
+redis.zadd("zset", "m3", 5);
+
+// For (longitude, latitude).
+redis.geoadd("geo", std::make_tuple("member", 13.5, 15.6));
+
+// ***** Time-related parameters *****
+
+using namespace std::chrono;
+
+redis.expire(key, seconds(1000));
+
+auto tp = time_point_cast<seconds>(system_clock::now() + seconds(100));
+redis.expireat(key, tp);
+
+// ***** Some options for commands *****
+
+if (redis.set(key, "value", milliseconds(100), UpdateType::NOT_EXIST)) {
+    std::cout << "set OK" << std::endl;
+}
+
+redis.linsert("list", InsertPosition::BEFORE, "pivot", "val");
+
+std::vector<std::string> res;
+
+// (-inf, inf)
+redis.zrangebyscore("zset", UnboundedInterval<double>{}, std::back_inserter(res));
+
+// [3, 6]
+redis.zrangebyscore("zset",
+    BoundedInterval<double>(3, 6, BoundType::CLOSED),
+    std::back_inserter(res));
+
+// (3, 6]
+redis.zrangebyscore("zset",
+    BoundedInterval<double>(3, 6, BoundType::LEFT_OPEN),
+    std::back_inserter(res));
+
+// (3, 6)
+redis.zrangebyscore("zset",
+    BoundedInterval<double>(3, 6, BoundType::OPEN),
+    std::back_inserter(res));
+
+// [3, 6)
+redis.zrangebyscore("zset",
+    BoundedInterval<double>(3, 6, BoundType::RIGHT_OPEN),
+    std::back_inserter(res));
+
+// [3, +inf)
+redis.zrangebyscore("zset",
+    LeftBoundedInterval<double>(3, BoundType::RIGHT_OPEN),
+    std::back_inserter(res));
+
+// (3, +inf)
+redis.zrangebyscore("zset",
+    LeftBoundedInterval<double>(3, BoundType::OPEN),
+    std::back_inserter(res));
+
+// (-inf, 6]
+redis.zrangebyscore("zset",
+    RightBoundedInterval<double>(6, BoundType::LEFT_OPEN),
+    std::back_inserter(res));
+
+// (-inf, 6)
+redis.zrangebyscore("zset",
+    RightBoundedInterval<double>(6, BoundType::OPEN),
+    std::back_inserter(res));
+
+// ***** Pair of iterators *****
+
+std::vector<std::pair<std::string, std::string>> kvs = {{"k1", "v1"}, {"k2", "v2"}, {"k3", "v3"}};
+redis.mset(kvs.begin(), kvs.end());
+
+std::unordered_map<std::string, std::string> kv_map = {{"k1", "v1"}, {"k2", "v2"}, {"k3", "v3"}};
+redis.mset(kv_map.begin(), kv_map.end());
+
+std::unordered_map<std::string, std::string> str_map = {{"f1", "v1"}, {"f2", "v2"}, {"f3", "v3"}};
+redis.hmset("hash", str_map.begin(), str_map.end());
+
+std::unordered_map<std::string, double> score_map = {{"m1", 20}, {"m2", 12.5}, {"m3", 3.14}};
+redis.zadd("zset", score_map.begin(), score_map.end());
+
+std::vector<std::string> keys = {"k1", "k2", "k3"};
+redis.del(keys.begin(), keys.end());
+
+// ***** Parameters of initializer_list type *****
+
+redis.mset({
+    std::make_pair("k1", "v1"),
+    std::make_pair("k2", "v2"),
+    std::make_pair("k3", "v3")
+});
+
+redis.hmset("hash",
+    {
+        std::make_pair("f1", "v1"),
+        std::make_pair("f2", "v2"),
+        std::make_pair("f3", "v3")
+    });
+
+redis.zadd("zset",
+    {
+        std::make_pair("m1", 20.0),
+        std::make_pair("m2", 34.5),
+        std::make_pair("m3", 23.4)
+    });
+
+redis.del({"k1", "k2", "k3"});
+```
+
+##### Various Return Types
+
+```C++
+// ***** Return void *****
+
+redis.save();
+
+// ***** Return std::string *****
+
+auto info = redis.info();
+
+// ***** Return bool *****
+
+if (!redis.expire("nonexistent", std::chrono::seconds(100))) {
+    std::cerr << "key doesn't exist" << std::endl;
+}
+
+if (redis.setnx("key", "val")) {
+    std::cout << "set OK" << std::endl;
+}
+
+// ***** Return long long *****
+
+auto len = redis.strlen("key");
+auto num = redis.del({"a", "b", "c"});
+num = redis.incr("a");
+
+// ***** Return double *****
+
+auto real = redis.incrbyfloat("b", 23.4);
+real = redis.hincrbyfloat("c", "f", 34.5);
+
+// ***** Return Optional<std::string>, i.e. OptionalString *****
+
+auto os = redis.get("kk");
+if (os) {
+    std::cout << *os << std::endl;
+} else {
+    std::cerr << "key doesn't exist" << std::endl;
+}
+
+os = redis.spop("set");
+if (os) {
+    std::cout << *os << std::endl;
+} else {
+    std::cerr << "set is empty" << std::endl;
+}
+
+// ***** Return Optional<long long>, i.e. OptionalLongLong *****
+
+auto oll = redis.zrank("zset", "mem");
+if (oll) {
+    std::cout << "rank is " << *oll << std::endl;
+} else {
+    std::cerr << "member doesn't exist" << std::endl;
+}
+
+// ***** Return Optional<double>, i.e. OptionalDouble *****
+
+auto ob = redis.zscore("zset", "m1");
+if (ob) {
+    std::cout << "score is " << *ob << std::endl;
+} else {
+    std::cerr << "member doesn't exist" << std::endl;
+}
+
+// ***** Return Optional<pair<string, string>> *****
+
+auto op = redis.blpop({"list1", "list2"}, std::chrono::seconds(2));
+if (op) {
+    std::cout << "key is " << op->first << ", value is " << op->second << std::endl;
+} else {
+    std::cerr << "timeout" << std::endl;
+}
+
+// ***** Output iterators *****
+
+std::vector<OptionalString> os_vec;
+redis.mget({"k1", "k2", "k3"}, std::back_inserter(os_vec));
+
+std::vector<std::string> s_vec;
+redis.lrange("list", 0, -1, std::back_inserter(s_vec));
+
+std::unordered_map<std::string, std::string> hash;
+redis.hgetall("hash", std::inserter(hash, hash.end()));
+// You can also save the result in a vecotr of string pair.
+std::vector<std::pair<std::string, std::string>> hash_vec;
+redis.hgetall("hash", std::back_inserter(hash_vec));
+
+std::unordered_set<std::string> str_set;
+redis.smembers("s1", std::inserter(str_set, str_set.end()));
+// You can also save the result in a vecotr of string.
+s_vec.clear();
+redis.smembers("s1", std::back_inserter(s_vec));
+```
+
+##### SCAN Commands
+
+```C++
+auto cursor = 0LL;
+auto pattern = "*pattern*";
+auto count = 5;
+std::vector<std::string> scan_vec;
+while (true) {
+    cursor = redis.scan(cursor, pattern, count, std::back_inserter(scan_vec));
+    // Default pattern is "*", and default count is 10
+    // cursor = redis.scan(cursor, std::back_inserter(scan_vec));
+
+    if (cursor == 0) {
+        break;
+    }
+}
+```
+
+##### Command Overloads
+
+Sometimes the type of output iterator decides which options to send with the command.
+
+```C++
+// If the output iterator is an iterator of a container of string,
+// we send *ZRANGE* command without the *WITHSCORES* option.
+std::vector<std::string> members;
+redis.zrange("list", 0, -1, std::back_inserter(members));
+
+// If it's an iterator of a container of a <string, double> pair,
+// we send *ZRANGE* command with *WITHSCORES* option.
+std::unordered_map<std::string, double> res_with_score;
+redis.zrange("list", 0, -1, std::inserter(res_with_score, res_with_score.end()));
+
+// The above examples also apply to other command with the *WITHSCORES* options,
+// e.g. *ZRANGEBYSCORE*, *ZREVRANGE*, *ZREVRANGEBYSCORE*.
+
+// Another example is the *GEORADIUS* command.
+
+// Only get members.
+members.clear();
+redis.georadius("geo",
+            std::make_pair(10.1, 11.1),
+            100,
+            GeoUnit::KM,
+            10,
+            true,
+            std::back_inserter(members));
+
+// If the iterator is an iterator of a container of tuple<string, double>,
+// we send the *GEORADIUS* command with *WITHDIST* option.
+std::vector<std::tuple<std::string, double>> mem_with_dist;
+redis.georadius("geo",
+            std::make_pair(10.1, 11.1),
+            100,
+            GeoUnit::KM,
+            10,
+            true,
+            std::back_inserter(mem_with_dist));
+
+// If the iterator is an iterator of a container of tuple<string, double, string>,
+// we send the *GEORADIUS* command with *WITHDIST* and *WITHHASH* options.
+std::vector<std::tuple<std::string, double, std::string>> mem_with_dist_hash;
+redis.georadius("geo",
+            std::make_pair(10.1, 11.1),
+            100,
+            GeoUnit::KM,
+            10,
+            true,
+            std::back_inserter(mem_with_dist_hash));
+
+// If the iterator is an iterator of a container of
+// tuple<string, string, pair<double, double>, double>,
+// we send the *GEORADIUS* command with *WITHHASH*, *WITHCOORD* and *WITHDIST* options.
+std::vector<std::tuple<std::string, double, std::string>> mem_with_hash_coord_dist;
+redis.georadius("geo",
+            std::make_pair(10.1, 11.1),
+            100,
+            GeoUnit::KM,
+            10,
+            true,
+            std::back_inserter(mem_with_hash_coord_dist));
+```
+
+Please see [redis.h](https://github.com/sewenew/redis-plus-plus/blob/master/src/sw/redis%2B%2B/redis.h) for more API references, and see the [tests](https://github.com/sewenew/redis-plus-plus/tree/master/test/src/sw/redis%2B%2B) for more examples.
+
+### Generic Command Interface
+
+There're too many Redis commands, we haven't implemented all of them. However, you can use the generic `Redis::command` methods to send any commands to Redis. Unlike other client libraries, `Redis::command` doesn't use format string to combine command arguments into a command string. Instead, you can directly pass command arguments of `StringView` type or arithmetic type as parameters of `Redis::command`. For the reason why we don't use format string, please see [this discussion](https://github.com/sewenew/redis-plus-plus/pull/2).
+
+```C++
+auto redis = Redis("tcp://127.0.0.1");
+
+// Redis class doesn't have built-in *CLIENT SETNAME* method.
+// However, you can use Redis::command to send the command manually.
+redis.command<void>("client", "setname", "name");
+auto val = redis.command<OptionalString>("client", "getname");
+if (val) {
+    std::cout << *val << std::endl;
+}
+
+// NOTE: the following code is for example only. In fact, Redis has built-in
+// methods for the following commands.
+
+// Arguments of the command can be strings.
+// NOTE: for SET command, the return value is NOT always void, I'll explain latter.
+redis.command<void>("set", "key", "100");
+
+// Arguments of the command can be a combination of strings and integers.
+auto num = redis.command<long long>("incrby", "key", 1);
+
+// Argument can also be double.
+auto real = redis.command<double>("incrbyfloat", "key", 2.3);
+
+// Even the key of the command can be of arithmetic type.
+redis.command<void>("set", 100, "value");
+
+val = redis.command<OptionalString>("get", 100);
+
+// If the command returns an array of elements.
+std::vector<OptionalString> result;
+redis.command("mget", "k1", "k2", "k3", std::back_inserter(result));
+
+// Or just parse it into a vector.
+result = redis.command<std::vector<OptionalString>>("mget", "k1", "k2", "k3");
+
+// Arguments of the command can be a range of strings.
+auto set_cmd_strs = {"set", "key", "value"};
+redis.command<void>(set_cmd_strs.begin(), set_cmd_strs.end());
+
+auto get_cmd_strs = {"get", "key"};
+val = redis.command<OptionalString>(get_cmd_strs.begin(), get_cmd_strs.end());
+
+// If it returns an array of elements.
+result.clear();
+auto mget_cmd_strs = {"mget", "key1", "key2"};
+redis.command(mget_cmd_strs.begin(), mget_cmd_strs.end(), std::back_inserter(result));
+```
+
+**NOTE**: The name of some Redis commands is composed with two strings, e.g. *CLIENT SETNAME*. In this case, you need to pass these two strings as two arguments for `Redis::command`.
+
+```C++
+// This is GOOD.
+redis.command<void>("client", "setname", "name");
+
+// This is BAD, and will fail to send command to Redis server.
+// redis.command<void>("client setname", "name");
+```
+
+As I mentioned in the comments, the `SET` command not always returns `void`. Because if you try to set a (key, value) pair with *NX* or *XX* option, you might fail, and Redis will return a *NULL REPLY*. Besides the `SET` command, there're other commands whose return value is NOT a fixed type, you need to parse it by yourself. For example, `Redis::set` method rewrite the reply of `SET` command, and make it return `bool` type, i.e. if no *NX* or *XX* option specified, Redis server will always return an "OK" string, and `Redis::set` returns `true`; if *NX* or *XX* specified, and Redis server returns a *NULL REPLY*, `Redis::set` returns `false`.
+
+So `Redis` class also has other overloaded `command` methods, these methods return a `ReplyUPtr`, i.e. `std::unique_ptr<redisReply, ReplyDeleter>`, object. Normally you don't need to parse it manually. Instead, you only need to pass the reply to `template <typename T> T reply::parse(redisReply &)` to get a value of type `T`. Check the [Return Type section](#return-type) for valid `T` types. If the command returns an array of elements, besides calling `reply::parse` to parse the reply to an STL container, you can also call `template <typename Output> reply::to_array(redisReply &reply, Output output)` to parse the result into an array or STL container with an output iterator.
+
+Let's rewrite the above examples:
+
+```C++
+auto redis = Redis("tcp://127.0.0.1");
+
+redis.command("client", "setname", "name");
+auto r = redis.command("client", "getname");
+assert(r);
+
+// If the command returns a single element,
+// use `reply::parse<T>(redisReply&)` to parse it.
+auto val = reply::parse<OptionalString>(*r);
+if (val) {
+    std::cout << *val << std::endl;
+}
+
+// Arguments of the command can be strings.
+redis.command("set", "key", "100");
+
+// Arguments of the command can be a combination of strings and integers.
+r = redis.command("incrby", "key", 1);
+auto num = reply::parse<long long>(*r);
+
+// Argument can also be double.
+r = redis.command("incrbyfloat", "key", 2.3);
+auto real = reply::parse<double>(*r);
+
+// Even the key of the command can be of arithmetic type.
+redis.command("set", 100, "value");
+
+r = redis.command("get", 100);
+val = reply::parse<OptionalString>(*r);
+
+// If the command returns an array of elements.
+r = redis.command("mget", "k1", "k2", "k3");
+// Use `reply::to_array(redisReply&, OutputIterator)` to parse the result into an STL container.
+std::vector<OptionalString> result;
+reply::to_array(*r, std::back_inserter(result));
+
+// Or just call `reply::parse` to parse it into vector.
+result = reply::parse<std::vector<OptionalString>>(*r);
+
+// Arguments of the command can be a range of strings.
+auto get_cmd_strs = {"get", "key"};
+r = redis.command(get_cmd_strs.begin(), get_cmd_strs.end());
+val = reply::parse<OptionalString>(*r);
+
+// If it returns an array of elements.
+result.clear();
+auto mget_cmd_strs = {"mget", "key1", "key2"};
+r = redis.command(mget_cmd_strs.begin(), mget_cmd_strs.end());
+reply::to_array(*r, std::back_inserter(result));
+```
+
+In fact, there's one more `Redis::command` method:
+
+```C++
+template <typename Cmd, typename ...Args>
+auto command(Cmd cmd, Args &&...args)
+    -> typename std::enable_if<!std::is_convertible<Cmd, StringView>::value, ReplyUPtr>::type;
+```
+
+However, this method exposes some implementation details, and is only for internal use. You should NOT use this method.
+
+### Publish/Subscribe
+
+You can use `Redis::publish` to publish messages to channels. `Redis` randomly picks a connection from the underlying connection pool, and publishes message with that connection. So you might publish two messages with two different connections.
+
+When you subscribe to a channel with a connection, all messages published to the channel are sent back to that connection. So there's NO `Redis::subscribe` method. Instead, you can call `Redis::subscriber` to create a `Subscriber` and the `Subscriber` maintains a connection to Redis. The underlying connection is a new connection, NOT picked from the connection pool. This new connection has the same `ConnectionOptions` as the `Redis` object.
+
+With `Subscriber`, you can call `Subscriber::subscribe`, `Subscriber::unsubscribe`, `Subscriber::psubscribe` and `Subscriber::punsubscribe` to send *SUBSCRIBE*, *UNSUBSCRIBE*, *PSUBSCRIBE* and *PUNSUBSCRIBE* commands to Redis.
+
+#### Thread Safety
+
+`Subscriber` is NOT thread-safe. If you want to call its member functions in multi-thread environment, you need to synchronize between threads manually.
+
+#### Subscriber Callbacks
+
+There are 6 kinds of messages:
+- *MESSAGE*: message sent to a channel.
+- *PMESSAGE*: message sent to channels of a given pattern.
+- *SUBSCRIBE*: message sent when we successfully subscribe to a channel.
+- *UNSUBSCRIBE*: message sent when we successfully unsubscribe to a channel.
+- *PSUBSCRIBE*: message sent when we successfully subscribe to a channel pattern.
+- *PUNSUBSCRIBE*: message sent when we successfully unsubscribe to a channel pattern.
+
+We call messages of *SUBSCRIBE*, *UNSUBSCRIBE*, *PSUBSCRIBE* and *PUNSUBSCRIBE* types as *META MESSAGE*s.
+
+In order to process these messages, you can set callback functions on `Subscriber`:
+- `Subscriber::on_message(MsgCallback)`: set callback function for messages of *MESSAGE* type, and the callback interface is: `void (std::string channel, std::string msg)`.
+- `Subscriber::on_pmessage(PatternMsgCallback)`: set the callback function for messages of *PMESSAGE* type, and the callback interface is: `void (std::string pattern, std::string channel, std::string msg)`.
+- `Subscriber::on_meta(MetaCallback)`: set callback function for messages of *META MESSAGE* type, and the callback interface is: `void (Subscriber::MsgType type, OptionalString channel, long long num)`. `type` is an enum, it can be one of the following enum: `Subscriber::MsgType::SUBSCRIBE`, `Subscriber::MsgType::UNSUBSCRIBE`, `Subscriber::MsgType::PSUBSCRIBE`, `Subscriber::MsgType::PUNSUBSCRIBE`, `Subscriber::MsgType::MESSAGE`, and `Subscriber::MsgType::PMESSAGE`. If you haven't subscribe/psubscribe to any channel/pattern, and try to unsubscribe/punsubscribe without any parameter, i.e. unsubscribe/punsubscribe all channels/patterns, *channel* will be null. So the second parameter of meta callback is of type `OptionalString`.
+
+All these callback interfaces pass `std::string` by value, and you can take their ownership (i.e. `std::move`) safely.
+
+#### Consume Messages
+
+You can call `Subscriber::consume` to consume messages published to channels/patterns that the `Subscriber` has been subscribed.
+
+`Subscriber::consume` waits for message from the underlying connection. If the `ConnectionOptions::socket_timeout` is reached, and there's no message sent to this connection, `Subscriber::consume` throws a `TimeoutError` exception. If `ConnectionOptions::socket_timeout` is `0ms`, `Subscriber::consume` blocks until it receives a message.
+
+After receiving the message, `Subscriber::consume` calls the callback function to process the message based on message type. However, if you don't set callback for a specific kind of message, `Subscriber::consume` will ignore the received message, i.e. no callback will be called.
+
+#### Examples
+
+The following example is a common pattern for using `Subscriber`:
+
+```C++
+// Create a Subscriber.
+auto sub = redis.subscriber();
+
+// Set callback functions.
+sub.on_message([](std::string channel, std::string msg) {
+            // Process message of MESSAGE type.
+        });
+
+sub.on_pmessage([](std::string pattern, std::string channel, std::string msg) {
+            // Process message of PMESSAGE type.
+        });
+
+sub.on_meta([](Subscriber::MsgType type, OptionalString channel, long long num) {
+            // Process message of META type.
+        });
+
+// Subscribe to channels and patterns.
+sub.subscribe("channel1");
+sub.subscribe({"channel2", "channel3"});
+
+sub.psubscribe("pattern1*");
+
+// Consume messages in a loop.
+while (true) {
+    try {
+        sub.consume();
+    } catch (const Error &err) {
+        // Handle exceptions.
+    }
+}
+```
+
+If `ConnectionOptions::socket_timeout` is set, you might get `TimeoutError` exception before receiving a message:
+
+```C++
+while (true) {
+    try {
+        sub.consume();
+    } catch (const TimeoutError &e) {
+        // Try again.
+        continue;
+    } catch (const Error &err) {
+        // Handle other exceptions.
+    }
+}
+```
+
+The above examples use lambda as callback. If you're not familiar with lambda, you can also set a free function as callback. Check [this issue](https://github.com/sewenew/redis-plus-plus/issues/16) for detail.
+
+### Pipeline
+
+[Pipeline](https://redis.io/topics/pipelining) is used to reduce *RTT* (Round Trip Time), and speed up Redis queries. *redis-plus-plus* supports pipeline with the `Pipeline` class.
+
+#### Create Pipeline
+
+You can create a pipeline with `Redis::pipeline` method, which returns a `Pipeline` object.
+
+```C++
+ConnectionOptions connection_options;
+ConnectionPoolOptions pool_options;
+
+Redis redis(connection_options, pool_options);
+
+auto pipe = redis.pipeline();
+```
+
+When creating a `Pipeline` object, `Redis::pipeline` method creates a new connection to Redis server. This connection is NOT picked from the connection pool, but a newly created connection. This connection has the same `ConnectionOptions` as other connections in the connection pool. `Pipeline` object maintains the new connection, and all piped commands are sent through this connection.
+
+**NOTE**: Creating a `Pipeline` object is NOT cheap, since it creates a new connection. So you'd better reuse the `Pipeline` object as much as possible.
+
+#### Send Commands
+
+You can send Redis commands through the `Pipeline` object. Just like the `Redis` class, `Pipeline` has one or more (overloaded) methods for each Redis command. However, you CANNOT get the replies until you call `Pipeline::exec`. So these methods do NOT return the reply, instead they return the `Pipeline` object itself. And you can chain these methods calls.
+
+```C++
+pipe.set("key", "val").incr("num").rpush("list", {0, 1, 2}).command("hset", "key", "field", "value");
+```
+
+#### Get Replies
+
+Once you finish sending commands to Redis, you can call `Pipeline::exec` to get replies of these commands. You can also chain `Pipeline::exec` with other commands.
+
+```C++
+pipe.set("key", "val").incr("num");
+auto replies = pipe.exec();
+
+// The same as:
+replies = pipe.set("key", "val").incr("num).exec();
+```
+
+In fact, these commands won't be sent to Redis, until you call `Pipeline::exec`. So `Pipeline::exec` does 2 work in order: send all piped commands, then get all replies from Redis.
+
+Also you can call `Pipeline::discard` to discard those piped commands.
+
+```C++
+pipe.set("key", "val").incr("num");
+
+pipe.discard();
+```
+
+#### Parse Replies
+
+`Pipeline::exec` returns a `QueuedReplies` object, which contains replies of all commands that have been sent to Redis. You can use `QueuedReplies::get` method to get and parse the `ith` reply. It has 3 overloads:
+
+- `template <typename Result> Result get(std::size_t idx)`: Return the `ith` reply as a return value, and you need to specify the return type as tempalte parameter.
+- `template <typename Output> void get(std::size_t idx, Output output)`: If the reply is of type *Array Reply*, you can call this method to write the `ith` reply to an output iterator. Normally, compiler will deduce the type of the output iterator, and you don't need to specify the type parameter explicitly.
+- `redisReply& get(std::size_t idx)`: If the reply is NOT a fixed type, call this method to get a reference to `redisReply` object. In this case, you need to call `template <typename T> T reply::parse(redisReply &)` to parse the reply manually.
+
+Check the [Return Type section](#return-type) for details on the return types of the result.
+
+```C++
+auto replies = pipe.set("key", "val").incr("num").lrange("list", 0, -1).exec();
+
+auto set_cmd_result = replies.get<bool>(0);
+
+auto incr_cmd_result = replies.get<long long>(1);
+
+std::vector<std::string> list_cmd_result;
+replies.get(2, std::back_inserter(list_cmd_result));
+```
+
+#### Exception
+
+If any of `Pipeline`'s method throws an exception, the `Pipeline` object enters an invalid state. You CANNOT use it any more, but only destroy the object, and create a new one.
+
+#### Thread Safety
+
+`Pipeline` is NOT thread-safe. If you want to call its member functions in multi-thread environment, you need to synchronize between threads manually.
+
+### Transaction
+
+[Transaction](https://redis.io/topics/transactions) is used to make multiple commands runs atomically.
+
+#### Create Transaction
+
+You can create a transaction with `Redis::transaction` method, which returns a `Transaction` object.
+
+```C++
+ConnectionOptions connection_options;
+ConnectionPoolOptions pool_options;
+
+Redis redis(connection_options, pool_options);
+
+auto tx = redis.transaction();
+```
+
+As the `Pipeline` class, `Transaction` maintains a newly created connection to Redis. This connection has the same `ConnectionOptions` as the `Redis` object.
+
+**NOTE**: Creating a `Transaction` object is NOT cheap, since it creates a new connection. So you'd better reuse the `Transaction` as much as possible.
+
+Also you don't need to send [MULTI](https://redis.io/commands/multi) command to Redis. `Transaction` will do that for you automatically.
+
+#### Send Commands
+
+`Transaction` shares most of implementation with `Pipeline`. It has the same interfaces as `Pipeline`. You can send commands as what you do with `Pipeline` object.
+
+```C++
+tx.set("key", "val").incr("num").lpush("list", {0, 1, 2}).command("hset", "key", "field", "val");
+```
+
+#### Execute Transaction
+
+When you call `Transaction::exec`, you explicitly ask Redis to execute those queued commands, and return the replies. Otherwise, these commands won't be executed. Also, you can call `Transaction::discard` to discard the execution, i.e. no command will be executed. Both `Transaction::exec` and `Transaction::discard` can be chained with other commands.
+
+```C++
+auto replies = tx.set("key", "val").incr("num").exec();
+
+tx.set("key", "val").incr("num");
+
+// Discard the transaction.
+tx.discard();
+```
+
+#### Parse Replies
+
+See [Pipeline's Parse Replies section](#parse-replies) for how to parse the replies.
+
+#### Piped Transaction
+
+Normally, we always send multiple commnds in a transaction. In order to improve the performance, you can send these commands in a pipeline. You can create a piped transaction by passing `true` as parameter of `Redis::transaction` method.
+
+```C++
+// Create a piped transaction
+auto tx = redis.transaction(true);
+```
+
+With this piped transaction, all commands are sent to Redis in a pipeline.
+
+#### Exception
+
+If any of `Transaction`'s method throws an exception other than `WatchError`, the `Transaction` object enters an invalid state. You CANNOT use it any more, but only destroy the object and create a new one.
+
+#### Thread Safety
+
+`Transacation` is NOT thread-safe. If you want to call its member functions in multi-thread environment, you need to synchronize between threads manually.
+
+#### Watch
+
+[WATCH is used to provide a check-and-set(CAS) behavior to Redis transactions](https://redis.io/topics/transactions#optimistic-locking-using-check-and-set).
+
+The `WATCH` command must be sent in the same connection as the transaction. And normally after the `WATCH` command, we also need to send some other commands to get data from Redis before executing the transaction. Take the following check-and-set case as an example:
+
+```
+WATCH key           // watch a key
+val = GET key       // get value of the key
+new_val = val + 1   // incr the value
+MULTI               // begin the transaction
+SET key new_val     // set value only if the value is NOT modified by others
+EXEC                // try to execute the transaction.
+                    // if val has been modified, the transaction won't be executed.
+```
+
+However, with `Transaction` object, you CANNOT get the result of commands until the whole transaction has been finished. Instead, you need to create a `Redis` object from the `Transaction` object. The created `Redis` object shares the connection with `Transaction` object. With this created `Redis` object, you can send `WATCH` command and any other Redis commands to Redis server, and get the result immediately.
+
+Let's see how to implement the above example with *redis-plus-plus*:
+
+```C++
+auto redis = Redis("tcp://127.0.0.1");
+
+// Create a transaction.
+auto tx = redis.transaction();
+
+// Create a Redis object from the Transaction object. Both objects share the same connection.
+auto r = tx.redis();
+
+// If the watched key has been modified by other clients, the transaction might fail.
+// So we need to retry the transaction in a loop.
+while (true) {
+    try {
+        // Watch a key.
+        r.watch("key");
+
+        // Get the old value.
+        auto val = r.get("key");
+        auto num = 0;
+        if (val) {
+            num = std::stoi(*val);
+        } // else use default value, i.e. 0.
+
+        // Incr value.
+        ++num;
+
+        // Execute the transaction.
+        auto replies = tx.set("key", std::to_string(num)).exec();
+
+        // Transaction has been executed successfully. Check the result and break.
+
+        assert(replies.size() == 1 && replies.get<bool>(0) == true);
+
+        break;
+    } catch (const WatchError &err) {
+        // Key has been modified by other clients, retry.
+        continue;
+    } catch (const Error &err) {
+        // Something bad happens, and the Transaction object is no longer valid.
+        throw;
+    }
+}
+```
+
+### Redis Cluster
+
+*redis-plus-plus* supports [Redis Cluster](https://redis.io/topics/cluster-tutorial). You can use `RedisCluster` class to send commands to Redis Cluster. It has similar interfaces as `Redis` class.
+
+#### Connection
+
+`RedisCluster` connects to all master nodes in the cluster. For each master node, it maintains a connection pool. By now, it doesn't connect to slave nodes.
+
+You can initialize a `RedisCluster` instance with `ConnectionOptions` and `ConnectionPoolOptions`. You only need to set one master node's host & port in `ConnectionOptions`, and `RedisCluster` will get other nodes' info automatically (with the *CLUSTER SLOTS* command). For each master node, it creates a connection pool with the specified `ConnectionPoolOptions`. If `ConnectionPoolOptions` is not specified, `RedisCluster` maintains a single connection to every master node.
+
+```C++
+// Set a master node's host & port.
+ConnectionOptions connection_options;
+connection_options.host = "127.0.0.1";  // Required.
+connection_options.port = 7000; // Optional. The default port is 6379.
+connection_options.password = "auth"; // Optional. No password by default.
+
+// Automatically get other nodes' info,
+// and connect to every master node with a single connection.
+RedisCluster cluster1(connection_options);
+
+ConnectionPoolOptions pool_options;
+pool_options.size = 3;
+
+// For each master node, maintains a connection pool of size 3.
+RedisCluster cluster2(connection_options, pool_options);
+```
+
+You can also specify connection option with an URI. However, in this way, you can only use default `ConnectionPoolOptions`, i.e. pool of size 1, and CANNOT specify password.
+
+```C++
+// Specify a master node's host & port.
+RedisCluster cluster3("tcp://127.0.0.1:7000");
+
+// Use default port, i.e. 6379.
+RedisCluster cluster4("tcp://127.0.0.1");
+```
+
+##### Note
+
+- `RedisCluster` only works with tcp connection. It CANNOT connect to Unix Domain Socket. If you specify Unix Domain Socket in `ConnectionOptions`, it throws an exception.
+- All nodes in the cluster should have the same password.
+- Since [Redis Cluster does NOT support multiple databses](https://redis.io/topics/cluster-spec#implemented-subset), `ConnectionOptions::db` is ignored.
+
+#### Interfaces
+
+As we mentioned above, `RedisCluster`'s interfaces are similar to `Redis`. It supports most of `Redis`' interfaces, including the [generic command interface](#generic-command-interface) (see `Redis`' [API Reference section](#api-reference) for details), except the following:
+
+- Not support commands without key as argument, e.g. `PING`, `INFO`.
+- Not support Lua script without key parameters.
+
+Since there's no key parameter, `RedisCluster` has no idea on to which node these commands should be sent. However there're 2 workarounds for this problem:
+
+- If you want to send these commands to a specific node, you can create a `Redis` object with that node's host and port, and use the `Redis` object to do the work.
+- Instead of host and port, you can also call `Redis RedisCluster::redis(const StringView &hash_tag)` to create a `Redis` object with a hash-tag specifying the node. In this case, the returned `Redis` object creates a new connection to Redis server.
+
+Also you can use the [hash tags](https://redis.io/topics/cluster-spec#keys-hash-tags) to send multiple-key commands.
+
+See the [example section](#examples-2) for details.
+
+##### Publish/Subscribe
+
+You can publish and subscribe messages with `RedisCluster`. The interfaces are exactly the same as `Redis`, i.e. use `RedisCluster::publish` to publish messages, and use `RedisCluster::subscriber` to create a subscriber to consume messages. See [Publish/Subscribe section](#publishsubscribe) for details.
+
+##### Pipeline and Transaction
+
+You can also create `Pipeline` and `Transaction` objects with `RedisCluster`, but the interfaces are different from `Redis`. Since all commands in the pipeline and transaction should be sent to a single node in a single connection, we need to tell `RedisCluster` with which node the pipeline or transaction should be created.
+
+Instead of specifing the node's IP and port, `RedisCluster`'s pipeline and transaction interfaces allow you to specify the node with a *hash tag*. `RedisCluster` will calculate the slot number with the given *hash tag*, and create a pipeline or transaction with the node holding the slot.
+
+```C++
+Pipeline RedisCluster::pipeline(const StringView &hash_tag);
+
+Transaction RedisCluster::transaction(const StringView &hash_tag, bool piped = false);
+```
+
+With the created `Pipeline` or `Transaction` object, you can send commands with keys located on the same node as the given *hash_tag*. See [Examples section](#examples-2) for an example.
+
+#### Examples
+
+```C++
+#include <sw/redis++/redis++.h>
+
+using namespace sw::redis;
+
+auto redis_cluster = RedisCluster("tcp://127.0.0.1:7000");
+
+redis_cluster.set("key", "value");
+auto val = redis_cluster.get("key");
+if (val) {
+    std::cout << *val << std::endl;
+}
+
+// With hash-tag.
+redis_cluster.set("key{tag}1", "val1");
+redis_cluster.set("key{tag}2", "val2");
+redis_cluster.set("key{tag}3", "val3");
+std::vector<OptionalString> hash_tag_res;
+redis_cluster.mget({"key{tag}1", "key{tag}2", "key{tag}3"},
+        std::back_inserter(hash_tag_res));
+
+redis_cluster.lpush("list", {"1", "2", "3"});
+std::vector<std::string> list;
+redis_cluster.lrange("list", 0, -1, std::back_inserter(list));
+
+// Pipline.
+auto pipe = redis_cluster.pipeline("counter");
+auto replies = pipe.incr("{counter}:1").incr("{counter}:2").exec();
+
+// Transaction.
+auto tx = redis_cluster.transaction("key");
+replies = tx.incr("key").get("key").exec();
+
+// Create a Redis object with hash-tag.
+// It connects to the Redis instance that holds the given key, i.e. hash-tag.
+auto r = redis_cluster.redis("hash-tag");
+
+// And send command without key parameter to the server.
+r.command("client", "setname", "connection-name");
+```
+
+**NOTE**: When you use `RedisCluster::redis(const StringView &hash_tag)` to create a `Redis` object, instead of picking a connection from the underlying connection pool, it creates a new connection to the corresponding Redis server. So this is NOT a cheap operation, and you should try to reuse this newly created `Redis` object as much as possible.
+
+```C++
+// This is BAD! It's very inefficient.
+// NEVER DO IT!!!
+// After sending PING command, the newly created Redis object will be destroied.
+cluster.redis("key").ping();
+
+// Then it creates a connection to Redis, and closes the connection after sending the command.
+cluster.redis("key").command("client", "setname", "hello");
+
+// Instead you should reuse the Redis object.
+// This is GOOD!
+auto redis = cluster.redis("key");
+
+redis.ping();
+redis.command("client", "setname", "hello");
+```
+
+#### Details
+
+`RedisCluster` maintains the newest slot-node mapping, and sends command directly to the right node. Normally it works as fast as `Redis`. If the cluster reshards, `RedisCluster` will follow the redirection, and it will finally update the slot-node mapping. It can correctly handle the following resharding cases:
+
+- Data migration between exist nodes.
+- Add new node to the cluster.
+- Remove node from the cluster.
+
+`redis-plus-plus` is able to handle both [MOVED](https://redis.io/topics/cluster-spec#moved-redirection) and [ASK](https://redis.io/topics/cluster-spec#ask-redirection) redirections, so it's a complete Redis Cluster client.
+
+If master is down, the cluster will promote one of its replicas to be the new master. *redis-plus-plus* can also handle this case:
+
+- When the master is down, *redis-plus-plus* losts connection to it. In this case, if you try to send commands to this master, *redis-plus-plus* will try to update slot-node mapping from other nodes. If the mapping remains unchanged, i.e. new master hasn't been elected yet, it fails to send command to Redis Cluster and throws exception.
+- When the new master has been elected, the slot-node mapping will be updated by the cluster. In this case, if you send commands to the cluster, *redis-plus-plus* can get an update-to-date mapping, and sends commands to the new master.
+
+### Redis Sentinel
+
+[Redis Sentinel provides high availability for Redis](https://redis.io/topics/sentinel). If Redis master is down, Redis Sentinels will elect a new master from slaves, i.e. failover. Besides, Redis Sentinel can also act like a configuration provider for clients, and clients can query master or slave address from Redis Sentinel. So that if a failover occurs, clients can ask the new master address from Redis Sentinel.
+
+*redis-plus-plus* supports getting Redis master or slave's IP and port from Redis Sentinel. In order to use this feature, you only need to initialize `Redis` object with Redis Sentinel info, which is composed with 3 parts: `std::shared_ptr<Sentinel>`, master name and role (master or slave).
+
+Before using Redis Sentinel with *redis-plus-plus*, ensure that you have read Redis Sentinel's [doc](https://redis.io/topics/sentinel).
+
+#### Sentinel
+
+You can create a `std::shared_ptr<Sentinel>` object with `SentinelOptions`.
+
+```C++
+SentinelOptions sentinel_opts;
+sentinel_opts.nodes = {{"127.0.0.1", 9000},
+                        {"127.0.0.1", 9001},
+                        {"127.0.0.1", 9002}};   // Required. List of Redis Sentinel nodes.
+
+// Optional. Timeout before we successfully connect to Redis Sentinel.
+// By default, the timeout is 100ms.
+sentinel_opts.connect_timeout = std::chrono::milliseconds(200);
+
+// Optional. Timeout before we successfully send request to or receive response from Redis Sentinel.
+// By default, the timeout is 100ms.
+sentinel_opts.socket_timeout = std::chrono::milliseconds(200);
+
+auto sentinel = std::make_shared<Sentinel>(sentinel_opts);
+```
+
+`SentinelOptions::connect_timeout` and `SentinelOptions::socket_timeout` CANNOT be 0ms, i.e. no timeout and block forever. Otherwise, *redis-plus-plus* will throw an exception.
+
+See [SentinelOptions](https://github.com/sewenew/redis-plus-plus/blob/master/src/sw/redis%2B%2B/sentinel.h#L33) for more options.
+
+#### Role
+
+Besides `std::shared_ptr<Sentinel>` and master name, you also need to specify a role. There are two roles: `Role::MASTER`, and `Role::SLAVE`.
+
+With `Role::MASTER`, *redis-plus-plus* will always connect to current master instance, even if a failover occurs. Each time when *redis-plus-plus* needs to create a new connection to master, or a connection is broken, and it needs to reconnect to master, *redis-plus-plus* will ask master address from Redis Sentinel, and connects to current master. If a failover occurs, *redis-plus-plus* can automatically get the address of the new master, and refresh all connections in the underlying connection pool.
+
+Similarly, with `Role::SLAVE`, *redis-plus-plus* will always connect to a slave instance. A master might have several slaves, *redis-plus-plus* will randomly pick one, and connect to it, i.e. all connections in the underlying connection pool, connect to the same slave instance. If the connection is broken, while this slave instance is still an alive slave, *redis-plus-plus* will reconnect to this slave. However, if this slave instance is down, or it has been promoted to be the master, *redis-plus-plus* will randomly connect to another slave. If there's no slave alive, it throws an exception.
+
+#### Create Redis With Sentinel
+
+When creating a `Redis` object with sentinel, besides the sentinel info, you should also provide `ConnectionOptions` and `ConnectionPoolOptions`. These two options are used to connect to Redis instance. `ConnectionPoolOptions` is optional, if not specified, it creates a single connection the instance.
+
+```C++
+ConnectionOptions connection_opts;
+connection_opts.password = "auth";  // Optional. No password by default.
+connection_opts.connect_timeout = std::chrono::milliseconds(100);   // Required.
+connection_opts.socket_timeout = std::chrono::milliseconds(100);    // Required.
+
+ConnectionPoolOptions pool_opts;
+pool_opts.size = 3; // Optional. The default size is 1.
+
+auto redis = Redis(sentinel, "master_name", Role::MASTER, connection_opts, pool_opts);
+```
+
+You might have noticed that we didn't specify the `host` and `port` fields for `ConnectionOptions`. Because, `Redis` will get these info from Redis Sentinel. Also, in this case, `ConnectionOptions::connect_timeout` and `ConnectionOptions::socket_timeout` CANNOT be 0ms, otherwise, it throws an exception. So you always need to specify these two timeouts manually.
+
+After creating the `Redis` object with sentinel, you can send commands with it, just like an ordinary `Redis` object.
+
+If you want to write to master, and scale read with slaves. You can use the following pattern:
+
+```C++
+auto sentinel = std::make_shared<Sentinel>(sentinel_opts);
+
+auto master = Redis(sentinel, "master_name", Role::MASTER, connection_opts, pool_opts);
+
+auto slave = Redis(sentinel, "master_name", Role::SLAVE, connection_opts, pool_opts);
+
+// Write to master.
+master.set("key", "value");
+
+// Read from slave.
+slave.get("key");
+```
+
+### Redis Stream
+
+Since Redis 5.0, it introduces a new data type: *Redis Stream*. *redis-plus-plus* has built-in methods for all stream commands except the *XINFO* command (of course, you can use the [Generic Command Interface](#generic-command-interface) to send *XINFO* command).
+
+However, the replies of some streams commands, i.e. *XPENDING*, *XREAD*, are complex. So I'll give some examples to show you how to work with these built-in methods.
+
+#### Examples
+
+```C++
+auto redis = Redis("tcp://127.0.0.1");
+
+using Attrs = std::vector<std::pair<std::string, std::string>>;
+
+// You can also use std::unordered_map, if you don't care the order of attributes:
+// using Attrs = std::unordered_map<std::string, std::string>;
+
+Attrs attrs = { {"f1", "v1"}, {"f2", "v2"} };
+
+// Add an item into the stream. This method returns the auto generated id.
+auto id = redis.xadd("key", "*", attrs.begin(), attrs.end());
+
+// Each item is assigned with an id: pair<id, attributes>.
+using Item = std::pair<std::string, Attrs>;
+using ItemStream = std::vector<Item>;
+
+// If you don't care the order of items in the stream, you can also use unordered_map:
+// using ItemStream = std::unordered_map<std::string, Attrs>;
+
+// Read items from a stream, and return at most 10 items.
+// You need to specify a key and an id (timestamp + offset).
+std::unordered_map<std::string, ItemStream> result;
+redis.xread("key", id, 10, std::inserter(result, result.end()));
+
+// Read from multiple streams. For each stream, you need to specify a key and an id.
+std::unordered_map<std::string, std::string> keys = { {"key", id}, {"another-key", "0-0"} };
+redis.xread(keys.begin(), keys.end(), 10, std::inserter(result, result.end()));
+
+// Block for at most 1 second if currently there's no data in the stream.
+redis.xread("key", id, std::chrono::seconds(1), 10, std::inserter(result, result.end()));
+
+// Block for multiple streams.
+redis.xread(keys.begin(), keys.end(), std::chrono::seconds(1), 10, std::inserter(result, result.end()));
+
+// Read items in a range:
+ItemStream item_stream;
+redis.xrange("key", "-", "+", std::back_inserter(item_stream));
+
+// Trim the stream to a given number of items. After the operation, the stream length is NOT exactly
+// 10. Instead, it might be much larger than 10.
+// `XTRIM key MAXLEN 10`
+redis.xtrim("key", 10);
+
+// In order to trim the stream to exactly 10 items, specify the third argument, i.e. approx, as false.
+// `XTRIM key MAXLEN ~ 10`
+redis.xtrim("key", 10, false);
+
+// Delete an item from the stream.
+redis.xdel("key", id);
+
+// Create a consumer group.
+redis.xgroup_create("key", "group", "$");
+
+// If the stream doesn't exist, you can set the fourth argument, i.e. MKSTREAM, to be true.
+// redis.xgroup_create("key", "group", "$", true);
+
+id = redis.xadd("key", "*", attrs.begin(), attrs.end());
+
+// Read item by a consumer of a consumer group.
+redis.xreadgroup("group", "consumer", "key", ">", 1, std::inserter(result, result.end()));
+
+using PendingItem = std::tuple<std::string, std::string, long long, long long>;
+std::vector<PendingItem> pending_items;
+
+// Get pending items of a speicified consumer.
+redis.xpending("key", "group", "-", "+", 1, "consumer", std::back_inserter(pending_items));
+
+redis.xack("key", "group", id);
+
+redis.xgroup_delconsumer("key", "group", "consumer");
+redis.xgroup_destroy("key", "group");
+```
+
+If you have any problem on sending stream commands to Redis, please feel free to let me know.
+
+## Author
+
+*redis-plus-plus* is written by sewenew, who is also active on [StackOverflow](https://stackoverflow.com/users/5384363/for-stack).

+ 2233 - 0
ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/command.h

@@ -0,0 +1,2233 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_COMMAND_H
+#define SEWENEW_REDISPLUSPLUS_COMMAND_H
+
+#include <cassert>
+#include <ctime>
+#include <string>
+#include <chrono>
+#include "connection.h"
+#include "command_options.h"
+#include "command_args.h"
+#include "utils.h"
+
+namespace sw {
+
+namespace redis {
+
+namespace cmd {
+
+// CONNECTION command.
+inline void auth(Connection &connection, const StringView &password) {
+    connection.send("AUTH %b", password.data(), password.size());
+}
+
+inline void echo(Connection &connection, const StringView &msg) {
+    connection.send("ECHO %b", msg.data(), msg.size());
+}
+
+inline void ping(Connection &connection) {
+    connection.send("PING");
+}
+
+inline void quit(Connection &connection) {
+    connection.send("QUIT");
+}
+
+inline void ping(Connection &connection, const StringView &msg) {
+    // If *msg* is empty, Redis returns am empty reply of REDIS_REPLY_STRING type.
+    connection.send("PING %b", msg.data(), msg.size());
+}
+
+inline void select(Connection &connection, long long idx) {
+    connection.send("SELECT %lld", idx);
+}
+
+inline void swapdb(Connection &connection, long long idx1, long long idx2) {
+    connection.send("SWAPDB %lld %lld", idx1, idx2);
+}
+
+// SERVER commands.
+
+inline void bgrewriteaof(Connection &connection) {
+    connection.send("BGREWRITEAOF");
+}
+
+inline void bgsave(Connection &connection) {
+    connection.send("BGSAVE");
+}
+
+inline void dbsize(Connection &connection) {
+    connection.send("DBSIZE");
+}
+
+inline void flushall(Connection &connection, bool async) {
+    if (async) {
+        connection.send("FLUSHALL ASYNC");
+    } else {
+        connection.send("FLUSHALL");
+    }
+}
+
+inline void flushdb(Connection &connection, bool async) {
+    if (async) {
+        connection.send("FLUSHDB ASYNC");
+    } else {
+        connection.send("FLUSHDB");
+    }
+}
+
+inline void info(Connection &connection) {
+    connection.send("INFO");
+}
+
+inline void info(Connection &connection, const StringView &section) {
+    connection.send("INFO %b", section.data(), section.size());
+}
+
+inline void lastsave(Connection &connection) {
+    connection.send("LASTSAVE");
+}
+
+inline void save(Connection &connection) {
+    connection.send("SAVE");
+}
+
+// KEY commands.
+
+inline void del(Connection &connection, const StringView &key) {
+    connection.send("DEL %b", key.data(), key.size());
+}
+
+template <typename Input>
+inline void del_range(Connection &connection, Input first, Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "DEL" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void dump(Connection &connection, const StringView &key) {
+    connection.send("DUMP %b", key.data(), key.size());
+}
+
+inline void exists(Connection &connection, const StringView &key) {
+    connection.send("EXISTS %b", key.data(), key.size());
+}
+
+template <typename Input>
+inline void exists_range(Connection &connection, Input first, Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "EXISTS" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void expire(Connection &connection,
+                    const StringView &key,
+                    long long timeout) {
+    connection.send("EXPIRE %b %lld",
+                    key.data(), key.size(),
+                    timeout);
+}
+
+inline void expireat(Connection &connection,
+                        const StringView &key,
+                        long long timestamp) {
+    connection.send("EXPIREAT %b %lld",
+                    key.data(), key.size(),
+                    timestamp);
+}
+
+inline void keys(Connection &connection, const StringView &pattern) {
+    connection.send("KEYS %b", pattern.data(), pattern.size());
+}
+
+inline void move(Connection &connection, const StringView &key, long long db) {
+    connection.send("MOVE %b %lld",
+                    key.data(), key.size(),
+                    db);
+}
+
+inline void persist(Connection &connection, const StringView &key) {
+    connection.send("PERSIST %b", key.data(), key.size());
+}
+
+inline void pexpire(Connection &connection,
+                    const StringView &key,
+                    long long timeout) {
+    connection.send("PEXPIRE %b %lld",
+                    key.data(), key.size(),
+                    timeout);
+}
+
+inline void pexpireat(Connection &connection,
+                        const StringView &key,
+                        long long timestamp) {
+    connection.send("PEXPIREAT %b %lld",
+                    key.data(), key.size(),
+                    timestamp);
+}
+
+inline void pttl(Connection &connection, const StringView &key) {
+    connection.send("PTTL %b", key.data(), key.size());
+}
+
+inline void randomkey(Connection &connection) {
+    connection.send("RANDOMKEY");
+}
+
+inline void rename(Connection &connection,
+                    const StringView &key,
+                    const StringView &newkey) {
+    connection.send("RENAME %b %b",
+                    key.data(), key.size(),
+                    newkey.data(), newkey.size());
+}
+
+inline void renamenx(Connection &connection,
+                        const StringView &key,
+                        const StringView &newkey) {
+    connection.send("RENAMENX %b %b",
+                    key.data(), key.size(),
+                    newkey.data(), newkey.size());
+}
+
+void restore(Connection &connection,
+                const StringView &key,
+                const StringView &val,
+                long long ttl,
+                bool replace);
+
+inline void scan(Connection &connection,
+                    long long cursor,
+                    const StringView &pattern,
+                    long long count) {
+    connection.send("SCAN %lld MATCH %b COUNT %lld",
+                    cursor,
+                    pattern.data(), pattern.size(),
+                    count);
+}
+
+inline void touch(Connection &connection, const StringView &key) {
+    connection.send("TOUCH %b", key.data(), key.size());
+}
+
+template <typename Input>
+inline void touch_range(Connection &connection, Input first, Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "TOUCH" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void ttl(Connection &connection, const StringView &key) {
+    connection.send("TTL %b", key.data(), key.size());
+}
+
+inline void type(Connection &connection, const StringView &key) {
+    connection.send("TYPE %b", key.data(), key.size());
+}
+
+inline void unlink(Connection &connection, const StringView &key) {
+    connection.send("UNLINK %b", key.data(), key.size());
+}
+
+template <typename Input>
+inline void unlink_range(Connection &connection, Input first, Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "UNLINK" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void wait(Connection &connection, long long numslave, long long timeout) {
+    connection.send("WAIT %lld %lld", numslave, timeout);
+}
+
+// STRING commands.
+
+inline void append(Connection &connection, const StringView &key, const StringView &str) {
+    connection.send("APPEND %b %b",
+                    key.data(), key.size(),
+                    str.data(), str.size());
+}
+
+inline void bitcount(Connection &connection,
+                        const StringView &key,
+                        long long start,
+                        long long end) {
+    connection.send("BITCOUNT %b %lld %lld",
+                    key.data(), key.size(),
+                    start, end);
+}
+
+void bitop(Connection &connection,
+            BitOp op,
+            const StringView &destination,
+            const StringView &key);
+
+template <typename Input>
+void bitop_range(Connection &connection,
+                    BitOp op,
+                    const StringView &destination,
+                    Input first,
+                    Input last);
+
+inline void bitpos(Connection &connection,
+                    const StringView &key,
+                    long long bit,
+                    long long start,
+                    long long end) {
+    connection.send("BITPOS %b %lld %lld %lld",
+                    key.data(), key.size(),
+                    bit,
+                    start,
+                    end);
+}
+
+inline void decr(Connection &connection, const StringView &key) {
+    connection.send("DECR %b", key.data(), key.size());
+}
+
+inline void decrby(Connection &connection, const StringView &key, long long decrement) {
+    connection.send("DECRBY %b %lld",
+                    key.data(), key.size(),
+                    decrement);
+}
+
+inline void get(Connection &connection, const StringView &key) {
+    connection.send("GET %b",
+                    key.data(), key.size());
+}
+
+inline void getbit(Connection &connection, const StringView &key, long long offset) {
+    connection.send("GETBIT %b %lld",
+                    key.data(), key.size(),
+                    offset);
+}
+
+inline void getrange(Connection &connection,
+                        const StringView &key,
+                        long long start,
+                        long long end) {
+    connection.send("GETRANGE %b %lld %lld",
+                    key.data(), key.size(),
+                    start,
+                    end);
+}
+
+inline void getset(Connection &connection,
+                    const StringView &key,
+                    const StringView &val) {
+    connection.send("GETSET %b %b",
+                    key.data(), key.size(),
+                    val.data(), val.size());
+}
+
+inline void incr(Connection &connection, const StringView &key) {
+    connection.send("INCR %b", key.data(), key.size());
+}
+
+inline void incrby(Connection &connection, const StringView &key, long long increment) {
+    connection.send("INCRBY %b %lld",
+                    key.data(), key.size(),
+                    increment);
+}
+
+inline void incrbyfloat(Connection &connection, const StringView &key, double increment) {
+    connection.send("INCRBYFLOAT %b %f",
+                    key.data(), key.size(),
+                    increment);
+}
+
+template <typename Input>
+inline void mget(Connection &connection, Input first, Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "MGET" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+template <typename Input>
+inline void mset(Connection &connection, Input first, Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "MSET" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+template <typename Input>
+inline void msetnx(Connection &connection, Input first, Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "MSETNX" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void psetex(Connection &connection,
+                    const StringView &key,
+                    long long ttl,
+                    const StringView &val) {
+    connection.send("PSETEX %b %lld %b",
+                    key.data(), key.size(),
+                    ttl,
+                    val.data(), val.size());
+}
+
+void set(Connection &connection,
+            const StringView &key,
+            const StringView &val,
+            long long ttl,
+            UpdateType type);
+
+inline void setex(Connection &connection,
+                    const StringView &key,
+                    long long ttl,
+                    const StringView &val) {
+    connection.send("SETEX %b %lld %b",
+                    key.data(), key.size(),
+                    ttl,
+                    val.data(), val.size());
+}
+
+inline void setnx(Connection &connection,
+                    const StringView &key,
+                    const StringView &val) {
+    connection.send("SETNX %b %b",
+                    key.data(), key.size(),
+                    val.data(), val.size());
+}
+
+inline void setrange(Connection &connection,
+                        const StringView &key,
+                        long long offset,
+                        const StringView &val) {
+    connection.send("SETRANGE %b %lld %b",
+                    key.data(), key.size(),
+                    offset,
+                    val.data(), val.size());
+}
+
+inline void strlen(Connection &connection, const StringView &key) {
+    connection.send("STRLEN %b", key.data(), key.size());
+}
+
+// LIST commands.
+
+inline void blpop(Connection &connection, const StringView &key, long long timeout) {
+    connection.send("BLPOP %b %lld",
+                    key.data(), key.size(),
+                    timeout);
+}
+
+template <typename Input>
+inline void blpop_range(Connection &connection,
+                        Input first,
+                        Input last,
+                        long long timeout) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "BLPOP" << std::make_pair(first, last) << timeout;
+
+    connection.send(args);
+}
+
+inline void brpop(Connection &connection, const StringView &key, long long timeout) {
+    connection.send("BRPOP %b %lld",
+                    key.data(), key.size(),
+                    timeout);
+}
+
+template <typename Input>
+inline void brpop_range(Connection &connection,
+                        Input first,
+                        Input last,
+                        long long timeout) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "BRPOP" << std::make_pair(first, last) << timeout;
+
+    connection.send(args);
+}
+
+inline void brpoplpush(Connection &connection,
+                        const StringView &source,
+                        const StringView &destination,
+                        long long timeout) {
+    connection.send("BRPOPLPUSH %b %b %lld",
+                    source.data(), source.size(),
+                    destination.data(), destination.size(),
+                    timeout);
+}
+
+inline void lindex(Connection &connection, const StringView &key, long long index) {
+    connection.send("LINDEX %b %lld",
+                    key.data(), key.size(),
+                    index);
+}
+
+void linsert(Connection &connection,
+                const StringView &key,
+                InsertPosition position,
+                const StringView &pivot,
+                const StringView &val);
+
+inline void llen(Connection &connection,
+                    const StringView &key) {
+    connection.send("LLEN %b", key.data(), key.size());
+}
+
+inline void lpop(Connection &connection, const StringView &key) {
+    connection.send("LPOP %b",
+                    key.data(), key.size());
+}
+
+inline void lpush(Connection &connection, const StringView &key, const StringView &val) {
+    connection.send("LPUSH %b %b",
+                    key.data(), key.size(),
+                    val.data(), val.size());
+}
+
+template <typename Input>
+inline void lpush_range(Connection &connection,
+                        const StringView &key,
+                        Input first,
+                        Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "LPUSH" << key << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void lpushx(Connection &connection, const StringView &key, const StringView &val) {
+    connection.send("LPUSHX %b %b",
+                    key.data(), key.size(),
+                    val.data(), val.size());
+}
+
+inline void lrange(Connection &connection,
+                    const StringView &key,
+                    long long start,
+                    long long stop) {
+    connection.send("LRANGE %b %lld %lld",
+                    key.data(), key.size(),
+                    start,
+                    stop);
+}
+
+inline void lrem(Connection &connection,
+                    const StringView &key,
+                    long long count,
+                    const StringView &val) {
+    connection.send("LREM %b %lld %b",
+                    key.data(), key.size(),
+                    count,
+                    val.data(), val.size());
+}
+
+inline void lset(Connection &connection,
+                    const StringView &key,
+                    long long index,
+                    const StringView &val) {
+    connection.send("LSET %b %lld %b",
+                    key.data(), key.size(),
+                    index,
+                    val.data(), val.size());
+}
+
+inline void ltrim(Connection &connection,
+                    const StringView &key,
+                    long long start,
+                    long long stop) {
+    connection.send("LTRIM %b %lld %lld",
+                    key.data(), key.size(),
+                    start,
+                    stop);
+}
+
+inline void rpop(Connection &connection, const StringView &key) {
+    connection.send("RPOP %b", key.data(), key.size());
+}
+
+inline void rpoplpush(Connection &connection,
+                        const StringView &source,
+                        const StringView &destination) {
+    connection.send("RPOPLPUSH %b %b",
+                    source.data(), source.size(),
+                    destination.data(), destination.size());
+}
+
+inline void rpush(Connection &connection, const StringView &key, const StringView &val) {
+    connection.send("RPUSH %b %b",
+                    key.data(), key.size(),
+                    val.data(), val.size());
+}
+
+template <typename Input>
+inline void rpush_range(Connection &connection,
+                        const StringView &key,
+                        Input first,
+                        Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "RPUSH" << key << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void rpushx(Connection &connection, const StringView &key, const StringView &val) {
+    connection.send("RPUSHX %b %b",
+                    key.data(), key.size(),
+                    val.data(), val.size());
+}
+
+// HASH commands.
+
+inline void hdel(Connection &connection, const StringView &key, const StringView &field) {
+    connection.send("HDEL %b %b",
+                    key.data(), key.size(),
+                    field.data(), field.size());
+}
+
+template <typename Input>
+inline void hdel_range(Connection &connection,
+                        const StringView &key,
+                        Input first,
+                        Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "HDEL" << key << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void hexists(Connection &connection, const StringView &key, const StringView &field) {
+    connection.send("HEXISTS %b %b",
+                    key.data(), key.size(),
+                    field.data(), field.size());
+}
+
+inline void hget(Connection &connection, const StringView &key, const StringView &field) {
+    connection.send("HGET %b %b",
+                    key.data(), key.size(),
+                    field.data(), field.size());
+}
+
+inline void hgetall(Connection &connection, const StringView &key) {
+    connection.send("HGETALL %b", key.data(), key.size());
+}
+
+inline void hincrby(Connection &connection,
+                    const StringView &key,
+                    const StringView &field,
+                    long long increment) {
+    connection.send("HINCRBY %b %b %lld",
+                    key.data(), key.size(),
+                    field.data(), field.size(),
+                    increment);
+}
+
+inline void hincrbyfloat(Connection &connection,
+                            const StringView &key,
+                            const StringView &field,
+                            double increment) {
+    connection.send("HINCRBYFLOAT %b %b %f",
+                    key.data(), key.size(),
+                    field.data(), field.size(),
+                    increment);
+}
+
+inline void hkeys(Connection &connection, const StringView &key) {
+    connection.send("HKEYS %b", key.data(), key.size());
+}
+
+inline void hlen(Connection &connection, const StringView &key) {
+    connection.send("HLEN %b", key.data(), key.size());
+}
+
+template <typename Input>
+inline void hmget(Connection &connection,
+                    const StringView &key,
+                    Input first,
+                    Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "HMGET" << key << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+template <typename Input>
+inline void hmset(Connection &connection,
+                    const StringView &key,
+                    Input first,
+                    Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "HMSET" << key << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void hscan(Connection &connection,
+                    const StringView &key,
+                    long long cursor,
+                    const StringView &pattern,
+                    long long count) {
+    connection.send("HSCAN %b %lld MATCH %b COUNT %lld",
+                    key.data(), key.size(),
+                    cursor,
+                    pattern.data(), pattern.size(),
+                    count);
+}
+
+inline void hset(Connection &connection,
+                    const StringView &key,
+                    const StringView &field,
+                    const StringView &val) {
+    connection.send("HSET %b %b %b",
+                    key.data(), key.size(),
+                    field.data(), field.size(),
+                    val.data(), val.size());
+}
+
+inline void hsetnx(Connection &connection,
+                    const StringView &key,
+                    const StringView &field,
+                    const StringView &val) {
+    connection.send("HSETNX %b %b %b",
+                    key.data(), key.size(),
+                    field.data(), field.size(),
+                    val.data(), val.size());
+}
+
+inline void hstrlen(Connection &connection,
+                    const StringView &key,
+                    const StringView &field) {
+    connection.send("HSTRLEN %b %b",
+                    key.data(), key.size(),
+                    field.data(), field.size());
+}
+
+inline void hvals(Connection &connection, const StringView &key) {
+    connection.send("HVALS %b", key.data(), key.size());
+}
+
+// SET commands
+
+inline void sadd(Connection &connection,
+                    const StringView &key,
+                    const StringView &member) {
+    connection.send("SADD %b %b",
+                    key.data(), key.size(),
+                    member.data(), member.size());
+}
+
+template <typename Input>
+inline void sadd_range(Connection &connection,
+                        const StringView &key,
+                        Input first,
+                        Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "SADD" << key << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void scard(Connection &connection, const StringView &key) {
+    connection.send("SCARD %b", key.data(), key.size());
+}
+
+template <typename Input>
+inline void sdiff(Connection &connection, Input first, Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "SDIFF" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void sdiffstore(Connection &connection,
+                        const StringView &destination,
+                        const StringView &key) {
+    connection.send("SDIFFSTORE %b %b",
+                    destination.data(), destination.size(),
+                    key.data(), key.size());
+}
+
+template <typename Input>
+inline void sdiffstore_range(Connection &connection,
+                                const StringView &destination,
+                                Input first,
+                                Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "SDIFFSTORE" << destination << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+template <typename Input>
+inline void sinter(Connection &connection, Input first, Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "SINTER" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void sinterstore(Connection &connection,
+                        const StringView &destination,
+                        const StringView &key) {
+    connection.send("SINTERSTORE %b %b",
+                    destination.data(), destination.size(),
+                    key.data(), key.size());
+}
+
+template <typename Input>
+inline void sinterstore_range(Connection &connection,
+                                const StringView &destination,
+                                Input first,
+                                Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "SINTERSTORE" << destination << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void sismember(Connection &connection,
+                        const StringView &key,
+                        const StringView &member) {
+    connection.send("SISMEMBER %b %b",
+                    key.data(), key.size(),
+                    member.data(), member.size());
+}
+
+inline void smembers(Connection &connection, const StringView &key) {
+    connection.send("SMEMBERS %b", key.data(), key.size());
+}
+
+inline void smove(Connection &connection,
+                    const StringView &source,
+                    const StringView &destination,
+                    const StringView &member) {
+    connection.send("SMOVE %b %b %b",
+                    source.data(), source.size(),
+                    destination.data(), destination.size(),
+                    member.data(), member.size());
+}
+
+inline void spop(Connection &connection, const StringView &key) {
+    connection.send("SPOP %b", key.data(), key.size());
+}
+
+inline void spop_range(Connection &connection, const StringView &key, long long count) {
+    connection.send("SPOP %b %lld",
+                    key.data(), key.size(),
+                    count);
+}
+
+inline void srandmember(Connection &connection, const StringView &key) {
+    connection.send("SRANDMEMBER %b", key.data(), key.size());
+}
+
+inline void srandmember_range(Connection &connection,
+                                const StringView &key,
+                                long long count) {
+    connection.send("SRANDMEMBER %b %lld",
+                    key.data(), key.size(),
+                    count);
+}
+
+inline void srem(Connection &connection,
+                    const StringView &key,
+                    const StringView &member) {
+    connection.send("SREM %b %b",
+                    key.data(), key.size(),
+                    member.data(), member.size());
+}
+
+template <typename Input>
+inline void srem_range(Connection &connection,
+                    const StringView &key,
+                    Input first,
+                    Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "SREM" << key << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void sscan(Connection &connection,
+                    const StringView &key,
+                    long long cursor,
+                    const StringView &pattern,
+                    long long count) {
+    connection.send("SSCAN %b %lld MATCH %b COUNT %lld",
+                    key.data(), key.size(),
+                    cursor,
+                    pattern.data(), pattern.size(),
+                    count);
+}
+
+template <typename Input>
+inline void sunion(Connection &connection, Input first, Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "SUNION" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void sunionstore(Connection &connection,
+                        const StringView &destination,
+                        const StringView &key) {
+    connection.send("SUNIONSTORE %b %b",
+                    destination.data(), destination.size(),
+                    key.data(), key.size());
+}
+
+template <typename Input>
+inline void sunionstore_range(Connection &connection,
+                                const StringView &destination,
+                                Input first,
+                                Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "SUNIONSTORE" << destination << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+// Sorted Set commands.
+
+inline void bzpopmax(Connection &connection, const StringView &key, long long timeout) {
+    connection.send("BZPOPMAX %b %lld", key.data(), key.size(), timeout);
+}
+
+template <typename Input>
+void bzpopmax_range(Connection &connection,
+                    Input first,
+                    Input last,
+                    long long timeout) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "BZPOPMAX" << std::make_pair(first, last) << timeout;
+
+    connection.send(args);
+}
+
+inline void bzpopmin(Connection &connection, const StringView &key, long long timeout) {
+    connection.send("BZPOPMIN %b %lld", key.data(), key.size(), timeout);
+}
+
+template <typename Input>
+void bzpopmin_range(Connection &connection,
+                    Input first,
+                    Input last,
+                    long long timeout) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "BZPOPMIN" << std::make_pair(first, last) << timeout;
+
+    connection.send(args);
+}
+
+template <typename Input>
+void zadd_range(Connection &connection,
+                const StringView &key,
+                Input first,
+                Input last,
+                UpdateType type,
+                bool changed);
+
+inline void zadd(Connection &connection,
+                    const StringView &key,
+                    const StringView &member,
+                    double score,
+                    UpdateType type,
+                    bool changed) {
+    auto tmp = {std::make_pair(member, score)};
+
+    zadd_range(connection, key, tmp.begin(), tmp.end(), type, changed);
+}
+
+inline void zcard(Connection &connection, const StringView &key) {
+    connection.send("ZCARD %b", key.data(), key.size());
+}
+
+template <typename Interval>
+inline void zcount(Connection &connection,
+                    const StringView &key,
+                    const Interval &interval) {
+    connection.send("ZCOUNT %b %s %s",
+                    key.data(), key.size(),
+                    interval.min().c_str(),
+                    interval.max().c_str());
+}
+
+inline void zincrby(Connection &connection,
+                    const StringView &key,
+                    double increment,
+                    const StringView &member) {
+    connection.send("ZINCRBY %b %f %b",
+                    key.data(), key.size(),
+                    increment,
+                    member.data(), member.size());
+}
+
+inline void zinterstore(Connection &connection,
+                        const StringView &destination,
+                        const StringView &key,
+                        double weight) {
+    connection.send("ZINTERSTORE %b 1 %b WEIGHTS %f",
+                    destination.data(), destination.size(),
+                    key.data(), key.size(),
+                    weight);
+}
+
+template <typename Input>
+void zinterstore_range(Connection &connection,
+                        const StringView &destination,
+                        Input first,
+                        Input last,
+                        Aggregation aggr);
+
+template <typename Interval>
+inline void zlexcount(Connection &connection,
+                        const StringView &key,
+                        const Interval &interval) {
+    const auto &min = interval.min();
+    const auto &max = interval.max();
+
+    connection.send("ZLEXCOUNT %b %b %b",
+                    key.data(), key.size(),
+                    min.data(), min.size(),
+                    max.data(), max.size());
+}
+
+inline void zpopmax(Connection &connection, const StringView &key, long long count) {
+    connection.send("ZPOPMAX %b %lld",
+                        key.data(), key.size(),
+                        count);
+}
+
+inline void zpopmin(Connection &connection, const StringView &key, long long count) {
+    connection.send("ZPOPMIN %b %lld",
+                        key.data(), key.size(),
+                        count);
+}
+
+inline void zrange(Connection &connection,
+                    const StringView &key,
+                    long long start,
+                    long long stop,
+                    bool with_scores) {
+    if (with_scores) {
+        connection.send("ZRANGE %b %lld %lld WITHSCORES",
+                        key.data(), key.size(),
+                        start,
+                        stop);
+    } else {
+        connection.send("ZRANGE %b %lld %lld",
+                        key.data(), key.size(),
+                        start,
+                        stop);
+    }
+}
+
+template <typename Interval>
+inline void zrangebylex(Connection &connection,
+                        const StringView &key,
+                        const Interval &interval,
+                        const LimitOptions &opts) {
+    const auto &min = interval.min();
+    const auto &max = interval.max();
+
+    connection.send("ZRANGEBYLEX %b %b %b LIMIT %lld %lld",
+                    key.data(), key.size(),
+                    min.data(), min.size(),
+                    max.data(), max.size(),
+                    opts.offset,
+                    opts.count);
+}
+
+template <typename Interval>
+void zrangebyscore(Connection &connection,
+                    const StringView &key,
+                    const Interval &interval,
+                    const LimitOptions &opts,
+                    bool with_scores) {
+    const auto &min = interval.min();
+    const auto &max = interval.max();
+
+    if (with_scores) {
+        connection.send("ZRANGEBYSCORE %b %b %b WITHSCORES LIMIT %lld %lld",
+                        key.data(), key.size(),
+                        min.data(), min.size(),
+                        max.data(), max.size(),
+                        opts.offset,
+                        opts.count);
+    } else {
+        connection.send("ZRANGEBYSCORE %b %b %b LIMIT %lld %lld",
+                        key.data(), key.size(),
+                        min.data(), min.size(),
+                        max.data(), max.size(),
+                        opts.offset,
+                        opts.count);
+    }
+}
+
+inline void zrank(Connection &connection,
+                    const StringView &key,
+                    const StringView &member) {
+    connection.send("ZRANK %b %b",
+                    key.data(), key.size(),
+                    member.data(), member.size());
+}
+
+inline void zrem(Connection &connection,
+                    const StringView &key,
+                    const StringView &member) {
+    connection.send("ZREM %b %b",
+                    key.data(), key.size(),
+                    member.data(), member.size());
+}
+
+template <typename Input>
+inline void zrem_range(Connection &connection,
+                        const StringView &key,
+                        Input first,
+                        Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "ZREM" << key << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+template <typename Interval>
+inline void zremrangebylex(Connection &connection,
+                            const StringView &key,
+                            const Interval &interval) {
+    const auto &min = interval.min();
+    const auto &max = interval.max();
+
+    connection.send("ZREMRANGEBYLEX %b %b %b",
+                    key.data(), key.size(),
+                    min.data(), min.size(),
+                    max.data(), max.size());
+}
+
+inline void zremrangebyrank(Connection &connection,
+                            const StringView &key,
+                            long long start,
+                            long long stop) {
+    connection.send("zremrangebyrank %b %lld %lld",
+                    key.data(), key.size(),
+                    start,
+                    stop);
+}
+
+template <typename Interval>
+inline void zremrangebyscore(Connection &connection,
+                                const StringView &key,
+                                const Interval &interval) {
+    const auto &min = interval.min();
+    const auto &max = interval.max();
+
+    connection.send("ZREMRANGEBYSCORE %b %b %b",
+                    key.data(), key.size(),
+                    min.data(), min.size(),
+                    max.data(), max.size());
+}
+
+inline void zrevrange(Connection &connection,
+                        const StringView &key,
+                        long long start,
+                        long long stop,
+                        bool with_scores) {
+    if (with_scores) {
+        connection.send("ZREVRANGE %b %lld %lld WITHSCORES",
+                        key.data(), key.size(),
+                        start,
+                        stop);
+    } else {
+        connection.send("ZREVRANGE %b %lld %lld",
+                        key.data(), key.size(),
+                        start,
+                        stop);
+    }
+}
+
+template <typename Interval>
+inline void zrevrangebylex(Connection &connection,
+                            const StringView &key,
+                            const Interval &interval,
+                            const LimitOptions &opts) {
+    const auto &min = interval.min();
+    const auto &max = interval.max();
+
+    connection.send("ZREVRANGEBYLEX %b %b %b LIMIT %lld %lld",
+                    key.data(), key.size(),
+                    max.data(), max.size(),
+                    min.data(), min.size(),
+                    opts.offset,
+                    opts.count);
+}
+
+template <typename Interval>
+void zrevrangebyscore(Connection &connection,
+                        const StringView &key,
+                        const Interval &interval,
+                        const LimitOptions &opts,
+                        bool with_scores) {
+    const auto &min = interval.min();
+    const auto &max = interval.max();
+
+    if (with_scores) {
+        connection.send("ZREVRANGEBYSCORE %b %b %b WITHSCORES LIMIT %lld %lld",
+                        key.data(), key.size(),
+                        max.data(), max.size(),
+                        min.data(), min.size(),
+                        opts.offset,
+                        opts.count);
+    } else {
+        connection.send("ZREVRANGEBYSCORE %b %b %b LIMIT %lld %lld",
+                        key.data(), key.size(),
+                        max.data(), max.size(),
+                        min.data(), min.size(),
+                        opts.offset,
+                        opts.count);
+    }
+}
+
+inline void zrevrank(Connection &connection,
+                        const StringView &key,
+                        const StringView &member) {
+    connection.send("ZREVRANK %b %b",
+                    key.data(), key.size(),
+                    member.data(), member.size());
+}
+
+inline void zscan(Connection &connection,
+                    const StringView &key,
+                    long long cursor,
+                    const StringView &pattern,
+                    long long count) {
+    connection.send("ZSCAN %b %lld MATCH %b COUNT %lld",
+                    key.data(), key.size(),
+                    cursor,
+                    pattern.data(), pattern.size(),
+                    count);
+}
+
+inline void zscore(Connection &connection,
+                    const StringView &key,
+                    const StringView &member) {
+    connection.send("ZSCORE %b %b",
+                    key.data(), key.size(),
+                    member.data(), member.size());
+}
+
+inline void zunionstore(Connection &connection,
+                        const StringView &destination,
+                        const StringView &key,
+                        double weight) {
+    connection.send("ZUNIONSTORE %b 1 %b WEIGHTS %f",
+                    destination.data(), destination.size(),
+                    key.data(), key.size(),
+                    weight);
+}
+
+template <typename Input>
+void zunionstore_range(Connection &connection,
+                        const StringView &destination,
+                        Input first,
+                        Input last,
+                        Aggregation aggr);
+
+// HYPERLOGLOG commands.
+
+inline void pfadd(Connection &connection,
+                    const StringView &key,
+                    const StringView &element) {
+    connection.send("PFADD %b %b",
+                    key.data(), key.size(),
+                    element.data(), element.size());
+}
+
+template <typename Input>
+inline void pfadd_range(Connection &connection,
+                        const StringView &key,
+                        Input first,
+                        Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "PFADD" << key << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void pfcount(Connection &connection, const StringView &key) {
+    connection.send("PFCOUNT %b", key.data(), key.size());
+}
+
+template <typename Input>
+inline void pfcount_range(Connection &connection,
+                            Input first,
+                            Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "PFCOUNT" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void pfmerge(Connection &connection, const StringView &destination, const StringView &key) {
+    connection.send("PFMERGE %b %b",
+                    destination.data(), destination.size(),
+                    key.data(), key.size());
+}
+
+template <typename Input>
+inline void pfmerge_range(Connection &connection,
+                            const StringView &destination,
+                            Input first,
+                            Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "PFMERGE" << destination << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+// GEO commands.
+
+inline void geoadd(Connection &connection,
+                    const StringView &key,
+                    const std::tuple<StringView, double, double> &member) {
+    const auto &mem = std::get<0>(member);
+
+    connection.send("GEOADD %b %f %f %b",
+                    key.data(), key.size(),
+                    std::get<1>(member),
+                    std::get<2>(member),
+                    mem.data(), mem.size());
+}
+
+template <typename Input>
+inline void geoadd_range(Connection &connection,
+                            const StringView &key,
+                            Input first,
+                            Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "GEOADD" << key;
+
+    while (first != last) {
+        const auto &member = *first;
+        args << std::get<1>(member) << std::get<2>(member) << std::get<0>(member);
+        ++first;
+    }
+
+    connection.send(args);
+}
+
+void geodist(Connection &connection,
+                const StringView &key,
+                const StringView &member1,
+                const StringView &member2,
+                GeoUnit unit);
+
+template <typename Input>
+inline void geohash_range(Connection &connection,
+                            const StringView &key,
+                            Input first,
+                            Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "GEOHASH" << key << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+template <typename Input>
+inline void geopos_range(Connection &connection,
+                            const StringView &key,
+                            Input first,
+                            Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "GEOPOS" << key << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+void georadius(Connection &connection,
+                const StringView &key,
+                const std::pair<double, double> &loc,
+                double radius,
+                GeoUnit unit,
+                long long count,
+                bool asc,
+                bool with_coord,
+                bool with_dist,
+                bool with_hash);
+
+void georadius_store(Connection &connection,
+                        const StringView &key,
+                        const std::pair<double, double> &loc,
+                        double radius,
+                        GeoUnit unit,
+                        const StringView &destination,
+                        bool store_dist,
+                        long long count);
+
+void georadiusbymember(Connection &connection,
+                        const StringView &key,
+                        const StringView &member,
+                        double radius,
+                        GeoUnit unit,
+                        long long count,
+                        bool asc,
+                        bool with_coord,
+                        bool with_dist,
+                        bool with_hash);
+
+void georadiusbymember_store(Connection &connection,
+                                const StringView &key,
+                                const StringView &member,
+                                double radius,
+                                GeoUnit unit,
+                                const StringView &destination,
+                                bool store_dist,
+                                long long count);
+
+// SCRIPTING commands.
+
+inline void eval(Connection &connection,
+                    const StringView &script,
+                    std::initializer_list<StringView> keys,
+                    std::initializer_list<StringView> args) {
+    CmdArgs cmd_args;
+
+    cmd_args << "EVAL" << script << keys.size()
+            << std::make_pair(keys.begin(), keys.end())
+            << std::make_pair(args.begin(), args.end());
+
+    connection.send(cmd_args);
+}
+
+inline void evalsha(Connection &connection,
+                    const StringView &script,
+                    std::initializer_list<StringView> keys,
+                    std::initializer_list<StringView> args) {
+    CmdArgs cmd_args;
+
+    cmd_args << "EVALSHA" << script << keys.size()
+            << std::make_pair(keys.begin(), keys.end())
+            << std::make_pair(args.begin(), args.end());
+
+    connection.send(cmd_args);
+}
+
+inline void script_exists(Connection &connection, const StringView &sha) {
+    connection.send("SCRIPT EXISTS %b", sha.data(), sha.size());
+}
+
+template <typename Input>
+inline void script_exists_range(Connection &connection, Input first, Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "SCRIPT" << "EXISTS" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void script_flush(Connection &connection) {
+    connection.send("SCRIPT FLUSH");
+}
+
+inline void script_kill(Connection &connection) {
+    connection.send("SCRIPT KILL");
+}
+
+inline void script_load(Connection &connection, const StringView &script) {
+    connection.send("SCRIPT LOAD %b", script.data(), script.size());
+}
+
+// PUBSUB commands.
+
+inline void psubscribe(Connection &connection, const StringView &pattern) {
+    connection.send("PSUBSCRIBE %b", pattern.data(), pattern.size());
+}
+
+template <typename Input>
+inline void psubscribe_range(Connection &connection, Input first, Input last) {
+    if (first == last) {
+        throw Error("PSUBSCRIBE: no key specified");
+    }
+
+    CmdArgs args;
+    args << "PSUBSCRIBE" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void publish(Connection &connection,
+                    const StringView &channel,
+                    const StringView &message) {
+    connection.send("PUBLISH %b %b",
+                    channel.data(), channel.size(),
+                    message.data(), message.size());
+}
+
+inline void punsubscribe(Connection &connection) {
+    connection.send("PUNSUBSCRIBE");
+}
+
+inline void punsubscribe(Connection &connection, const StringView &pattern) {
+    connection.send("PUNSUBSCRIBE %b", pattern.data(), pattern.size());
+}
+
+template <typename Input>
+inline void punsubscribe_range(Connection &connection, Input first, Input last) {
+    if (first == last) {
+        throw Error("PUNSUBSCRIBE: no key specified");
+    }
+
+    CmdArgs args;
+    args << "PUNSUBSCRIBE" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void subscribe(Connection &connection, const StringView &channel) {
+    connection.send("SUBSCRIBE %b", channel.data(), channel.size());
+}
+
+template <typename Input>
+inline void subscribe_range(Connection &connection, Input first, Input last) {
+    if (first == last) {
+        throw Error("SUBSCRIBE: no key specified");
+    }
+
+    CmdArgs args;
+    args << "SUBSCRIBE" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void unsubscribe(Connection &connection) {
+    connection.send("UNSUBSCRIBE");
+}
+
+inline void unsubscribe(Connection &connection, const StringView &channel) {
+    connection.send("UNSUBSCRIBE %b", channel.data(), channel.size());
+}
+
+template <typename Input>
+inline void unsubscribe_range(Connection &connection, Input first, Input last) {
+    if (first == last) {
+        throw Error("UNSUBSCRIBE: no key specified");
+    }
+
+    CmdArgs args;
+    args << "UNSUBSCRIBE" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+// Transaction commands.
+
+inline void discard(Connection &connection) {
+    connection.send("DISCARD");
+}
+
+inline void exec(Connection &connection) {
+    connection.send("EXEC");
+}
+
+inline void multi(Connection &connection) {
+    connection.send("MULTI");
+}
+
+inline void unwatch(Connection &connection, const StringView &key) {
+    connection.send("UNWATCH %b", key.data(), key.size());
+}
+
+template <typename Input>
+inline void unwatch_range(Connection &connection, Input first, Input last) {
+    if (first == last) {
+        throw Error("UNWATCH: no key specified");
+    }
+
+    CmdArgs args;
+    args << "UNWATCH" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void watch(Connection &connection, const StringView &key) {
+    connection.send("WATCH %b", key.data(), key.size());
+}
+
+template <typename Input>
+inline void watch_range(Connection &connection, Input first, Input last) {
+    if (first == last) {
+        throw Error("WATCH: no key specified");
+    }
+
+    CmdArgs args;
+    args << "WATCH" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+// Stream commands.
+
+inline void xack(Connection &connection,
+                    const StringView &key,
+                    const StringView &group,
+                    const StringView &id) {
+    connection.send("XACK %b %b %b",
+                    key.data(), key.size(),
+                    group.data(), group.size(),
+                    id.data(), id.size());
+}
+
+template <typename Input>
+void xack_range(Connection &connection,
+                const StringView &key,
+                const StringView &group,
+                Input first,
+                Input last) {
+    CmdArgs args;
+    args << "XACK" << key << group << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+template <typename Input>
+void xadd_range(Connection &connection,
+                const StringView &key,
+                const StringView &id,
+                Input first,
+                Input last) {
+    CmdArgs args;
+    args << "XADD" << key << id << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+template <typename Input>
+void xadd_maxlen_range(Connection &connection,
+                        const StringView &key,
+                        const StringView &id,
+                        Input first,
+                        Input last,
+                        long long count,
+                        bool approx) {
+    CmdArgs args;
+    args << "XADD" << key << "MAXLEN";
+
+    if (approx) {
+        args << "~";
+    }
+
+    args << count << id << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void xclaim(Connection &connection,
+                    const StringView &key,
+                    const StringView &group,
+                    const StringView &consumer,
+                    long long min_idle_time,
+                    const StringView &id) {
+    connection.send("XCLAIM %b %b %b %lld %b",
+                    key.data(), key.size(),
+                    group.data(), group.size(),
+                    consumer.data(), consumer.size(),
+                    min_idle_time,
+                    id.data(), id.size());
+}
+
+template <typename Input>
+void xclaim_range(Connection &connection,
+                    const StringView &key,
+                    const StringView &group,
+                    const StringView &consumer,
+                    long long min_idle_time,
+                    Input first,
+                    Input last) {
+    CmdArgs args;
+    args << "XCLAIM" << key << group << consumer << min_idle_time << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void xdel(Connection &connection, const StringView &key, const StringView &id) {
+    connection.send("XDEL %b %b", key.data(), key.size(), id.data(), id.size());
+}
+
+template <typename Input>
+void xdel_range(Connection &connection, const StringView &key, Input first, Input last) {
+    CmdArgs args;
+    args << "XDEL" << key << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void xgroup_create(Connection &connection,
+                            const StringView &key,
+                            const StringView &group,
+                            const StringView &id,
+                            bool mkstream) {
+    CmdArgs args;
+    args << "XGROUP" << "CREATE" << key << group << id;
+
+    if (mkstream) {
+        args << "MKSTREAM";
+    }
+
+    connection.send(args);
+}
+
+inline void xgroup_setid(Connection &connection,
+                            const StringView &key,
+                            const StringView &group,
+                            const StringView &id) {
+    connection.send("XGROUP SETID %b %b %b",
+                    key.data(), key.size(),
+                    group.data(), group.size(),
+                    id.data(), id.size());
+}
+
+inline void xgroup_destroy(Connection &connection,
+                            const StringView &key,
+                            const StringView &group) {
+    connection.send("XGROUP DESTROY %b %b",
+                    key.data(), key.size(),
+                    group.data(), group.size());
+}
+
+inline void xgroup_delconsumer(Connection &connection,
+                                const StringView &key,
+                                const StringView &group,
+                                const StringView &consumer) {
+    connection.send("XGROUP DELCONSUMER %b %b %b",
+                    key.data(), key.size(),
+                    group.data(), group.size(),
+                    consumer.data(), consumer.size());
+}
+
+inline void xlen(Connection &connection, const StringView &key) {
+    connection.send("XLEN %b", key.data(), key.size());
+}
+
+inline void xpending(Connection &connection, const StringView &key, const StringView &group) {
+    connection.send("XPENDING %b %b",
+                    key.data(), key.size(),
+                    group.data(), group.size());
+}
+
+inline void xpending_detail(Connection &connection,
+                            const StringView &key,
+                            const StringView &group,
+                            const StringView &start,
+                            const StringView &end,
+                            long long count) {
+    connection.send("XPENDING %b %b %b %b %lld",
+                    key.data(), key.size(),
+                    group.data(), group.size(),
+                    start.data(), start.size(),
+                    end.data(), end.size(),
+                    count);
+}
+
+inline void xpending_per_consumer(Connection &connection,
+                                    const StringView &key,
+                                    const StringView &group,
+                                    const StringView &start,
+                                    const StringView &end,
+                                    long long count,
+                                    const StringView &consumer) {
+    connection.send("XPENDING %b %b %b %b %lld %b",
+                    key.data(), key.size(),
+                    group.data(), group.size(),
+                    start.data(), start.size(),
+                    end.data(), end.size(),
+                    count,
+                    consumer.data(), consumer.size());
+}
+
+inline void xrange(Connection &connection,
+                    const StringView &key,
+                    const StringView &start,
+                    const StringView &end) {
+    connection.send("XRANGE %b %b %b",
+                    key.data(), key.size(),
+                    start.data(), start.size(),
+                    end.data(), end.size());
+}
+
+inline void xrange_count(Connection &connection,
+                            const StringView &key,
+                            const StringView &start,
+                            const StringView &end,
+                            long long count) {
+    connection.send("XRANGE %b %b %b COUNT %lld",
+                    key.data(), key.size(),
+                    start.data(), start.size(),
+                    end.data(), end.size(),
+                    count);
+}
+
+inline void xread(Connection &connection,
+                    const StringView &key,
+                    const StringView &id,
+                    long long count) {
+    connection.send("XREAD COUNT %lld STREAMS %b %b",
+                    count,
+                    key.data(), key.size(),
+                    id.data(), id.size());
+}
+
+template <typename Input>
+void xread_range(Connection &connection, Input first, Input last, long long count) {
+    CmdArgs args;
+    args << "XREAD" << "COUNT" << count << "STREAMS";
+
+    for (auto iter = first; iter != last; ++iter) {
+        args << iter->first;
+    }
+
+    for (auto iter = first; iter != last; ++iter) {
+        args << iter->second;
+    }
+
+    connection.send(args);
+}
+
+inline void xread_block(Connection &connection,
+                        const StringView &key,
+                        const StringView &id,
+                        long long timeout,
+                        long long count) {
+    connection.send("XREAD COUNT %lld BLOCK %lld STREAMS %b %b",
+                    count,
+                    timeout,
+                    key.data(), key.size(),
+                    id.data(), id.size());
+}
+
+template <typename Input>
+void xread_block_range(Connection &connection,
+                        Input first,
+                        Input last,
+                        long long timeout,
+                        long long count) {
+    CmdArgs args;
+    args << "XREAD" << "COUNT" << count << "BLOCK" << timeout << "STREAMS";
+
+    for (auto iter = first; iter != last; ++iter) {
+        args << iter->first;
+    }
+
+    for (auto iter = first; iter != last; ++iter) {
+        args << iter->second;
+    }
+
+    connection.send(args);
+}
+
+inline void xreadgroup(Connection &connection,
+                        const StringView &group,
+                        const StringView &consumer,
+                        const StringView &key,
+                        const StringView &id,
+                        long long count,
+                        bool noack) {
+    CmdArgs args;
+    args << "XREADGROUP" << "GROUP" << group << consumer << "COUNT" << count;
+
+    if (noack) {
+        args << "NOACK";
+    }
+
+    args << "STREAMS" << key << id;
+
+    connection.send(args);
+}
+
+template <typename Input>
+void xreadgroup_range(Connection &connection,
+                        const StringView &group,
+                        const StringView &consumer,
+                        Input first,
+                        Input last,
+                        long long count,
+                        bool noack) {
+    CmdArgs args;
+    args << "XREADGROUP" << "GROUP" << group << consumer << "COUNT" << count;
+
+    if (noack) {
+        args << "NOACK";
+    }
+
+    args << "STREAMS";
+
+    for (auto iter = first; iter != last; ++iter) {
+        args << iter->first;
+    }
+
+    for (auto iter = first; iter != last; ++iter) {
+        args << iter->second;
+    }
+
+    connection.send(args);
+}
+
+inline void xreadgroup_block(Connection &connection,
+                                const StringView &group,
+                                const StringView &consumer,
+                                const StringView &key,
+                                const StringView &id,
+                                long long timeout,
+                                long long count,
+                                bool noack) {
+    CmdArgs args;
+    args << "XREADGROUP" << "GROUP" << group << consumer
+        << "COUNT" << count << "BLOCK" << timeout;
+
+    if (noack) {
+        args << "NOACK";
+    }
+
+    args << "STREAMS" << key << id;
+
+    connection.send(args);
+}
+
+template <typename Input>
+void xreadgroup_block_range(Connection &connection,
+                            const StringView &group,
+                            const StringView &consumer,
+                            Input first,
+                            Input last,
+                            long long timeout,
+                            long long count,
+                            bool noack) {
+    CmdArgs args;
+    args << "XREADGROUP" << "GROUP" << group << consumer
+        << "COUNT" << count << "BLOCK" << timeout;
+
+    if (noack) {
+        args << "NOACK";
+    }
+
+    args << "STREAMS";
+
+    for (auto iter = first; iter != last; ++iter) {
+        args << iter->first;
+    }
+
+    for (auto iter = first; iter != last; ++iter) {
+        args << iter->second;
+    }
+
+    connection.send(args);
+}
+
+inline void xrevrange(Connection &connection,
+                    const StringView &key,
+                    const StringView &end,
+                    const StringView &start) {
+    connection.send("XREVRANGE %b %b %b",
+                    key.data(), key.size(),
+                    end.data(), end.size(),
+                    start.data(), start.size());
+}
+
+inline void xrevrange_count(Connection &connection,
+                            const StringView &key,
+                            const StringView &end,
+                            const StringView &start,
+                            long long count) {
+    connection.send("XREVRANGE %b %b %b COUNT %lld",
+                    key.data(), key.size(),
+                    end.data(), end.size(),
+                    start.data(), start.size(),
+                    count);
+}
+
+void xtrim(Connection &connection, const StringView &key, long long count, bool approx);
+
+namespace detail {
+
+void set_bitop(CmdArgs &args, BitOp op);
+
+void set_update_type(CmdArgs &args, UpdateType type);
+
+void set_aggregation_type(CmdArgs &args, Aggregation type);
+
+template <typename Input>
+void zinterstore(std::false_type,
+                    Connection &connection,
+                    const StringView &destination,
+                    Input first,
+                    Input last,
+                    Aggregation aggr) {
+    CmdArgs args;
+    args << "ZINTERSTORE" << destination << std::distance(first, last)
+        << std::make_pair(first, last);
+
+    set_aggregation_type(args, aggr);
+
+    connection.send(args);
+}
+
+template <typename Input>
+void zinterstore(std::true_type,
+                    Connection &connection,
+                    const StringView &destination,
+                    Input first,
+                    Input last,
+                    Aggregation aggr) {
+    CmdArgs args;
+    args << "ZINTERSTORE" << destination << std::distance(first, last);
+
+    for (auto iter = first; iter != last; ++iter) {
+        args << iter->first;
+    }
+
+    args << "WEIGHTS";
+
+    for (auto iter = first; iter != last; ++iter) {
+        args << iter->second;
+    }
+
+    set_aggregation_type(args, aggr);
+
+    connection.send(args);
+}
+
+template <typename Input>
+void zunionstore(std::false_type,
+                    Connection &connection,
+                    const StringView &destination,
+                    Input first,
+                    Input last,
+                    Aggregation aggr) {
+    CmdArgs args;
+    args << "ZUNIONSTORE" << destination << std::distance(first, last)
+        << std::make_pair(first, last);
+
+    set_aggregation_type(args, aggr);
+
+    connection.send(args);
+}
+
+template <typename Input>
+void zunionstore(std::true_type,
+                    Connection &connection,
+                    const StringView &destination,
+                    Input first,
+                    Input last,
+                    Aggregation aggr) {
+    CmdArgs args;
+    args << "ZUNIONSTORE" << destination << std::distance(first, last);
+
+    for (auto iter = first; iter != last; ++iter) {
+        args << iter->first;
+    }
+
+    args << "WEIGHTS";
+
+    for (auto iter = first; iter != last; ++iter) {
+        args << iter->second;
+    }
+
+    set_aggregation_type(args, aggr);
+
+    connection.send(args);
+}
+
+void set_geo_unit(CmdArgs &args, GeoUnit unit);
+
+void set_georadius_store_parameters(CmdArgs &args,
+                                    double radius,
+                                    GeoUnit unit,
+                                    const StringView &destination,
+                                    bool store_dist,
+                                    long long count);
+
+void set_georadius_parameters(CmdArgs &args,
+                                double radius,
+                                GeoUnit unit,
+                                long long count,
+                                bool asc,
+                                bool with_coord,
+                                bool with_dist,
+                                bool with_hash);
+
+}
+
+}
+
+}
+
+}
+
+namespace sw {
+
+namespace redis {
+
+namespace cmd {
+
+template <typename Input>
+void bitop_range(Connection &connection,
+                    BitOp op,
+                    const StringView &destination,
+                    Input first,
+                    Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+
+    detail::set_bitop(args, op);
+
+    args << destination << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+template <typename Input>
+void zadd_range(Connection &connection,
+                const StringView &key,
+                Input first,
+                Input last,
+                UpdateType type,
+                bool changed) {
+    assert(first != last);
+
+    CmdArgs args;
+
+    args << "ZADD" << key;
+
+    detail::set_update_type(args, type);
+
+    if (changed) {
+        args << "CH";
+    }
+
+    while (first != last) {
+        // Swap the <member, score> pair to <score, member> pair.
+        args << first->second << first->first;
+        ++first;
+    }
+
+    connection.send(args);
+}
+
+template <typename Input>
+void zinterstore_range(Connection &connection,
+                        const StringView &destination,
+                        Input first,
+                        Input last,
+                        Aggregation aggr) {
+    assert(first != last);
+
+    detail::zinterstore(typename IsKvPairIter<Input>::type(),
+                        connection,
+                        destination,
+                        first,
+                        last,
+                        aggr);
+}
+
+template <typename Input>
+void zunionstore_range(Connection &connection,
+                        const StringView &destination,
+                        Input first,
+                        Input last,
+                        Aggregation aggr) {
+    assert(first != last);
+
+    detail::zunionstore(typename IsKvPairIter<Input>::type(),
+                        connection,
+                        destination,
+                        first,
+                        last,
+                        aggr);
+}
+
+}
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_COMMAND_H

+ 180 - 0
ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/command_args.h

@@ -0,0 +1,180 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_COMMAND_ARGS_H
+#define SEWENEW_REDISPLUSPLUS_COMMAND_ARGS_H
+
+#include <vector>
+#include <list>
+#include <string>
+#include <tuple>
+#include "utils.h"
+
+namespace sw {
+
+namespace redis {
+
+class CmdArgs {
+public:
+    template <typename Arg>
+    CmdArgs& append(Arg &&arg);
+
+    template <typename Arg, typename ...Args>
+    CmdArgs& append(Arg &&arg, Args &&...args);
+
+    // All overloads of operator<< are for internal use only.
+    CmdArgs& operator<<(const StringView &arg);
+
+    template <typename T,
+                 typename std::enable_if<std::is_arithmetic<typename std::decay<T>::type>::value,
+                                        int>::type = 0>
+    CmdArgs& operator<<(T &&arg);
+
+    template <typename Iter>
+    CmdArgs& operator<<(const std::pair<Iter, Iter> &range);
+
+    template <std::size_t N, typename ...Args>
+    auto operator<<(const std::tuple<Args...> &) ->
+        typename std::enable_if<N == sizeof...(Args), CmdArgs&>::type {
+        return *this;
+    }
+
+    template <std::size_t N = 0, typename ...Args>
+    auto operator<<(const std::tuple<Args...> &arg) ->
+        typename std::enable_if<N < sizeof...(Args), CmdArgs&>::type;
+
+    const char** argv() {
+        return _argv.data();
+    }
+
+    const std::size_t* argv_len() {
+        return _argv_len.data();
+    }
+
+    std::size_t size() const {
+        return _argv.size();
+    }
+
+private:
+    // Deep copy.
+    CmdArgs& _append(std::string arg);
+
+    // Shallow copy.
+    CmdArgs& _append(const StringView &arg);
+
+    // Shallow copy.
+    CmdArgs& _append(const char *arg);
+
+    template <typename T,
+                 typename std::enable_if<std::is_arithmetic<typename std::decay<T>::type>::value,
+                                        int>::type = 0>
+    CmdArgs& _append(T &&arg) {
+        return operator<<(std::forward<T>(arg));
+    }
+
+    template <typename Iter>
+    CmdArgs& _append(std::true_type, const std::pair<Iter, Iter> &range);
+
+    template <typename Iter>
+    CmdArgs& _append(std::false_type, const std::pair<Iter, Iter> &range);
+
+    std::vector<const char *> _argv;
+    std::vector<std::size_t> _argv_len;
+
+    std::list<std::string> _args;
+};
+
+template <typename Arg>
+inline CmdArgs& CmdArgs::append(Arg &&arg) {
+    return _append(std::forward<Arg>(arg));
+}
+
+template <typename Arg, typename ...Args>
+inline CmdArgs& CmdArgs::append(Arg &&arg, Args &&...args) {
+    _append(std::forward<Arg>(arg));
+
+    return append(std::forward<Args>(args)...);
+}
+
+inline CmdArgs& CmdArgs::operator<<(const StringView &arg) {
+    _argv.push_back(arg.data());
+    _argv_len.push_back(arg.size());
+
+    return *this;
+}
+
+template <typename Iter>
+inline CmdArgs& CmdArgs::operator<<(const std::pair<Iter, Iter> &range) {
+    return _append(IsKvPair<typename std::decay<decltype(*std::declval<Iter>())>::type>(), range);
+}
+
+template <typename T,
+             typename std::enable_if<std::is_arithmetic<typename std::decay<T>::type>::value,
+                                    int>::type>
+inline CmdArgs& CmdArgs::operator<<(T &&arg) {
+    return _append(std::to_string(std::forward<T>(arg)));
+}
+
+template <std::size_t N, typename ...Args>
+auto CmdArgs::operator<<(const std::tuple<Args...> &arg) ->
+    typename std::enable_if<N < sizeof...(Args), CmdArgs&>::type {
+    operator<<(std::get<N>(arg));
+
+    return operator<<<N + 1, Args...>(arg);
+}
+
+inline CmdArgs& CmdArgs::_append(std::string arg) {
+    _args.push_back(std::move(arg));
+    return operator<<(_args.back());
+}
+
+inline CmdArgs& CmdArgs::_append(const StringView &arg) {
+    return operator<<(arg);
+}
+
+inline CmdArgs& CmdArgs::_append(const char *arg) {
+    return operator<<(arg);
+}
+
+template <typename Iter>
+CmdArgs& CmdArgs::_append(std::false_type, const std::pair<Iter, Iter> &range) {
+    auto first = range.first;
+    auto last = range.second;
+    while (first != last) {
+        *this << *first;
+        ++first;
+    }
+
+    return *this;
+}
+
+template <typename Iter>
+CmdArgs& CmdArgs::_append(std::true_type, const std::pair<Iter, Iter> &range) {
+    auto first = range.first;
+    auto last = range.second;
+    while (first != last) {
+        *this << first->first << first->second;
+        ++first;
+    }
+
+    return *this;
+}
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_COMMAND_ARGS_H

+ 211 - 0
ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/command_options.h

@@ -0,0 +1,211 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_COMMAND_OPTIONS_H
+#define SEWENEW_REDISPLUSPLUS_COMMAND_OPTIONS_H
+
+#include <string>
+#include "utils.h"
+
+namespace sw {
+
+namespace redis {
+
+enum class UpdateType {
+    EXIST,
+    NOT_EXIST,
+    ALWAYS
+};
+
+enum class InsertPosition {
+    BEFORE,
+    AFTER
+};
+
+enum class BoundType {
+    CLOSED,
+    OPEN,
+    LEFT_OPEN,
+    RIGHT_OPEN
+};
+
+// (-inf, +inf)
+template <typename T>
+class UnboundedInterval;
+
+// [min, max], (min, max), (min, max], [min, max)
+template <typename T>
+class BoundedInterval;
+
+// [min, +inf), (min, +inf)
+template <typename T>
+class LeftBoundedInterval;
+
+// (-inf, max], (-inf, max)
+template <typename T>
+class RightBoundedInterval;
+
+template <>
+class UnboundedInterval<double> {
+public:
+    const std::string& min() const;
+
+    const std::string& max() const;
+};
+
+template <>
+class BoundedInterval<double> {
+public:
+    BoundedInterval(double min, double max, BoundType type);
+
+    const std::string& min() const {
+        return _min;
+    }
+
+    const std::string& max() const {
+        return _max;
+    }
+
+private:
+    std::string _min;
+    std::string _max;
+};
+
+template <>
+class LeftBoundedInterval<double> {
+public:
+    LeftBoundedInterval(double min, BoundType type);
+
+    const std::string& min() const {
+        return _min;
+    }
+
+    const std::string& max() const;
+
+private:
+    std::string _min;
+};
+
+template <>
+class RightBoundedInterval<double> {
+public:
+    RightBoundedInterval(double max, BoundType type);
+
+    const std::string& min() const;
+
+    const std::string& max() const {
+        return _max;
+    }
+
+private:
+    std::string _max;
+};
+
+template <>
+class UnboundedInterval<std::string> {
+public:
+    const std::string& min() const;
+
+    const std::string& max() const;
+};
+
+template <>
+class BoundedInterval<std::string> {
+public:
+    BoundedInterval(const std::string &min, const std::string &max, BoundType type);
+
+    const std::string& min() const {
+        return _min;
+    }
+
+    const std::string& max() const {
+        return _max;
+    }
+
+private:
+    std::string _min;
+    std::string _max;
+};
+
+template <>
+class LeftBoundedInterval<std::string> {
+public:
+    LeftBoundedInterval(const std::string &min, BoundType type);
+
+    const std::string& min() const {
+        return _min;
+    }
+
+    const std::string& max() const;
+
+private:
+    std::string _min;
+};
+
+template <>
+class RightBoundedInterval<std::string> {
+public:
+    RightBoundedInterval(const std::string &max, BoundType type);
+
+    const std::string& min() const;
+
+    const std::string& max() const {
+        return _max;
+    }
+
+private:
+    std::string _max;
+};
+
+struct LimitOptions {
+    long long offset = 0;
+    long long count = -1;
+};
+
+enum class Aggregation {
+    SUM,
+    MIN,
+    MAX
+};
+
+enum class BitOp {
+    AND,
+    OR,
+    XOR,
+    NOT
+};
+
+enum class GeoUnit {
+    M,
+    KM,
+    MI,
+    FT
+};
+
+template <typename T>
+struct WithCoord : TupleWithType<std::pair<double, double>, T> {};
+
+template <typename T>
+struct WithDist : TupleWithType<double, T> {};
+
+template <typename T>
+struct WithHash : TupleWithType<long long, T> {};
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_COMMAND_OPTIONS_H

+ 194 - 0
ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/connection.h

@@ -0,0 +1,194 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_CONNECTION_H
+#define SEWENEW_REDISPLUSPLUS_CONNECTION_H
+
+#include <cerrno>
+#include <cstring>
+#include <memory>
+#include <string>
+#include <sstream>
+#include <chrono>
+#include <hiredis/hiredis.h>
+#include "errors.h"
+#include "reply.h"
+#include "utils.h"
+
+namespace sw {
+
+namespace redis {
+
+enum class ConnectionType {
+    TCP = 0,
+    UNIX
+};
+
+struct ConnectionOptions {
+public:
+    ConnectionOptions() = default;
+
+    explicit ConnectionOptions(const std::string &uri);
+
+    ConnectionOptions(const ConnectionOptions &) = default;
+    ConnectionOptions& operator=(const ConnectionOptions &) = default;
+
+    ConnectionOptions(ConnectionOptions &&) = default;
+    ConnectionOptions& operator=(ConnectionOptions &&) = default;
+
+    ~ConnectionOptions() = default;
+
+    ConnectionType type = ConnectionType::TCP;
+
+    std::string host;
+
+    int port = 6379;
+
+    std::string path;
+
+    std::string password;
+
+    int db = 0;
+
+    bool keep_alive = false;
+
+    std::chrono::milliseconds connect_timeout{0};
+
+    std::chrono::milliseconds socket_timeout{0};
+
+private:
+    ConnectionOptions _parse_options(const std::string &uri) const;
+
+    ConnectionOptions _parse_tcp_options(const std::string &path) const;
+
+    ConnectionOptions _parse_unix_options(const std::string &path) const;
+
+    auto _split_string(const std::string &str, const std::string &delimiter) const ->
+            std::pair<std::string, std::string>;
+};
+
+class CmdArgs;
+
+class Connection {
+public:
+    explicit Connection(const ConnectionOptions &opts);
+
+    Connection(const Connection &) = delete;
+    Connection& operator=(const Connection &) = delete;
+
+    Connection(Connection &&) = default;
+    Connection& operator=(Connection &&) = default;
+
+    ~Connection() = default;
+
+    // Check if the connection is broken. Client needs to do this check
+    // before sending some command to the connection. If it's broken,
+    // client needs to reconnect it.
+    bool broken() const noexcept {
+        return _ctx->err != REDIS_OK;
+    }
+
+    void reset() noexcept {
+        _ctx->err = 0;
+    }
+
+    void reconnect();
+
+    auto last_active() const
+        -> std::chrono::time_point<std::chrono::steady_clock> {
+        return _last_active;
+    }
+
+    template <typename ...Args>
+    void send(const char *format, Args &&...args);
+
+    void send(int argc, const char **argv, const std::size_t *argv_len);
+
+    void send(CmdArgs &args);
+
+    ReplyUPtr recv();
+
+    const ConnectionOptions& options() const {
+        return _opts;
+    }
+
+    friend void swap(Connection &lhs, Connection &rhs) noexcept;
+
+private:
+    class Connector;
+
+    struct ContextDeleter {
+        void operator()(redisContext *context) const {
+            if (context != nullptr) {
+                redisFree(context);
+            }
+        };
+    };
+
+    using ContextUPtr = std::unique_ptr<redisContext, ContextDeleter>;
+
+    void _set_options();
+
+    void _auth();
+
+    void _select_db();
+
+    redisContext* _context();
+
+    ContextUPtr _ctx;
+
+    // The time that the connection is created or the time that
+    // the connection is used, i.e. *context()* is called.
+    std::chrono::time_point<std::chrono::steady_clock> _last_active{};
+
+    ConnectionOptions _opts;
+};
+
+using ConnectionSPtr = std::shared_ptr<Connection>;
+
+enum class Role {
+    MASTER,
+    SLAVE
+};
+
+// Inline implementaions.
+
+template <typename ...Args>
+inline void Connection::send(const char *format, Args &&...args) {
+    auto ctx = _context();
+
+    assert(ctx != nullptr);
+
+    if (redisAppendCommand(ctx,
+                format,
+                std::forward<Args>(args)...) != REDIS_OK) {
+        throw_error(*ctx, "Failed to send command");
+    }
+
+    assert(!broken());
+}
+
+inline redisContext* Connection::_context() {
+    _last_active = std::chrono::steady_clock::now();
+
+    return _ctx.get();
+}
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_CONNECTION_H

+ 115 - 0
ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/connection_pool.h

@@ -0,0 +1,115 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_CONNECTION_POOL_H
+#define SEWENEW_REDISPLUSPLUS_CONNECTION_POOL_H
+
+#include <chrono>
+#include <mutex>
+#include <memory>
+#include <condition_variable>
+#include <deque>
+#include "connection.h"
+#include "sentinel.h"
+
+namespace sw {
+
+namespace redis {
+
+struct ConnectionPoolOptions {
+    // Max number of connections, including both in-use and idle ones.
+    std::size_t size = 1;
+
+    // Max time to wait for a connection. 0ms means client waits forever.
+    std::chrono::milliseconds wait_timeout{0};
+
+    // Max lifetime of a connection. 0ms means we never expire the connection.
+    std::chrono::milliseconds connection_lifetime{0};
+};
+
+class ConnectionPool {
+public:
+    ConnectionPool(const ConnectionPoolOptions &pool_opts,
+                    const ConnectionOptions &connection_opts);
+
+    ConnectionPool(SimpleSentinel sentinel,
+                    const ConnectionPoolOptions &pool_opts,
+                    const ConnectionOptions &connection_opts);
+
+    ConnectionPool() = default;
+
+    ConnectionPool(ConnectionPool &&that);
+    ConnectionPool& operator=(ConnectionPool &&that);
+
+    ConnectionPool(const ConnectionPool &) = delete;
+    ConnectionPool& operator=(const ConnectionPool &) = delete;
+
+    ~ConnectionPool() = default;
+
+    // Fetch a connection from pool.
+    Connection fetch();
+
+    ConnectionOptions connection_options();
+
+    void release(Connection connection);
+
+    // Create a new connection.
+    Connection create();
+
+private:
+    void _move(ConnectionPool &&that);
+
+    // NOT thread-safe
+    Connection _create();
+
+    Connection _create(SimpleSentinel &sentinel, const ConnectionOptions &opts, bool locked);
+
+    Connection _fetch();
+
+    void _wait_for_connection(std::unique_lock<std::mutex> &lock);
+
+    bool _need_reconnect(const Connection &connection,
+                            const std::chrono::milliseconds &connection_lifetime) const;
+
+    void _update_connection_opts(const std::string &host, int port) {
+        _opts.host = host;
+        _opts.port = port;
+    }
+
+    bool _role_changed(const ConnectionOptions &opts) const {
+        return opts.port != _opts.port || opts.host != _opts.host;
+    }
+
+    ConnectionOptions _opts;
+
+    ConnectionPoolOptions _pool_opts;
+
+    std::deque<Connection> _pool;
+
+    std::size_t _used_connections = 0;
+
+    std::mutex _mutex;
+
+    std::condition_variable _cv;
+
+    SimpleSentinel _sentinel;
+};
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_CONNECTION_POOL_H

+ 159 - 0
ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/errors.h

@@ -0,0 +1,159 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_ERRORS_H
+#define SEWENEW_REDISPLUSPLUS_ERRORS_H
+
+#include <exception>
+#include <string>
+#include <hiredis/hiredis.h>
+
+namespace sw {
+
+namespace redis {
+
+enum ReplyErrorType {
+    ERR,
+    MOVED,
+    ASK
+};
+
+class Error : public std::exception {
+public:
+    explicit Error(const std::string &msg) : _msg(msg) {}
+
+    Error(const Error &) = default;
+    Error& operator=(const Error &) = default;
+
+    Error(Error &&) = default;
+    Error& operator=(Error &&) = default;
+
+    virtual ~Error() = default;
+
+    virtual const char* what() const noexcept {
+        return _msg.data();
+    }
+
+private:
+    std::string _msg;
+};
+
+class IoError : public Error {
+public:
+    explicit IoError(const std::string &msg) : Error(msg) {}
+
+    IoError(const IoError &) = default;
+    IoError& operator=(const IoError &) = default;
+
+    IoError(IoError &&) = default;
+    IoError& operator=(IoError &&) = default;
+
+    virtual ~IoError() = default;
+};
+
+class TimeoutError : public IoError {
+public:
+    explicit TimeoutError(const std::string &msg) : IoError(msg) {}
+
+    TimeoutError(const TimeoutError &) = default;
+    TimeoutError& operator=(const TimeoutError &) = default;
+
+    TimeoutError(TimeoutError &&) = default;
+    TimeoutError& operator=(TimeoutError &&) = default;
+
+    virtual ~TimeoutError() = default;
+};
+
+class ClosedError : public Error {
+public:
+    explicit ClosedError(const std::string &msg) : Error(msg) {}
+
+    ClosedError(const ClosedError &) = default;
+    ClosedError& operator=(const ClosedError &) = default;
+
+    ClosedError(ClosedError &&) = default;
+    ClosedError& operator=(ClosedError &&) = default;
+
+    virtual ~ClosedError() = default;
+};
+
+class ProtoError : public Error {
+public:
+    explicit ProtoError(const std::string &msg) : Error(msg) {}
+
+    ProtoError(const ProtoError &) = default;
+    ProtoError& operator=(const ProtoError &) = default;
+
+    ProtoError(ProtoError &&) = default;
+    ProtoError& operator=(ProtoError &&) = default;
+
+    virtual ~ProtoError() = default;
+};
+
+class OomError : public Error {
+public:
+    explicit OomError(const std::string &msg) : Error(msg) {}
+
+    OomError(const OomError &) = default;
+    OomError& operator=(const OomError &) = default;
+
+    OomError(OomError &&) = default;
+    OomError& operator=(OomError &&) = default;
+
+    virtual ~OomError() = default;
+};
+
+class ReplyError : public Error {
+public:
+    explicit ReplyError(const std::string &msg) : Error(msg) {}
+
+    ReplyError(const ReplyError &) = default;
+    ReplyError& operator=(const ReplyError &) = default;
+
+    ReplyError(ReplyError &&) = default;
+    ReplyError& operator=(ReplyError &&) = default;
+
+    virtual ~ReplyError() = default;
+};
+
+class WatchError : public Error {
+public:
+    explicit WatchError() : Error("Watched key has been modified") {}
+
+    WatchError(const WatchError &) = default;
+    WatchError& operator=(const WatchError &) = default;
+
+    WatchError(WatchError &&) = default;
+    WatchError& operator=(WatchError &&) = default;
+
+    virtual ~WatchError() = default;
+};
+
+
+// MovedError and AskError are defined in shards.h
+class MovedError;
+
+class AskError;
+
+void throw_error(redisContext &context, const std::string &err_info);
+
+void throw_error(const redisReply &reply);
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_ERRORS_H

+ 49 - 0
ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/pipeline.h

@@ -0,0 +1,49 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_PIPELINE_H
+#define SEWENEW_REDISPLUSPLUS_PIPELINE_H
+
+#include <cassert>
+#include <vector>
+#include "connection.h"
+
+namespace sw {
+
+namespace redis {
+
+class PipelineImpl {
+public:
+    template <typename Cmd, typename ...Args>
+    void command(Connection &connection, Cmd cmd, Args &&...args) {
+        assert(!connection.broken());
+
+        cmd(connection, std::forward<Args>(args)...);
+    }
+
+    std::vector<ReplyUPtr> exec(Connection &connection, std::size_t cmd_num);
+
+    void discard(Connection &connection, std::size_t /*cmd_num*/) {
+        // Reconnect to Redis to discard all commands.
+        connection.reconnect();
+    }
+};
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_PIPELINE_H

+ 1844 - 0
ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/queued_redis.h

@@ -0,0 +1,1844 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_QUEUED_REDIS_H
+#define SEWENEW_REDISPLUSPLUS_QUEUED_REDIS_H
+
+#include <cassert>
+#include <chrono>
+#include <initializer_list>
+#include <vector>
+#include "connection.h"
+#include "utils.h"
+#include "reply.h"
+#include "command.h"
+#include "redis.h"
+
+namespace sw {
+
+namespace redis {
+
+class QueuedReplies;
+
+// If any command throws, QueuedRedis resets the connection, and becomes invalid.
+// In this case, the only thing we can do is to destory the QueuedRedis object.
+template <typename Impl>
+class QueuedRedis {
+public:
+    QueuedRedis(QueuedRedis &&) = default;
+    QueuedRedis& operator=(QueuedRedis &&) = default;
+
+    // When it destructs, the underlying *Connection* will be closed,
+    // and any command that has NOT been executed will be ignored.
+    ~QueuedRedis() = default;
+
+    Redis redis();
+
+    template <typename Cmd, typename ...Args>
+    auto command(Cmd cmd, Args &&...args)
+        -> typename std::enable_if<!std::is_convertible<Cmd, StringView>::value,
+                                    QueuedRedis&>::type;
+
+    template <typename ...Args>
+    QueuedRedis& command(const StringView &cmd_name, Args &&...args);
+
+    template <typename Input>
+    auto command(Input first, Input last)
+        -> typename std::enable_if<IsIter<Input>::value, QueuedRedis&>::type;
+
+    QueuedReplies exec();
+
+    void discard();
+
+    // CONNECTION commands.
+
+    QueuedRedis& auth(const StringView &password) {
+        return command(cmd::auth, password);
+    }
+
+    QueuedRedis& echo(const StringView &msg) {
+        return command(cmd::echo, msg);
+    }
+
+    QueuedRedis& ping() {
+        return command<void (*)(Connection &)>(cmd::ping);
+    }
+
+    QueuedRedis& ping(const StringView &msg) {
+        return command<void (*)(Connection &, const StringView &)>(cmd::ping, msg);
+    }
+
+    // We DO NOT support the QUIT command. See *Redis::quit* doc for details.
+    //
+    // QueuedRedis& quit();
+
+    QueuedRedis& select(long long idx) {
+        return command(cmd::select, idx);
+    }
+
+    QueuedRedis& swapdb(long long idx1, long long idx2) {
+        return command(cmd::swapdb, idx1, idx2);
+    }
+
+    // SERVER commands.
+
+    QueuedRedis& bgrewriteaof() {
+        return command(cmd::bgrewriteaof);
+    }
+
+    QueuedRedis& bgsave() {
+        return command(cmd::bgsave);
+    }
+
+    QueuedRedis& dbsize() {
+        return command(cmd::dbsize);
+    }
+
+    QueuedRedis& flushall(bool async = false) {
+        return command(cmd::flushall, async);
+    }
+
+    QueuedRedis& flushdb(bool async = false) {
+        return command(cmd::flushdb, async);
+    }
+
+    QueuedRedis& info() {
+        return command<void (*)(Connection &)>(cmd::info);
+    }
+
+    QueuedRedis& info(const StringView &section) {
+        return command<void (*)(Connection &, const StringView &)>(cmd::info, section);
+    }
+
+    QueuedRedis& lastsave() {
+        return command(cmd::lastsave);
+    }
+
+    QueuedRedis& save() {
+        return command(cmd::save);
+    }
+
+    // KEY commands.
+
+    QueuedRedis& del(const StringView &key) {
+        return command(cmd::del, key);
+    }
+
+    template <typename Input>
+    QueuedRedis& del(Input first, Input last) {
+        return command(cmd::del_range<Input>, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& del(std::initializer_list<T> il) {
+        return del(il.begin(), il.end());
+    }
+
+    QueuedRedis& dump(const StringView &key) {
+        return command(cmd::dump, key);
+    }
+
+    QueuedRedis& exists(const StringView &key) {
+        return command(cmd::exists, key);
+    }
+
+    template <typename Input>
+    QueuedRedis& exists(Input first, Input last) {
+        return command(cmd::exists_range<Input>, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& exists(std::initializer_list<T> il) {
+        return exists(il.begin(), il.end());
+    }
+
+    QueuedRedis& expire(const StringView &key, long long timeout) {
+        return command(cmd::expire, key, timeout);
+    }
+
+    QueuedRedis& expire(const StringView &key,
+                        const std::chrono::seconds &timeout) {
+        return expire(key, timeout.count());
+    }
+
+    QueuedRedis& expireat(const StringView &key, long long timestamp) {
+        return command(cmd::expireat, key, timestamp);
+    }
+
+    QueuedRedis& expireat(const StringView &key,
+                            const std::chrono::time_point<std::chrono::system_clock,
+                                                            std::chrono::seconds> &tp) {
+        return expireat(key, tp.time_since_epoch().count());
+    }
+
+    QueuedRedis& keys(const StringView &pattern) {
+        return command(cmd::keys, pattern);
+    }
+
+    QueuedRedis& move(const StringView &key, long long db) {
+        return command(cmd::move, key, db);
+    }
+
+    QueuedRedis& persist(const StringView &key) {
+        return command(cmd::persist, key);
+    }
+
+    QueuedRedis& pexpire(const StringView &key, long long timeout) {
+        return command(cmd::pexpire, key, timeout);
+    }
+
+    QueuedRedis& pexpire(const StringView &key,
+                            const std::chrono::milliseconds &timeout) {
+        return pexpire(key, timeout.count());
+    }
+
+    QueuedRedis& pexpireat(const StringView &key, long long timestamp) {
+        return command(cmd::pexpireat, key, timestamp);
+    }
+
+    QueuedRedis& pexpireat(const StringView &key,
+                            const std::chrono::time_point<std::chrono::system_clock,
+                                                            std::chrono::milliseconds> &tp) {
+        return pexpireat(key, tp.time_since_epoch().count());
+    }
+
+    QueuedRedis& pttl(const StringView &key) {
+        return command(cmd::pttl, key);
+    }
+
+    QueuedRedis& randomkey() {
+        return command(cmd::randomkey);
+    }
+
+    QueuedRedis& rename(const StringView &key, const StringView &newkey) {
+        return command(cmd::rename, key, newkey);
+    }
+
+    QueuedRedis& renamenx(const StringView &key, const StringView &newkey) {
+        return command(cmd::renamenx, key, newkey);
+    }
+
+    QueuedRedis& restore(const StringView &key,
+                                const StringView &val,
+                                long long ttl,
+                                bool replace = false) {
+        return command(cmd::restore, key, val, ttl, replace);
+    }
+
+    QueuedRedis& restore(const StringView &key,
+                            const StringView &val,
+                            const std::chrono::milliseconds &ttl = std::chrono::milliseconds{0},
+                            bool replace = false) {
+        return restore(key, val, ttl.count(), replace);
+    }
+
+    // TODO: sort
+
+    QueuedRedis& scan(long long cursor,
+                        const StringView &pattern,
+                        long long count) {
+        return command(cmd::scan, cursor, pattern, count);
+    }
+
+    QueuedRedis& scan(long long cursor) {
+        return scan(cursor, "*", 10);
+    }
+
+    QueuedRedis& scan(long long cursor,
+                        const StringView &pattern) {
+        return scan(cursor, pattern, 10);
+    }
+
+    QueuedRedis& scan(long long cursor,
+                        long long count) {
+        return scan(cursor, "*", count);
+    }
+
+    QueuedRedis& touch(const StringView &key) {
+        return command(cmd::touch, key);
+    }
+
+    template <typename Input>
+    QueuedRedis& touch(Input first, Input last) {
+        return command(cmd::touch_range<Input>, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& touch(std::initializer_list<T> il) {
+        return touch(il.begin(), il.end());
+    }
+
+    QueuedRedis& ttl(const StringView &key) {
+        return command(cmd::ttl, key);
+    }
+
+    QueuedRedis& type(const StringView &key) {
+        return command(cmd::type, key);
+    }
+
+    QueuedRedis& unlink(const StringView &key) {
+        return command(cmd::unlink, key);
+    }
+
+    template <typename Input>
+    QueuedRedis& unlink(Input first, Input last) {
+        return command(cmd::unlink_range<Input>, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& unlink(std::initializer_list<T> il) {
+        return unlink(il.begin(), il.end());
+    }
+
+    QueuedRedis& wait(long long numslaves, long long timeout) {
+        return command(cmd::wait, numslaves, timeout);
+    }
+
+    QueuedRedis& wait(long long numslaves, const std::chrono::milliseconds &timeout) {
+        return wait(numslaves, timeout.count());
+    }
+
+    // STRING commands.
+
+    QueuedRedis& append(const StringView &key, const StringView &str) {
+        return command(cmd::append, key, str);
+    }
+
+    QueuedRedis& bitcount(const StringView &key,
+                            long long start = 0,
+                            long long end = -1) {
+        return command(cmd::bitcount, key, start, end);
+    }
+
+    QueuedRedis& bitop(BitOp op,
+                        const StringView &destination,
+                        const StringView &key) {
+        return command(cmd::bitop, op, destination, key);
+    }
+
+    template <typename Input>
+    QueuedRedis& bitop(BitOp op,
+                        const StringView &destination,
+                        Input first,
+                        Input last) {
+        return command(cmd::bitop_range<Input>, op, destination, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& bitop(BitOp op,
+                        const StringView &destination,
+                        std::initializer_list<T> il) {
+        return bitop(op, destination, il.begin(), il.end());
+    }
+
+    QueuedRedis& bitpos(const StringView &key,
+                        long long bit,
+                        long long start = 0,
+                        long long end = -1) {
+        return command(cmd::bitpos, key, bit, start, end);
+    }
+
+    QueuedRedis& decr(const StringView &key) {
+        return command(cmd::decr, key);
+    }
+
+    QueuedRedis& decrby(const StringView &key, long long decrement) {
+        return command(cmd::decrby, key, decrement);
+    }
+
+    QueuedRedis& get(const StringView &key) {
+        return command(cmd::get, key);
+    }
+
+    QueuedRedis& getbit(const StringView &key, long long offset) {
+        return command(cmd::getbit, key, offset);
+    }
+
+    QueuedRedis& getrange(const StringView &key, long long start, long long end) {
+        return command(cmd::getrange, key, start, end);
+    }
+
+    QueuedRedis& getset(const StringView &key, const StringView &val) {
+        return command(cmd::getset, key, val);
+    }
+
+    QueuedRedis& incr(const StringView &key) {
+        return command(cmd::incr, key);
+    }
+
+    QueuedRedis& incrby(const StringView &key, long long increment) {
+        return command(cmd::incrby, key, increment);
+    }
+
+    QueuedRedis& incrbyfloat(const StringView &key, double increment) {
+        return command(cmd::incrbyfloat, key, increment);
+    }
+
+    template <typename Input>
+    QueuedRedis& mget(Input first, Input last) {
+        return command(cmd::mget<Input>, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& mget(std::initializer_list<T> il) {
+        return mget(il.begin(), il.end());
+    }
+
+    template <typename Input>
+    QueuedRedis& mset(Input first, Input last) {
+        return command(cmd::mset<Input>, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& mset(std::initializer_list<T> il) {
+        return mset(il.begin(), il.end());
+    }
+
+    template <typename Input>
+    QueuedRedis& msetnx(Input first, Input last) {
+        return command(cmd::msetnx<Input>, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& msetnx(std::initializer_list<T> il) {
+        return msetnx(il.begin(), il.end());
+    }
+
+    QueuedRedis& psetex(const StringView &key,
+                        long long ttl,
+                        const StringView &val) {
+        return command(cmd::psetex, key, ttl, val);
+    }
+
+    QueuedRedis& psetex(const StringView &key,
+                        const std::chrono::milliseconds &ttl,
+                        const StringView &val) {
+        return psetex(key, ttl.count(), val);
+    }
+
+    QueuedRedis& set(const StringView &key,
+                        const StringView &val,
+                        const std::chrono::milliseconds &ttl = std::chrono::milliseconds(0),
+                        UpdateType type = UpdateType::ALWAYS) {
+        _set_cmd_indexes.push_back(_cmd_num);
+
+        return command(cmd::set, key, val, ttl.count(), type);
+    }
+
+    QueuedRedis& setex(const StringView &key,
+                        long long ttl,
+                        const StringView &val) {
+        return command(cmd::setex, key, ttl, val);
+    }
+
+    QueuedRedis& setex(const StringView &key,
+                        const std::chrono::seconds &ttl,
+                        const StringView &val) {
+        return setex(key, ttl.count(), val);
+    }
+
+    QueuedRedis& setnx(const StringView &key, const StringView &val) {
+        return command(cmd::setnx, key, val);
+    }
+
+    QueuedRedis& setrange(const StringView &key,
+                            long long offset,
+                            const StringView &val) {
+        return command(cmd::setrange, key, offset, val);
+    }
+
+    QueuedRedis& strlen(const StringView &key) {
+        return command(cmd::strlen, key);
+    }
+
+    // LIST commands.
+
+    QueuedRedis& blpop(const StringView &key, long long timeout) {
+        return command(cmd::blpop, key, timeout);
+    }
+
+    QueuedRedis& blpop(const StringView &key,
+                        const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
+        return blpop(key, timeout.count());
+    }
+
+    template <typename Input>
+    QueuedRedis& blpop(Input first, Input last, long long timeout) {
+        return command(cmd::blpop_range<Input>, first, last, timeout);
+    }
+
+    template <typename T>
+    QueuedRedis& blpop(std::initializer_list<T> il, long long timeout) {
+        return blpop(il.begin(), il.end(), timeout);
+    }
+
+    template <typename Input>
+    QueuedRedis& blpop(Input first,
+                        Input last,
+                        const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
+        return blpop(first, last, timeout.count());
+    }
+
+    template <typename T>
+    QueuedRedis& blpop(std::initializer_list<T> il,
+                        const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
+        return blpop(il.begin(), il.end(), timeout);
+    }
+
+    QueuedRedis& brpop(const StringView &key, long long timeout) {
+        return command(cmd::brpop, key, timeout);
+    }
+
+    QueuedRedis& brpop(const StringView &key,
+                        const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
+        return brpop(key, timeout.count());
+    }
+
+    template <typename Input>
+    QueuedRedis& brpop(Input first, Input last, long long timeout) {
+        return command(cmd::brpop_range<Input>, first, last, timeout);
+    }
+
+    template <typename T>
+    QueuedRedis& brpop(std::initializer_list<T> il, long long timeout) {
+        return brpop(il.begin(), il.end(), timeout);
+    }
+
+    template <typename Input>
+    QueuedRedis& brpop(Input first,
+                        Input last,
+                        const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
+        return brpop(first, last, timeout.count());
+    }
+
+    template <typename T>
+    QueuedRedis& brpop(std::initializer_list<T> il,
+                        const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
+        return brpop(il.begin(), il.end(), timeout);
+    }
+
+    QueuedRedis& brpoplpush(const StringView &source,
+                            const StringView &destination,
+                            long long timeout) {
+        return command(cmd::brpoplpush, source, destination, timeout);
+    }
+
+    QueuedRedis& brpoplpush(const StringView &source,
+                            const StringView &destination,
+                            const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
+        return brpoplpush(source, destination, timeout.count());
+    }
+
+    QueuedRedis& lindex(const StringView &key, long long index) {
+        return command(cmd::lindex, key, index);
+    }
+
+    QueuedRedis& linsert(const StringView &key,
+                            InsertPosition position,
+                            const StringView &pivot,
+                            const StringView &val) {
+        return command(cmd::linsert, key, position, pivot, val);
+    }
+
+    QueuedRedis& llen(const StringView &key) {
+        return command(cmd::llen, key);
+    }
+
+    QueuedRedis& lpop(const StringView &key) {
+        return command(cmd::lpop, key);
+    }
+
+    QueuedRedis& lpush(const StringView &key, const StringView &val) {
+        return command(cmd::lpush, key, val);
+    }
+
+    template <typename Input>
+    QueuedRedis& lpush(const StringView &key, Input first, Input last) {
+        return command(cmd::lpush_range<Input>, key, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& lpush(const StringView &key, std::initializer_list<T> il) {
+        return lpush(key, il.begin(), il.end());
+    }
+
+    QueuedRedis& lpushx(const StringView &key, const StringView &val) {
+        return command(cmd::lpushx, key, val);
+    }
+
+    QueuedRedis& lrange(const StringView &key,
+                        long long start,
+                        long long stop) {
+        return command(cmd::lrange, key, start, stop);
+    }
+
+    QueuedRedis& lrem(const StringView &key, long long count, const StringView &val) {
+        return command(cmd::lrem, key, count, val);
+    }
+
+    QueuedRedis& lset(const StringView &key, long long index, const StringView &val) {
+        return command(cmd::lset, key, index, val);
+    }
+
+    QueuedRedis& ltrim(const StringView &key, long long start, long long stop) {
+        return command(cmd::ltrim, key, start, stop);
+    }
+
+    QueuedRedis& rpop(const StringView &key) {
+        return command(cmd::rpop, key);
+    }
+
+    QueuedRedis& rpoplpush(const StringView &source, const StringView &destination) {
+        return command(cmd::rpoplpush, source, destination);
+    }
+
+    QueuedRedis& rpush(const StringView &key, const StringView &val) {
+        return command(cmd::rpush, key, val);
+    }
+
+    template <typename Input>
+    QueuedRedis& rpush(const StringView &key, Input first, Input last) {
+        return command(cmd::rpush_range<Input>, key, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& rpush(const StringView &key, std::initializer_list<T> il) {
+        return rpush(key, il.begin(), il.end());
+    }
+
+    QueuedRedis& rpushx(const StringView &key, const StringView &val) {
+        return command(cmd::rpushx, key, val);
+    }
+
+    // HASH commands.
+
+    QueuedRedis& hdel(const StringView &key, const StringView &field) {
+        return command(cmd::hdel, key, field);
+    }
+
+    template <typename Input>
+    QueuedRedis& hdel(const StringView &key, Input first, Input last) {
+        return command(cmd::hdel_range<Input>, key, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& hdel(const StringView &key, std::initializer_list<T> il) {
+        return hdel(key, il.begin(), il.end());
+    }
+
+    QueuedRedis& hexists(const StringView &key, const StringView &field) {
+        return command(cmd::hexists, key, field);
+    }
+
+    QueuedRedis& hget(const StringView &key, const StringView &field) {
+        return command(cmd::hget, key, field);
+    }
+
+    QueuedRedis& hgetall(const StringView &key) {
+        return command(cmd::hgetall, key);
+    }
+
+    QueuedRedis& hincrby(const StringView &key,
+                            const StringView &field,
+                            long long increment) {
+        return command(cmd::hincrby, key, field, increment);
+    }
+
+    QueuedRedis& hincrbyfloat(const StringView &key,
+                                const StringView &field,
+                                double increment) {
+        return command(cmd::hincrbyfloat, key, field, increment);
+    }
+
+    QueuedRedis& hkeys(const StringView &key) {
+        return command(cmd::hkeys, key);
+    }
+
+    QueuedRedis& hlen(const StringView &key) {
+        return command(cmd::hlen, key);
+    }
+
+    template <typename Input>
+    QueuedRedis& hmget(const StringView &key, Input first, Input last) {
+        return command(cmd::hmget<Input>, key, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& hmget(const StringView &key, std::initializer_list<T> il) {
+        return hmget(key, il.begin(), il.end());
+    }
+
+    template <typename Input>
+    QueuedRedis& hmset(const StringView &key, Input first, Input last) {
+        return command(cmd::hmset<Input>, key, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& hmset(const StringView &key, std::initializer_list<T> il) {
+        return hmset(key, il.begin(), il.end());
+    }
+
+    QueuedRedis& hscan(const StringView &key,
+                        long long cursor,
+                        const StringView &pattern,
+                        long long count) {
+        return command(cmd::hscan, key, cursor, pattern, count);
+    }
+
+    QueuedRedis& hscan(const StringView &key,
+                        long long cursor,
+                        const StringView &pattern) {
+        return hscan(key, cursor, pattern, 10);
+    }
+
+    QueuedRedis& hscan(const StringView &key,
+                        long long cursor,
+                        long long count) {
+        return hscan(key, cursor, "*", count);
+    }
+
+    QueuedRedis& hscan(const StringView &key,
+                        long long cursor) {
+        return hscan(key, cursor, "*", 10);
+    }
+
+    QueuedRedis& hset(const StringView &key, const StringView &field, const StringView &val) {
+        return command(cmd::hset, key, field, val);
+    }
+
+    QueuedRedis& hset(const StringView &key, const std::pair<StringView, StringView> &item) {
+        return hset(key, item.first, item.second);
+    }
+
+    QueuedRedis& hsetnx(const StringView &key, const StringView &field, const StringView &val) {
+        return command(cmd::hsetnx, key, field, val);
+    }
+
+    QueuedRedis& hsetnx(const StringView &key, const std::pair<StringView, StringView> &item) {
+        return hsetnx(key, item.first, item.second);
+    }
+
+    QueuedRedis& hstrlen(const StringView &key, const StringView &field) {
+        return command(cmd::hstrlen, key, field);
+    }
+
+    QueuedRedis& hvals(const StringView &key) {
+        return command(cmd::hvals, key);
+    }
+
+    // SET commands.
+
+    QueuedRedis& sadd(const StringView &key, const StringView &member) {
+        return command(cmd::sadd, key, member);
+    }
+
+    template <typename Input>
+    QueuedRedis& sadd(const StringView &key, Input first, Input last) {
+        return command(cmd::sadd_range<Input>, key, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& sadd(const StringView &key, std::initializer_list<T> il) {
+        return sadd(key, il.begin(), il.end());
+    }
+
+    QueuedRedis& scard(const StringView &key) {
+        return command(cmd::scard, key);
+    }
+
+    template <typename Input>
+    QueuedRedis& sdiff(Input first, Input last) {
+        return command(cmd::sdiff<Input>, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& sdiff(std::initializer_list<T> il) {
+        return sdiff(il.begin(), il.end());
+    }
+
+    QueuedRedis& sdiffstore(const StringView &destination, const StringView &key) {
+        return command(cmd::sdiffstore, destination, key);
+    }
+
+    template <typename Input>
+    QueuedRedis& sdiffstore(const StringView &destination,
+                            Input first,
+                            Input last) {
+        return command(cmd::sdiffstore_range<Input>, destination, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& sdiffstore(const StringView &destination, std::initializer_list<T> il) {
+        return sdiffstore(destination, il.begin(), il.end());
+    }
+
+    template <typename Input>
+    QueuedRedis& sinter(Input first, Input last) {
+        return command(cmd::sinter<Input>, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& sinter(std::initializer_list<T> il) {
+        return sinter(il.begin(), il.end());
+    }
+
+    QueuedRedis& sinterstore(const StringView &destination, const StringView &key) {
+        return command(cmd::sinterstore, destination, key);
+    }
+
+    template <typename Input>
+    QueuedRedis& sinterstore(const StringView &destination,
+                                Input first,
+                                Input last) {
+        return command(cmd::sinterstore_range<Input>, destination, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& sinterstore(const StringView &destination, std::initializer_list<T> il) {
+        return sinterstore(destination, il.begin(), il.end());
+    }
+
+    QueuedRedis& sismember(const StringView &key, const StringView &member) {
+        return command(cmd::sismember, key, member);
+    }
+
+    QueuedRedis& smembers(const StringView &key) {
+        return command(cmd::smembers, key);
+    }
+
+    QueuedRedis& smove(const StringView &source,
+                        const StringView &destination,
+                        const StringView &member) {
+        return command(cmd::smove, source, destination, member);
+    }
+
+    QueuedRedis& spop(const StringView &key) {
+        return command(cmd::spop, key);
+    }
+
+    QueuedRedis& spop(const StringView &key, long long count) {
+        return command(cmd::spop_range, key, count);
+    }
+
+    QueuedRedis& srandmember(const StringView &key) {
+        return command(cmd::srandmember, key);
+    }
+
+    QueuedRedis& srandmember(const StringView &key, long long count) {
+        return command(cmd::srandmember_range, key, count);
+    }
+
+    QueuedRedis& srem(const StringView &key, const StringView &member) {
+        return command(cmd::srem, key, member);
+    }
+
+    template <typename Input>
+    QueuedRedis& srem(const StringView &key, Input first, Input last) {
+        return command(cmd::srem_range<Input>, key, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& srem(const StringView &key, std::initializer_list<T> il) {
+        return srem(key, il.begin(), il.end());
+    }
+
+    QueuedRedis& sscan(const StringView &key,
+                        long long cursor,
+                        const StringView &pattern,
+                        long long count) {
+        return command(cmd::sscan, key, cursor, pattern, count);
+    }
+
+    QueuedRedis& sscan(const StringView &key,
+                    long long cursor,
+                    const StringView &pattern) {
+        return sscan(key, cursor, pattern, 10);
+    }
+
+    QueuedRedis& sscan(const StringView &key,
+                        long long cursor,
+                        long long count) {
+        return sscan(key, cursor, "*", count);
+    }
+
+    QueuedRedis& sscan(const StringView &key,
+                        long long cursor) {
+        return sscan(key, cursor, "*", 10);
+    }
+
+    template <typename Input>
+    QueuedRedis& sunion(Input first, Input last) {
+        return command(cmd::sunion<Input>, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& sunion(std::initializer_list<T> il) {
+        return sunion(il.begin(), il.end());
+    }
+
+    QueuedRedis& sunionstore(const StringView &destination, const StringView &key) {
+        return command(cmd::sunionstore, destination, key);
+    }
+
+    template <typename Input>
+    QueuedRedis& sunionstore(const StringView &destination, Input first, Input last) {
+        return command(cmd::sunionstore_range<Input>, destination, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& sunionstore(const StringView &destination, std::initializer_list<T> il) {
+        return sunionstore(destination, il.begin(), il.end());
+    }
+
+    // SORTED SET commands.
+
+    QueuedRedis& bzpopmax(const StringView &key, long long timeout) {
+        return command(cmd::bzpopmax, key, timeout);
+    }
+
+    QueuedRedis& bzpopmax(const StringView &key,
+                    const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
+        return bzpopmax(key, timeout.count());
+    }
+
+    template <typename Input>
+    QueuedRedis& bzpopmax(Input first, Input last, long long timeout) {
+        return command(cmd::bzpopmax_range<Input>, first, last, timeout);
+    }
+
+    template <typename Input>
+    QueuedRedis& bzpopmax(Input first,
+                            Input last,
+                            const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
+        return bzpopmax(first, last, timeout.count());
+    }
+
+    template <typename T>
+    QueuedRedis& bzpopmax(std::initializer_list<T> il, long long timeout) {
+        return bzpopmax(il.begin(), il.end(), timeout);
+    }
+
+    template <typename T>
+    QueuedRedis& bzpopmax(std::initializer_list<T> il,
+                            const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
+        return bzpopmax(il.begin(), il.end(), timeout);
+    }
+
+    QueuedRedis& bzpopmin(const StringView &key, long long timeout) {
+        return command(cmd::bzpopmin, key, timeout);
+    }
+
+    QueuedRedis& bzpopmin(const StringView &key,
+                            const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
+        return bzpopmin(key, timeout.count());
+    }
+
+    template <typename Input>
+    QueuedRedis& bzpopmin(Input first, Input last, long long timeout) {
+        return command(cmd::bzpopmin_range<Input>, first, last, timeout);
+    }
+
+    template <typename Input>
+    QueuedRedis& bzpopmin(Input first,
+                            Input last,
+                            const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
+        return bzpopmin(first, last, timeout.count());
+    }
+
+    template <typename T>
+    QueuedRedis& bzpopmin(std::initializer_list<T> il, long long timeout) {
+        return bzpopmin(il.begin(), il.end(), timeout);
+    }
+
+    template <typename T>
+    QueuedRedis& bzpopmin(std::initializer_list<T> il,
+                            const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
+        return bzpopmin(il.begin(), il.end(), timeout);
+    }
+
+    // We don't support the INCR option, since you can always use ZINCRBY instead.
+    QueuedRedis& zadd(const StringView &key,
+                        const StringView &member,
+                        double score,
+                        UpdateType type = UpdateType::ALWAYS,
+                        bool changed = false) {
+        return command(cmd::zadd, key, member, score, type, changed);
+    }
+
+    template <typename Input>
+    QueuedRedis& zadd(const StringView &key,
+                        Input first,
+                        Input last,
+                        UpdateType type = UpdateType::ALWAYS,
+                        bool changed = false) {
+        return command(cmd::zadd_range<Input>, key, first, last, type, changed);
+    }
+
+    QueuedRedis& zcard(const StringView &key) {
+        return command(cmd::zcard, key);
+    }
+
+    template <typename Interval>
+    QueuedRedis& zcount(const StringView &key, const Interval &interval) {
+        return command(cmd::zcount<Interval>, key, interval);
+    }
+
+    QueuedRedis& zincrby(const StringView &key, double increment, const StringView &member) {
+        return command(cmd::zincrby, key, increment, member);
+    }
+
+    QueuedRedis& zinterstore(const StringView &destination,
+                                const StringView &key,
+                                double weight) {
+        return command(cmd::zinterstore, destination, key, weight);
+    }
+
+    template <typename Input>
+    QueuedRedis& zinterstore(const StringView &destination,
+                                Input first,
+                                Input last,
+                                Aggregation type = Aggregation::SUM) {
+        return command(cmd::zinterstore_range<Input>, destination, first, last, type);
+    }
+
+    template <typename T>
+    QueuedRedis& zinterstore(const StringView &destination,
+                                std::initializer_list<T> il,
+                                Aggregation type = Aggregation::SUM) {
+        return zinterstore(destination, il.begin(), il.end(), type);
+    }
+
+    template <typename Interval>
+    QueuedRedis& zlexcount(const StringView &key, const Interval &interval) {
+        return command(cmd::zlexcount<Interval>, key, interval);
+    }
+
+    QueuedRedis& zpopmax(const StringView &key) {
+        return command(cmd::zpopmax, key, 1);
+    }
+
+    QueuedRedis& zpopmax(const StringView &key, long long count) {
+        return command(cmd::zpopmax, key, count);
+    }
+
+    QueuedRedis& zpopmin(const StringView &key) {
+        return command(cmd::zpopmin, key, 1);
+    }
+
+    QueuedRedis& zpopmin(const StringView &key, long long count) {
+        return command(cmd::zpopmin, key, count);
+    }
+
+    // NOTE: *QueuedRedis::zrange*'s parameters are different from *Redis::zrange*.
+    // *Redis::zrange* is overloaded by the output iterator, however, there's no such
+    // iterator in *QueuedRedis::zrange*. So we have to use an extra parameter: *with_scores*,
+    // to decide whether we should send *WITHSCORES* option to Redis. This also applies to
+    // other commands with the *WITHSCORES* option, e.g. *ZRANGEBYSCORE*, *ZREVRANGE*,
+    // *ZREVRANGEBYSCORE*.
+    QueuedRedis& zrange(const StringView &key,
+                        long long start,
+                        long long stop,
+                        bool with_scores = false) {
+        return command(cmd::zrange, key, start, stop, with_scores);
+    }
+
+    template <typename Interval>
+    QueuedRedis& zrangebylex(const StringView &key,
+                                const Interval &interval,
+                                const LimitOptions &opts) {
+        return command(cmd::zrangebylex<Interval>, key, interval, opts);
+    }
+
+    template <typename Interval>
+    QueuedRedis& zrangebylex(const StringView &key, const Interval &interval) {
+        return zrangebylex(key, interval, {});
+    }
+
+    // See comments on *ZRANGE*.
+    template <typename Interval>
+    QueuedRedis& zrangebyscore(const StringView &key,
+                                const Interval &interval,
+                                const LimitOptions &opts,
+                                bool with_scores = false) {
+        return command(cmd::zrangebyscore<Interval>, key, interval, opts, with_scores);
+    }
+
+    // See comments on *ZRANGE*.
+    template <typename Interval>
+    QueuedRedis& zrangebyscore(const StringView &key,
+                                const Interval &interval,
+                                bool with_scores = false) {
+        return zrangebyscore(key, interval, {}, with_scores);
+    }
+
+    QueuedRedis& zrank(const StringView &key, const StringView &member) {
+        return command(cmd::zrank, key, member);
+    }
+
+    QueuedRedis& zrem(const StringView &key, const StringView &member) {
+        return command(cmd::zrem, key, member);
+    }
+
+    template <typename Input>
+    QueuedRedis& zrem(const StringView &key, Input first, Input last) {
+        return command(cmd::zrem_range<Input>, key, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& zrem(const StringView &key, std::initializer_list<T> il) {
+        return zrem(key, il.begin(), il.end());
+    }
+
+    template <typename Interval>
+    QueuedRedis& zremrangebylex(const StringView &key, const Interval &interval) {
+        return command(cmd::zremrangebylex<Interval>, key, interval);
+    }
+
+    QueuedRedis& zremrangebyrank(const StringView &key, long long start, long long stop) {
+        return command(cmd::zremrangebyrank, key, start, stop);
+    }
+
+    template <typename Interval>
+    QueuedRedis& zremrangebyscore(const StringView &key, const Interval &interval) {
+        return command(cmd::zremrangebyscore<Interval>, key, interval);
+    }
+
+    // See comments on *ZRANGE*.
+    QueuedRedis& zrevrange(const StringView &key,
+                            long long start,
+                            long long stop,
+                            bool with_scores = false) {
+        return command(cmd::zrevrange, key, start, stop, with_scores);
+    }
+
+    template <typename Interval>
+    QueuedRedis& zrevrangebylex(const StringView &key,
+                                const Interval &interval,
+                                const LimitOptions &opts) {
+        return command(cmd::zrevrangebylex<Interval>, key, interval, opts);
+    }
+
+    template <typename Interval>
+    QueuedRedis& zrevrangebylex(const StringView &key, const Interval &interval) {
+        return zrevrangebylex(key, interval, {});
+    }
+
+    // See comments on *ZRANGE*.
+    template <typename Interval>
+    QueuedRedis& zrevrangebyscore(const StringView &key,
+                                    const Interval &interval,
+                                    const LimitOptions &opts,
+                                    bool with_scores = false) {
+        return command(cmd::zrevrangebyscore<Interval>, key, interval, opts, with_scores);
+    }
+
+    // See comments on *ZRANGE*.
+    template <typename Interval>
+    QueuedRedis& zrevrangebyscore(const StringView &key,
+                                    const Interval &interval,
+                                    bool with_scores = false) {
+        return zrevrangebyscore(key, interval, {}, with_scores);
+    }
+
+    QueuedRedis& zrevrank(const StringView &key, const StringView &member) {
+        return command(cmd::zrevrank, key, member);
+    }
+
+    QueuedRedis& zscan(const StringView &key,
+                        long long cursor,
+                        const StringView &pattern,
+                        long long count) {
+        return command(cmd::zscan, key, cursor, pattern, count);
+    }
+
+    QueuedRedis& zscan(const StringView &key,
+                        long long cursor,
+                        const StringView &pattern) {
+        return zscan(key, cursor, pattern, 10);
+    }
+
+    QueuedRedis& zscan(const StringView &key,
+                        long long cursor,
+                        long long count) {
+        return zscan(key, cursor, "*", count);
+    }
+
+    QueuedRedis& zscan(const StringView &key,
+                        long long cursor) {
+        return zscan(key, cursor, "*", 10);
+    }
+
+    QueuedRedis& zscore(const StringView &key, const StringView &member) {
+        return command(cmd::zscore, key, member);
+    }
+
+    QueuedRedis& zunionstore(const StringView &destination,
+                                const StringView &key,
+                                double weight) {
+        return command(cmd::zunionstore, destination, key, weight);
+    }
+
+    template <typename Input>
+    QueuedRedis& zunionstore(const StringView &destination,
+                                Input first,
+                                Input last,
+                                Aggregation type = Aggregation::SUM) {
+        return command(cmd::zunionstore_range<Input>, destination, first, last, type);
+    }
+
+    template <typename T>
+    QueuedRedis& zunionstore(const StringView &destination,
+                                std::initializer_list<T> il,
+                                Aggregation type = Aggregation::SUM) {
+        return zunionstore(destination, il.begin(), il.end(), type);
+    }
+
+    // HYPERLOGLOG commands.
+
+    QueuedRedis& pfadd(const StringView &key, const StringView &element) {
+        return command(cmd::pfadd, key, element);
+    }
+
+    template <typename Input>
+    QueuedRedis& pfadd(const StringView &key, Input first, Input last) {
+        return command(cmd::pfadd_range<Input>, key, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& pfadd(const StringView &key, std::initializer_list<T> il) {
+        return pfadd(key, il.begin(), il.end());
+    }
+
+    QueuedRedis& pfcount(const StringView &key) {
+        return command(cmd::pfcount, key);
+    }
+
+    template <typename Input>
+    QueuedRedis& pfcount(Input first, Input last) {
+        return command(cmd::pfcount_range<Input>, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& pfcount(std::initializer_list<T> il) {
+        return pfcount(il.begin(), il.end());
+    }
+
+    QueuedRedis& pfmerge(const StringView &destination, const StringView &key) {
+        return command(cmd::pfmerge, destination, key);
+    }
+
+    template <typename Input>
+    QueuedRedis& pfmerge(const StringView &destination, Input first, Input last) {
+        return command(cmd::pfmerge_range<Input>, destination, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& pfmerge(const StringView &destination, std::initializer_list<T> il) {
+        return pfmerge(destination, il.begin(), il.end());
+    }
+
+    // GEO commands.
+
+    QueuedRedis& geoadd(const StringView &key,
+                        const std::tuple<StringView, double, double> &member) {
+        return command(cmd::geoadd, key, member);
+    }
+
+    template <typename Input>
+    QueuedRedis& geoadd(const StringView &key,
+                        Input first,
+                        Input last) {
+        return command(cmd::geoadd_range<Input>, key, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& geoadd(const StringView &key, std::initializer_list<T> il) {
+        return geoadd(key, il.begin(), il.end());
+    }
+
+    QueuedRedis& geodist(const StringView &key,
+                            const StringView &member1,
+                            const StringView &member2,
+                            GeoUnit unit = GeoUnit::M) {
+        return command(cmd::geodist, key, member1, member2, unit);
+    }
+
+    template <typename Input>
+    QueuedRedis& geohash(const StringView &key, Input first, Input last) {
+        return command(cmd::geohash_range<Input>, key, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& geohash(const StringView &key, std::initializer_list<T> il) {
+        return geohash(key, il.begin(), il.end());
+    }
+
+    template <typename Input>
+    QueuedRedis& geopos(const StringView &key, Input first, Input last) {
+        return command(cmd::geopos_range<Input>, key, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& geopos(const StringView &key, std::initializer_list<T> il) {
+        return geopos(key, il.begin(), il.end());
+    }
+
+    // TODO:
+    // 1. since we have different overloads for georadius and georadius-store,
+    //    we might use the GEORADIUS_RO command in the future.
+    // 2. there're too many parameters for this method, we might refactor it.
+    QueuedRedis& georadius(const StringView &key,
+                            const std::pair<double, double> &loc,
+                            double radius,
+                            GeoUnit unit,
+                            const StringView &destination,
+                            bool store_dist,
+                            long long count) {
+        _georadius_cmd_indexes.push_back(_cmd_num);
+
+        return command(cmd::georadius_store,
+                        key,
+                        loc,
+                        radius,
+                        unit,
+                        destination,
+                        store_dist,
+                        count);
+    }
+
+    // NOTE: *QueuedRedis::georadius*'s parameters are different from *Redis::georadius*.
+    // *Redis::georadius* is overloaded by the output iterator, however, there's no such
+    // iterator in *QueuedRedis::georadius*. So we have to use extra parameters to decide
+    // whether we should send options to Redis. This also applies to *GEORADIUSBYMEMBER*.
+    QueuedRedis& georadius(const StringView &key,
+                            const std::pair<double, double> &loc,
+                            double radius,
+                            GeoUnit unit,
+                            long long count,
+                            bool asc,
+                            bool with_coord,
+                            bool with_dist,
+                            bool with_hash) {
+        return command(cmd::georadius,
+                        key,
+                        loc,
+                        radius,
+                        unit,
+                        count,
+                        asc,
+                        with_coord,
+                        with_dist,
+                        with_hash);
+    }
+
+    QueuedRedis& georadiusbymember(const StringView &key,
+                                    const StringView &member,
+                                    double radius,
+                                    GeoUnit unit,
+                                    const StringView &destination,
+                                    bool store_dist,
+                                    long long count) {
+        _georadius_cmd_indexes.push_back(_cmd_num);
+
+        return command(cmd::georadiusbymember,
+                        key,
+                        member,
+                        radius,
+                        unit,
+                        destination,
+                        store_dist,
+                        count);
+    }
+
+    // See the comments on *GEORADIUS*.
+    QueuedRedis& georadiusbymember(const StringView &key,
+                                    const StringView &member,
+                                    double radius,
+                                    GeoUnit unit,
+                                    long long count,
+                                    bool asc,
+                                    bool with_coord,
+                                    bool with_dist,
+                                    bool with_hash) {
+        return command(cmd::georadiusbymember,
+                        key,
+                        member,
+                        radius,
+                        unit,
+                        count,
+                        asc,
+                        with_coord,
+                        with_dist,
+                        with_hash);
+    }
+
+    // SCRIPTING commands.
+
+    QueuedRedis& eval(const StringView &script,
+                        std::initializer_list<StringView> keys,
+                        std::initializer_list<StringView> args) {
+        return command(cmd::eval, script, keys, args);
+    }
+
+    QueuedRedis& evalsha(const StringView &script,
+                            std::initializer_list<StringView> keys,
+                            std::initializer_list<StringView> args) {
+        return command(cmd::evalsha, script, keys, args);
+    }
+
+    template <typename Input>
+    QueuedRedis& script_exists(Input first, Input last) {
+        return command(cmd::script_exists_range<Input>, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& script_exists(std::initializer_list<T> il) {
+        return script_exists(il.begin(), il.end());
+    }
+
+    QueuedRedis& script_flush() {
+        return command(cmd::script_flush);
+    }
+
+    QueuedRedis& script_kill() {
+        return command(cmd::script_kill);
+    }
+
+    QueuedRedis& script_load(const StringView &script) {
+        return command(cmd::script_load, script);
+    }
+
+    // PUBSUB commands.
+
+    QueuedRedis& publish(const StringView &channel, const StringView &message) {
+        return command(cmd::publish, channel, message);
+    }
+
+    // Stream commands.
+
+    QueuedRedis& xack(const StringView &key, const StringView &group, const StringView &id) {
+        return command(cmd::xack, key, group, id);
+    }
+
+    template <typename Input>
+    QueuedRedis& xack(const StringView &key, const StringView &group, Input first, Input last) {
+        return command(cmd::xack_range<Input>, key, group, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& xack(const StringView &key, const StringView &group, std::initializer_list<T> il) {
+        return xack(key, group, il.begin(), il.end());
+    }
+
+    template <typename Input>
+    QueuedRedis& xadd(const StringView &key, const StringView &id, Input first, Input last) {
+        return command(cmd::xadd_range<Input>, key, id, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& xadd(const StringView &key, const StringView &id, std::initializer_list<T> il) {
+        return xadd(key, id, il.begin(), il.end());
+    }
+
+    template <typename Input>
+    QueuedRedis& xadd(const StringView &key,
+                        const StringView &id,
+                        Input first,
+                        Input last,
+                        long long count,
+                        bool approx = true) {
+        return command(cmd::xadd_maxlen_range<Input>, key, id, first, last, count, approx);
+    }
+
+    template <typename T>
+    QueuedRedis& xadd(const StringView &key,
+                        const StringView &id,
+                        std::initializer_list<T> il,
+                        long long count,
+                        bool approx = true) {
+        return xadd(key, id, il.begin(), il.end(), count, approx);
+    }
+
+    QueuedRedis& xclaim(const StringView &key,
+                        const StringView &group,
+                        const StringView &consumer,
+                        const std::chrono::milliseconds &min_idle_time,
+                        const StringView &id) {
+        return command(cmd::xclaim, key, group, consumer, min_idle_time.count(), id);
+    }
+
+    template <typename Input>
+    QueuedRedis& xclaim(const StringView &key,
+                const StringView &group,
+                const StringView &consumer,
+                const std::chrono::milliseconds &min_idle_time,
+                Input first,
+                Input last) {
+        return command(cmd::xclaim_range<Input>,
+                        key,
+                        group,
+                        consumer,
+                        min_idle_time.count(),
+                        first,
+                        last);
+    }
+
+    template <typename T>
+    QueuedRedis& xclaim(const StringView &key,
+                const StringView &group,
+                const StringView &consumer,
+                const std::chrono::milliseconds &min_idle_time,
+                std::initializer_list<T> il) {
+        return xclaim(key, group, consumer, min_idle_time, il.begin(), il.end());
+    }
+
+    QueuedRedis& xdel(const StringView &key, const StringView &id) {
+        return command(cmd::xdel, key, id);
+    }
+
+    template <typename Input>
+    QueuedRedis& xdel(const StringView &key, Input first, Input last) {
+        return command(cmd::xdel_range<Input>, key, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& xdel(const StringView &key, std::initializer_list<T> il) {
+        return xdel(key, il.begin(), il.end());
+    }
+
+    QueuedRedis& xgroup_create(const StringView &key,
+                                const StringView &group,
+                                const StringView &id,
+                                bool mkstream = false) {
+        return command(cmd::xgroup_create, key, group, id, mkstream);
+    }
+
+    QueuedRedis& xgroup_setid(const StringView &key,
+                                const StringView &group,
+                                const StringView &id) {
+        return command(cmd::xgroup_setid, key, group, id);
+    }
+
+    QueuedRedis& xgroup_destroy(const StringView &key, const StringView &group) {
+        return command(cmd::xgroup_destroy, key, group);
+    }
+
+    QueuedRedis& xgroup_delconsumer(const StringView &key,
+                                    const StringView &group,
+                                    const StringView &consumer) {
+        return command(cmd::xgroup_delconsumer, key, group, consumer);
+    }
+
+    QueuedRedis& xlen(const StringView &key) {
+        return command(cmd::xlen, key);
+    }
+
+    QueuedRedis& xpending(const StringView &key, const StringView &group) {
+        return command(cmd::xpending, key, group);
+    }
+
+    QueuedRedis& xpending(const StringView &key,
+                            const StringView &group,
+                            const StringView &start,
+                            const StringView &end,
+                            long long count) {
+        return command(cmd::xpending_detail, key, group, start, end, count);
+    }
+
+    QueuedRedis& xpending(const StringView &key,
+                            const StringView &group,
+                            const StringView &start,
+                            const StringView &end,
+                            long long count,
+                            const StringView &consumer) {
+        return command(cmd::xpending_per_consumer, key, group, start, end, count, consumer);
+    }
+
+    QueuedRedis& xrange(const StringView &key,
+                        const StringView &start,
+                        const StringView &end) {
+        return command(cmd::xrange, key, start, end);
+    }
+
+    QueuedRedis& xrange(const StringView &key,
+                        const StringView &start,
+                        const StringView &end,
+                        long long count) {
+        return command(cmd::xrange, key, start, end, count);
+    }
+
+    QueuedRedis& xread(const StringView &key, const StringView &id, long long count) {
+        return command(cmd::xread, key, id, count);
+    }
+
+    QueuedRedis& xread(const StringView &key, const StringView &id) {
+        return xread(key, id, 0);
+    }
+
+    template <typename Input>
+    auto xread(Input first, Input last, long long count)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value,
+                                    QueuedRedis&>::type {
+        return command(cmd::xread_range<Input>, first, last, count);
+    }
+
+    template <typename Input>
+    auto xread(Input first, Input last)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value,
+                                    QueuedRedis&>::type {
+        return xread(first, last, 0);
+    }
+
+    QueuedRedis& xread(const StringView &key,
+                        const StringView &id,
+                        const std::chrono::milliseconds &timeout,
+                        long long count) {
+        return command(cmd::xread_block, key, id, timeout.count(), count);
+    }
+
+    QueuedRedis& xread(const StringView &key,
+                        const StringView &id,
+                        const std::chrono::milliseconds &timeout) {
+        return xread(key, id, timeout, 0);
+    }
+
+    template <typename Input>
+    auto xread(Input first,
+                Input last,
+                const std::chrono::milliseconds &timeout,
+                long long count)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value,
+                                    QueuedRedis&>::type {
+        return command(cmd::xread_block_range<Input>, first, last, timeout.count(), count);
+    }
+
+    template <typename Input>
+    auto xread(Input first,
+                Input last,
+                const std::chrono::milliseconds &timeout)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value,
+                                    QueuedRedis&>::type {
+        return xread(first, last, timeout, 0);
+    }
+
+    QueuedRedis& xreadgroup(const StringView &group,
+                            const StringView &consumer,
+                            const StringView &key,
+                            const StringView &id,
+                            long long count,
+                            bool noack) {
+        return command(cmd::xreadgroup, group, consumer, key, id, count, noack);
+    }
+
+    QueuedRedis& xreadgroup(const StringView &group,
+                            const StringView &consumer,
+                            const StringView &key,
+                            const StringView &id,
+                            long long count) {
+        return xreadgroup(group, consumer, key, id, count, false);
+    }
+
+    template <typename Input>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last,
+                    long long count,
+                    bool noack)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value,
+                                    QueuedRedis&>::type {
+        return command(cmd::xreadgroup_range<Input>, group, consumer, first, last, count, noack);
+    }
+
+    template <typename Input>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last,
+                    long long count)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value,
+                                    QueuedRedis&>::type {
+        return xreadgroup(group, consumer, first ,last, count, false);
+    }
+
+    template <typename Input>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value,
+                                    QueuedRedis&>::type {
+        return xreadgroup(group, consumer, first ,last, 0, false);
+    }
+
+    QueuedRedis& xreadgroup(const StringView &group,
+                            const StringView &consumer,
+                            const StringView &key,
+                            const StringView &id,
+                            const std::chrono::milliseconds &timeout,
+                            long long count,
+                            bool noack) {
+        return command(cmd::xreadgroup_block,
+                        group,
+                        consumer,
+                        key,
+                        id,
+                        timeout.count(),
+                        count,
+                        noack);
+    }
+
+    QueuedRedis& xreadgroup(const StringView &group,
+                            const StringView &consumer,
+                            const StringView &key,
+                            const StringView &id,
+                            const std::chrono::milliseconds &timeout,
+                            long long count) {
+        return xreadgroup(group, consumer, key, id, timeout, count, false);
+    }
+
+    QueuedRedis& xreadgroup(const StringView &group,
+                            const StringView &consumer,
+                            const StringView &key,
+                            const StringView &id,
+                            const std::chrono::milliseconds &timeout) {
+        return xreadgroup(group, consumer, key, id, timeout, 0, false);
+    }
+
+    template <typename Input>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last,
+                    const std::chrono::milliseconds &timeout,
+                    long long count,
+                    bool noack)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value,
+                                    QueuedRedis&>::type {
+        return command(cmd::xreadgroup_block_range<Input>,
+                        group,
+                        consumer,
+                        first,
+                        last,
+                        timeout.count(),
+                        count,
+                        noack);
+    }
+
+    template <typename Input>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last,
+                    const std::chrono::milliseconds &timeout,
+                    long long count)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value,
+                                    QueuedRedis&>::type {
+        return xreadgroup(group, consumer, first, last, timeout, count, false);
+    }
+
+    template <typename Input>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last,
+                    const std::chrono::milliseconds &timeout)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value,
+                                    QueuedRedis&>::type {
+        return xreadgroup(group, consumer, first, last, timeout, 0, false);
+    }
+
+    QueuedRedis& xrevrange(const StringView &key,
+                            const StringView &end,
+                            const StringView &start) {
+        return command(cmd::xrevrange, key, end, start);
+    }
+
+    QueuedRedis& xrevrange(const StringView &key,
+                            const StringView &end,
+                            const StringView &start,
+                            long long count) {
+        return command(cmd::xrevrange, key, end, start, count);
+    }
+
+    QueuedRedis& xtrim(const StringView &key, long long count, bool approx = true) {
+        return command(cmd::xtrim, key, count, approx);
+    }
+
+private:
+    friend class Redis;
+
+    friend class RedisCluster;
+
+    template <typename ...Args>
+    QueuedRedis(const ConnectionSPtr &connection, Args &&...args);
+
+    void _sanity_check() const;
+
+    void _reset();
+
+    void _invalidate();
+
+    void _rewrite_replies(std::vector<ReplyUPtr> &replies) const;
+
+    template <typename Func>
+    void _rewrite_replies(const std::vector<std::size_t> &indexes,
+                            Func rewriter,
+                            std::vector<ReplyUPtr> &replies) const;
+
+    ConnectionSPtr _connection;
+
+    Impl _impl;
+
+    std::size_t _cmd_num = 0;
+
+    std::vector<std::size_t> _set_cmd_indexes;
+
+    std::vector<std::size_t> _georadius_cmd_indexes;
+
+    bool _valid = true;
+};
+
+class QueuedReplies {
+public:
+    std::size_t size() const;
+
+    redisReply& get(std::size_t idx);
+
+    template <typename Result>
+    Result get(std::size_t idx);
+
+    template <typename Output>
+    void get(std::size_t idx, Output output);
+
+private:
+    template <typename Impl>
+    friend class QueuedRedis;
+
+    explicit QueuedReplies(std::vector<ReplyUPtr> replies) : _replies(std::move(replies)) {}
+
+    void _index_check(std::size_t idx) const;
+
+    std::vector<ReplyUPtr> _replies;
+};
+
+}
+
+}
+
+#include "queued_redis.hpp"
+
+#endif // end SEWENEW_REDISPLUSPLUS_QUEUED_REDIS_H

+ 208 - 0
ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/queued_redis.hpp

@@ -0,0 +1,208 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_QUEUED_REDIS_HPP
+#define SEWENEW_REDISPLUSPLUS_QUEUED_REDIS_HPP
+
+namespace sw {
+
+namespace redis {
+
+template <typename Impl>
+template <typename ...Args>
+QueuedRedis<Impl>::QueuedRedis(const ConnectionSPtr &connection, Args &&...args) :
+            _connection(connection),
+            _impl(std::forward<Args>(args)...) {
+    assert(_connection);
+}
+
+template <typename Impl>
+Redis QueuedRedis<Impl>::redis() {
+    return Redis(_connection);
+}
+
+template <typename Impl>
+template <typename Cmd, typename ...Args>
+auto QueuedRedis<Impl>::command(Cmd cmd, Args &&...args)
+    -> typename std::enable_if<!std::is_convertible<Cmd, StringView>::value,
+                                QueuedRedis<Impl>&>::type {
+    try {
+        _sanity_check();
+
+        _impl.command(*_connection, cmd, std::forward<Args>(args)...);
+
+        ++_cmd_num;
+    } catch (const Error &e) {
+        _invalidate();
+        throw;
+    }
+
+    return *this;
+}
+
+template <typename Impl>
+template <typename ...Args>
+QueuedRedis<Impl>& QueuedRedis<Impl>::command(const StringView &cmd_name, Args &&...args) {
+    auto cmd = [](Connection &connection, const StringView &cmd_name, Args &&...args) {
+                    CmdArgs cmd_args;
+                    cmd_args.append(cmd_name, std::forward<Args>(args)...);
+                    connection.send(cmd_args);
+    };
+
+    return command(cmd, cmd_name, std::forward<Args>(args)...);
+}
+
+template <typename Impl>
+template <typename Input>
+auto QueuedRedis<Impl>::command(Input first, Input last)
+    -> typename std::enable_if<IsIter<Input>::value, QueuedRedis<Impl>&>::type {
+    if (first == last) {
+        throw Error("command: empty range");
+    }
+
+    auto cmd = [](Connection &connection, Input first, Input last) {
+                    CmdArgs cmd_args;
+                    while (first != last) {
+                        cmd_args.append(*first);
+                        ++first;
+                    }
+                    connection.send(cmd_args);
+    };
+
+    return command(cmd, first, last);
+}
+
+template <typename Impl>
+QueuedReplies QueuedRedis<Impl>::exec() {
+    try {
+        _sanity_check();
+
+        auto replies = _impl.exec(*_connection, _cmd_num);
+
+        _rewrite_replies(replies);
+
+        _reset();
+
+        return QueuedReplies(std::move(replies));
+    } catch (const Error &e) {
+        _invalidate();
+        throw;
+    }
+}
+
+template <typename Impl>
+void QueuedRedis<Impl>::discard() {
+    try {
+        _sanity_check();
+
+        _impl.discard(*_connection, _cmd_num);
+
+        _reset();
+    } catch (const Error &e) {
+        _invalidate();
+        throw;
+    }
+}
+
+template <typename Impl>
+void QueuedRedis<Impl>::_sanity_check() const {
+    if (!_valid) {
+        throw Error("Not in valid state");
+    }
+
+    if (_connection->broken()) {
+        throw Error("Connection is broken");
+    }
+}
+
+template <typename Impl>
+inline void QueuedRedis<Impl>::_reset() {
+    _cmd_num = 0;
+
+    _set_cmd_indexes.clear();
+
+    _georadius_cmd_indexes.clear();
+}
+
+template <typename Impl>
+void QueuedRedis<Impl>::_invalidate() {
+    _valid = false;
+
+    _reset();
+}
+
+template <typename Impl>
+void QueuedRedis<Impl>::_rewrite_replies(std::vector<ReplyUPtr> &replies) const {
+    _rewrite_replies(_set_cmd_indexes, reply::rewrite_set_reply, replies);
+
+    _rewrite_replies(_georadius_cmd_indexes, reply::rewrite_georadius_reply, replies);
+}
+
+template <typename Impl>
+template <typename Func>
+void QueuedRedis<Impl>::_rewrite_replies(const std::vector<std::size_t> &indexes,
+                                            Func rewriter,
+                                            std::vector<ReplyUPtr> &replies) const {
+    for (auto idx : indexes) {
+        assert(idx < replies.size());
+
+        auto &reply = replies[idx];
+
+        assert(reply);
+
+        rewriter(*reply);
+    }
+}
+
+inline std::size_t QueuedReplies::size() const {
+    return _replies.size();
+}
+
+inline redisReply& QueuedReplies::get(std::size_t idx) {
+    _index_check(idx);
+
+    auto &reply = _replies[idx];
+
+    assert(reply);
+
+    return *reply;
+}
+
+template <typename Result>
+inline Result QueuedReplies::get(std::size_t idx) {
+    auto &reply = get(idx);
+
+    return reply::parse<Result>(reply);
+}
+
+template <typename Output>
+inline void QueuedReplies::get(std::size_t idx, Output output) {
+    auto &reply = get(idx);
+
+    reply::to_array(reply, output);
+}
+
+inline void QueuedReplies::_index_check(std::size_t idx) const {
+    if (idx >= size()) {
+        throw Error("Out of range");
+    }
+}
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_QUEUED_REDIS_HPP

+ 25 - 0
ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/redis++.h

@@ -0,0 +1,25 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_REDISPLUSPLUS_H
+#define SEWENEW_REDISPLUSPLUS_REDISPLUSPLUS_H
+
+#include "redis.h"
+#include "redis_cluster.h"
+#include "queued_redis.h"
+#include "sentinel.h"
+
+#endif // end SEWENEW_REDISPLUSPLUS_REDISPLUSPLUS_H

+ 1523 - 0
ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/redis.h

@@ -0,0 +1,1523 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_REDIS_H
+#define SEWENEW_REDISPLUSPLUS_REDIS_H
+
+#include <string>
+#include <chrono>
+#include <memory>
+#include <initializer_list>
+#include <tuple>
+#include "connection_pool.h"
+#include "reply.h"
+#include "command_options.h"
+#include "utils.h"
+#include "subscriber.h"
+#include "pipeline.h"
+#include "transaction.h"
+#include "sentinel.h"
+
+namespace sw {
+
+namespace redis {
+
+template <typename Impl>
+class QueuedRedis;
+
+using Transaction = QueuedRedis<TransactionImpl>;
+
+using Pipeline = QueuedRedis<PipelineImpl>;
+
+class Redis {
+public:
+    Redis(const ConnectionOptions &connection_opts,
+            const ConnectionPoolOptions &pool_opts = {}) : _pool(pool_opts, connection_opts) {}
+
+    // Construct Redis instance with URI:
+    // "tcp://127.0.0.1", "tcp://127.0.0.1:6379", or "unix://path/to/socket"
+    explicit Redis(const std::string &uri);
+
+    Redis(const std::shared_ptr<Sentinel> &sentinel,
+            const std::string &master_name,
+            Role role,
+            const ConnectionOptions &connection_opts,
+            const ConnectionPoolOptions &pool_opts = {}) :
+                _pool(SimpleSentinel(sentinel, master_name, role), pool_opts, connection_opts) {}
+
+    Redis(const Redis &) = delete;
+    Redis& operator=(const Redis &) = delete;
+
+    Redis(Redis &&) = default;
+    Redis& operator=(Redis &&) = default;
+
+    Pipeline pipeline();
+
+    Transaction transaction(bool piped = false);
+
+    Subscriber subscriber();
+
+    template <typename Cmd, typename ...Args>
+    auto command(Cmd cmd, Args &&...args)
+        -> typename std::enable_if<!std::is_convertible<Cmd, StringView>::value, ReplyUPtr>::type;
+
+    template <typename ...Args>
+    auto command(const StringView &cmd_name, Args &&...args)
+        -> typename std::enable_if<!IsIter<typename LastType<Args...>::type>::value,
+                                    ReplyUPtr>::type;
+
+    template <typename ...Args>
+    auto command(const StringView &cmd_name, Args &&...args)
+        -> typename std::enable_if<IsIter<typename LastType<Args...>::type>::value, void>::type;
+
+    template <typename Result, typename ...Args>
+    Result command(const StringView &cmd_name, Args &&...args);
+
+    template <typename Input>
+    auto command(Input first, Input last)
+        -> typename std::enable_if<IsIter<Input>::value, ReplyUPtr>::type;
+
+    template <typename Result, typename Input>
+    auto command(Input first, Input last)
+        -> typename std::enable_if<IsIter<Input>::value, Result>::type;
+
+    template <typename Input, typename Output>
+    auto command(Input first, Input last, Output output)
+        -> typename std::enable_if<IsIter<Input>::value, void>::type;
+
+    // CONNECTION commands.
+
+    void auth(const StringView &password);
+
+    std::string echo(const StringView &msg);
+
+    std::string ping();
+
+    std::string ping(const StringView &msg);
+
+    // After sending QUIT, only the current connection will be close, while
+    // other connections in the pool is still open. This is a strange behavior.
+    // So we DO NOT support the QUIT command. If you want to quit connection to
+    // server, just destroy the Redis object.
+    //
+    // void quit();
+
+    // We get a connection from the pool, and send the SELECT command to switch
+    // to a specified DB. However, when we try to send other commands to the
+    // given DB, we might get a different connection from the pool, and these
+    // commands, in fact, work on other DB. e.g.
+    //
+    // redis.select(1); // get a connection from the pool and switch to the 1th DB
+    // redis.get("key"); // might get another connection from the pool,
+    //                   // and try to get 'key' on the default DB
+    //
+    // Obviously, this is NOT what we expect. So we DO NOT support SELECT command.
+    // In order to select a DB, we can specify the DB index with the ConnectionOptions.
+    //
+    // However, since Pipeline and Transaction always send multiple commands on a
+    // single connection, these two classes have a *select* method.
+    //
+    // void select(long long idx);
+
+    void swapdb(long long idx1, long long idx2);
+
+    // SERVER commands.
+
+    void bgrewriteaof();
+
+    void bgsave();
+
+    long long dbsize();
+
+    void flushall(bool async = false);
+
+    void flushdb(bool async = false);
+
+    std::string info();
+
+    std::string info(const StringView &section);
+
+    long long lastsave();
+
+    void save();
+
+    // KEY commands.
+
+    long long del(const StringView &key);
+
+    template <typename Input>
+    long long del(Input first, Input last);
+
+    template <typename T>
+    long long del(std::initializer_list<T> il) {
+        return del(il.begin(), il.end());
+    }
+
+    OptionalString dump(const StringView &key);
+
+    long long exists(const StringView &key);
+
+    template <typename Input>
+    long long exists(Input first, Input last);
+
+    template <typename T>
+    long long exists(std::initializer_list<T> il) {
+        return exists(il.begin(), il.end());
+    }
+
+    bool expire(const StringView &key, long long timeout);
+
+    bool expire(const StringView &key, const std::chrono::seconds &timeout);
+
+    bool expireat(const StringView &key, long long timestamp);
+
+    bool expireat(const StringView &key,
+                    const std::chrono::time_point<std::chrono::system_clock,
+                                                    std::chrono::seconds> &tp);
+
+    template <typename Output>
+    void keys(const StringView &pattern, Output output);
+
+    bool move(const StringView &key, long long db);
+
+    bool persist(const StringView &key);
+
+    bool pexpire(const StringView &key, long long timeout);
+
+    bool pexpire(const StringView &key, const std::chrono::milliseconds &timeout);
+
+    bool pexpireat(const StringView &key, long long timestamp);
+
+    bool pexpireat(const StringView &key,
+                    const std::chrono::time_point<std::chrono::system_clock,
+                                                    std::chrono::milliseconds> &tp);
+
+    long long pttl(const StringView &key);
+
+    OptionalString randomkey();
+
+    void rename(const StringView &key, const StringView &newkey);
+
+    bool renamenx(const StringView &key, const StringView &newkey);
+
+    void restore(const StringView &key,
+                    const StringView &val,
+                    long long ttl,
+                    bool replace = false);
+
+    void restore(const StringView &key,
+                    const StringView &val,
+                    const std::chrono::milliseconds &ttl = std::chrono::milliseconds{0},
+                    bool replace = false);
+
+    // TODO: sort
+
+    template <typename Output>
+    long long scan(long long cursor,
+                    const StringView &pattern,
+                    long long count,
+                    Output output);
+
+    template <typename Output>
+    long long scan(long long cursor,
+                    Output output);
+
+    template <typename Output>
+    long long scan(long long cursor,
+                    const StringView &pattern,
+                    Output output);
+
+    template <typename Output>
+    long long scan(long long cursor,
+                    long long count,
+                    Output output);
+
+    long long touch(const StringView &key);
+
+    template <typename Input>
+    long long touch(Input first, Input last);
+
+    template <typename T>
+    long long touch(std::initializer_list<T> il) {
+        return touch(il.begin(), il.end());
+    }
+
+    long long ttl(const StringView &key);
+
+    std::string type(const StringView &key);
+
+    long long unlink(const StringView &key);
+
+    template <typename Input>
+    long long unlink(Input first, Input last);
+
+    template <typename T>
+    long long unlink(std::initializer_list<T> il) {
+        return unlink(il.begin(), il.end());
+    }
+
+    long long wait(long long numslaves, long long timeout);
+
+    long long wait(long long numslaves, const std::chrono::milliseconds &timeout);
+
+    // STRING commands.
+
+    long long append(const StringView &key, const StringView &str);
+
+    long long bitcount(const StringView &key, long long start = 0, long long end = -1);
+
+    long long bitop(BitOp op, const StringView &destination, const StringView &key);
+
+    template <typename Input>
+    long long bitop(BitOp op, const StringView &destination, Input first, Input last);
+
+    template <typename T>
+    long long bitop(BitOp op, const StringView &destination, std::initializer_list<T> il) {
+        return bitop(op, destination, il.begin(), il.end());
+    }
+
+    long long bitpos(const StringView &key,
+                        long long bit,
+                        long long start = 0,
+                        long long end = -1);
+
+    long long decr(const StringView &key);
+
+    long long decrby(const StringView &key, long long decrement);
+
+    OptionalString get(const StringView &key);
+
+    long long getbit(const StringView &key, long long offset);
+
+    std::string getrange(const StringView &key, long long start, long long end);
+
+    OptionalString getset(const StringView &key, const StringView &val);
+
+    long long incr(const StringView &key);
+
+    long long incrby(const StringView &key, long long increment);
+
+    double incrbyfloat(const StringView &key, double increment);
+
+    template <typename Input, typename Output>
+    void mget(Input first, Input last, Output output);
+
+    template <typename T, typename Output>
+    void mget(std::initializer_list<T> il, Output output) {
+        mget(il.begin(), il.end(), output);
+    }
+
+    template <typename Input>
+    void mset(Input first, Input last);
+
+    template <typename T>
+    void mset(std::initializer_list<T> il) {
+        mset(il.begin(), il.end());
+    }
+
+    template <typename Input>
+    bool msetnx(Input first, Input last);
+
+    template <typename T>
+    bool msetnx(std::initializer_list<T> il) {
+        return msetnx(il.begin(), il.end());
+    }
+
+    void psetex(const StringView &key,
+                long long ttl,
+                const StringView &val);
+
+    void psetex(const StringView &key,
+                const std::chrono::milliseconds &ttl,
+                const StringView &val);
+
+    bool set(const StringView &key,
+                const StringView &val,
+                const std::chrono::milliseconds &ttl = std::chrono::milliseconds(0),
+                UpdateType type = UpdateType::ALWAYS);
+
+    void setex(const StringView &key,
+                long long ttl,
+                const StringView &val);
+
+    void setex(const StringView &key,
+                const std::chrono::seconds &ttl,
+                const StringView &val);
+
+    bool setnx(const StringView &key, const StringView &val);
+
+    long long setrange(const StringView &key, long long offset, const StringView &val);
+
+    long long strlen(const StringView &key);
+
+    // LIST commands.
+
+    OptionalStringPair blpop(const StringView &key, long long timeout);
+
+    OptionalStringPair blpop(const StringView &key,
+                                const std::chrono::seconds &timeout = std::chrono::seconds{0});
+
+    template <typename Input>
+    OptionalStringPair blpop(Input first, Input last, long long timeout);
+
+    template <typename T>
+    OptionalStringPair blpop(std::initializer_list<T> il, long long timeout) {
+        return blpop(il.begin(), il.end(), timeout);
+    }
+
+    template <typename Input>
+    OptionalStringPair blpop(Input first,
+                                Input last,
+                                const std::chrono::seconds &timeout = std::chrono::seconds{0});
+
+    template <typename T>
+    OptionalStringPair blpop(std::initializer_list<T> il,
+                                const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
+        return blpop(il.begin(), il.end(), timeout);
+    }
+
+    OptionalStringPair brpop(const StringView &key, long long timeout);
+
+    OptionalStringPair brpop(const StringView &key,
+                                const std::chrono::seconds &timeout = std::chrono::seconds{0});
+
+    template <typename Input>
+    OptionalStringPair brpop(Input first, Input last, long long timeout);
+
+    template <typename T>
+    OptionalStringPair brpop(std::initializer_list<T> il, long long timeout) {
+        return brpop(il.begin(), il.end(), timeout);
+    }
+
+    template <typename Input>
+    OptionalStringPair brpop(Input first,
+                                Input last,
+                                const std::chrono::seconds &timeout = std::chrono::seconds{0});
+
+    template <typename T>
+    OptionalStringPair brpop(std::initializer_list<T> il,
+                                const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
+        return brpop(il.begin(), il.end(), timeout);
+    }
+
+    OptionalString brpoplpush(const StringView &source,
+                                const StringView &destination,
+                                long long timeout);
+
+    OptionalString brpoplpush(const StringView &source,
+                                const StringView &destination,
+                                const std::chrono::seconds &timeout = std::chrono::seconds{0});
+
+    OptionalString lindex(const StringView &key, long long index);
+
+    long long linsert(const StringView &key,
+                        InsertPosition position,
+                        const StringView &pivot,
+                        const StringView &val);
+
+    long long llen(const StringView &key);
+
+    OptionalString lpop(const StringView &key);
+
+    long long lpush(const StringView &key, const StringView &val);
+
+    template <typename Input>
+    long long lpush(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    long long lpush(const StringView &key, std::initializer_list<T> il) {
+        return lpush(key, il.begin(), il.end());
+    }
+
+    long long lpushx(const StringView &key, const StringView &val);
+
+    template <typename Output>
+    void lrange(const StringView &key, long long start, long long stop, Output output);
+
+    long long lrem(const StringView &key, long long count, const StringView &val);
+
+    void lset(const StringView &key, long long index, const StringView &val);
+
+    void ltrim(const StringView &key, long long start, long long stop);
+
+    OptionalString rpop(const StringView &key);
+
+    OptionalString rpoplpush(const StringView &source, const StringView &destination);
+
+    long long rpush(const StringView &key, const StringView &val);
+
+    template <typename Input>
+    long long rpush(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    long long rpush(const StringView &key, std::initializer_list<T> il) {
+        return rpush(key, il.begin(), il.end());
+    }
+
+    long long rpushx(const StringView &key, const StringView &val);
+
+    // HASH commands.
+
+    long long hdel(const StringView &key, const StringView &field);
+
+    template <typename Input>
+    long long hdel(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    long long hdel(const StringView &key, std::initializer_list<T> il) {
+        return hdel(key, il.begin(), il.end());
+    }
+
+    bool hexists(const StringView &key, const StringView &field);
+
+    OptionalString hget(const StringView &key, const StringView &field);
+
+    template <typename Output>
+    void hgetall(const StringView &key, Output output);
+
+    long long hincrby(const StringView &key, const StringView &field, long long increment);
+
+    double hincrbyfloat(const StringView &key, const StringView &field, double increment);
+
+    template <typename Output>
+    void hkeys(const StringView &key, Output output);
+
+    long long hlen(const StringView &key);
+
+    template <typename Input, typename Output>
+    void hmget(const StringView &key, Input first, Input last, Output output);
+
+    template <typename T, typename Output>
+    void hmget(const StringView &key, std::initializer_list<T> il, Output output) {
+        hmget(key, il.begin(), il.end(), output);
+    }
+
+    template <typename Input>
+    void hmset(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    void hmset(const StringView &key, std::initializer_list<T> il) {
+        hmset(key, il.begin(), il.end());
+    }
+
+    template <typename Output>
+    long long hscan(const StringView &key,
+                    long long cursor,
+                    const StringView &pattern,
+                    long long count,
+                    Output output);
+
+    template <typename Output>
+    long long hscan(const StringView &key,
+                    long long cursor,
+                    const StringView &pattern,
+                    Output output);
+
+    template <typename Output>
+    long long hscan(const StringView &key,
+                    long long cursor,
+                    long long count,
+                    Output output);
+
+    template <typename Output>
+    long long hscan(const StringView &key,
+                    long long cursor,
+                    Output output);
+
+    bool hset(const StringView &key, const StringView &field, const StringView &val);
+
+    bool hset(const StringView &key, const std::pair<StringView, StringView> &item);
+
+    bool hsetnx(const StringView &key, const StringView &field, const StringView &val);
+
+    bool hsetnx(const StringView &key, const std::pair<StringView, StringView> &item);
+
+    long long hstrlen(const StringView &key, const StringView &field);
+
+    template <typename Output>
+    void hvals(const StringView &key, Output output);
+
+    // SET commands.
+
+    long long sadd(const StringView &key, const StringView &member);
+
+    template <typename Input>
+    long long sadd(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    long long sadd(const StringView &key, std::initializer_list<T> il) {
+        return sadd(key, il.begin(), il.end());
+    }
+
+    long long scard(const StringView &key);
+
+    template <typename Input, typename Output>
+    void sdiff(Input first, Input last, Output output);
+
+    template <typename T, typename Output>
+    void sdiff(std::initializer_list<T> il, Output output) {
+        sdiff(il.begin(), il.end(), output);
+    }
+
+    long long sdiffstore(const StringView &destination, const StringView &key);
+
+    template <typename Input>
+    long long sdiffstore(const StringView &destination,
+                            Input first,
+                            Input last);
+
+    template <typename T>
+    long long sdiffstore(const StringView &destination,
+                            std::initializer_list<T> il) {
+        return sdiffstore(destination, il.begin(), il.end());
+    }
+
+    template <typename Input, typename Output>
+    void sinter(Input first, Input last, Output output);
+
+    template <typename T, typename Output>
+    void sinter(std::initializer_list<T> il, Output output) {
+        sinter(il.begin(), il.end(), output);
+    }
+
+    long long sinterstore(const StringView &destination, const StringView &key);
+
+    template <typename Input>
+    long long sinterstore(const StringView &destination,
+                            Input first,
+                            Input last);
+
+    template <typename T>
+    long long sinterstore(const StringView &destination,
+                            std::initializer_list<T> il) {
+        return sinterstore(destination, il.begin(), il.end());
+    }
+
+    bool sismember(const StringView &key, const StringView &member);
+
+    template <typename Output>
+    void smembers(const StringView &key, Output output);
+
+    bool smove(const StringView &source,
+                const StringView &destination,
+                const StringView &member);
+
+    OptionalString spop(const StringView &key);
+
+    template <typename Output>
+    void spop(const StringView &key, long long count, Output output);
+
+    OptionalString srandmember(const StringView &key);
+
+    template <typename Output>
+    void srandmember(const StringView &key, long long count, Output output);
+
+    long long srem(const StringView &key, const StringView &member);
+
+    template <typename Input>
+    long long srem(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    long long srem(const StringView &key, std::initializer_list<T> il) {
+        return srem(key, il.begin(), il.end());
+    }
+
+    template <typename Output>
+    long long sscan(const StringView &key,
+                    long long cursor,
+                    const StringView &pattern,
+                    long long count,
+                    Output output);
+
+    template <typename Output>
+    long long sscan(const StringView &key,
+                    long long cursor,
+                    const StringView &pattern,
+                    Output output);
+
+    template <typename Output>
+    long long sscan(const StringView &key,
+                    long long cursor,
+                    long long count,
+                    Output output);
+
+    template <typename Output>
+    long long sscan(const StringView &key,
+                    long long cursor,
+                    Output output);
+
+    template <typename Input, typename Output>
+    void sunion(Input first, Input last, Output output);
+
+    template <typename T, typename Output>
+    void sunion(std::initializer_list<T> il, Output output) {
+        sunion(il.begin(), il.end(), output);
+    }
+
+    long long sunionstore(const StringView &destination, const StringView &key);
+
+    template <typename Input>
+    long long sunionstore(const StringView &destination, Input first, Input last);
+
+    template <typename T>
+    long long sunionstore(const StringView &destination, std::initializer_list<T> il) {
+        return sunionstore(destination, il.begin(), il.end());
+    }
+
+    // SORTED SET commands.
+
+    auto bzpopmax(const StringView &key, long long timeout)
+        -> Optional<std::tuple<std::string, std::string, double>>;
+
+    auto bzpopmax(const StringView &key,
+                    const std::chrono::seconds &timeout = std::chrono::seconds{0})
+        -> Optional<std::tuple<std::string, std::string, double>>;
+
+    template <typename Input>
+    auto bzpopmax(Input first, Input last, long long timeout)
+        -> Optional<std::tuple<std::string, std::string, double>>;
+
+    template <typename Input>
+    auto bzpopmax(Input first,
+                    Input last,
+                    const std::chrono::seconds &timeout = std::chrono::seconds{0})
+        -> Optional<std::tuple<std::string, std::string, double>>;
+
+    template <typename T>
+    auto bzpopmax(std::initializer_list<T> il, long long timeout)
+        -> Optional<std::tuple<std::string, std::string, double>> {
+        return bzpopmax(il.begin(), il.end(), timeout);
+    }
+
+    template <typename T>
+    auto bzpopmax(std::initializer_list<T> il,
+                    const std::chrono::seconds &timeout = std::chrono::seconds{0})
+        -> Optional<std::tuple<std::string, std::string, double>> {
+        return bzpopmax(il.begin(), il.end(), timeout);
+    }
+
+    auto bzpopmin(const StringView &key, long long timeout)
+        -> Optional<std::tuple<std::string, std::string, double>>;
+
+    auto bzpopmin(const StringView &key,
+                    const std::chrono::seconds &timeout = std::chrono::seconds{0})
+        -> Optional<std::tuple<std::string, std::string, double>>;
+
+    template <typename Input>
+    auto bzpopmin(Input first, Input last, long long timeout)
+        -> Optional<std::tuple<std::string, std::string, double>>;
+
+    template <typename Input>
+    auto bzpopmin(Input first,
+                    Input last,
+                    const std::chrono::seconds &timeout = std::chrono::seconds{0})
+        -> Optional<std::tuple<std::string, std::string, double>>;
+
+    template <typename T>
+    auto bzpopmin(std::initializer_list<T> il, long long timeout)
+        -> Optional<std::tuple<std::string, std::string, double>> {
+        return bzpopmin(il.begin(), il.end(), timeout);
+    }
+
+    template <typename T>
+    auto bzpopmin(std::initializer_list<T> il,
+                    const std::chrono::seconds &timeout = std::chrono::seconds{0})
+        -> Optional<std::tuple<std::string, std::string, double>> {
+        return bzpopmin(il.begin(), il.end(), timeout);
+    }
+
+    // We don't support the INCR option, since you can always use ZINCRBY instead.
+    long long zadd(const StringView &key,
+                    const StringView &member,
+                    double score,
+                    UpdateType type = UpdateType::ALWAYS,
+                    bool changed = false);
+
+    template <typename Input>
+    long long zadd(const StringView &key,
+                    Input first,
+                    Input last,
+                    UpdateType type = UpdateType::ALWAYS,
+                    bool changed = false);
+
+    template <typename T>
+    long long zadd(const StringView &key,
+                    std::initializer_list<T> il,
+                    UpdateType type = UpdateType::ALWAYS,
+                    bool changed = false) {
+        return zadd(key, il.begin(), il.end(), type, changed);
+    }
+
+    long long zcard(const StringView &key);
+
+    template <typename Interval>
+    long long zcount(const StringView &key, const Interval &interval);
+
+    double zincrby(const StringView &key, double increment, const StringView &member);
+
+    // There's no aggregation type parameter for single key overload, since these 3 types
+    // have the same effect.
+    long long zinterstore(const StringView &destination, const StringView &key, double weight);
+
+    // If *Input* is an iterator of a container of string,
+    // we use the default weight, i.e. 1, and send
+    // *ZINTERSTORE destination numkeys key [key ...] [AGGREGATE SUM|MIN|MAX]* command.
+    // If *Input* is an iterator of a container of pair<string, double>, i.e. key-weight pair,
+    // we send the command with the given weights:
+    // *ZINTERSTORE destination numkeys key [key ...]
+    // [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]*
+    //
+    // The following code use the default weight:
+    //
+    // vector<string> keys = {"k1", "k2", "k3"};
+    // redis.zinterstore(destination, keys.begin(), keys.end());
+    //
+    // On the other hand, the following code use the given weights:
+    //
+    // vector<pair<string, double>> keys_with_weights = {{"k1", 1}, {"k2", 2}, {"k3", 3}};
+    // redis.zinterstore(destination, keys_with_weights.begin(), keys_with_weights.end());
+    //
+    // NOTE: `keys_with_weights` can also be of type `unordered_map<string, double>`.
+    // However, it will be slower than vector<pair<string, double>>, since we use
+    // `distance(first, last)` to calculate the *numkeys* parameter.
+    //
+    // This also applies to *ZUNIONSTORE* command.
+    template <typename Input>
+    long long zinterstore(const StringView &destination,
+                            Input first,
+                            Input last,
+                            Aggregation type = Aggregation::SUM);
+
+    template <typename T>
+    long long zinterstore(const StringView &destination,
+                            std::initializer_list<T> il,
+                            Aggregation type = Aggregation::SUM) {
+        return zinterstore(destination, il.begin(), il.end(), type);
+    }
+
+    template <typename Interval>
+    long long zlexcount(const StringView &key, const Interval &interval);
+
+    Optional<std::pair<std::string, double>> zpopmax(const StringView &key);
+
+    template <typename Output>
+    void zpopmax(const StringView &key, long long count, Output output);
+
+    Optional<std::pair<std::string, double>> zpopmin(const StringView &key);
+
+    template <typename Output>
+    void zpopmin(const StringView &key, long long count, Output output);
+
+    // If *output* is an iterator of a container of string,
+    // we send *ZRANGE key start stop* command.
+    // If it's an iterator of a container of pair<string, double>,
+    // we send *ZRANGE key start stop WITHSCORES* command.
+    //
+    // The following code sends *ZRANGE* without the *WITHSCORES* option:
+    //
+    // vector<string> result;
+    // redis.zrange("key", 0, -1, back_inserter(result));
+    //
+    // On the other hand, the following code sends command with *WITHSCORES* option:
+    //
+    // unordered_map<string, double> with_score;
+    // redis.zrange("key", 0, -1, inserter(with_score, with_score.end()));
+    //
+    // This also applies to other commands with the *WITHSCORES* option,
+    // e.g. *ZRANGEBYSCORE*, *ZREVRANGE*, *ZREVRANGEBYSCORE*.
+    template <typename Output>
+    void zrange(const StringView &key, long long start, long long stop, Output output);
+
+    template <typename Interval, typename Output>
+    void zrangebylex(const StringView &key, const Interval &interval, Output output);
+
+    template <typename Interval, typename Output>
+    void zrangebylex(const StringView &key,
+                        const Interval &interval,
+                        const LimitOptions &opts,
+                        Output output);
+
+    // See *zrange* comment on how to send command with *WITHSCORES* option.
+    template <typename Interval, typename Output>
+    void zrangebyscore(const StringView &key, const Interval &interval, Output output);
+
+    // See *zrange* comment on how to send command with *WITHSCORES* option.
+    template <typename Interval, typename Output>
+    void zrangebyscore(const StringView &key,
+                        const Interval &interval,
+                        const LimitOptions &opts,
+                        Output output);
+
+    OptionalLongLong zrank(const StringView &key, const StringView &member);
+
+    long long zrem(const StringView &key, const StringView &member);
+
+    template <typename Input>
+    long long zrem(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    long long zrem(const StringView &key, std::initializer_list<T> il) {
+        return zrem(key, il.begin(), il.end());
+    }
+
+    template <typename Interval>
+    long long zremrangebylex(const StringView &key, const Interval &interval);
+
+    long long zremrangebyrank(const StringView &key, long long start, long long stop);
+
+    template <typename Interval>
+    long long zremrangebyscore(const StringView &key, const Interval &interval);
+
+    // See *zrange* comment on how to send command with *WITHSCORES* option.
+    template <typename Output>
+    void zrevrange(const StringView &key, long long start, long long stop, Output output);
+
+    template <typename Interval, typename Output>
+    void zrevrangebylex(const StringView &key, const Interval &interval, Output output);
+
+    template <typename Interval, typename Output>
+    void zrevrangebylex(const StringView &key,
+                        const Interval &interval,
+                        const LimitOptions &opts,
+                        Output output);
+
+    // See *zrange* comment on how to send command with *WITHSCORES* option.
+    template <typename Interval, typename Output>
+    void zrevrangebyscore(const StringView &key, const Interval &interval, Output output);
+
+    // See *zrange* comment on how to send command with *WITHSCORES* option.
+    template <typename Interval, typename Output>
+    void zrevrangebyscore(const StringView &key,
+                            const Interval &interval,
+                            const LimitOptions &opts,
+                            Output output);
+
+    OptionalLongLong zrevrank(const StringView &key, const StringView &member);
+
+    template <typename Output>
+    long long zscan(const StringView &key,
+                    long long cursor,
+                    const StringView &pattern,
+                    long long count,
+                    Output output);
+
+    template <typename Output>
+    long long zscan(const StringView &key,
+                    long long cursor,
+                    const StringView &pattern,
+                    Output output);
+
+    template <typename Output>
+    long long zscan(const StringView &key,
+                    long long cursor,
+                    long long count,
+                    Output output);
+
+    template <typename Output>
+    long long zscan(const StringView &key,
+                    long long cursor,
+                    Output output);
+
+    OptionalDouble zscore(const StringView &key, const StringView &member);
+
+    // There's no aggregation type parameter for single key overload, since these 3 types
+    // have the same effect.
+    long long zunionstore(const StringView &destination, const StringView &key, double weight);
+
+    // See *zinterstore* comment for how to use this method.
+    template <typename Input>
+    long long zunionstore(const StringView &destination,
+                            Input first,
+                            Input last,
+                            Aggregation type = Aggregation::SUM);
+
+    template <typename T>
+    long long zunionstore(const StringView &destination,
+                            std::initializer_list<T> il,
+                            Aggregation type = Aggregation::SUM) {
+        return zunionstore(destination, il.begin(), il.end(), type);
+    }
+
+    // HYPERLOGLOG commands.
+
+    bool pfadd(const StringView &key, const StringView &element);
+
+    template <typename Input>
+    bool pfadd(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    bool pfadd(const StringView &key, std::initializer_list<T> il) {
+        return pfadd(key, il.begin(), il.end());
+    }
+
+    long long pfcount(const StringView &key);
+
+    template <typename Input>
+    long long pfcount(Input first, Input last);
+
+    template <typename T>
+    long long pfcount(std::initializer_list<T> il) {
+        return pfcount(il.begin(), il.end());
+    }
+
+    void pfmerge(const StringView &destination, const StringView &key);
+
+    template <typename Input>
+    void pfmerge(const StringView &destination, Input first, Input last);
+
+    template <typename T>
+    void pfmerge(const StringView &destination, std::initializer_list<T> il) {
+        pfmerge(destination, il.begin(), il.end());
+    }
+
+    // GEO commands.
+
+    long long geoadd(const StringView &key,
+                        const std::tuple<StringView, double, double> &member);
+
+    template <typename Input>
+    long long geoadd(const StringView &key,
+                        Input first,
+                        Input last);
+
+    template <typename T>
+    long long geoadd(const StringView &key,
+                        std::initializer_list<T> il) {
+        return geoadd(key, il.begin(), il.end());
+    }
+
+    OptionalDouble geodist(const StringView &key,
+                            const StringView &member1,
+                            const StringView &member2,
+                            GeoUnit unit = GeoUnit::M);
+
+    template <typename Input, typename Output>
+    void geohash(const StringView &key, Input first, Input last, Output output);
+
+    template <typename T, typename Output>
+    void geohash(const StringView &key, std::initializer_list<T> il, Output output) {
+        geohash(key, il.begin(), il.end(), output);
+    }
+
+    template <typename Input, typename Output>
+    void geopos(const StringView &key, Input first, Input last, Output output);
+
+    template <typename T, typename Output>
+    void geopos(const StringView &key, std::initializer_list<T> il, Output output) {
+        geopos(key, il.begin(), il.end(), output);
+    }
+
+    // TODO:
+    // 1. since we have different overloads for georadius and georadius-store,
+    //    we might use the GEORADIUS_RO command in the future.
+    // 2. there're too many parameters for this method, we might refactor it.
+    OptionalLongLong georadius(const StringView &key,
+                                const std::pair<double, double> &loc,
+                                double radius,
+                                GeoUnit unit,
+                                const StringView &destination,
+                                bool store_dist,
+                                long long count);
+
+    // If *output* is an iterator of a container of string, we send *GEORADIUS* command
+    // without any options and only get the members in the specified geo range.
+    // If *output* is an iterator of a container of a tuple, the type of the tuple decides
+    // options we send with the *GEORADIUS* command. If the tuple has an element of type
+    // double, we send the *WITHDIST* option. If it has an element of type string, we send
+    // the *WITHHASH* option. If it has an element of type pair<double, double>, we send
+    // the *WITHCOORD* option. For example:
+    //
+    // The following code only gets the members in range, i.e. without any option.
+    //
+    // vector<string> members;
+    // redis.georadius("key", make_pair(10.1, 10.2), 10, GeoUnit::KM, 10, true,
+    //                  back_inserter(members))
+    //
+    // The following code sends the command with *WITHDIST* option.
+    //
+    // vector<tuple<string, double>> with_dist;
+    // redis.georadius("key", make_pair(10.1, 10.2), 10, GeoUnit::KM, 10, true,
+    //                  back_inserter(with_dist))
+    //
+    // The following code sends the command with *WITHDIST* and *WITHHASH* options.
+    //
+    // vector<tuple<string, double, string>> with_dist_hash;
+    // redis.georadius("key", make_pair(10.1, 10.2), 10, GeoUnit::KM, 10, true,
+    //                  back_inserter(with_dist_hash))
+    //
+    // The following code sends the command with *WITHDIST*, *WITHCOORD* and *WITHHASH* options.
+    //
+    // vector<tuple<string, double, pair<double, double>, string>> with_dist_coord_hash;
+    // redis.georadius("key", make_pair(10.1, 10.2), 10, GeoUnit::KM, 10, true,
+    //                  back_inserter(with_dist_coord_hash))
+    //
+    // This also applies to *GEORADIUSBYMEMBER*.
+    template <typename Output>
+    void georadius(const StringView &key,
+                    const std::pair<double, double> &loc,
+                    double radius,
+                    GeoUnit unit,
+                    long long count,
+                    bool asc,
+                    Output output);
+
+    OptionalLongLong georadiusbymember(const StringView &key,
+                                        const StringView &member,
+                                        double radius,
+                                        GeoUnit unit,
+                                        const StringView &destination,
+                                        bool store_dist,
+                                        long long count);
+
+    // See comments on *GEORADIUS*.
+    template <typename Output>
+    void georadiusbymember(const StringView &key,
+                            const StringView &member,
+                            double radius,
+                            GeoUnit unit,
+                            long long count,
+                            bool asc,
+                            Output output);
+
+    // SCRIPTING commands.
+
+    template <typename Result>
+    Result eval(const StringView &script,
+                std::initializer_list<StringView> keys,
+                std::initializer_list<StringView> args);
+
+    template <typename Output>
+    void eval(const StringView &script,
+                std::initializer_list<StringView> keys,
+                std::initializer_list<StringView> args,
+                Output output);
+
+    template <typename Result>
+    Result evalsha(const StringView &script,
+                    std::initializer_list<StringView> keys,
+                    std::initializer_list<StringView> args);
+
+    template <typename Output>
+    void evalsha(const StringView &script,
+                    std::initializer_list<StringView> keys,
+                    std::initializer_list<StringView> args,
+                    Output output);
+
+    template <typename Input, typename Output>
+    void script_exists(Input first, Input last, Output output);
+
+    template <typename T, typename Output>
+    void script_exists(std::initializer_list<T> il, Output output) {
+        script_exists(il.begin(), il.end(), output);
+    }
+
+    void script_flush();
+
+    void script_kill();
+
+    std::string script_load(const StringView &script);
+
+    // PUBSUB commands.
+
+    long long publish(const StringView &channel, const StringView &message);
+
+    // Transaction commands.
+    void watch(const StringView &key);
+
+    template <typename Input>
+    void watch(Input first, Input last);
+
+    template <typename T>
+    void watch(std::initializer_list<T> il) {
+        watch(il.begin(), il.end());
+    }
+
+    // Stream commands.
+
+    long long xack(const StringView &key, const StringView &group, const StringView &id);
+
+    template <typename Input>
+    long long xack(const StringView &key, const StringView &group, Input first, Input last);
+
+    template <typename T>
+    long long xack(const StringView &key, const StringView &group, std::initializer_list<T> il) {
+        return xack(key, group, il.begin(), il.end());
+    }
+
+    template <typename Input>
+    std::string xadd(const StringView &key, const StringView &id, Input first, Input last);
+
+    template <typename T>
+    std::string xadd(const StringView &key, const StringView &id, std::initializer_list<T> il) {
+        return xadd(key, id, il.begin(), il.end());
+    }
+
+    template <typename Input>
+    std::string xadd(const StringView &key,
+                        const StringView &id,
+                        Input first,
+                        Input last,
+                        long long count,
+                        bool approx = true);
+
+    template <typename T>
+    std::string xadd(const StringView &key,
+                        const StringView &id,
+                        std::initializer_list<T> il,
+                        long long count,
+                        bool approx = true) {
+        return xadd(key, id, il.begin(), il.end(), count, approx);
+    }
+
+    template <typename Output>
+    void xclaim(const StringView &key,
+                const StringView &group,
+                const StringView &consumer,
+                const std::chrono::milliseconds &min_idle_time,
+                const StringView &id,
+                Output output);
+
+    template <typename Input, typename Output>
+    void xclaim(const StringView &key,
+                const StringView &group,
+                const StringView &consumer,
+                const std::chrono::milliseconds &min_idle_time,
+                Input first,
+                Input last,
+                Output output);
+
+    template <typename T, typename Output>
+    void xclaim(const StringView &key,
+                const StringView &group,
+                const StringView &consumer,
+                const std::chrono::milliseconds &min_idle_time,
+                std::initializer_list<T> il,
+                Output output) {
+        xclaim(key, group, consumer, min_idle_time, il.begin(), il.end(), output);
+    }
+
+    long long xdel(const StringView &key, const StringView &id);
+
+    template <typename Input>
+    long long xdel(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    long long xdel(const StringView &key, std::initializer_list<T> il) {
+        return xdel(key, il.begin(), il.end());
+    }
+
+    void xgroup_create(const StringView &key,
+                        const StringView &group,
+                        const StringView &id,
+                        bool mkstream = false);
+
+    void xgroup_setid(const StringView &key, const StringView &group, const StringView &id);
+
+    long long xgroup_destroy(const StringView &key, const StringView &group);
+
+    long long xgroup_delconsumer(const StringView &key,
+                                    const StringView &group,
+                                    const StringView &consumer);
+
+    long long xlen(const StringView &key);
+
+    template <typename Output>
+    auto xpending(const StringView &key, const StringView &group, Output output)
+        -> std::tuple<long long, OptionalString, OptionalString>;
+
+    template <typename Output>
+    void xpending(const StringView &key,
+                    const StringView &group,
+                    const StringView &start,
+                    const StringView &end,
+                    long long count,
+                    Output output);
+
+    template <typename Output>
+    void xpending(const StringView &key,
+                    const StringView &group,
+                    const StringView &start,
+                    const StringView &end,
+                    long long count,
+                    const StringView &consumer,
+                    Output output);
+
+    template <typename Output>
+    void xrange(const StringView &key,
+                const StringView &start,
+                const StringView &end,
+                Output output);
+
+    template <typename Output>
+    void xrange(const StringView &key,
+                const StringView &start,
+                const StringView &end,
+                long long count,
+                Output output);
+
+    template <typename Output>
+    void xread(const StringView &key,
+                const StringView &id,
+                long long count,
+                Output output);
+
+    template <typename Output>
+    void xread(const StringView &key,
+                const StringView &id,
+                Output output) {
+        xread(key, id, 0, output);
+    }
+
+    template <typename Input, typename Output>
+    auto xread(Input first, Input last, long long count, Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type;
+
+    template <typename Input, typename Output>
+    auto xread(Input first, Input last, Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+        xread(first ,last, 0, output);
+    }
+
+    template <typename Output>
+    void xread(const StringView &key,
+                const StringView &id,
+                const std::chrono::milliseconds &timeout,
+                long long count,
+                Output output);
+
+    template <typename Output>
+    void xread(const StringView &key,
+                const StringView &id,
+                const std::chrono::milliseconds &timeout,
+                Output output) {
+        xread(key, id, timeout, 0, output);
+    }
+
+    template <typename Input, typename Output>
+    auto xread(Input first,
+                Input last,
+                const std::chrono::milliseconds &timeout,
+                long long count,
+                Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type;
+
+    template <typename Input, typename Output>
+    auto xread(Input first,
+                Input last,
+                const std::chrono::milliseconds &timeout,
+                Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+        xread(first, last, timeout, 0, output);
+    }
+
+    template <typename Output>
+    void xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    const StringView &key,
+                    const StringView &id,
+                    long long count,
+                    bool noack,
+                    Output output);
+
+    template <typename Output>
+    void xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    const StringView &key,
+                    const StringView &id,
+                    long long count,
+                    Output output) {
+        xreadgroup(group, consumer, key, id, count, false, output);
+    }
+
+    template <typename Output>
+    void xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    const StringView &key,
+                    const StringView &id,
+                    Output output) {
+        xreadgroup(group, consumer, key, id, 0, false, output);
+    }
+
+    template <typename Input, typename Output>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last,
+                    long long count,
+                    bool noack,
+                    Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type;
+
+    template <typename Input, typename Output>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last,
+                    long long count,
+                    Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+        xreadgroup(group, consumer, first ,last, count, false, output);
+    }
+
+    template <typename Input, typename Output>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last,
+                    Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+        xreadgroup(group, consumer, first ,last, 0, false, output);
+    }
+
+    template <typename Output>
+    void xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    const StringView &key,
+                    const StringView &id,
+                    const std::chrono::milliseconds &timeout,
+                    long long count,
+                    bool noack,
+                    Output output);
+
+    template <typename Output>
+    void xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    const StringView &key,
+                    const StringView &id,
+                    const std::chrono::milliseconds &timeout,
+                    long long count,
+                    Output output) {
+        xreadgroup(group, consumer, key, id, timeout, count, false, output);
+    }
+
+    template <typename Output>
+    void xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    const StringView &key,
+                    const StringView &id,
+                    const std::chrono::milliseconds &timeout,
+                    Output output) {
+        xreadgroup(group, consumer, key, id, timeout, 0, false, output);
+    }
+
+    template <typename Input, typename Output>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last,
+                    const std::chrono::milliseconds &timeout,
+                    long long count,
+                    bool noack,
+                    Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type;
+
+    template <typename Input, typename Output>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last,
+                    const std::chrono::milliseconds &timeout,
+                    long long count,
+                    Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+        xreadgroup(group, consumer, first, last, timeout, count, false, output);
+    }
+
+    template <typename Input, typename Output>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last,
+                    const std::chrono::milliseconds &timeout,
+                    Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+        xreadgroup(group, consumer, first, last, timeout, 0, false, output);
+    }
+
+    template <typename Output>
+    void xrevrange(const StringView &key,
+                    const StringView &end,
+                    const StringView &start,
+                    Output output);
+
+    template <typename Output>
+    void xrevrange(const StringView &key,
+                    const StringView &end,
+                    const StringView &start,
+                    long long count,
+                    Output output);
+
+    long long xtrim(const StringView &key, long long count, bool approx = true);
+
+private:
+    class ConnectionPoolGuard {
+    public:
+        ConnectionPoolGuard(ConnectionPool &pool,
+                            Connection &connection) : _pool(pool), _connection(connection) {}
+
+        ~ConnectionPoolGuard() {
+            _pool.release(std::move(_connection));
+        }
+
+    private:
+        ConnectionPool &_pool;
+        Connection &_connection;
+    };
+
+    template <typename Impl>
+    friend class QueuedRedis;
+
+    friend class RedisCluster;
+
+    // For internal use.
+    explicit Redis(const ConnectionSPtr &connection);
+
+    template <std::size_t ...Is, typename ...Args>
+    ReplyUPtr _command(const StringView &cmd_name, const IndexSequence<Is...> &, Args &&...args) {
+        return command(cmd_name, NthValue<Is>(std::forward<Args>(args)...)...);
+    }
+
+    template <typename Cmd, typename ...Args>
+    ReplyUPtr _command(Connection &connection, Cmd cmd, Args &&...args);
+
+    template <typename Cmd, typename ...Args>
+    ReplyUPtr _score_command(std::true_type, Cmd cmd, Args &&... args);
+
+    template <typename Cmd, typename ...Args>
+    ReplyUPtr _score_command(std::false_type, Cmd cmd, Args &&... args);
+
+    template <typename Output, typename Cmd, typename ...Args>
+    ReplyUPtr _score_command(Cmd cmd, Args &&... args);
+
+    // Pool Mode.
+    // Public constructors create a *Redis* instance with a pool.
+    // In this case, *_connection* is a null pointer, and is never used.
+    ConnectionPool _pool;
+
+    // Single Connection Mode.
+    // Private constructor creats a *Redis* instance with a single connection.
+    // This is used when we create Transaction, Pipeline and Subscriber.
+    // In this case, *_pool* is empty, and is never used.
+    ConnectionSPtr _connection;
+};
+
+}
+
+}
+
+#include "redis.hpp"
+
+#endif // end SEWENEW_REDISPLUSPLUS_REDIS_H

+ 1365 - 0
ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/redis.hpp

@@ -0,0 +1,1365 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_REDIS_HPP
+#define SEWENEW_REDISPLUSPLUS_REDIS_HPP
+
+#include "command.h"
+#include "reply.h"
+#include "utils.h"
+#include "errors.h"
+
+namespace sw {
+
+namespace redis {
+
+template <typename Cmd, typename ...Args>
+auto Redis::command(Cmd cmd, Args &&...args)
+    -> typename std::enable_if<!std::is_convertible<Cmd, StringView>::value, ReplyUPtr>::type {
+    if (_connection) {
+        // Single Connection Mode.
+        // TODO: In this case, should we reconnect?
+        if (_connection->broken()) {
+            throw Error("Connection is broken");
+        }
+
+        return _command(*_connection, cmd, std::forward<Args>(args)...);
+    } else {
+        // Pool Mode, i.e. get connection from pool.
+        auto connection = _pool.fetch();
+
+        assert(!connection.broken());
+
+        ConnectionPoolGuard guard(_pool, connection);
+
+        return _command(connection, cmd, std::forward<Args>(args)...);
+    }
+}
+
+template <typename ...Args>
+auto Redis::command(const StringView &cmd_name, Args &&...args)
+    -> typename std::enable_if<!IsIter<typename LastType<Args...>::type>::value, ReplyUPtr>::type {
+    auto cmd = [](Connection &connection, const StringView &cmd_name, Args &&...args) {
+                    CmdArgs cmd_args;
+                    cmd_args.append(cmd_name, std::forward<Args>(args)...);
+                    connection.send(cmd_args);
+    };
+
+    return command(cmd, cmd_name, std::forward<Args>(args)...);
+}
+
+template <typename Input>
+auto Redis::command(Input first, Input last)
+    -> typename std::enable_if<IsIter<Input>::value, ReplyUPtr>::type {
+    if (first == last) {
+        throw Error("command: empty range");
+    }
+
+    auto cmd = [](Connection &connection, Input first, Input last) {
+                    CmdArgs cmd_args;
+                    while (first != last) {
+                        cmd_args.append(*first);
+                        ++first;
+                    }
+                    connection.send(cmd_args);
+    };
+
+    return command(cmd, first, last);
+}
+
+template <typename Result, typename ...Args>
+Result Redis::command(const StringView &cmd_name, Args &&...args) {
+    auto r = command(cmd_name, std::forward<Args>(args)...);
+
+    assert(r);
+
+    return reply::parse<Result>(*r);
+}
+
+template <typename ...Args>
+auto Redis::command(const StringView &cmd_name, Args &&...args)
+    -> typename std::enable_if<IsIter<typename LastType<Args...>::type>::value, void>::type {
+    auto r = _command(cmd_name,
+                        MakeIndexSequence<sizeof...(Args) - 1>(),
+                        std::forward<Args>(args)...);
+
+    assert(r);
+
+    reply::to_array(*r, LastValue(std::forward<Args>(args)...));
+}
+
+template <typename Result, typename Input>
+auto Redis::command(Input first, Input last)
+    -> typename std::enable_if<IsIter<Input>::value, Result>::type {
+    auto r = command(first, last);
+
+    assert(r);
+
+    return reply::parse<Result>(*r);
+}
+
+template <typename Input, typename Output>
+auto Redis::command(Input first, Input last, Output output)
+    -> typename std::enable_if<IsIter<Input>::value, void>::type {
+    auto r = command(first, last);
+
+    assert(r);
+
+    reply::to_array(*r, output);
+}
+
+// KEY commands.
+
+template <typename Input>
+long long Redis::del(Input first, Input last) {
+    if (first == last) {
+        throw Error("DEL: no key specified");
+    }
+
+    auto reply = command(cmd::del_range<Input>, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input>
+long long Redis::exists(Input first, Input last) {
+    if (first == last) {
+        throw Error("EXISTS: no key specified");
+    }
+
+    auto reply = command(cmd::exists_range<Input>, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+inline bool Redis::expire(const StringView &key, const std::chrono::seconds &timeout) {
+    return expire(key, timeout.count());
+}
+
+inline bool Redis::expireat(const StringView &key,
+                                    const std::chrono::time_point<std::chrono::system_clock,
+                                                                    std::chrono::seconds> &tp) {
+    return expireat(key, tp.time_since_epoch().count());
+}
+
+template <typename Output>
+void Redis::keys(const StringView &pattern, Output output) {
+    auto reply = command(cmd::keys, pattern);
+
+    reply::to_array(*reply, output);
+}
+
+inline bool Redis::pexpire(const StringView &key, const std::chrono::milliseconds &timeout) {
+    return pexpire(key, timeout.count());
+}
+
+inline bool Redis::pexpireat(const StringView &key,
+                                const std::chrono::time_point<std::chrono::system_clock,
+                                                                std::chrono::milliseconds> &tp) {
+    return pexpireat(key, tp.time_since_epoch().count());
+}
+
+inline void Redis::restore(const StringView &key,
+                            const StringView &val,
+                            const std::chrono::milliseconds &ttl,
+                            bool replace) {
+    return restore(key, val, ttl.count(), replace);
+}
+
+template <typename Output>
+long long Redis::scan(long long cursor,
+                    const StringView &pattern,
+                    long long count,
+                    Output output) {
+    auto reply = command(cmd::scan, cursor, pattern, count);
+
+    return reply::parse_scan_reply(*reply, output);
+}
+
+template <typename Output>
+inline long long Redis::scan(long long cursor,
+                                const StringView &pattern,
+                                Output output) {
+    return scan(cursor, pattern, 10, output);
+}
+
+template <typename Output>
+inline long long Redis::scan(long long cursor,
+                                long long count,
+                                Output output) {
+    return scan(cursor, "*", count, output);
+}
+
+template <typename Output>
+inline long long Redis::scan(long long cursor,
+                                Output output) {
+    return scan(cursor, "*", 10, output);
+}
+
+template <typename Input>
+long long Redis::touch(Input first, Input last) {
+    if (first == last) {
+        throw Error("TOUCH: no key specified");
+    }
+
+    auto reply = command(cmd::touch_range<Input>, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input>
+long long Redis::unlink(Input first, Input last) {
+    if (first == last) {
+        throw Error("UNLINK: no key specified");
+    }
+
+    auto reply = command(cmd::unlink_range<Input>, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+inline long long Redis::wait(long long numslaves, const std::chrono::milliseconds &timeout) {
+    return wait(numslaves, timeout.count());
+}
+
+// STRING commands.
+
+template <typename Input>
+long long Redis::bitop(BitOp op, const StringView &destination, Input first, Input last) {
+    if (first == last) {
+        throw Error("BITOP: no key specified");
+    }
+
+    auto reply = command(cmd::bitop_range<Input>, op, destination, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input, typename Output>
+void Redis::mget(Input first, Input last, Output output) {
+    if (first == last) {
+        throw Error("MGET: no key specified");
+    }
+
+    auto reply = command(cmd::mget<Input>, first, last);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+void Redis::mset(Input first, Input last) {
+    if (first == last) {
+        throw Error("MSET: no key specified");
+    }
+
+    auto reply = command(cmd::mset<Input>, first, last);
+
+    reply::parse<void>(*reply);
+}
+
+template <typename Input>
+bool Redis::msetnx(Input first, Input last) {
+    if (first == last) {
+        throw Error("MSETNX: no key specified");
+    }
+
+    auto reply = command(cmd::msetnx<Input>, first, last);
+
+    return reply::parse<bool>(*reply);
+}
+
+inline void Redis::psetex(const StringView &key,
+                            const std::chrono::milliseconds &ttl,
+                            const StringView &val) {
+    return psetex(key, ttl.count(), val);
+}
+
+inline void Redis::setex(const StringView &key,
+                            const std::chrono::seconds &ttl,
+                            const StringView &val) {
+    setex(key, ttl.count(), val);
+}
+
+// LIST commands.
+
+template <typename Input>
+OptionalStringPair Redis::blpop(Input first, Input last, long long timeout) {
+    if (first == last) {
+        throw Error("BLPOP: no key specified");
+    }
+
+    auto reply = command(cmd::blpop_range<Input>, first, last, timeout);
+
+    return reply::parse<OptionalStringPair>(*reply);
+}
+
+template <typename Input>
+OptionalStringPair Redis::blpop(Input first,
+                                Input last,
+                                const std::chrono::seconds &timeout) {
+    return blpop(first, last, timeout.count());
+}
+
+template <typename Input>
+OptionalStringPair Redis::brpop(Input first, Input last, long long timeout) {
+    if (first == last) {
+        throw Error("BRPOP: no key specified");
+    }
+
+    auto reply = command(cmd::brpop_range<Input>, first, last, timeout);
+
+    return reply::parse<OptionalStringPair>(*reply);
+}
+
+template <typename Input>
+OptionalStringPair Redis::brpop(Input first,
+                                Input last,
+                                const std::chrono::seconds &timeout) {
+    return brpop(first, last, timeout.count());
+}
+
+inline OptionalString Redis::brpoplpush(const StringView &source,
+                                        const StringView &destination,
+                                        const std::chrono::seconds &timeout) {
+    return brpoplpush(source, destination, timeout.count());
+}
+
+template <typename Input>
+inline long long Redis::lpush(const StringView &key, Input first, Input last) {
+    if (first == last) {
+        throw Error("LPUSH: no key specified");
+    }
+
+    auto reply = command(cmd::lpush_range<Input>, key, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Output>
+inline void Redis::lrange(const StringView &key, long long start, long long stop, Output output) {
+    auto reply = command(cmd::lrange, key, start, stop);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+inline long long Redis::rpush(const StringView &key, Input first, Input last) {
+    if (first == last) {
+        throw Error("RPUSH: no key specified");
+    }
+
+    auto reply = command(cmd::rpush_range<Input>, key, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+// HASH commands.
+
+template <typename Input>
+inline long long Redis::hdel(const StringView &key, Input first, Input last) {
+    if (first == last) {
+        throw Error("HDEL: no key specified");
+    }
+
+    auto reply = command(cmd::hdel_range<Input>, key, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Output>
+inline void Redis::hgetall(const StringView &key, Output output) {
+    auto reply = command(cmd::hgetall, key);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+inline void Redis::hkeys(const StringView &key, Output output) {
+    auto reply = command(cmd::hkeys, key);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input, typename Output>
+inline void Redis::hmget(const StringView &key, Input first, Input last, Output output) {
+    if (first == last) {
+        throw Error("HMGET: no key specified");
+    }
+
+    auto reply = command(cmd::hmget<Input>, key, first, last);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+inline void Redis::hmset(const StringView &key, Input first, Input last) {
+    if (first == last) {
+        throw Error("HMSET: no key specified");
+    }
+
+    auto reply = command(cmd::hmset<Input>, key, first, last);
+
+    reply::parse<void>(*reply);
+}
+
+template <typename Output>
+long long Redis::hscan(const StringView &key,
+                        long long cursor,
+                        const StringView &pattern,
+                        long long count,
+                        Output output) {
+    auto reply = command(cmd::hscan, key, cursor, pattern, count);
+
+    return reply::parse_scan_reply(*reply, output);
+}
+
+template <typename Output>
+inline long long Redis::hscan(const StringView &key,
+                                long long cursor,
+                                const StringView &pattern,
+                                Output output) {
+    return hscan(key, cursor, pattern, 10, output);
+}
+
+template <typename Output>
+inline long long Redis::hscan(const StringView &key,
+                                long long cursor,
+                                long long count,
+                                Output output) {
+    return hscan(key, cursor, "*", count, output);
+}
+
+template <typename Output>
+inline long long Redis::hscan(const StringView &key,
+                                long long cursor,
+                                Output output) {
+    return hscan(key, cursor, "*", 10, output);
+}
+
+template <typename Output>
+inline void Redis::hvals(const StringView &key, Output output) {
+    auto reply = command(cmd::hvals, key);
+
+    reply::to_array(*reply, output);
+}
+
+// SET commands.
+
+template <typename Input>
+long long Redis::sadd(const StringView &key, Input first, Input last) {
+    if (first == last) {
+        throw Error("SADD: no key specified");
+    }
+
+    auto reply = command(cmd::sadd_range<Input>, key, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input, typename Output>
+void Redis::sdiff(Input first, Input last, Output output) {
+    if (first == last) {
+        throw Error("SDIFF: no key specified");
+    }
+
+    auto reply = command(cmd::sdiff<Input>, first, last);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+long long Redis::sdiffstore(const StringView &destination,
+                            Input first,
+                            Input last) {
+    if (first == last) {
+        throw Error("SDIFFSTORE: no key specified");
+    }
+
+    auto reply = command(cmd::sdiffstore_range<Input>, destination, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input, typename Output>
+void Redis::sinter(Input first, Input last, Output output) {
+    if (first == last) {
+        throw Error("SINTER: no key specified");
+    }
+
+    auto reply = command(cmd::sinter<Input>, first, last);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+long long Redis::sinterstore(const StringView &destination,
+                            Input first,
+                            Input last) {
+    if (first == last) {
+        throw Error("SINTERSTORE: no key specified");
+    }
+
+    auto reply = command(cmd::sinterstore_range<Input>, destination, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Output>
+void Redis::smembers(const StringView &key, Output output) {
+    auto reply = command(cmd::smembers, key);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void Redis::spop(const StringView &key, long long count, Output output) {
+    auto reply = command(cmd::spop_range, key, count);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void Redis::srandmember(const StringView &key, long long count, Output output) {
+    auto reply = command(cmd::srandmember_range, key, count);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+long long Redis::srem(const StringView &key, Input first, Input last) {
+    if (first == last) {
+        throw Error("SREM: no key specified");
+    }
+
+    auto reply = command(cmd::srem_range<Input>, key, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Output>
+long long Redis::sscan(const StringView &key,
+                        long long cursor,
+                        const StringView &pattern,
+                        long long count,
+                        Output output) {
+    auto reply = command(cmd::sscan, key, cursor, pattern, count);
+
+    return reply::parse_scan_reply(*reply, output);
+}
+
+template <typename Output>
+inline long long Redis::sscan(const StringView &key,
+                                long long cursor,
+                                const StringView &pattern,
+                                Output output) {
+    return sscan(key, cursor, pattern, 10, output);
+}
+
+template <typename Output>
+inline long long Redis::sscan(const StringView &key,
+                                long long cursor,
+                                long long count,
+                                Output output) {
+    return sscan(key, cursor, "*", count, output);
+}
+
+template <typename Output>
+inline long long Redis::sscan(const StringView &key,
+                                long long cursor,
+                                Output output) {
+    return sscan(key, cursor, "*", 10, output);
+}
+
+template <typename Input, typename Output>
+void Redis::sunion(Input first, Input last, Output output) {
+    if (first == last) {
+        throw Error("SUNION: no key specified");
+    }
+
+    auto reply = command(cmd::sunion<Input>, first, last);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+long long Redis::sunionstore(const StringView &destination, Input first, Input last) {
+    if (first == last) {
+        throw Error("SUNIONSTORE: no key specified");
+    }
+
+    auto reply = command(cmd::sunionstore_range<Input>, destination, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+// SORTED SET commands.
+
+inline auto Redis::bzpopmax(const StringView &key, const std::chrono::seconds &timeout)
+    -> Optional<std::tuple<std::string, std::string, double>> {
+    return bzpopmax(key, timeout.count());
+}
+
+template <typename Input>
+auto Redis::bzpopmax(Input first, Input last, long long timeout)
+    -> Optional<std::tuple<std::string, std::string, double>> {
+    auto reply = command(cmd::bzpopmax_range<Input>, first, last, timeout);
+
+    return reply::parse<Optional<std::tuple<std::string, std::string, double>>>(*reply);
+}
+
+template <typename Input>
+inline auto Redis::bzpopmax(Input first,
+                            Input last,
+                            const std::chrono::seconds &timeout)
+    -> Optional<std::tuple<std::string, std::string, double>> {
+    return bzpopmax(first, last, timeout.count());
+}
+
+inline auto Redis::bzpopmin(const StringView &key, const std::chrono::seconds &timeout)
+    -> Optional<std::tuple<std::string, std::string, double>> {
+    return bzpopmin(key, timeout.count());
+}
+
+template <typename Input>
+auto Redis::bzpopmin(Input first, Input last, long long timeout)
+    -> Optional<std::tuple<std::string, std::string, double>> {
+    auto reply = command(cmd::bzpopmin_range<Input>, first, last, timeout);
+
+    return reply::parse<Optional<std::tuple<std::string, std::string, double>>>(*reply);
+}
+
+template <typename Input>
+inline auto Redis::bzpopmin(Input first,
+                            Input last,
+                            const std::chrono::seconds &timeout)
+    -> Optional<std::tuple<std::string, std::string, double>> {
+    return bzpopmin(first, last, timeout.count());
+}
+
+template <typename Input>
+long long Redis::zadd(const StringView &key,
+                        Input first,
+                        Input last,
+                        UpdateType type,
+                        bool changed) {
+    if (first == last) {
+        throw Error("ZADD: no key specified");
+    }
+
+    auto reply = command(cmd::zadd_range<Input>, key, first, last, type, changed);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Interval>
+long long Redis::zcount(const StringView &key, const Interval &interval) {
+    auto reply = command(cmd::zcount<Interval>, key, interval);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input>
+long long Redis::zinterstore(const StringView &destination,
+                                Input first,
+                                Input last,
+                                Aggregation type) {
+    if (first == last) {
+        throw Error("ZINTERSTORE: no key specified");
+    }
+
+    auto reply = command(cmd::zinterstore_range<Input>,
+                            destination,
+                            first,
+                            last,
+                            type);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Interval>
+long long Redis::zlexcount(const StringView &key, const Interval &interval) {
+    auto reply = command(cmd::zlexcount<Interval>, key, interval);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Output>
+void Redis::zpopmax(const StringView &key, long long count, Output output) {
+    auto reply = command(cmd::zpopmax, key, count);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void Redis::zpopmin(const StringView &key, long long count, Output output) {
+    auto reply = command(cmd::zpopmin, key, count);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void Redis::zrange(const StringView &key, long long start, long long stop, Output output) {
+    auto reply = _score_command<Output>(cmd::zrange, key, start, stop);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Interval, typename Output>
+void Redis::zrangebylex(const StringView &key, const Interval &interval, Output output) {
+    zrangebylex(key, interval, {}, output);
+}
+
+template <typename Interval, typename Output>
+void Redis::zrangebylex(const StringView &key,
+                        const Interval &interval,
+                        const LimitOptions &opts,
+                        Output output) {
+    auto reply = command(cmd::zrangebylex<Interval>, key, interval, opts);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Interval, typename Output>
+void Redis::zrangebyscore(const StringView &key,
+                            const Interval &interval,
+                            Output output) {
+    zrangebyscore(key, interval, {}, output);
+}
+
+template <typename Interval, typename Output>
+void Redis::zrangebyscore(const StringView &key,
+                            const Interval &interval,
+                            const LimitOptions &opts,
+                            Output output) {
+    auto reply = _score_command<Output>(cmd::zrangebyscore<Interval>,
+                                        key,
+                                        interval,
+                                        opts);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+long long Redis::zrem(const StringView &key, Input first, Input last) {
+    if (first == last) {
+        throw Error("ZREM: no key specified");
+    }
+
+    auto reply = command(cmd::zrem_range<Input>, key, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Interval>
+long long Redis::zremrangebylex(const StringView &key, const Interval &interval) {
+    auto reply = command(cmd::zremrangebylex<Interval>, key, interval);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Interval>
+long long Redis::zremrangebyscore(const StringView &key, const Interval &interval) {
+    auto reply = command(cmd::zremrangebyscore<Interval>, key, interval);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Output>
+void Redis::zrevrange(const StringView &key, long long start, long long stop, Output output) {
+    auto reply = _score_command<Output>(cmd::zrevrange, key, start, stop);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Interval, typename Output>
+inline void Redis::zrevrangebylex(const StringView &key,
+                                    const Interval &interval,
+                                    Output output) {
+    zrevrangebylex(key, interval, {}, output);
+}
+
+template <typename Interval, typename Output>
+void Redis::zrevrangebylex(const StringView &key,
+                            const Interval &interval,
+                            const LimitOptions &opts,
+                            Output output) {
+    auto reply = command(cmd::zrevrangebylex<Interval>, key, interval, opts);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Interval, typename Output>
+void Redis::zrevrangebyscore(const StringView &key, const Interval &interval, Output output) {
+    zrevrangebyscore(key, interval, {}, output);
+}
+
+template <typename Interval, typename Output>
+void Redis::zrevrangebyscore(const StringView &key,
+                                const Interval &interval,
+                                const LimitOptions &opts,
+                                Output output) {
+    auto reply = _score_command<Output>(cmd::zrevrangebyscore<Interval>, key, interval, opts);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+long long Redis::zscan(const StringView &key,
+                        long long cursor,
+                        const StringView &pattern,
+                        long long count,
+                        Output output) {
+    auto reply = command(cmd::zscan, key, cursor, pattern, count);
+
+    return reply::parse_scan_reply(*reply, output);
+}
+
+template <typename Output>
+inline long long Redis::zscan(const StringView &key,
+                                long long cursor,
+                                const StringView &pattern,
+                                Output output) {
+    return zscan(key, cursor, pattern, 10, output);
+}
+
+template <typename Output>
+inline long long Redis::zscan(const StringView &key,
+                                long long cursor,
+                                long long count,
+                                Output output) {
+    return zscan(key, cursor, "*", count, output);
+}
+
+template <typename Output>
+inline long long Redis::zscan(const StringView &key,
+                                long long cursor,
+                                Output output) {
+    return zscan(key, cursor, "*", 10, output);
+}
+
+template <typename Input>
+long long Redis::zunionstore(const StringView &destination,
+                                    Input first,
+                                    Input last,
+                                    Aggregation type) {
+    if (first == last) {
+        throw Error("ZUNIONSTORE: no key specified");
+    }
+
+    auto reply = command(cmd::zunionstore_range<Input>,
+                            destination,
+                            first,
+                            last,
+                            type);
+
+    return reply::parse<long long>(*reply);
+}
+
+// HYPERLOGLOG commands.
+
+template <typename Input>
+bool Redis::pfadd(const StringView &key, Input first, Input last) {
+    if (first == last) {
+        throw Error("PFADD: no key specified");
+    }
+
+    auto reply = command(cmd::pfadd_range<Input>, key, first, last);
+
+    return reply::parse<bool>(*reply);
+}
+
+template <typename Input>
+long long Redis::pfcount(Input first, Input last) {
+    if (first == last) {
+        throw Error("PFCOUNT: no key specified");
+    }
+
+    auto reply = command(cmd::pfcount_range<Input>, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input>
+void Redis::pfmerge(const StringView &destination,
+                    Input first,
+                    Input last) {
+    if (first == last) {
+        throw Error("PFMERGE: no key specified");
+    }
+
+    auto reply = command(cmd::pfmerge_range<Input>, destination, first, last);
+
+    reply::parse<void>(*reply);
+}
+
+// GEO commands.
+
+template <typename Input>
+inline long long Redis::geoadd(const StringView &key,
+                                Input first,
+                                Input last) {
+    if (first == last) {
+        throw Error("GEOADD: no key specified");
+    }
+
+    auto reply = command(cmd::geoadd_range<Input>, key, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input, typename Output>
+void Redis::geohash(const StringView &key, Input first, Input last, Output output) {
+    if (first == last) {
+        throw Error("GEOHASH: no key specified");
+    }
+
+    auto reply = command(cmd::geohash_range<Input>, key, first, last);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input, typename Output>
+void Redis::geopos(const StringView &key, Input first, Input last, Output output) {
+    if (first == last) {
+        throw Error("GEOPOS: no key specified");
+    }
+
+    auto reply = command(cmd::geopos_range<Input>, key, first, last);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void Redis::georadius(const StringView &key,
+                        const std::pair<double, double> &loc,
+                        double radius,
+                        GeoUnit unit,
+                        long long count,
+                        bool asc,
+                        Output output) {
+    auto reply = command(cmd::georadius,
+                            key,
+                            loc,
+                            radius,
+                            unit,
+                            count,
+                            asc,
+                            WithCoord<typename IterType<Output>::type>::value,
+                            WithDist<typename IterType<Output>::type>::value,
+                            WithHash<typename IterType<Output>::type>::value);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void Redis::georadiusbymember(const StringView &key,
+                                const StringView &member,
+                                double radius,
+                                GeoUnit unit,
+                                long long count,
+                                bool asc,
+                                Output output) {
+    auto reply = command(cmd::georadiusbymember,
+                            key,
+                            member,
+                            radius,
+                            unit,
+                            count,
+                            asc,
+                            WithCoord<typename IterType<Output>::type>::value,
+                            WithDist<typename IterType<Output>::type>::value,
+                            WithHash<typename IterType<Output>::type>::value);
+
+    reply::to_array(*reply, output);
+}
+
+// SCRIPTING commands.
+
+template <typename Result>
+Result Redis::eval(const StringView &script,
+                    std::initializer_list<StringView> keys,
+                    std::initializer_list<StringView> args) {
+    auto reply = command(cmd::eval, script, keys, args);
+
+    return reply::parse<Result>(*reply);
+}
+
+template <typename Output>
+void Redis::eval(const StringView &script,
+                    std::initializer_list<StringView> keys,
+                    std::initializer_list<StringView> args,
+                    Output output) {
+    auto reply = command(cmd::eval, script, keys, args);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Result>
+Result Redis::evalsha(const StringView &script,
+                        std::initializer_list<StringView> keys,
+                        std::initializer_list<StringView> args) {
+    auto reply = command(cmd::evalsha, script, keys, args);
+
+    return reply::parse<Result>(*reply);
+}
+
+template <typename Output>
+void Redis::evalsha(const StringView &script,
+                        std::initializer_list<StringView> keys,
+                        std::initializer_list<StringView> args,
+                        Output output) {
+    auto reply = command(cmd::evalsha, script, keys, args);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input, typename Output>
+void Redis::script_exists(Input first, Input last, Output output) {
+    if (first == last) {
+        throw Error("SCRIPT EXISTS: no key specified");
+    }
+
+    auto reply = command(cmd::script_exists_range<Input>, first, last);
+
+    reply::to_array(*reply, output);
+}
+
+// Transaction commands.
+
+template <typename Input>
+void Redis::watch(Input first, Input last) {
+    auto reply = command(cmd::watch_range<Input>, first, last);
+
+    reply::parse<void>(*reply);
+}
+
+// Stream commands.
+
+template <typename Input>
+long long Redis::xack(const StringView &key, const StringView &group, Input first, Input last) {
+    auto reply = command(cmd::xack_range<Input>, key, group, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input>
+std::string Redis::xadd(const StringView &key, const StringView &id, Input first, Input last) {
+    auto reply = command(cmd::xadd_range<Input>, key, id, first, last);
+
+    return reply::parse<std::string>(*reply);
+}
+
+template <typename Input>
+std::string Redis::xadd(const StringView &key,
+                        const StringView &id,
+                        Input first,
+                        Input last,
+                        long long count,
+                        bool approx) {
+    auto reply = command(cmd::xadd_maxlen_range<Input>, key, id, first, last, count, approx);
+
+    return reply::parse<std::string>(*reply);
+}
+
+template <typename Output>
+void Redis::xclaim(const StringView &key,
+                    const StringView &group,
+                    const StringView &consumer,
+                    const std::chrono::milliseconds &min_idle_time,
+                    const StringView &id,
+                    Output output) {
+    auto reply = command(cmd::xclaim, key, group, consumer, min_idle_time.count(), id);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input, typename Output>
+void Redis::xclaim(const StringView &key,
+                    const StringView &group,
+                    const StringView &consumer,
+                    const std::chrono::milliseconds &min_idle_time,
+                    Input first,
+                    Input last,
+                    Output output) {
+    auto reply = command(cmd::xclaim_range<Input>,
+                            key,
+                            group,
+                            consumer,
+                            min_idle_time.count(),
+                            first,
+                            last);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+long long Redis::xdel(const StringView &key, Input first, Input last) {
+    auto reply = command(cmd::xdel_range<Input>, key, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Output>
+auto Redis::xpending(const StringView &key, const StringView &group, Output output)
+    -> std::tuple<long long, OptionalString, OptionalString> {
+    auto reply = command(cmd::xpending, key, group);
+
+    return reply::parse_xpending_reply(*reply, output);
+}
+
+template <typename Output>
+void Redis::xpending(const StringView &key,
+                        const StringView &group,
+                        const StringView &start,
+                        const StringView &end,
+                        long long count,
+                        Output output) {
+    auto reply = command(cmd::xpending_detail, key, group, start, end, count);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void Redis::xpending(const StringView &key,
+                        const StringView &group,
+                        const StringView &start,
+                        const StringView &end,
+                        long long count,
+                        const StringView &consumer,
+                        Output output) {
+    auto reply = command(cmd::xpending_per_consumer, key, group, start, end, count, consumer);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void Redis::xrange(const StringView &key,
+                    const StringView &start,
+                    const StringView &end,
+                    Output output) {
+    auto reply = command(cmd::xrange, key, start, end);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void Redis::xrange(const StringView &key,
+                    const StringView &start,
+                    const StringView &end,
+                    long long count,
+                    Output output) {
+    auto reply = command(cmd::xrange_count, key, start, end, count);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void Redis::xread(const StringView &key,
+                    const StringView &id,
+                    long long count,
+                    Output output) {
+    auto reply = command(cmd::xread, key, id, count);
+
+    if (!reply::is_nil(*reply)) {
+        reply::to_array(*reply, output);
+    }
+}
+
+template <typename Input, typename Output>
+auto Redis::xread(Input first, Input last, long long count, Output output)
+    -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+    if (first == last) {
+        throw Error("XREAD: no key specified");
+    }
+
+    auto reply = command(cmd::xread_range<Input>, first, last, count);
+
+    if (!reply::is_nil(*reply)) {
+        reply::to_array(*reply, output);
+    }
+}
+
+template <typename Output>
+void Redis::xread(const StringView &key,
+                    const StringView &id,
+                    const std::chrono::milliseconds &timeout,
+                    long long count,
+                    Output output) {
+    auto reply = command(cmd::xread_block, key, id, timeout.count(), count);
+
+    if (!reply::is_nil(*reply)) {
+        reply::to_array(*reply, output);
+    }
+}
+
+template <typename Input, typename Output>
+auto Redis::xread(Input first,
+                    Input last,
+                    const std::chrono::milliseconds &timeout,
+                    long long count,
+                    Output output)
+    -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+    if (first == last) {
+        throw Error("XREAD: no key specified");
+    }
+
+    auto reply = command(cmd::xread_block_range<Input>, first, last, timeout.count(), count);
+
+    if (!reply::is_nil(*reply)) {
+        reply::to_array(*reply, output);
+    }
+}
+
+template <typename Output>
+void Redis::xreadgroup(const StringView &group,
+                        const StringView &consumer,
+                        const StringView &key,
+                        const StringView &id,
+                        long long count,
+                        bool noack,
+                        Output output) {
+    auto reply = command(cmd::xreadgroup, group, consumer, key, id, count, noack);
+
+    if (!reply::is_nil(*reply)) {
+        reply::to_array(*reply, output);
+    }
+}
+
+template <typename Input, typename Output>
+auto Redis::xreadgroup(const StringView &group,
+                        const StringView &consumer,
+                        Input first,
+                        Input last,
+                        long long count,
+                        bool noack,
+                        Output output)
+    -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+    if (first == last) {
+        throw Error("XREADGROUP: no key specified");
+    }
+
+    auto reply = command(cmd::xreadgroup_range<Input>, group, consumer, first, last, count, noack);
+
+    if (!reply::is_nil(*reply)) {
+        reply::to_array(*reply, output);
+    }
+}
+
+template <typename Output>
+void Redis::xreadgroup(const StringView &group,
+                        const StringView &consumer,
+                        const StringView &key,
+                        const StringView &id,
+                        const std::chrono::milliseconds &timeout,
+                        long long count,
+                        bool noack,
+                        Output output) {
+    auto reply = command(cmd::xreadgroup_block,
+                            group,
+                            consumer,
+                            key,
+                            id,
+                            timeout.count(),
+                            count,
+                            noack);
+
+    if (!reply::is_nil(*reply)) {
+        reply::to_array(*reply, output);
+    }
+}
+
+template <typename Input, typename Output>
+auto Redis::xreadgroup(const StringView &group,
+                        const StringView &consumer,
+                        Input first,
+                        Input last,
+                        const std::chrono::milliseconds &timeout,
+                        long long count,
+                        bool noack,
+                        Output output)
+    -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+    if (first == last) {
+        throw Error("XREADGROUP: no key specified");
+    }
+
+    auto reply = command(cmd::xreadgroup_block_range<Input>,
+                            group,
+                            consumer,
+                            first,
+                            last,
+                            timeout.count(),
+                            count,
+                            noack);
+
+    if (!reply::is_nil(*reply)) {
+        reply::to_array(*reply, output);
+    }
+}
+
+template <typename Output>
+void Redis::xrevrange(const StringView &key,
+                        const StringView &end,
+                        const StringView &start,
+                        Output output) {
+    auto reply = command(cmd::xrevrange, key, end, start);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void Redis::xrevrange(const StringView &key,
+                        const StringView &end,
+                        const StringView &start,
+                        long long count,
+                        Output output) {
+    auto reply = command(cmd::xrevrange_count, key, end, start, count);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Cmd, typename ...Args>
+ReplyUPtr Redis::_command(Connection &connection, Cmd cmd, Args &&...args) {
+    assert(!connection.broken());
+
+    cmd(connection, std::forward<Args>(args)...);
+
+    auto reply = connection.recv();
+
+    return reply;
+}
+
+template <typename Cmd, typename ...Args>
+inline ReplyUPtr Redis::_score_command(std::true_type, Cmd cmd, Args &&... args) {
+    return command(cmd, std::forward<Args>(args)..., true);
+}
+
+template <typename Cmd, typename ...Args>
+inline ReplyUPtr Redis::_score_command(std::false_type, Cmd cmd, Args &&... args) {
+    return command(cmd, std::forward<Args>(args)..., false);
+}
+
+template <typename Output, typename Cmd, typename ...Args>
+inline ReplyUPtr Redis::_score_command(Cmd cmd, Args &&... args) {
+    return _score_command(typename IsKvPairIter<Output>::type(),
+                            cmd,
+                            std::forward<Args>(args)...);
+}
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_REDIS_HPP

+ 1395 - 0
ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/redis_cluster.h

@@ -0,0 +1,1395 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_REDIS_CLUSTER_H
+#define SEWENEW_REDISPLUSPLUS_REDIS_CLUSTER_H
+
+#include <string>
+#include <chrono>
+#include <initializer_list>
+#include <tuple>
+#include "shards_pool.h"
+#include "reply.h"
+#include "command_options.h"
+#include "utils.h"
+#include "subscriber.h"
+#include "pipeline.h"
+#include "transaction.h"
+#include "redis.h"
+
+namespace sw {
+
+namespace redis {
+
+template <typename Impl>
+class QueuedRedis;
+
+using Transaction = QueuedRedis<TransactionImpl>;
+
+using Pipeline = QueuedRedis<PipelineImpl>;
+
+class RedisCluster {
+public:
+    RedisCluster(const ConnectionOptions &connection_opts,
+                    const ConnectionPoolOptions &pool_opts = {}) :
+                        _pool(pool_opts, connection_opts) {}
+
+    // Construct RedisCluster with URI:
+    // "tcp://127.0.0.1" or "tcp://127.0.0.1:6379"
+    // Only need to specify one URI.
+    explicit RedisCluster(const std::string &uri);
+
+    RedisCluster(const RedisCluster &) = delete;
+    RedisCluster& operator=(const RedisCluster &) = delete;
+
+    RedisCluster(RedisCluster &&) = default;
+    RedisCluster& operator=(RedisCluster &&) = default;
+
+    Redis redis(const StringView &hash_tag);
+
+    Pipeline pipeline(const StringView &hash_tag);
+
+    Transaction transaction(const StringView &hash_tag, bool piped = false);
+
+    Subscriber subscriber();
+
+    template <typename Cmd, typename Key, typename ...Args>
+    auto command(Cmd cmd, Key &&key, Args &&...args)
+        -> typename std::enable_if<!std::is_convertible<Cmd, StringView>::value, ReplyUPtr>::type;
+
+    template <typename Key, typename ...Args>
+    auto command(const StringView &cmd_name, Key &&key, Args &&...args)
+        -> typename std::enable_if<(std::is_convertible<Key, StringView>::value
+                || std::is_arithmetic<typename std::decay<Key>::type>::value)
+                && !IsIter<typename LastType<Key, Args...>::type>::value, ReplyUPtr>::type;
+
+    template <typename Key, typename ...Args>
+    auto command(const StringView &cmd_name, Key &&key, Args &&...args)
+        -> typename std::enable_if<(std::is_convertible<Key, StringView>::value
+                || std::is_arithmetic<typename std::decay<Key>::type>::value)
+                && IsIter<typename LastType<Key, Args...>::type>::value, void>::type;
+
+    template <typename Result, typename Key, typename ...Args>
+    auto command(const StringView &cmd_name, Key &&key, Args &&...args)
+        -> typename std::enable_if<std::is_convertible<Key, StringView>::value
+                || std::is_arithmetic<typename std::decay<Key>::type>::value, Result>::type;
+
+    template <typename Input>
+    auto command(Input first, Input last)
+        -> typename std::enable_if<IsIter<Input>::value, ReplyUPtr>::type;
+
+    template <typename Result, typename Input>
+    auto command(Input first, Input last)
+        -> typename std::enable_if<IsIter<Input>::value, Result>::type;
+
+    template <typename Input, typename Output>
+    auto command(Input first, Input last, Output output)
+        -> typename std::enable_if<IsIter<Input>::value, void>::type;
+
+    // KEY commands.
+
+    long long del(const StringView &key);
+
+    template <typename Input>
+    long long del(Input first, Input last);
+
+    template <typename T>
+    long long del(std::initializer_list<T> il) {
+        return del(il.begin(), il.end());
+    }
+
+    OptionalString dump(const StringView &key);
+
+    long long exists(const StringView &key);
+
+    template <typename Input>
+    long long exists(Input first, Input last);
+
+    template <typename T>
+    long long exists(std::initializer_list<T> il) {
+        return exists(il.begin(), il.end());
+    }
+
+    bool expire(const StringView &key, long long timeout);
+
+    bool expire(const StringView &key, const std::chrono::seconds &timeout);
+
+    bool expireat(const StringView &key, long long timestamp);
+
+    bool expireat(const StringView &key,
+                    const std::chrono::time_point<std::chrono::system_clock,
+                                                    std::chrono::seconds> &tp);
+
+    bool persist(const StringView &key);
+
+    bool pexpire(const StringView &key, long long timeout);
+
+    bool pexpire(const StringView &key, const std::chrono::milliseconds &timeout);
+
+    bool pexpireat(const StringView &key, long long timestamp);
+
+    bool pexpireat(const StringView &key,
+                    const std::chrono::time_point<std::chrono::system_clock,
+                                                    std::chrono::milliseconds> &tp);
+
+    long long pttl(const StringView &key);
+
+    void rename(const StringView &key, const StringView &newkey);
+
+    bool renamenx(const StringView &key, const StringView &newkey);
+
+    void restore(const StringView &key,
+                    const StringView &val,
+                    long long ttl,
+                    bool replace = false);
+
+    void restore(const StringView &key,
+                    const StringView &val,
+                    const std::chrono::milliseconds &ttl = std::chrono::milliseconds{0},
+                    bool replace = false);
+
+    // TODO: sort
+
+    long long touch(const StringView &key);
+
+    template <typename Input>
+    long long touch(Input first, Input last);
+
+    template <typename T>
+    long long touch(std::initializer_list<T> il) {
+        return touch(il.begin(), il.end());
+    }
+
+    long long ttl(const StringView &key);
+
+    std::string type(const StringView &key);
+
+    long long unlink(const StringView &key);
+
+    template <typename Input>
+    long long unlink(Input first, Input last);
+
+    template <typename T>
+    long long unlink(std::initializer_list<T> il) {
+        return unlink(il.begin(), il.end());
+    }
+
+    // STRING commands.
+
+    long long append(const StringView &key, const StringView &str);
+
+    long long bitcount(const StringView &key, long long start = 0, long long end = -1);
+
+    long long bitop(BitOp op, const StringView &destination, const StringView &key);
+
+    template <typename Input>
+    long long bitop(BitOp op, const StringView &destination, Input first, Input last);
+
+    template <typename T>
+    long long bitop(BitOp op, const StringView &destination, std::initializer_list<T> il) {
+        return bitop(op, destination, il.begin(), il.end());
+    }
+
+    long long bitpos(const StringView &key,
+                        long long bit,
+                        long long start = 0,
+                        long long end = -1);
+
+    long long decr(const StringView &key);
+
+    long long decrby(const StringView &key, long long decrement);
+
+    OptionalString get(const StringView &key);
+
+    long long getbit(const StringView &key, long long offset);
+
+    std::string getrange(const StringView &key, long long start, long long end);
+
+    OptionalString getset(const StringView &key, const StringView &val);
+
+    long long incr(const StringView &key);
+
+    long long incrby(const StringView &key, long long increment);
+
+    double incrbyfloat(const StringView &key, double increment);
+
+    template <typename Input, typename Output>
+    void mget(Input first, Input last, Output output);
+
+    template <typename T, typename Output>
+    void mget(std::initializer_list<T> il, Output output) {
+        mget(il.begin(), il.end(), output);
+    }
+
+    template <typename Input>
+    void mset(Input first, Input last);
+
+    template <typename T>
+    void mset(std::initializer_list<T> il) {
+        mset(il.begin(), il.end());
+    }
+
+    template <typename Input>
+    bool msetnx(Input first, Input last);
+
+    template <typename T>
+    bool msetnx(std::initializer_list<T> il) {
+        return msetnx(il.begin(), il.end());
+    }
+
+    void psetex(const StringView &key,
+                long long ttl,
+                const StringView &val);
+
+    void psetex(const StringView &key,
+                const std::chrono::milliseconds &ttl,
+                const StringView &val);
+
+    bool set(const StringView &key,
+                const StringView &val,
+                const std::chrono::milliseconds &ttl = std::chrono::milliseconds(0),
+                UpdateType type = UpdateType::ALWAYS);
+
+    void setex(const StringView &key,
+                long long ttl,
+                const StringView &val);
+
+    void setex(const StringView &key,
+                const std::chrono::seconds &ttl,
+                const StringView &val);
+
+    bool setnx(const StringView &key, const StringView &val);
+
+    long long setrange(const StringView &key, long long offset, const StringView &val);
+
+    long long strlen(const StringView &key);
+
+    // LIST commands.
+
+    OptionalStringPair blpop(const StringView &key, long long timeout);
+
+    OptionalStringPair blpop(const StringView &key,
+                                const std::chrono::seconds &timeout = std::chrono::seconds{0});
+
+    template <typename Input>
+    OptionalStringPair blpop(Input first, Input last, long long timeout);
+
+    template <typename T>
+    OptionalStringPair blpop(std::initializer_list<T> il, long long timeout) {
+        return blpop(il.begin(), il.end(), timeout);
+    }
+
+    template <typename Input>
+    OptionalStringPair blpop(Input first,
+                                Input last,
+                                const std::chrono::seconds &timeout = std::chrono::seconds{0});
+
+    template <typename T>
+    OptionalStringPair blpop(std::initializer_list<T> il,
+                                const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
+        return blpop(il.begin(), il.end(), timeout);
+    }
+
+    OptionalStringPair brpop(const StringView &key, long long timeout);
+
+    OptionalStringPair brpop(const StringView &key,
+                                const std::chrono::seconds &timeout = std::chrono::seconds{0});
+
+    template <typename Input>
+    OptionalStringPair brpop(Input first, Input last, long long timeout);
+
+    template <typename T>
+    OptionalStringPair brpop(std::initializer_list<T> il, long long timeout) {
+        return brpop(il.begin(), il.end(), timeout);
+    }
+
+    template <typename Input>
+    OptionalStringPair brpop(Input first,
+                                Input last,
+                                const std::chrono::seconds &timeout = std::chrono::seconds{0});
+
+    template <typename T>
+    OptionalStringPair brpop(std::initializer_list<T> il,
+                                const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
+        return brpop(il.begin(), il.end(), timeout);
+    }
+
+    OptionalString brpoplpush(const StringView &source,
+                                const StringView &destination,
+                                long long timeout);
+
+    OptionalString brpoplpush(const StringView &source,
+                                const StringView &destination,
+                                const std::chrono::seconds &timeout = std::chrono::seconds{0});
+
+    OptionalString lindex(const StringView &key, long long index);
+
+    long long linsert(const StringView &key,
+                        InsertPosition position,
+                        const StringView &pivot,
+                        const StringView &val);
+
+    long long llen(const StringView &key);
+
+    OptionalString lpop(const StringView &key);
+
+    long long lpush(const StringView &key, const StringView &val);
+
+    template <typename Input>
+    long long lpush(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    long long lpush(const StringView &key, std::initializer_list<T> il) {
+        return lpush(key, il.begin(), il.end());
+    }
+
+    long long lpushx(const StringView &key, const StringView &val);
+
+    template <typename Output>
+    void lrange(const StringView &key, long long start, long long stop, Output output);
+
+    long long lrem(const StringView &key, long long count, const StringView &val);
+
+    void lset(const StringView &key, long long index, const StringView &val);
+
+    void ltrim(const StringView &key, long long start, long long stop);
+
+    OptionalString rpop(const StringView &key);
+
+    OptionalString rpoplpush(const StringView &source, const StringView &destination);
+
+    long long rpush(const StringView &key, const StringView &val);
+
+    template <typename Input>
+    long long rpush(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    long long rpush(const StringView &key, std::initializer_list<T> il) {
+        return rpush(key, il.begin(), il.end());
+    }
+
+    long long rpushx(const StringView &key, const StringView &val);
+
+    // HASH commands.
+
+    long long hdel(const StringView &key, const StringView &field);
+
+    template <typename Input>
+    long long hdel(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    long long hdel(const StringView &key, std::initializer_list<T> il) {
+        return hdel(key, il.begin(), il.end());
+    }
+
+    bool hexists(const StringView &key, const StringView &field);
+
+    OptionalString hget(const StringView &key, const StringView &field);
+
+    template <typename Output>
+    void hgetall(const StringView &key, Output output);
+
+    long long hincrby(const StringView &key, const StringView &field, long long increment);
+
+    double hincrbyfloat(const StringView &key, const StringView &field, double increment);
+
+    template <typename Output>
+    void hkeys(const StringView &key, Output output);
+
+    long long hlen(const StringView &key);
+
+    template <typename Input, typename Output>
+    void hmget(const StringView &key, Input first, Input last, Output output);
+
+    template <typename T, typename Output>
+    void hmget(const StringView &key, std::initializer_list<T> il, Output output) {
+        hmget(key, il.begin(), il.end(), output);
+    }
+
+    template <typename Input>
+    void hmset(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    void hmset(const StringView &key, std::initializer_list<T> il) {
+        hmset(key, il.begin(), il.end());
+    }
+
+    template <typename Output>
+    long long hscan(const StringView &key,
+                    long long cursor,
+                    const StringView &pattern,
+                    long long count,
+                    Output output);
+
+    template <typename Output>
+    long long hscan(const StringView &key,
+                    long long cursor,
+                    const StringView &pattern,
+                    Output output);
+
+    template <typename Output>
+    long long hscan(const StringView &key,
+                    long long cursor,
+                    long long count,
+                    Output output);
+
+    template <typename Output>
+    long long hscan(const StringView &key,
+                    long long cursor,
+                    Output output);
+
+    bool hset(const StringView &key, const StringView &field, const StringView &val);
+
+    bool hset(const StringView &key, const std::pair<StringView, StringView> &item);
+
+    bool hsetnx(const StringView &key, const StringView &field, const StringView &val);
+
+    bool hsetnx(const StringView &key, const std::pair<StringView, StringView> &item);
+
+    long long hstrlen(const StringView &key, const StringView &field);
+
+    template <typename Output>
+    void hvals(const StringView &key, Output output);
+
+    // SET commands.
+
+    long long sadd(const StringView &key, const StringView &member);
+
+    template <typename Input>
+    long long sadd(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    long long sadd(const StringView &key, std::initializer_list<T> il) {
+        return sadd(key, il.begin(), il.end());
+    }
+
+    long long scard(const StringView &key);
+
+    template <typename Input, typename Output>
+    void sdiff(Input first, Input last, Output output);
+
+    template <typename T, typename Output>
+    void sdiff(std::initializer_list<T> il, Output output) {
+        sdiff(il.begin(), il.end(), output);
+    }
+
+    long long sdiffstore(const StringView &destination, const StringView &key);
+
+    template <typename Input>
+    long long sdiffstore(const StringView &destination,
+                            Input first,
+                            Input last);
+
+    template <typename T>
+    long long sdiffstore(const StringView &destination,
+                            std::initializer_list<T> il) {
+        return sdiffstore(destination, il.begin(), il.end());
+    }
+
+    template <typename Input, typename Output>
+    void sinter(Input first, Input last, Output output);
+
+    template <typename T, typename Output>
+    void sinter(std::initializer_list<T> il, Output output) {
+        sinter(il.begin(), il.end(), output);
+    }
+
+    long long sinterstore(const StringView &destination, const StringView &key);
+
+    template <typename Input>
+    long long sinterstore(const StringView &destination,
+                            Input first,
+                            Input last);
+
+    template <typename T>
+    long long sinterstore(const StringView &destination,
+                            std::initializer_list<T> il) {
+        return sinterstore(destination, il.begin(), il.end());
+    }
+
+    bool sismember(const StringView &key, const StringView &member);
+
+    template <typename Output>
+    void smembers(const StringView &key, Output output);
+
+    bool smove(const StringView &source,
+                const StringView &destination,
+                const StringView &member);
+
+    OptionalString spop(const StringView &key);
+
+    template <typename Output>
+    void spop(const StringView &key, long long count, Output output);
+
+    OptionalString srandmember(const StringView &key);
+
+    template <typename Output>
+    void srandmember(const StringView &key, long long count, Output output);
+
+    long long srem(const StringView &key, const StringView &member);
+
+    template <typename Input>
+    long long srem(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    long long srem(const StringView &key, std::initializer_list<T> il) {
+        return srem(key, il.begin(), il.end());
+    }
+
+    template <typename Output>
+    long long sscan(const StringView &key,
+                    long long cursor,
+                    const StringView &pattern,
+                    long long count,
+                    Output output);
+
+    template <typename Output>
+    long long sscan(const StringView &key,
+                    long long cursor,
+                    const StringView &pattern,
+                    Output output);
+
+    template <typename Output>
+    long long sscan(const StringView &key,
+                    long long cursor,
+                    long long count,
+                    Output output);
+
+    template <typename Output>
+    long long sscan(const StringView &key,
+                    long long cursor,
+                    Output output);
+
+    template <typename Input, typename Output>
+    void sunion(Input first, Input last, Output output);
+
+    template <typename T, typename Output>
+    void sunion(std::initializer_list<T> il, Output output) {
+        sunion(il.begin(), il.end(), output);
+    }
+
+    long long sunionstore(const StringView &destination, const StringView &key);
+
+    template <typename Input>
+    long long sunionstore(const StringView &destination, Input first, Input last);
+
+    template <typename T>
+    long long sunionstore(const StringView &destination, std::initializer_list<T> il) {
+        return sunionstore(destination, il.begin(), il.end());
+    }
+
+    // SORTED SET commands.
+
+    auto bzpopmax(const StringView &key, long long timeout)
+        -> Optional<std::tuple<std::string, std::string, double>>;
+
+    auto bzpopmax(const StringView &key,
+                    const std::chrono::seconds &timeout = std::chrono::seconds{0})
+        -> Optional<std::tuple<std::string, std::string, double>>;
+
+    template <typename Input>
+    auto bzpopmax(Input first, Input last, long long timeout)
+        -> Optional<std::tuple<std::string, std::string, double>>;
+
+    template <typename Input>
+    auto bzpopmax(Input first,
+                    Input last,
+                    const std::chrono::seconds &timeout = std::chrono::seconds{0})
+        -> Optional<std::tuple<std::string, std::string, double>>;
+
+    template <typename T>
+    auto bzpopmax(std::initializer_list<T> il, long long timeout)
+        -> Optional<std::tuple<std::string, std::string, double>> {
+        return bzpopmax(il.begin(), il.end(), timeout);
+    }
+
+    template <typename T>
+    auto bzpopmax(std::initializer_list<T> il,
+                    const std::chrono::seconds &timeout = std::chrono::seconds{0})
+        -> Optional<std::tuple<std::string, std::string, double>> {
+        return bzpopmax(il.begin(), il.end(), timeout);
+    }
+
+    auto bzpopmin(const StringView &key, long long timeout)
+        -> Optional<std::tuple<std::string, std::string, double>>;
+
+    auto bzpopmin(const StringView &key,
+                    const std::chrono::seconds &timeout = std::chrono::seconds{0})
+        -> Optional<std::tuple<std::string, std::string, double>>;
+
+    template <typename Input>
+    auto bzpopmin(Input first, Input last, long long timeout)
+        -> Optional<std::tuple<std::string, std::string, double>>;
+
+    template <typename Input>
+    auto bzpopmin(Input first,
+                    Input last,
+                    const std::chrono::seconds &timeout = std::chrono::seconds{0})
+        -> Optional<std::tuple<std::string, std::string, double>>;
+
+    template <typename T>
+    auto bzpopmin(std::initializer_list<T> il, long long timeout)
+        -> Optional<std::tuple<std::string, std::string, double>> {
+        return bzpopmin(il.begin(), il.end(), timeout);
+    }
+
+    template <typename T>
+    auto bzpopmin(std::initializer_list<T> il,
+                    const std::chrono::seconds &timeout = std::chrono::seconds{0})
+        -> Optional<std::tuple<std::string, std::string, double>> {
+        return bzpopmin(il.begin(), il.end(), timeout);
+    }
+
+    // We don't support the INCR option, since you can always use ZINCRBY instead.
+    long long zadd(const StringView &key,
+                    const StringView &member,
+                    double score,
+                    UpdateType type = UpdateType::ALWAYS,
+                    bool changed = false);
+
+    template <typename Input>
+    long long zadd(const StringView &key,
+                    Input first,
+                    Input last,
+                    UpdateType type = UpdateType::ALWAYS,
+                    bool changed = false);
+
+    template <typename T>
+    long long zadd(const StringView &key,
+                    std::initializer_list<T> il,
+                    UpdateType type = UpdateType::ALWAYS,
+                    bool changed = false) {
+        return zadd(key, il.begin(), il.end(), type, changed);
+    }
+
+    long long zcard(const StringView &key);
+
+    template <typename Interval>
+    long long zcount(const StringView &key, const Interval &interval);
+
+    double zincrby(const StringView &key, double increment, const StringView &member);
+
+    long long zinterstore(const StringView &destination, const StringView &key, double weight);
+
+    template <typename Input>
+    long long zinterstore(const StringView &destination,
+                            Input first,
+                            Input last,
+                            Aggregation type = Aggregation::SUM);
+
+    template <typename T>
+    long long zinterstore(const StringView &destination,
+                            std::initializer_list<T> il,
+                            Aggregation type = Aggregation::SUM) {
+        return zinterstore(destination, il.begin(), il.end(), type);
+    }
+
+    template <typename Interval>
+    long long zlexcount(const StringView &key, const Interval &interval);
+
+    Optional<std::pair<std::string, double>> zpopmax(const StringView &key);
+
+    template <typename Output>
+    void zpopmax(const StringView &key, long long count, Output output);
+
+    Optional<std::pair<std::string, double>> zpopmin(const StringView &key);
+
+    template <typename Output>
+    void zpopmin(const StringView &key, long long count, Output output);
+
+    // If *output* is an iterator of a container of string,
+    // we send *ZRANGE key start stop* command.
+    // If it's an iterator of a container of pair<string, double>,
+    // we send *ZRANGE key start stop WITHSCORES* command.
+    //
+    // The following code sends *ZRANGE* without the *WITHSCORES* option:
+    //
+    // vector<string> result;
+    // redis.zrange("key", 0, -1, back_inserter(result));
+    //
+    // On the other hand, the following code sends command with *WITHSCORES* option:
+    //
+    // unordered_map<string, double> with_score;
+    // redis.zrange("key", 0, -1, inserter(with_score, with_score.end()));
+    //
+    // This also applies to other commands with the *WITHSCORES* option,
+    // e.g. *ZRANGEBYSCORE*, *ZREVRANGE*, *ZREVRANGEBYSCORE*.
+    template <typename Output>
+    void zrange(const StringView &key, long long start, long long stop, Output output);
+
+    template <typename Interval, typename Output>
+    void zrangebylex(const StringView &key, const Interval &interval, Output output);
+
+    template <typename Interval, typename Output>
+    void zrangebylex(const StringView &key,
+                        const Interval &interval,
+                        const LimitOptions &opts,
+                        Output output);
+
+    // See *zrange* comment on how to send command with *WITHSCORES* option.
+    template <typename Interval, typename Output>
+    void zrangebyscore(const StringView &key, const Interval &interval, Output output);
+
+    // See *zrange* comment on how to send command with *WITHSCORES* option.
+    template <typename Interval, typename Output>
+    void zrangebyscore(const StringView &key,
+                        const Interval &interval,
+                        const LimitOptions &opts,
+                        Output output);
+
+    OptionalLongLong zrank(const StringView &key, const StringView &member);
+
+    long long zrem(const StringView &key, const StringView &member);
+
+    template <typename Input>
+    long long zrem(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    long long zrem(const StringView &key, std::initializer_list<T> il) {
+        return zrem(key, il.begin(), il.end());
+    }
+
+    template <typename Interval>
+    long long zremrangebylex(const StringView &key, const Interval &interval);
+
+    long long zremrangebyrank(const StringView &key, long long start, long long stop);
+
+    template <typename Interval>
+    long long zremrangebyscore(const StringView &key, const Interval &interval);
+
+    // See *zrange* comment on how to send command with *WITHSCORES* option.
+    template <typename Output>
+    void zrevrange(const StringView &key, long long start, long long stop, Output output);
+
+    template <typename Interval, typename Output>
+    void zrevrangebylex(const StringView &key, const Interval &interval, Output output);
+
+    template <typename Interval, typename Output>
+    void zrevrangebylex(const StringView &key,
+                        const Interval &interval,
+                        const LimitOptions &opts,
+                        Output output);
+
+    // See *zrange* comment on how to send command with *WITHSCORES* option.
+    template <typename Interval, typename Output>
+    void zrevrangebyscore(const StringView &key, const Interval &interval, Output output);
+
+    // See *zrange* comment on how to send command with *WITHSCORES* option.
+    template <typename Interval, typename Output>
+    void zrevrangebyscore(const StringView &key,
+                            const Interval &interval,
+                            const LimitOptions &opts,
+                            Output output);
+
+    OptionalLongLong zrevrank(const StringView &key, const StringView &member);
+
+    template <typename Output>
+    long long zscan(const StringView &key,
+                    long long cursor,
+                    const StringView &pattern,
+                    long long count,
+                    Output output);
+
+    template <typename Output>
+    long long zscan(const StringView &key,
+                    long long cursor,
+                    const StringView &pattern,
+                    Output output);
+
+    template <typename Output>
+    long long zscan(const StringView &key,
+                    long long cursor,
+                    long long count,
+                    Output output);
+
+    template <typename Output>
+    long long zscan(const StringView &key,
+                    long long cursor,
+                    Output output);
+
+    OptionalDouble zscore(const StringView &key, const StringView &member);
+
+    long long zunionstore(const StringView &destination, const StringView &key, double weight);
+
+    template <typename Input>
+    long long zunionstore(const StringView &destination,
+                            Input first,
+                            Input last,
+                            Aggregation type = Aggregation::SUM);
+
+    template <typename T>
+    long long zunionstore(const StringView &destination,
+                            std::initializer_list<T> il,
+                            Aggregation type = Aggregation::SUM) {
+        return zunionstore(destination, il.begin(), il.end(), type);
+    }
+
+    // HYPERLOGLOG commands.
+
+    bool pfadd(const StringView &key, const StringView &element);
+
+    template <typename Input>
+    bool pfadd(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    bool pfadd(const StringView &key, std::initializer_list<T> il) {
+        return pfadd(key, il.begin(), il.end());
+    }
+
+    long long pfcount(const StringView &key);
+
+    template <typename Input>
+    long long pfcount(Input first, Input last);
+
+    template <typename T>
+    long long pfcount(std::initializer_list<T> il) {
+        return pfcount(il.begin(), il.end());
+    }
+
+    void pfmerge(const StringView &destination, const StringView &key);
+
+    template <typename Input>
+    void pfmerge(const StringView &destination, Input first, Input last);
+
+    template <typename T>
+    void pfmerge(const StringView &destination, std::initializer_list<T> il) {
+        pfmerge(destination, il.begin(), il.end());
+    }
+
+    // GEO commands.
+
+    long long geoadd(const StringView &key,
+                        const std::tuple<StringView, double, double> &member);
+
+    template <typename Input>
+    long long geoadd(const StringView &key,
+                        Input first,
+                        Input last);
+
+    template <typename T>
+    long long geoadd(const StringView &key,
+                        std::initializer_list<T> il) {
+        return geoadd(key, il.begin(), il.end());
+    }
+
+    OptionalDouble geodist(const StringView &key,
+                            const StringView &member1,
+                            const StringView &member2,
+                            GeoUnit unit = GeoUnit::M);
+
+    template <typename Input, typename Output>
+    void geohash(const StringView &key, Input first, Input last, Output output);
+
+    template <typename T, typename Output>
+    void geohash(const StringView &key, std::initializer_list<T> il, Output output) {
+        geohash(key, il.begin(), il.end(), output);
+    }
+
+    template <typename Input, typename Output>
+    void geopos(const StringView &key, Input first, Input last, Output output);
+
+    template <typename T, typename Output>
+    void geopos(const StringView &key, std::initializer_list<T> il, Output output) {
+        geopos(key, il.begin(), il.end(), output);
+    }
+
+    // TODO:
+    // 1. since we have different overloads for georadius and georadius-store,
+    //    we might use the GEORADIUS_RO command in the future.
+    // 2. there're too many parameters for this method, we might refactor it.
+    OptionalLongLong georadius(const StringView &key,
+                                const std::pair<double, double> &loc,
+                                double radius,
+                                GeoUnit unit,
+                                const StringView &destination,
+                                bool store_dist,
+                                long long count);
+
+    // If *output* is an iterator of a container of string, we send *GEORADIUS* command
+    // without any options and only get the members in the specified geo range.
+    // If *output* is an iterator of a container of a tuple, the type of the tuple decides
+    // options we send with the *GEORADIUS* command. If the tuple has an element of type
+    // double, we send the *WITHDIST* option. If it has an element of type string, we send
+    // the *WITHHASH* option. If it has an element of type pair<double, double>, we send
+    // the *WITHCOORD* option. For example:
+    //
+    // The following code only gets the members in range, i.e. without any option.
+    //
+    // vector<string> members;
+    // redis.georadius("key", make_pair(10.1, 10.2), 10, GeoUnit::KM, 10, true,
+    //                  back_inserter(members))
+    //
+    // The following code sends the command with *WITHDIST* option.
+    //
+    // vector<tuple<string, double>> with_dist;
+    // redis.georadius("key", make_pair(10.1, 10.2), 10, GeoUnit::KM, 10, true,
+    //                  back_inserter(with_dist))
+    //
+    // The following code sends the command with *WITHDIST* and *WITHHASH* options.
+    //
+    // vector<tuple<string, double, string>> with_dist_hash;
+    // redis.georadius("key", make_pair(10.1, 10.2), 10, GeoUnit::KM, 10, true,
+    //                  back_inserter(with_dist_hash))
+    //
+    // The following code sends the command with *WITHDIST*, *WITHCOORD* and *WITHHASH* options.
+    //
+    // vector<tuple<string, double, pair<double, double>, string>> with_dist_coord_hash;
+    // redis.georadius("key", make_pair(10.1, 10.2), 10, GeoUnit::KM, 10, true,
+    //                  back_inserter(with_dist_coord_hash))
+    //
+    // This also applies to *GEORADIUSBYMEMBER*.
+    template <typename Output>
+    void georadius(const StringView &key,
+                    const std::pair<double, double> &loc,
+                    double radius,
+                    GeoUnit unit,
+                    long long count,
+                    bool asc,
+                    Output output);
+
+    OptionalLongLong georadiusbymember(const StringView &key,
+                                        const StringView &member,
+                                        double radius,
+                                        GeoUnit unit,
+                                        const StringView &destination,
+                                        bool store_dist,
+                                        long long count);
+
+    // See comments on *GEORADIUS*.
+    template <typename Output>
+    void georadiusbymember(const StringView &key,
+                            const StringView &member,
+                            double radius,
+                            GeoUnit unit,
+                            long long count,
+                            bool asc,
+                            Output output);
+
+    // SCRIPTING commands.
+
+    template <typename Result>
+    Result eval(const StringView &script,
+                std::initializer_list<StringView> keys,
+                std::initializer_list<StringView> args);
+
+    template <typename Output>
+    void eval(const StringView &script,
+                std::initializer_list<StringView> keys,
+                std::initializer_list<StringView> args,
+                Output output);
+
+    template <typename Result>
+    Result evalsha(const StringView &script,
+                    std::initializer_list<StringView> keys,
+                    std::initializer_list<StringView> args);
+
+    template <typename Output>
+    void evalsha(const StringView &script,
+                    std::initializer_list<StringView> keys,
+                    std::initializer_list<StringView> args,
+                    Output output);
+
+    // PUBSUB commands.
+
+    long long publish(const StringView &channel, const StringView &message);
+
+    // Stream commands.
+
+    long long xack(const StringView &key, const StringView &group, const StringView &id);
+
+    template <typename Input>
+    long long xack(const StringView &key, const StringView &group, Input first, Input last);
+
+    template <typename T>
+    long long xack(const StringView &key, const StringView &group, std::initializer_list<T> il) {
+        return xack(key, group, il.begin(), il.end());
+    }
+
+    template <typename Input>
+    std::string xadd(const StringView &key, const StringView &id, Input first, Input last);
+
+    template <typename T>
+    std::string xadd(const StringView &key, const StringView &id, std::initializer_list<T> il) {
+        return xadd(key, id, il.begin(), il.end());
+    }
+
+    template <typename Input>
+    std::string xadd(const StringView &key,
+                        const StringView &id,
+                        Input first,
+                        Input last,
+                        long long count,
+                        bool approx = true);
+
+    template <typename T>
+    std::string xadd(const StringView &key,
+                        const StringView &id,
+                        std::initializer_list<T> il,
+                        long long count,
+                        bool approx = true) {
+        return xadd(key, id, il.begin(), il.end(), count, approx);
+    }
+
+    template <typename Output>
+    void xclaim(const StringView &key,
+                const StringView &group,
+                const StringView &consumer,
+                const std::chrono::milliseconds &min_idle_time,
+                const StringView &id,
+                Output output);
+
+    template <typename Input, typename Output>
+    void xclaim(const StringView &key,
+                const StringView &group,
+                const StringView &consumer,
+                const std::chrono::milliseconds &min_idle_time,
+                Input first,
+                Input last,
+                Output output);
+
+    template <typename T, typename Output>
+    void xclaim(const StringView &key,
+                const StringView &group,
+                const StringView &consumer,
+                const std::chrono::milliseconds &min_idle_time,
+                std::initializer_list<T> il,
+                Output output) {
+        xclaim(key, group, consumer, min_idle_time, il.begin(), il.end(), output);
+    }
+
+    long long xdel(const StringView &key, const StringView &id);
+
+    template <typename Input>
+    long long xdel(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    long long xdel(const StringView &key, std::initializer_list<T> il) {
+        return xdel(key, il.begin(), il.end());
+    }
+
+    void xgroup_create(const StringView &key,
+                        const StringView &group,
+                        const StringView &id,
+                        bool mkstream = false);
+
+    void xgroup_setid(const StringView &key, const StringView &group, const StringView &id);
+
+    long long xgroup_destroy(const StringView &key, const StringView &group);
+
+    long long xgroup_delconsumer(const StringView &key,
+                                    const StringView &group,
+                                    const StringView &consumer);
+
+    long long xlen(const StringView &key);
+
+    template <typename Output>
+    auto xpending(const StringView &key, const StringView &group, Output output)
+        -> std::tuple<long long, OptionalString, OptionalString>;
+
+    template <typename Output>
+    void xpending(const StringView &key,
+                    const StringView &group,
+                    const StringView &start,
+                    const StringView &end,
+                    long long count,
+                    Output output);
+
+    template <typename Output>
+    void xpending(const StringView &key,
+                    const StringView &group,
+                    const StringView &start,
+                    const StringView &end,
+                    long long count,
+                    const StringView &consumer,
+                    Output output);
+
+    template <typename Output>
+    void xrange(const StringView &key,
+                const StringView &start,
+                const StringView &end,
+                Output output);
+
+    template <typename Output>
+    void xrange(const StringView &key,
+                const StringView &start,
+                const StringView &end,
+                long long count,
+                Output output);
+
+    template <typename Output>
+    void xread(const StringView &key,
+                const StringView &id,
+                long long count,
+                Output output);
+
+    template <typename Output>
+    void xread(const StringView &key,
+                const StringView &id,
+                Output output) {
+        xread(key, id, 0, output);
+    }
+
+    template <typename Input, typename Output>
+    auto xread(Input first, Input last, long long count, Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type;
+
+    template <typename Input, typename Output>
+    auto xread(Input first, Input last, Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+        xread(first, last, 0, output);
+    }
+
+    template <typename Output>
+    void xread(const StringView &key,
+                const StringView &id,
+                const std::chrono::milliseconds &timeout,
+                long long count,
+                Output output);
+
+    template <typename Output>
+    void xread(const StringView &key,
+                const StringView &id,
+                const std::chrono::milliseconds &timeout,
+                Output output) {
+        xread(key, id, timeout, 0, output);
+    }
+
+    template <typename Input, typename Output>
+    auto xread(Input first,
+                Input last,
+                const std::chrono::milliseconds &timeout,
+                long long count,
+                Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type;
+
+    template <typename Input, typename Output>
+    auto xread(Input first,
+                Input last,
+                const std::chrono::milliseconds &timeout,
+                Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+        xread(first, last, timeout, 0, output);
+    }
+
+    template <typename Output>
+    void xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    const StringView &key,
+                    const StringView &id,
+                    long long count,
+                    bool noack,
+                    Output output);
+
+    template <typename Output>
+    void xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    const StringView &key,
+                    const StringView &id,
+                    long long count,
+                    Output output) {
+        xreadgroup(group, consumer, key, id, count, false, output);
+    }
+
+    template <typename Output>
+    void xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    const StringView &key,
+                    const StringView &id,
+                    Output output) {
+        xreadgroup(group, consumer, key, id, 0, false, output);
+    }
+
+    template <typename Input, typename Output>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last,
+                    long long count,
+                    bool noack,
+                    Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type;
+
+    template <typename Input, typename Output>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last,
+                    long long count,
+                    Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+        xreadgroup(group, consumer, first ,last, count, false, output);
+    }
+
+    template <typename Input, typename Output>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last,
+                    Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+        xreadgroup(group, consumer, first ,last, 0, false, output);
+    }
+
+    template <typename Output>
+    void xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    const StringView &key,
+                    const StringView &id,
+                    const std::chrono::milliseconds &timeout,
+                    long long count,
+                    bool noack,
+                    Output output);
+
+    template <typename Output>
+    void xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    const StringView &key,
+                    const StringView &id,
+                    const std::chrono::milliseconds &timeout,
+                    long long count,
+                    Output output) {
+        return xreadgroup(group, consumer, key, id, timeout, count, false, output);
+    }
+
+    template <typename Output>
+    void xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    const StringView &key,
+                    const StringView &id,
+                    const std::chrono::milliseconds &timeout,
+                    Output output) {
+        return xreadgroup(group, consumer, key, id, timeout, 0, false, output);
+    }
+
+    template <typename Input, typename Output>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last,
+                    const std::chrono::milliseconds &timeout,
+                    long long count,
+                    bool noack,
+                    Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type;
+
+    template <typename Input, typename Output>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last,
+                    const std::chrono::milliseconds &timeout,
+                    long long count,
+                    Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+        xreadgroup(group, consumer, first, last, timeout, count, false, output);
+    }
+
+    template <typename Input, typename Output>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last,
+                    const std::chrono::milliseconds &timeout,
+                    Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+        xreadgroup(group, consumer, first, last, timeout, 0, false, output);
+    }
+
+    template <typename Output>
+    void xrevrange(const StringView &key,
+                    const StringView &end,
+                    const StringView &start,
+                    Output output);
+
+    template <typename Output>
+    void xrevrange(const StringView &key,
+                    const StringView &end,
+                    const StringView &start,
+                    long long count,
+                    Output output);
+
+    long long xtrim(const StringView &key, long long count, bool approx = true);
+
+private:
+    class Command {
+    public:
+        explicit Command(const StringView &cmd_name) : _cmd_name(cmd_name) {}
+
+        template <typename ...Args>
+        void operator()(Connection &connection, Args &&...args) const {
+            CmdArgs cmd_args;
+            cmd_args.append(_cmd_name, std::forward<Args>(args)...);
+            connection.send(cmd_args);
+        }
+
+    private:
+        StringView _cmd_name;
+    };
+
+    template <typename Cmd, typename Key, typename ...Args>
+    auto _generic_command(Cmd cmd, Key &&key, Args &&...args)
+        -> typename std::enable_if<std::is_convertible<Key, StringView>::value,
+                                    ReplyUPtr>::type;
+
+    template <typename Cmd, typename Key, typename ...Args>
+    auto _generic_command(Cmd cmd, Key &&key, Args &&...args)
+        -> typename std::enable_if<std::is_arithmetic<typename std::decay<Key>::type>::value,
+                                    ReplyUPtr>::type;
+
+    template <typename Cmd, typename ...Args>
+    ReplyUPtr _command(Cmd cmd, Connection &connection, Args &&...args);
+
+    template <typename Cmd, typename ...Args>
+    ReplyUPtr _command(Cmd cmd, const StringView &key, Args &&...args);
+
+    template <typename Cmd, typename ...Args>
+    ReplyUPtr _command(Cmd cmd, std::true_type, const StringView &key, Args &&...args);
+
+    template <typename Cmd, typename Input, typename ...Args>
+    ReplyUPtr _command(Cmd cmd, std::false_type, Input &&first, Args &&...args);
+
+    template <std::size_t ...Is, typename ...Args>
+    ReplyUPtr _command(const StringView &cmd_name, const IndexSequence<Is...> &, Args &&...args) {
+        return command(cmd_name, NthValue<Is>(std::forward<Args>(args)...)...);
+    }
+
+    template <typename Cmd, typename Input, typename ...Args>
+    ReplyUPtr _range_command(Cmd cmd, std::true_type, Input input, Args &&...args);
+
+    template <typename Cmd, typename Input, typename ...Args>
+    ReplyUPtr _range_command(Cmd cmd, std::false_type, Input input, Args &&...args);
+
+    void _asking(Connection &connection);
+
+    template <typename Cmd, typename ...Args>
+    ReplyUPtr _score_command(std::true_type, Cmd cmd, Args &&... args);
+
+    template <typename Cmd, typename ...Args>
+    ReplyUPtr _score_command(std::false_type, Cmd cmd, Args &&... args);
+
+    template <typename Output, typename Cmd, typename ...Args>
+    ReplyUPtr _score_command(Cmd cmd, Args &&... args);
+
+    ShardsPool _pool;
+};
+
+}
+
+}
+
+#include "redis_cluster.hpp"
+
+#endif // end SEWENEW_REDISPLUSPLUS_REDIS_CLUSTER_H

+ 1415 - 0
ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/redis_cluster.hpp

@@ -0,0 +1,1415 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_REDIS_CLUSTER_HPP
+#define SEWENEW_REDISPLUSPLUS_REDIS_CLUSTER_HPP
+
+#include <utility>
+#include "command.h"
+#include "reply.h"
+#include "utils.h"
+#include "errors.h"
+#include "shards_pool.h"
+
+namespace sw {
+
+namespace redis {
+
+template <typename Cmd, typename Key, typename ...Args>
+auto RedisCluster::command(Cmd cmd, Key &&key, Args &&...args)
+    -> typename std::enable_if<!std::is_convertible<Cmd, StringView>::value, ReplyUPtr>::type {
+    return _command(cmd,
+                    std::is_convertible<typename std::decay<Key>::type, StringView>(),
+                    std::forward<Key>(key),
+                    std::forward<Args>(args)...);
+}
+
+template <typename Key, typename ...Args>
+auto RedisCluster::command(const StringView &cmd_name, Key &&key, Args &&...args)
+    -> typename std::enable_if<(std::is_convertible<Key, StringView>::value
+        || std::is_arithmetic<typename std::decay<Key>::type>::value)
+        && !IsIter<typename LastType<Key, Args...>::type>::value, ReplyUPtr>::type {
+    auto cmd = Command(cmd_name);
+
+    return _generic_command(cmd, std::forward<Key>(key), std::forward<Args>(args)...);
+}
+
+template <typename Result, typename Key, typename ...Args>
+auto RedisCluster::command(const StringView &cmd_name, Key &&key, Args &&...args)
+    -> typename std::enable_if<std::is_convertible<Key, StringView>::value
+            || std::is_arithmetic<typename std::decay<Key>::type>::value, Result>::type {
+    auto r = command(cmd_name, std::forward<Key>(key), std::forward<Args>(args)...);
+
+    assert(r);
+
+    return reply::parse<Result>(*r);
+}
+
+template <typename Key, typename ...Args>
+auto RedisCluster::command(const StringView &cmd_name, Key &&key, Args &&...args)
+    -> typename std::enable_if<(std::is_convertible<Key, StringView>::value
+            || std::is_arithmetic<typename std::decay<Key>::type>::value)
+            && IsIter<typename LastType<Key, Args...>::type>::value, void>::type {
+    auto r = _command(cmd_name,
+                        MakeIndexSequence<sizeof...(Args)>(),
+                        std::forward<Key>(key),
+                        std::forward<Args>(args)...);
+
+    assert(r);
+
+    reply::to_array(*r, LastValue(std::forward<Args>(args)...));
+}
+
+template <typename Input>
+auto RedisCluster::command(Input first, Input last)
+    -> typename std::enable_if<IsIter<Input>::value, ReplyUPtr>::type {
+    if (first == last || std::next(first) == last) {
+        throw Error("command: invalid range");
+    }
+
+    const auto &key = *first;
+    ++first;
+
+    auto cmd = [&key](Connection &connection, Input first, Input last) {
+                        CmdArgs cmd_args;
+                        cmd_args.append(key);
+                        while (first != last) {
+                            cmd_args.append(*first);
+                            ++first;
+                        }
+                        connection.send(cmd_args);
+    };
+
+    return command(cmd, first, last);
+}
+
+template <typename Result, typename Input>
+auto RedisCluster::command(Input first, Input last)
+    -> typename std::enable_if<IsIter<Input>::value, Result>::type {
+    auto r = command(first, last);
+
+    assert(r);
+
+    return reply::parse<Result>(*r);
+}
+
+template <typename Input, typename Output>
+auto RedisCluster::command(Input first, Input last, Output output)
+    -> typename std::enable_if<IsIter<Input>::value, void>::type {
+    auto r = command(first, last);
+
+    assert(r);
+
+    reply::to_array(*r, output);
+}
+
+// KEY commands.
+
+template <typename Input>
+long long RedisCluster::del(Input first, Input last) {
+    if (first == last) {
+        throw Error("DEL: no key specified");
+    }
+
+    auto reply = command(cmd::del_range<Input>, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input>
+long long RedisCluster::exists(Input first, Input last) {
+    if (first == last) {
+        throw Error("EXISTS: no key specified");
+    }
+
+    auto reply = command(cmd::exists_range<Input>, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+inline bool RedisCluster::expire(const StringView &key, const std::chrono::seconds &timeout) {
+    return expire(key, timeout.count());
+}
+
+inline bool RedisCluster::expireat(const StringView &key,
+                                    const std::chrono::time_point<std::chrono::system_clock,
+                                                                    std::chrono::seconds> &tp) {
+    return expireat(key, tp.time_since_epoch().count());
+}
+
+inline bool RedisCluster::pexpire(const StringView &key, const std::chrono::milliseconds &timeout) {
+    return pexpire(key, timeout.count());
+}
+
+inline bool RedisCluster::pexpireat(const StringView &key,
+                                const std::chrono::time_point<std::chrono::system_clock,
+                                                                std::chrono::milliseconds> &tp) {
+    return pexpireat(key, tp.time_since_epoch().count());
+}
+
+inline void RedisCluster::restore(const StringView &key,
+                            const StringView &val,
+                            const std::chrono::milliseconds &ttl,
+                            bool replace) {
+    return restore(key, val, ttl.count(), replace);
+}
+
+template <typename Input>
+long long RedisCluster::touch(Input first, Input last) {
+    if (first == last) {
+        throw Error("TOUCH: no key specified");
+    }
+
+    auto reply = command(cmd::touch_range<Input>, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input>
+long long RedisCluster::unlink(Input first, Input last) {
+    if (first == last) {
+        throw Error("UNLINK: no key specified");
+    }
+
+    auto reply = command(cmd::unlink_range<Input>, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+// STRING commands.
+
+template <typename Input>
+long long RedisCluster::bitop(BitOp op, const StringView &destination, Input first, Input last) {
+    if (first == last) {
+        throw Error("BITOP: no key specified");
+    }
+
+    auto reply = _command(cmd::bitop_range<Input>, destination, op, destination, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input, typename Output>
+void RedisCluster::mget(Input first, Input last, Output output) {
+    if (first == last) {
+        throw Error("MGET: no key specified");
+    }
+
+    auto reply = command(cmd::mget<Input>, first, last);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+void RedisCluster::mset(Input first, Input last) {
+    if (first == last) {
+        throw Error("MSET: no key specified");
+    }
+
+    auto reply = command(cmd::mset<Input>, first, last);
+
+    reply::parse<void>(*reply);
+}
+
+template <typename Input>
+bool RedisCluster::msetnx(Input first, Input last) {
+    if (first == last) {
+        throw Error("MSETNX: no key specified");
+    }
+
+    auto reply = command(cmd::msetnx<Input>, first, last);
+
+    return reply::parse<bool>(*reply);
+}
+
+inline void RedisCluster::psetex(const StringView &key,
+                            const std::chrono::milliseconds &ttl,
+                            const StringView &val) {
+    return psetex(key, ttl.count(), val);
+}
+
+inline void RedisCluster::setex(const StringView &key,
+                            const std::chrono::seconds &ttl,
+                            const StringView &val) {
+    setex(key, ttl.count(), val);
+}
+
+// LIST commands.
+
+template <typename Input>
+OptionalStringPair RedisCluster::blpop(Input first, Input last, long long timeout) {
+    if (first == last) {
+        throw Error("BLPOP: no key specified");
+    }
+
+    auto reply = command(cmd::blpop_range<Input>, first, last, timeout);
+
+    return reply::parse<OptionalStringPair>(*reply);
+}
+
+template <typename Input>
+OptionalStringPair RedisCluster::blpop(Input first,
+                                Input last,
+                                const std::chrono::seconds &timeout) {
+    return blpop(first, last, timeout.count());
+}
+
+template <typename Input>
+OptionalStringPair RedisCluster::brpop(Input first, Input last, long long timeout) {
+    if (first == last) {
+        throw Error("BRPOP: no key specified");
+    }
+
+    auto reply = command(cmd::brpop_range<Input>, first, last, timeout);
+
+    return reply::parse<OptionalStringPair>(*reply);
+}
+
+template <typename Input>
+OptionalStringPair RedisCluster::brpop(Input first,
+                                Input last,
+                                const std::chrono::seconds &timeout) {
+    return brpop(first, last, timeout.count());
+}
+
+inline OptionalString RedisCluster::brpoplpush(const StringView &source,
+                                        const StringView &destination,
+                                        const std::chrono::seconds &timeout) {
+    return brpoplpush(source, destination, timeout.count());
+}
+
+template <typename Input>
+inline long long RedisCluster::lpush(const StringView &key, Input first, Input last) {
+    if (first == last) {
+        throw Error("LPUSH: no key specified");
+    }
+
+    auto reply = command(cmd::lpush_range<Input>, key, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Output>
+inline void RedisCluster::lrange(const StringView &key, long long start, long long stop, Output output) {
+    auto reply = command(cmd::lrange, key, start, stop);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+inline long long RedisCluster::rpush(const StringView &key, Input first, Input last) {
+    if (first == last) {
+        throw Error("RPUSH: no key specified");
+    }
+
+    auto reply = command(cmd::rpush_range<Input>, key, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+// HASH commands.
+
+template <typename Input>
+inline long long RedisCluster::hdel(const StringView &key, Input first, Input last) {
+    if (first == last) {
+        throw Error("HDEL: no key specified");
+    }
+
+    auto reply = command(cmd::hdel_range<Input>, key, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Output>
+inline void RedisCluster::hgetall(const StringView &key, Output output) {
+    auto reply = command(cmd::hgetall, key);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+inline void RedisCluster::hkeys(const StringView &key, Output output) {
+    auto reply = command(cmd::hkeys, key);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input, typename Output>
+inline void RedisCluster::hmget(const StringView &key, Input first, Input last, Output output) {
+    if (first == last) {
+        throw Error("HMGET: no key specified");
+    }
+
+    auto reply = command(cmd::hmget<Input>, key, first, last);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+inline void RedisCluster::hmset(const StringView &key, Input first, Input last) {
+    if (first == last) {
+        throw Error("HMSET: no key specified");
+    }
+
+    auto reply = command(cmd::hmset<Input>, key, first, last);
+
+    reply::parse<void>(*reply);
+}
+
+template <typename Output>
+long long RedisCluster::hscan(const StringView &key,
+                        long long cursor,
+                        const StringView &pattern,
+                        long long count,
+                        Output output) {
+    auto reply = command(cmd::hscan, key, cursor, pattern, count);
+
+    return reply::parse_scan_reply(*reply, output);
+}
+
+template <typename Output>
+inline long long RedisCluster::hscan(const StringView &key,
+                                long long cursor,
+                                const StringView &pattern,
+                                Output output) {
+    return hscan(key, cursor, pattern, 10, output);
+}
+
+template <typename Output>
+inline long long RedisCluster::hscan(const StringView &key,
+                                long long cursor,
+                                long long count,
+                                Output output) {
+    return hscan(key, cursor, "*", count, output);
+}
+
+template <typename Output>
+inline long long RedisCluster::hscan(const StringView &key,
+                                long long cursor,
+                                Output output) {
+    return hscan(key, cursor, "*", 10, output);
+}
+
+template <typename Output>
+inline void RedisCluster::hvals(const StringView &key, Output output) {
+    auto reply = command(cmd::hvals, key);
+
+    reply::to_array(*reply, output);
+}
+
+// SET commands.
+
+template <typename Input>
+long long RedisCluster::sadd(const StringView &key, Input first, Input last) {
+    if (first == last) {
+        throw Error("SADD: no key specified");
+    }
+
+    auto reply = command(cmd::sadd_range<Input>, key, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input, typename Output>
+void RedisCluster::sdiff(Input first, Input last, Output output) {
+    if (first == last) {
+        throw Error("SDIFF: no key specified");
+    }
+
+    auto reply = command(cmd::sdiff<Input>, first, last);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+long long RedisCluster::sdiffstore(const StringView &destination,
+                                    Input first,
+                                    Input last) {
+    if (first == last) {
+        throw Error("SDIFFSTORE: no key specified");
+    }
+
+    auto reply = command(cmd::sdiffstore_range<Input>, destination, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input, typename Output>
+void RedisCluster::sinter(Input first, Input last, Output output) {
+    if (first == last) {
+        throw Error("SINTER: no key specified");
+    }
+
+    auto reply = command(cmd::sinter<Input>, first, last);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+long long RedisCluster::sinterstore(const StringView &destination,
+                                    Input first,
+                                    Input last) {
+    if (first == last) {
+        throw Error("SINTERSTORE: no key specified");
+    }
+
+    auto reply = command(cmd::sinterstore_range<Input>, destination, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Output>
+void RedisCluster::smembers(const StringView &key, Output output) {
+    auto reply = command(cmd::smembers, key);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void RedisCluster::spop(const StringView &key, long long count, Output output) {
+    auto reply = command(cmd::spop_range, key, count);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void RedisCluster::srandmember(const StringView &key, long long count, Output output) {
+    auto reply = command(cmd::srandmember_range, key, count);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+long long RedisCluster::srem(const StringView &key, Input first, Input last) {
+    if (first == last) {
+        throw Error("SREM: no key specified");
+    }
+
+    auto reply = command(cmd::srem_range<Input>, key, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Output>
+long long RedisCluster::sscan(const StringView &key,
+                        long long cursor,
+                        const StringView &pattern,
+                        long long count,
+                        Output output) {
+    auto reply = command(cmd::sscan, key, cursor, pattern, count);
+
+    return reply::parse_scan_reply(*reply, output);
+}
+
+template <typename Output>
+inline long long RedisCluster::sscan(const StringView &key,
+                                long long cursor,
+                                const StringView &pattern,
+                                Output output) {
+    return sscan(key, cursor, pattern, 10, output);
+}
+
+template <typename Output>
+inline long long RedisCluster::sscan(const StringView &key,
+                                long long cursor,
+                                long long count,
+                                Output output) {
+    return sscan(key, cursor, "*", count, output);
+}
+
+template <typename Output>
+inline long long RedisCluster::sscan(const StringView &key,
+                                long long cursor,
+                                Output output) {
+    return sscan(key, cursor, "*", 10, output);
+}
+
+template <typename Input, typename Output>
+void RedisCluster::sunion(Input first, Input last, Output output) {
+    if (first == last) {
+        throw Error("SUNION: no key specified");
+    }
+
+    auto reply = command(cmd::sunion<Input>, first, last);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+long long RedisCluster::sunionstore(const StringView &destination, Input first, Input last) {
+    if (first == last) {
+        throw Error("SUNIONSTORE: no key specified");
+    }
+
+    auto reply = command(cmd::sunionstore_range<Input>, destination, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+// SORTED SET commands.
+
+inline auto RedisCluster::bzpopmax(const StringView &key, const std::chrono::seconds &timeout)
+    -> Optional<std::tuple<std::string, std::string, double>> {
+    return bzpopmax(key, timeout.count());
+}
+
+template <typename Input>
+auto RedisCluster::bzpopmax(Input first, Input last, long long timeout)
+    -> Optional<std::tuple<std::string, std::string, double>> {
+    auto reply = command(cmd::bzpopmax_range<Input>, first, last, timeout);
+
+    return reply::parse<Optional<std::tuple<std::string, std::string, double>>>(*reply);
+}
+
+template <typename Input>
+inline auto RedisCluster::bzpopmax(Input first,
+                                    Input last,
+                                    const std::chrono::seconds &timeout)
+    -> Optional<std::tuple<std::string, std::string, double>> {
+    return bzpopmax(first, last, timeout.count());
+}
+
+inline auto RedisCluster::bzpopmin(const StringView &key, const std::chrono::seconds &timeout)
+    -> Optional<std::tuple<std::string, std::string, double>> {
+    return bzpopmin(key, timeout.count());
+}
+
+template <typename Input>
+auto RedisCluster::bzpopmin(Input first, Input last, long long timeout)
+    -> Optional<std::tuple<std::string, std::string, double>> {
+    auto reply = command(cmd::bzpopmin_range<Input>, first, last, timeout);
+
+    return reply::parse<Optional<std::tuple<std::string, std::string, double>>>(*reply);
+}
+
+template <typename Input>
+inline auto RedisCluster::bzpopmin(Input first,
+                                    Input last,
+                                    const std::chrono::seconds &timeout)
+    -> Optional<std::tuple<std::string, std::string, double>> {
+    return bzpopmin(first, last, timeout.count());
+}
+
+template <typename Input>
+long long RedisCluster::zadd(const StringView &key,
+                        Input first,
+                        Input last,
+                        UpdateType type,
+                        bool changed) {
+    if (first == last) {
+        throw Error("ZADD: no key specified");
+    }
+
+    auto reply = command(cmd::zadd_range<Input>, key, first, last, type, changed);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Interval>
+long long RedisCluster::zcount(const StringView &key, const Interval &interval) {
+    auto reply = command(cmd::zcount<Interval>, key, interval);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input>
+long long RedisCluster::zinterstore(const StringView &destination,
+                                Input first,
+                                Input last,
+                                Aggregation type) {
+    if (first == last) {
+        throw Error("ZINTERSTORE: no key specified");
+    }
+
+    auto reply = command(cmd::zinterstore_range<Input>,
+                            destination,
+                            first,
+                            last,
+                            type);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Interval>
+long long RedisCluster::zlexcount(const StringView &key, const Interval &interval) {
+    auto reply = command(cmd::zlexcount<Interval>, key, interval);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Output>
+void RedisCluster::zpopmax(const StringView &key, long long count, Output output) {
+    auto reply = command(cmd::zpopmax, key, count);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void RedisCluster::zpopmin(const StringView &key, long long count, Output output) {
+    auto reply = command(cmd::zpopmin, key, count);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void RedisCluster::zrange(const StringView &key, long long start, long long stop, Output output) {
+    auto reply = _score_command<Output>(cmd::zrange, key, start, stop);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Interval, typename Output>
+void RedisCluster::zrangebylex(const StringView &key, const Interval &interval, Output output) {
+    zrangebylex(key, interval, {}, output);
+}
+
+template <typename Interval, typename Output>
+void RedisCluster::zrangebylex(const StringView &key,
+                        const Interval &interval,
+                        const LimitOptions &opts,
+                        Output output) {
+    auto reply = command(cmd::zrangebylex<Interval>, key, interval, opts);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Interval, typename Output>
+void RedisCluster::zrangebyscore(const StringView &key,
+                            const Interval &interval,
+                            Output output) {
+    zrangebyscore(key, interval, {}, output);
+}
+
+template <typename Interval, typename Output>
+void RedisCluster::zrangebyscore(const StringView &key,
+                            const Interval &interval,
+                            const LimitOptions &opts,
+                            Output output) {
+    auto reply = _score_command<Output>(cmd::zrangebyscore<Interval>,
+                                        key,
+                                        interval,
+                                        opts);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+long long RedisCluster::zrem(const StringView &key, Input first, Input last) {
+    if (first == last) {
+        throw Error("ZREM: no key specified");
+    }
+
+    auto reply = command(cmd::zrem_range<Input>, key, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Interval>
+long long RedisCluster::zremrangebylex(const StringView &key, const Interval &interval) {
+    auto reply = command(cmd::zremrangebylex<Interval>, key, interval);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Interval>
+long long RedisCluster::zremrangebyscore(const StringView &key, const Interval &interval) {
+    auto reply = command(cmd::zremrangebyscore<Interval>, key, interval);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Output>
+void RedisCluster::zrevrange(const StringView &key, long long start, long long stop, Output output) {
+    auto reply = _score_command<Output>(cmd::zrevrange, key, start, stop);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Interval, typename Output>
+inline void RedisCluster::zrevrangebylex(const StringView &key,
+                                    const Interval &interval,
+                                    Output output) {
+    zrevrangebylex(key, interval, {}, output);
+}
+
+template <typename Interval, typename Output>
+void RedisCluster::zrevrangebylex(const StringView &key,
+                            const Interval &interval,
+                            const LimitOptions &opts,
+                            Output output) {
+    auto reply = command(cmd::zrevrangebylex<Interval>, key, interval, opts);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Interval, typename Output>
+void RedisCluster::zrevrangebyscore(const StringView &key, const Interval &interval, Output output) {
+    zrevrangebyscore(key, interval, {}, output);
+}
+
+template <typename Interval, typename Output>
+void RedisCluster::zrevrangebyscore(const StringView &key,
+                                const Interval &interval,
+                                const LimitOptions &opts,
+                                Output output) {
+    auto reply = _score_command<Output>(cmd::zrevrangebyscore<Interval>, key, interval, opts);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+long long RedisCluster::zscan(const StringView &key,
+                        long long cursor,
+                        const StringView &pattern,
+                        long long count,
+                        Output output) {
+    auto reply = command(cmd::zscan, key, cursor, pattern, count);
+
+    return reply::parse_scan_reply(*reply, output);
+}
+
+template <typename Output>
+inline long long RedisCluster::zscan(const StringView &key,
+                                long long cursor,
+                                const StringView &pattern,
+                                Output output) {
+    return zscan(key, cursor, pattern, 10, output);
+}
+
+template <typename Output>
+inline long long RedisCluster::zscan(const StringView &key,
+                                long long cursor,
+                                long long count,
+                                Output output) {
+    return zscan(key, cursor, "*", count, output);
+}
+
+template <typename Output>
+inline long long RedisCluster::zscan(const StringView &key,
+                                long long cursor,
+                                Output output) {
+    return zscan(key, cursor, "*", 10, output);
+}
+
+template <typename Input>
+long long RedisCluster::zunionstore(const StringView &destination,
+                                    Input first,
+                                    Input last,
+                                    Aggregation type) {
+    if (first == last) {
+        throw Error("ZUNIONSTORE: no key specified");
+    }
+
+    auto reply = command(cmd::zunionstore_range<Input>,
+                            destination,
+                            first,
+                            last,
+                            type);
+
+    return reply::parse<long long>(*reply);
+}
+
+// HYPERLOGLOG commands.
+
+template <typename Input>
+bool RedisCluster::pfadd(const StringView &key, Input first, Input last) {
+    if (first == last) {
+        throw Error("PFADD: no key specified");
+    }
+
+    auto reply = command(cmd::pfadd_range<Input>, key, first, last);
+
+    return reply::parse<bool>(*reply);
+}
+
+template <typename Input>
+long long RedisCluster::pfcount(Input first, Input last) {
+    if (first == last) {
+        throw Error("PFCOUNT: no key specified");
+    }
+
+    auto reply = command(cmd::pfcount_range<Input>, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input>
+void RedisCluster::pfmerge(const StringView &destination,
+                    Input first,
+                    Input last) {
+    if (first == last) {
+        throw Error("PFMERGE: no key specified");
+    }
+
+    auto reply = command(cmd::pfmerge_range<Input>, destination, first, last);
+
+    reply::parse<void>(*reply);
+}
+
+// GEO commands.
+
+template <typename Input>
+inline long long RedisCluster::geoadd(const StringView &key,
+                                Input first,
+                                Input last) {
+    if (first == last) {
+        throw Error("GEOADD: no key specified");
+    }
+
+    auto reply = command(cmd::geoadd_range<Input>, key, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input, typename Output>
+void RedisCluster::geohash(const StringView &key, Input first, Input last, Output output) {
+    if (first == last) {
+        throw Error("GEOHASH: no key specified");
+    }
+
+    auto reply = command(cmd::geohash_range<Input>, key, first, last);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input, typename Output>
+void RedisCluster::geopos(const StringView &key, Input first, Input last, Output output) {
+    if (first == last) {
+        throw Error("GEOPOS: no key specified");
+    }
+
+    auto reply = command(cmd::geopos_range<Input>, key, first, last);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void RedisCluster::georadius(const StringView &key,
+                        const std::pair<double, double> &loc,
+                        double radius,
+                        GeoUnit unit,
+                        long long count,
+                        bool asc,
+                        Output output) {
+    auto reply = command(cmd::georadius,
+                            key,
+                            loc,
+                            radius,
+                            unit,
+                            count,
+                            asc,
+                            WithCoord<typename IterType<Output>::type>::value,
+                            WithDist<typename IterType<Output>::type>::value,
+                            WithHash<typename IterType<Output>::type>::value);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void RedisCluster::georadiusbymember(const StringView &key,
+                                const StringView &member,
+                                double radius,
+                                GeoUnit unit,
+                                long long count,
+                                bool asc,
+                                Output output) {
+    auto reply = command(cmd::georadiusbymember,
+                            key,
+                            member,
+                            radius,
+                            unit,
+                            count,
+                            asc,
+                            WithCoord<typename IterType<Output>::type>::value,
+                            WithDist<typename IterType<Output>::type>::value,
+                            WithHash<typename IterType<Output>::type>::value);
+
+    reply::to_array(*reply, output);
+}
+
+// SCRIPTING commands.
+
+template <typename Result>
+Result RedisCluster::eval(const StringView &script,
+                            std::initializer_list<StringView> keys,
+                            std::initializer_list<StringView> args) {
+    if (keys.size() == 0) {
+        throw Error("DO NOT support Lua script without key");
+    }
+
+    auto reply = _command(cmd::eval, *keys.begin(), script, keys, args);
+
+    return reply::parse<Result>(*reply);
+}
+
+template <typename Output>
+void RedisCluster::eval(const StringView &script,
+                        std::initializer_list<StringView> keys,
+                        std::initializer_list<StringView> args,
+                        Output output) {
+    if (keys.size() == 0) {
+        throw Error("DO NOT support Lua script without key");
+    }
+
+    auto reply = _command(cmd::eval, *keys.begin(), script, keys, args);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Result>
+Result RedisCluster::evalsha(const StringView &script,
+                                std::initializer_list<StringView> keys,
+                                std::initializer_list<StringView> args) {
+    if (keys.size() == 0) {
+        throw Error("DO NOT support Lua script without key");
+    }
+
+    auto reply = _command(cmd::evalsha, *keys.begin(), script, keys, args);
+
+    return reply::parse<Result>(*reply);
+}
+
+template <typename Output>
+void RedisCluster::evalsha(const StringView &script,
+                            std::initializer_list<StringView> keys,
+                            std::initializer_list<StringView> args,
+                            Output output) {
+    if (keys.size() == 0) {
+        throw Error("DO NOT support Lua script without key");
+    }
+
+    auto reply = command(cmd::evalsha, *keys.begin(), script, keys, args);
+
+    reply::to_array(*reply, output);
+}
+
+// Stream commands.
+
+template <typename Input>
+long long RedisCluster::xack(const StringView &key,
+                                const StringView &group,
+                                Input first,
+                                Input last) {
+    auto reply = command(cmd::xack_range<Input>, key, group, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input>
+std::string RedisCluster::xadd(const StringView &key,
+                                const StringView &id,
+                                Input first,
+                                Input last) {
+    auto reply = command(cmd::xadd_range<Input>, key, id, first, last);
+
+    return reply::parse<std::string>(*reply);
+}
+
+template <typename Input>
+std::string RedisCluster::xadd(const StringView &key,
+                                const StringView &id,
+                                Input first,
+                                Input last,
+                                long long count,
+                                bool approx) {
+    auto reply = command(cmd::xadd_maxlen_range<Input>, key, id, first, last, count, approx);
+
+    return reply::parse<std::string>(*reply);
+}
+
+template <typename Output>
+void RedisCluster::xclaim(const StringView &key,
+                            const StringView &group,
+                            const StringView &consumer,
+                            const std::chrono::milliseconds &min_idle_time,
+                            const StringView &id,
+                            Output output) {
+    auto reply = command(cmd::xclaim, key, group, consumer, min_idle_time.count(), id);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input, typename Output>
+void RedisCluster::xclaim(const StringView &key,
+                            const StringView &group,
+                            const StringView &consumer,
+                            const std::chrono::milliseconds &min_idle_time,
+                            Input first,
+                            Input last,
+                            Output output) {
+    auto reply = command(cmd::xclaim_range<Input>,
+                            key,
+                            group,
+                            consumer,
+                            min_idle_time.count(),
+                            first,
+                            last);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+long long RedisCluster::xdel(const StringView &key, Input first, Input last) {
+    auto reply = command(cmd::xdel_range<Input>, key, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Output>
+auto RedisCluster::xpending(const StringView &key, const StringView &group, Output output)
+    -> std::tuple<long long, OptionalString, OptionalString> {
+    auto reply = command(cmd::xpending, key, group);
+
+    return reply::parse_xpending_reply(*reply, output);
+}
+
+template <typename Output>
+void RedisCluster::xpending(const StringView &key,
+                            const StringView &group,
+                            const StringView &start,
+                            const StringView &end,
+                            long long count,
+                            Output output) {
+    auto reply = command(cmd::xpending_detail, key, group, start, end, count);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void RedisCluster::xpending(const StringView &key,
+                            const StringView &group,
+                            const StringView &start,
+                            const StringView &end,
+                            long long count,
+                            const StringView &consumer,
+                            Output output) {
+    auto reply = command(cmd::xpending_per_consumer, key, group, start, end, count, consumer);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void RedisCluster::xrange(const StringView &key,
+                            const StringView &start,
+                            const StringView &end,
+                            Output output) {
+    auto reply = command(cmd::xrange, key, start, end);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void RedisCluster::xrange(const StringView &key,
+                            const StringView &start,
+                            const StringView &end,
+                            long long count,
+                            Output output) {
+    auto reply = command(cmd::xrange_count, key, start, end, count);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void RedisCluster::xread(const StringView &key,
+                            const StringView &id,
+                            long long count,
+                            Output output) {
+    auto reply = command(cmd::xread, key, id, count);
+
+    if (!reply::is_nil(*reply)) {
+        reply::to_array(*reply, output);
+    }
+}
+
+template <typename Input, typename Output>
+auto RedisCluster::xread(Input first, Input last, long long count, Output output)
+    -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+    if (first == last) {
+        throw Error("XREAD: no key specified");
+    }
+
+    auto reply = command(cmd::xread_range<Input>, first, last, count);
+
+    if (!reply::is_nil(*reply)) {
+        reply::to_array(*reply, output);
+    }
+}
+
+template <typename Output>
+void RedisCluster::xread(const StringView &key,
+                            const StringView &id,
+                            const std::chrono::milliseconds &timeout,
+                            long long count,
+                            Output output) {
+    auto reply = command(cmd::xread_block, key, id, timeout.count(), count);
+
+    if (!reply::is_nil(*reply)) {
+        reply::to_array(*reply, output);
+    }
+}
+
+template <typename Input, typename Output>
+auto RedisCluster::xread(Input first,
+                            Input last,
+                            const std::chrono::milliseconds &timeout,
+                            long long count,
+                            Output output)
+    -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+    if (first == last) {
+        throw Error("XREAD: no key specified");
+    }
+
+    auto reply = command(cmd::xread_block_range<Input>, first, last, timeout.count(), count);
+
+    if (!reply::is_nil(*reply)) {
+        reply::to_array(*reply, output);
+    }
+}
+
+template <typename Output>
+void RedisCluster::xreadgroup(const StringView &group,
+                                const StringView &consumer,
+                                const StringView &key,
+                                const StringView &id,
+                                long long count,
+                                bool noack,
+                                Output output) {
+    auto reply = _command(cmd::xreadgroup, key, group, consumer, key, id, count, noack);
+
+    if (!reply::is_nil(*reply)) {
+        reply::to_array(*reply, output);
+    }
+}
+
+template <typename Input, typename Output>
+auto RedisCluster::xreadgroup(const StringView &group,
+                                const StringView &consumer,
+                                Input first,
+                                Input last,
+                                long long count,
+                                bool noack,
+                                Output output)
+    -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+    if (first == last) {
+        throw Error("XREADGROUP: no key specified");
+    }
+
+    auto reply = _command(cmd::xreadgroup_range<Input>,
+                            first->first,
+                            group,
+                            consumer,
+                            first,
+                            last,
+                            count,
+                            noack);
+
+    if (!reply::is_nil(*reply)) {
+        reply::to_array(*reply, output);
+    }
+}
+
+template <typename Output>
+void RedisCluster::xreadgroup(const StringView &group,
+                                const StringView &consumer,
+                                const StringView &key,
+                                const StringView &id,
+                                const std::chrono::milliseconds &timeout,
+                                long long count,
+                                bool noack,
+                                Output output) {
+    auto reply = _command(cmd::xreadgroup_block,
+                            key,
+                            group,
+                            consumer,
+                            key,
+                            id,
+                            timeout.count(),
+                            count,
+                            noack);
+
+    if (!reply::is_nil(*reply)) {
+        reply::to_array(*reply, output);
+    }
+}
+
+template <typename Input, typename Output>
+auto RedisCluster::xreadgroup(const StringView &group,
+                                const StringView &consumer,
+                                Input first,
+                                Input last,
+                                const std::chrono::milliseconds &timeout,
+                                long long count,
+                                bool noack,
+                                Output output)
+    -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+    if (first == last) {
+        throw Error("XREADGROUP: no key specified");
+    }
+
+    auto reply = _command(cmd::xreadgroup_block_range<Input>,
+                            first->first,
+                            group,
+                            consumer,
+                            first,
+                            last,
+                            timeout.count(),
+                            count,
+                            noack);
+
+    if (!reply::is_nil(*reply)) {
+        reply::to_array(*reply, output);
+    }
+}
+
+template <typename Output>
+void RedisCluster::xrevrange(const StringView &key,
+                            const StringView &end,
+                            const StringView &start,
+                            Output output) {
+    auto reply = command(cmd::xrevrange, key, end, start);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void RedisCluster::xrevrange(const StringView &key,
+                                const StringView &end,
+                                const StringView &start,
+                                long long count,
+                                Output output) {
+    auto reply = command(cmd::xrevrange_count, key, end, start, count);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Cmd, typename Key, typename ...Args>
+auto RedisCluster::_generic_command(Cmd cmd, Key &&key, Args &&...args)
+    -> typename std::enable_if<std::is_convertible<Key, StringView>::value,
+                                ReplyUPtr>::type {
+    return command(cmd, std::forward<Key>(key), std::forward<Args>(args)...);
+}
+
+template <typename Cmd, typename Key, typename ...Args>
+auto RedisCluster::_generic_command(Cmd cmd, Key &&key, Args &&...args)
+    -> typename std::enable_if<std::is_arithmetic<typename std::decay<Key>::type>::value,
+                                ReplyUPtr>::type {
+    auto k = std::to_string(std::forward<Key>(key));
+    return command(cmd, k, std::forward<Args>(args)...);
+}
+
+template <typename Cmd, typename ...Args>
+ReplyUPtr RedisCluster::_command(Cmd cmd, std::true_type, const StringView &key, Args &&...args) {
+    return _command(cmd, key, key, std::forward<Args>(args)...);
+}
+
+template <typename Cmd, typename Input, typename ...Args>
+ReplyUPtr RedisCluster::_command(Cmd cmd, std::false_type, Input &&first, Args &&...args) {
+    return _range_command(cmd,
+                            std::is_convertible<
+                                typename std::decay<
+                                    decltype(*std::declval<Input>())>::type, StringView>(),
+                            std::forward<Input>(first),
+                            std::forward<Args>(args)...);
+}
+
+template <typename Cmd, typename Input, typename ...Args>
+ReplyUPtr RedisCluster::_range_command(Cmd cmd, std::true_type, Input input, Args &&...args) {
+    return _command(cmd, *input, input, std::forward<Args>(args)...);
+}
+
+template <typename Cmd, typename Input, typename ...Args>
+ReplyUPtr RedisCluster::_range_command(Cmd cmd, std::false_type, Input input, Args &&...args) {
+    return _command(cmd, std::get<0>(*input), input, std::forward<Args>(args)...);
+}
+
+template <typename Cmd, typename ...Args>
+ReplyUPtr RedisCluster::_command(Cmd cmd, Connection &connection, Args &&...args) {
+    assert(!connection.broken());
+
+    cmd(connection, std::forward<Args>(args)...);
+
+    return connection.recv();
+}
+
+template <typename Cmd, typename ...Args>
+ReplyUPtr RedisCluster::_command(Cmd cmd, const StringView &key, Args &&...args) {
+    for (auto idx = 0; idx < 2; ++idx) {
+        try {
+            auto guarded_connection = _pool.fetch(key);
+
+            return _command(cmd, guarded_connection.connection(), std::forward<Args>(args)...);
+        } catch (const IoError &err) {
+            // When master is down, one of its replicas will be promoted to be the new master.
+            // If we try to send command to the old master, we'll get an *IoError*.
+            // In this case, we need to update the slots mapping.
+            _pool.update();
+        } catch (const ClosedError &err) {
+            // Node might be removed.
+            // 1. Get up-to-date slot mapping to check if the node still exists.
+            _pool.update();
+
+            // TODO:
+            // 2. If it's NOT exist, update slot mapping, and retry.
+            // 3. If it's still exist, that means the node is down, NOT removed, throw exception.
+        } catch (const MovedError &err) {
+            // Slot mapping has been changed, update it and try again.
+            _pool.update();
+        } catch (const AskError &err) {
+            auto guarded_connection = _pool.fetch(err.node());
+            auto &connection = guarded_connection.connection();
+
+            // 1. send ASKING command.
+            _asking(connection);
+
+            // 2. resend last command.
+            try {
+                return _command(cmd, connection, std::forward<Args>(args)...);
+            } catch (const MovedError &err) {
+                throw Error("Slot migrating... ASKING node hasn't been set to IMPORTING state");
+            }
+        } // For other exceptions, just throw it.
+    }
+
+    // Possible failures:
+    // 1. Source node has already run 'CLUSTER SETSLOT xxx NODE xxx',
+    //    while the destination node has NOT run it.
+    //    In this case, client will be redirected by both nodes with MovedError.
+    // 2. Other failures...
+    throw Error("Failed to send command with key: " + std::string(key.data(), key.size()));
+}
+
+template <typename Cmd, typename ...Args>
+inline ReplyUPtr RedisCluster::_score_command(std::true_type, Cmd cmd, Args &&... args) {
+    return command(cmd, std::forward<Args>(args)..., true);
+}
+
+template <typename Cmd, typename ...Args>
+inline ReplyUPtr RedisCluster::_score_command(std::false_type, Cmd cmd, Args &&... args) {
+    return command(cmd, std::forward<Args>(args)..., false);
+}
+
+template <typename Output, typename Cmd, typename ...Args>
+inline ReplyUPtr RedisCluster::_score_command(Cmd cmd, Args &&... args) {
+    return _score_command(typename IsKvPairIter<Output>::type(),
+                            cmd,
+                            std::forward<Args>(args)...);
+}
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_REDIS_CLUSTER_HPP

+ 363 - 0
ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/reply.h

@@ -0,0 +1,363 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_REPLY_H
+#define SEWENEW_REDISPLUSPLUS_REPLY_H
+
+#include <cassert>
+#include <string>
+#include <memory>
+#include <functional>
+#include <tuple>
+#include <hiredis/hiredis.h>
+#include "errors.h"
+#include "utils.h"
+
+namespace sw {
+
+namespace redis {
+
+struct ReplyDeleter {
+    void operator()(redisReply *reply) const {
+        if (reply != nullptr) {
+            freeReplyObject(reply);
+        }
+    }
+};
+
+using ReplyUPtr = std::unique_ptr<redisReply, ReplyDeleter>;
+
+namespace reply {
+
+template <typename T>
+struct ParseTag {};
+
+template <typename T>
+inline T parse(redisReply &reply) {
+    return parse(ParseTag<T>(), reply);
+}
+
+void parse(ParseTag<void>, redisReply &reply);
+
+std::string parse(ParseTag<std::string>, redisReply &reply);
+
+long long parse(ParseTag<long long>, redisReply &reply);
+
+double parse(ParseTag<double>, redisReply &reply);
+
+bool parse(ParseTag<bool>, redisReply &reply);
+
+template <typename T>
+Optional<T> parse(ParseTag<Optional<T>>, redisReply &reply);
+
+template <typename T, typename U>
+std::pair<T, U> parse(ParseTag<std::pair<T, U>>, redisReply &reply);
+
+template <typename ...Args>
+std::tuple<Args...> parse(ParseTag<std::tuple<Args...>>, redisReply &reply);
+
+template <typename T, typename std::enable_if<IsSequenceContainer<T>::value, int>::type = 0>
+T parse(ParseTag<T>, redisReply &reply);
+
+template <typename T, typename std::enable_if<IsAssociativeContainer<T>::value, int>::type = 0>
+T parse(ParseTag<T>, redisReply &reply);
+
+template <typename Output>
+long long parse_scan_reply(redisReply &reply, Output output);
+
+inline bool is_error(redisReply &reply) {
+    return reply.type == REDIS_REPLY_ERROR;
+}
+
+inline bool is_nil(redisReply &reply) {
+    return reply.type == REDIS_REPLY_NIL;
+}
+
+inline bool is_string(redisReply &reply) {
+    return reply.type == REDIS_REPLY_STRING;
+}
+
+inline bool is_status(redisReply &reply) {
+    return reply.type == REDIS_REPLY_STATUS;
+}
+
+inline bool is_integer(redisReply &reply) {
+    return reply.type == REDIS_REPLY_INTEGER;
+}
+
+inline bool is_array(redisReply &reply) {
+    return reply.type == REDIS_REPLY_ARRAY;
+}
+
+std::string to_status(redisReply &reply);
+
+template <typename Output>
+void to_array(redisReply &reply, Output output);
+
+// Rewrite set reply to bool type
+void rewrite_set_reply(redisReply &reply);
+
+// Rewrite georadius reply to OptionalLongLong type
+void rewrite_georadius_reply(redisReply &reply);
+
+template <typename Output>
+auto parse_xpending_reply(redisReply &reply, Output output)
+    -> std::tuple<long long, OptionalString, OptionalString>;
+
+}
+
+// Inline implementations.
+
+namespace reply {
+
+namespace detail {
+
+template <typename Output>
+void to_array(redisReply &reply, Output output) {
+    if (!is_array(reply)) {
+        throw ProtoError("Expect ARRAY reply");
+    }
+
+    if (reply.element == nullptr) {
+        // Empty array.
+        return;
+    }
+
+    for (std::size_t idx = 0; idx != reply.elements; ++idx) {
+        auto *sub_reply = reply.element[idx];
+        if (sub_reply == nullptr) {
+            throw ProtoError("Null array element reply");
+        }
+
+        *output = parse<typename IterType<Output>::type>(*sub_reply);
+
+        ++output;
+    }
+}
+
+bool is_flat_array(redisReply &reply);
+
+template <typename Output>
+void to_flat_array(redisReply &reply, Output output) {
+    if (reply.element == nullptr) {
+        // Empty array.
+        return;
+    }
+
+    if (reply.elements % 2 != 0) {
+        throw ProtoError("Not string pair array reply");
+    }
+
+    for (std::size_t idx = 0; idx != reply.elements; idx += 2) {
+        auto *key_reply = reply.element[idx];
+        auto *val_reply = reply.element[idx + 1];
+        if (key_reply == nullptr || val_reply == nullptr) {
+            throw ProtoError("Null string array reply");
+        }
+
+        using Pair = typename IterType<Output>::type;
+        using FirstType = typename std::decay<typename Pair::first_type>::type;
+        using SecondType = typename std::decay<typename Pair::second_type>::type;
+        *output = std::make_pair(parse<FirstType>(*key_reply),
+                                    parse<SecondType>(*val_reply));
+
+        ++output;
+    }
+}
+
+template <typename Output>
+void to_array(std::true_type, redisReply &reply, Output output) {
+    if (is_flat_array(reply)) {
+        to_flat_array(reply, output);
+    } else {
+        to_array(reply, output);
+    }
+}
+
+template <typename Output>
+void to_array(std::false_type, redisReply &reply, Output output) {
+    to_array(reply, output);
+}
+
+template <typename T>
+std::tuple<T> parse_tuple(redisReply **reply, std::size_t idx) {
+    assert(reply != nullptr);
+
+    auto *sub_reply = reply[idx];
+    if (sub_reply == nullptr) {
+        throw ProtoError("Null reply");
+    }
+
+    return std::make_tuple(parse<T>(*sub_reply));
+}
+
+template <typename T, typename ...Args>
+auto parse_tuple(redisReply **reply, std::size_t idx) ->
+    typename std::enable_if<sizeof...(Args) != 0, std::tuple<T, Args...>>::type {
+    assert(reply != nullptr);
+
+    return std::tuple_cat(parse_tuple<T>(reply, idx),
+                            parse_tuple<Args...>(reply, idx + 1));
+}
+
+}
+
+template <typename T>
+Optional<T> parse(ParseTag<Optional<T>>, redisReply &reply) {
+    if (reply::is_nil(reply)) {
+        return {};
+    }
+
+    return Optional<T>(parse<T>(reply));
+}
+
+template <typename T, typename U>
+std::pair<T, U> parse(ParseTag<std::pair<T, U>>, redisReply &reply) {
+    if (!is_array(reply)) {
+        throw ProtoError("Expect ARRAY reply");
+    }
+
+    if (reply.elements != 2) {
+        throw ProtoError("NOT key-value PAIR reply");
+    }
+
+    if (reply.element == nullptr) {
+        throw ProtoError("Null PAIR reply");
+    }
+
+    auto *first = reply.element[0];
+    auto *second = reply.element[1];
+    if (first == nullptr || second == nullptr) {
+        throw ProtoError("Null pair reply");
+    }
+
+    return std::make_pair(parse<typename std::decay<T>::type>(*first),
+                            parse<typename std::decay<U>::type>(*second));
+}
+
+template <typename ...Args>
+std::tuple<Args...> parse(ParseTag<std::tuple<Args...>>, redisReply &reply) {
+    constexpr auto size = sizeof...(Args);
+
+    static_assert(size > 0, "DO NOT support parsing tuple with 0 element");
+
+    if (!is_array(reply)) {
+        throw ProtoError("Expect ARRAY reply");
+    }
+
+    if (reply.elements != size) {
+        throw ProtoError("Expect tuple reply with " + std::to_string(size) + "elements");
+    }
+
+    if (reply.element == nullptr) {
+        throw ProtoError("Null TUPLE reply");
+    }
+
+    return detail::parse_tuple<Args...>(reply.element, 0);
+}
+
+template <typename T, typename std::enable_if<IsSequenceContainer<T>::value, int>::type>
+T parse(ParseTag<T>, redisReply &reply) {
+    if (!is_array(reply)) {
+        throw ProtoError("Expect ARRAY reply");
+    }
+
+    T container;
+
+    to_array(reply, std::back_inserter(container));
+
+    return container;
+}
+
+template <typename T, typename std::enable_if<IsAssociativeContainer<T>::value, int>::type>
+T parse(ParseTag<T>, redisReply &reply) {
+    if (!is_array(reply)) {
+        throw ProtoError("Expect ARRAY reply");
+    }
+
+    T container;
+
+    to_array(reply, std::inserter(container, container.end()));
+
+    return container;
+}
+
+template <typename Output>
+long long parse_scan_reply(redisReply &reply, Output output) {
+    if (reply.elements != 2 || reply.element == nullptr) {
+        throw ProtoError("Invalid scan reply");
+    }
+
+    auto *cursor_reply = reply.element[0];
+    auto *data_reply = reply.element[1];
+    if (cursor_reply == nullptr || data_reply == nullptr) {
+        throw ProtoError("Invalid cursor reply or data reply");
+    }
+
+    auto cursor_str = reply::parse<std::string>(*cursor_reply);
+    auto new_cursor = 0;
+    try {
+        new_cursor = std::stoll(cursor_str);
+    } catch (const std::exception &e) {
+        throw ProtoError("Invalid cursor reply: " + cursor_str);
+    }
+
+    reply::to_array(*data_reply, output);
+
+    return new_cursor;
+}
+
+template <typename Output>
+void to_array(redisReply &reply, Output output) {
+    if (!is_array(reply)) {
+        throw ProtoError("Expect ARRAY reply");
+    }
+
+    detail::to_array(typename IsKvPairIter<Output>::type(), reply, output);
+}
+
+template <typename Output>
+auto parse_xpending_reply(redisReply &reply, Output output)
+    -> std::tuple<long long, OptionalString, OptionalString> {
+    if (!is_array(reply) || reply.elements != 4) {
+        throw ProtoError("expect array reply with 4 elements");
+    }
+
+    for (std::size_t idx = 0; idx != reply.elements; ++idx) {
+        if (reply.element[idx] == nullptr) {
+            throw ProtoError("null array reply");
+        }
+    }
+
+    auto num = parse<long long>(*(reply.element[0]));
+    auto start = parse<OptionalString>(*(reply.element[1]));
+    auto end = parse<OptionalString>(*(reply.element[2]));
+
+    auto &entry_reply = *(reply.element[3]);
+    if (!is_nil(entry_reply)) {
+        to_array(entry_reply, output);
+    }
+
+    return std::make_tuple(num, std::move(start), std::move(end));
+}
+
+}
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_REPLY_H

+ 138 - 0
ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/sentinel.h

@@ -0,0 +1,138 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_SENTINEL_H
+#define SEWENEW_REDISPLUSPLUS_SENTINEL_H
+
+#include <string>
+#include <list>
+#include <vector>
+#include <memory>
+#include <mutex>
+#include "connection.h"
+#include "shards.h"
+#include "reply.h"
+
+namespace sw {
+
+namespace redis {
+
+struct SentinelOptions {
+    std::vector<std::pair<std::string, int>> nodes;
+
+    std::string password;
+
+    bool keep_alive = true;
+
+    std::chrono::milliseconds connect_timeout{100};
+
+    std::chrono::milliseconds socket_timeout{100};
+
+    std::chrono::milliseconds retry_interval{100};
+
+    std::size_t max_retry = 2;
+};
+
+class Sentinel {
+public:
+    explicit Sentinel(const SentinelOptions &sentinel_opts);
+
+    Sentinel(const Sentinel &) = delete;
+    Sentinel& operator=(const Sentinel &) = delete;
+
+    Sentinel(Sentinel &&) = delete;
+    Sentinel& operator=(Sentinel &&) = delete;
+
+    ~Sentinel() = default;
+
+private:
+    Connection master(const std::string &master_name, const ConnectionOptions &opts);
+
+    Connection slave(const std::string &master_name, const ConnectionOptions &opts);
+
+    class Iterator;
+
+    friend class SimpleSentinel;
+
+    std::list<ConnectionOptions> _parse_options(const SentinelOptions &opts) const;
+
+    Optional<Node> _get_master_addr_by_name(Connection &connection, const StringView &name);
+
+    std::vector<Node> _get_slave_addr_by_name(Connection &connection, const StringView &name);
+
+    Connection _connect_redis(const Node &node, ConnectionOptions opts);
+
+    Role _get_role(Connection &connection);
+
+    std::vector<Node> _parse_slave_info(redisReply &reply) const;
+
+    std::list<Connection> _healthy_sentinels;
+
+    std::list<ConnectionOptions> _broken_sentinels;
+
+    SentinelOptions _sentinel_opts;
+
+    std::mutex _mutex;
+};
+
+class SimpleSentinel {
+public:
+    SimpleSentinel(const std::shared_ptr<Sentinel> &sentinel,
+                    const std::string &master_name,
+                    Role role);
+
+    SimpleSentinel() = default;
+
+    SimpleSentinel(const SimpleSentinel &) = default;
+    SimpleSentinel& operator=(const SimpleSentinel &) = default;
+
+    SimpleSentinel(SimpleSentinel &&) = default;
+    SimpleSentinel& operator=(SimpleSentinel &&) = default;
+
+    ~SimpleSentinel() = default;
+
+    explicit operator bool() const {
+        return bool(_sentinel);
+    }
+
+    Connection create(const ConnectionOptions &opts);
+
+private:
+    std::shared_ptr<Sentinel> _sentinel;
+
+    std::string _master_name;
+
+    Role _role = Role::MASTER;
+};
+
+class StopIterError : public Error {
+public:
+    StopIterError() : Error("StopIterError") {}
+
+    StopIterError(const StopIterError &) = default;
+    StopIterError& operator=(const StopIterError &) = default;
+
+    StopIterError(StopIterError &&) = default;
+    StopIterError& operator=(StopIterError &&) = default;
+
+    virtual ~StopIterError() = default;
+};
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_SENTINEL_H

+ 115 - 0
ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/shards.h

@@ -0,0 +1,115 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_SHARDS_H
+#define SEWENEW_REDISPLUSPLUS_SHARDS_H
+
+#include <string>
+#include <map>
+#include "errors.h"
+
+namespace sw {
+
+namespace redis {
+
+using Slot = std::size_t;
+
+struct SlotRange {
+    Slot min;
+    Slot max;
+};
+
+inline bool operator<(const SlotRange &lhs, const SlotRange &rhs) {
+    return lhs.max < rhs.max;
+}
+
+struct Node {
+    std::string host;
+    int port;
+};
+
+inline bool operator==(const Node &lhs, const Node &rhs) {
+    return lhs.host == rhs.host && lhs.port == rhs.port;
+}
+
+struct NodeHash {
+    std::size_t operator()(const Node &node) const noexcept {
+        auto host_hash = std::hash<std::string>{}(node.host);
+        auto port_hash = std::hash<int>{}(node.port);
+        return host_hash ^ (port_hash << 1);
+    }
+};
+
+using Shards = std::map<SlotRange, Node>;
+
+class RedirectionError : public ReplyError {
+public:
+    RedirectionError(const std::string &msg);
+
+    RedirectionError(const RedirectionError &) = default;
+    RedirectionError& operator=(const RedirectionError &) = default;
+
+    RedirectionError(RedirectionError &&) = default;
+    RedirectionError& operator=(RedirectionError &&) = default;
+
+    virtual ~RedirectionError() = default;
+
+    Slot slot() const {
+        return _slot;
+    }
+
+    const Node& node() const {
+        return _node;
+    }
+
+private:
+    std::pair<Slot, Node> _parse_error(const std::string &msg) const;
+
+    Slot _slot = 0;
+    Node _node;
+};
+
+class MovedError : public RedirectionError {
+public:
+    explicit MovedError(const std::string &msg) : RedirectionError(msg) {}
+
+    MovedError(const MovedError &) = default;
+    MovedError& operator=(const MovedError &) = default;
+
+    MovedError(MovedError &&) = default;
+    MovedError& operator=(MovedError &&) = default;
+
+    virtual ~MovedError() = default;
+};
+
+class AskError : public RedirectionError {
+public:
+    explicit AskError(const std::string &msg) : RedirectionError(msg) {}
+
+    AskError(const AskError &) = default;
+    AskError& operator=(const AskError &) = default;
+
+    AskError(AskError &&) = default;
+    AskError& operator=(AskError &&) = default;
+
+    virtual ~AskError() = default;
+};
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_SHARDS_H

+ 137 - 0
ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/shards_pool.h

@@ -0,0 +1,137 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_SHARDS_POOL_H
+#define SEWENEW_REDISPLUSPLUS_SHARDS_POOL_H
+
+#include <cassert>
+#include <unordered_map>
+#include <string>
+#include <random>
+#include <memory>
+#include "reply.h"
+#include "connection_pool.h"
+#include "shards.h"
+
+namespace sw {
+
+namespace redis {
+
+using ConnectionPoolSPtr = std::shared_ptr<ConnectionPool>;
+
+class GuardedConnection {
+public:
+    GuardedConnection(const ConnectionPoolSPtr &pool) : _pool(pool),
+                                                        _connection(_pool->fetch()) {
+        assert(!_connection.broken());
+    }
+
+    GuardedConnection(const GuardedConnection &) = delete;
+    GuardedConnection& operator=(const GuardedConnection &) = delete;
+
+    GuardedConnection(GuardedConnection &&) = default;
+    GuardedConnection& operator=(GuardedConnection &&) = default;
+
+    ~GuardedConnection() {
+        _pool->release(std::move(_connection));
+    }
+
+    Connection& connection() {
+        return _connection;
+    }
+
+private:
+    ConnectionPoolSPtr _pool;
+    Connection _connection;
+};
+
+class ShardsPool {
+public:
+    ShardsPool() = default;
+
+    ShardsPool(const ShardsPool &that) = delete;
+    ShardsPool& operator=(const ShardsPool &that) = delete;
+
+    ShardsPool(ShardsPool &&that);
+    ShardsPool& operator=(ShardsPool &&that);
+
+    ~ShardsPool() = default;
+
+    ShardsPool(const ConnectionPoolOptions &pool_opts,
+                const ConnectionOptions &connection_opts);
+
+    // Fetch a connection by key.
+    GuardedConnection fetch(const StringView &key);
+
+    // Randomly pick a connection.
+    GuardedConnection fetch();
+
+    // Fetch a connection by node.
+    GuardedConnection fetch(const Node &node);
+
+    void update();
+
+    ConnectionOptions connection_options(const StringView &key);
+
+    ConnectionOptions connection_options();
+
+private:
+    void _move(ShardsPool &&that);
+
+    void _init_pool(const Shards &shards);
+
+    Shards _cluster_slots(Connection &connection) const;
+
+    ReplyUPtr _cluster_slots_command(Connection &connection) const;
+
+    Shards _parse_reply(redisReply &reply) const;
+
+    std::pair<SlotRange, Node> _parse_slot_info(redisReply &reply) const;
+
+    // Get slot by key.
+    std::size_t _slot(const StringView &key) const;
+
+    // Randomly pick a slot.
+    std::size_t _slot() const;
+
+    ConnectionPoolSPtr& _get_pool(Slot slot);
+
+    GuardedConnection _fetch(Slot slot);
+
+    ConnectionOptions _connection_options(Slot slot);
+
+    using NodeMap = std::unordered_map<Node, ConnectionPoolSPtr, NodeHash>;
+
+    NodeMap::iterator _add_node(const Node &node);
+
+    ConnectionPoolOptions _pool_opts;
+
+    ConnectionOptions _connection_opts;
+
+    Shards _shards;
+
+    NodeMap _pools;
+
+    std::mutex _mutex;
+
+    static const std::size_t SHARDS = 16383;
+};
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_SHARDS_POOL_H

+ 231 - 0
ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/subscriber.h

@@ -0,0 +1,231 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_SUBSCRIBER_H
+#define SEWENEW_REDISPLUSPLUS_SUBSCRIBER_H
+
+#include <unordered_map>
+#include <string>
+#include <functional>
+#include "connection.h"
+#include "reply.h"
+#include "command.h"
+#include "utils.h"
+
+namespace sw {
+
+namespace redis {
+
+// @NOTE: Subscriber is NOT thread-safe.
+// Subscriber uses callbacks to handle messages. There are 6 kinds of messages:
+// 1) MESSAGE: message sent to a channel.
+// 2) PMESSAGE: message sent to channels of a given pattern.
+// 3) SUBSCRIBE: meta message sent when we successfully subscribe to a channel.
+// 4) UNSUBSCRIBE: meta message sent when we successfully unsubscribe to a channel.
+// 5) PSUBSCRIBE: meta message sent when we successfully subscribe to a channel pattern.
+// 6) PUNSUBSCRIBE: meta message sent when we successfully unsubscribe to a channel pattern.
+//
+// Use Subscriber::on_message(MsgCallback) to set the callback function for message of
+// *MESSAGE* type, and the callback interface is:
+// void (std::string channel, std::string msg)
+//
+// Use Subscriber::on_pmessage(PatternMsgCallback) to set the callback function for message of
+// *PMESSAGE* type, and the callback interface is:
+// void (std::string pattern, std::string channel, std::string msg)
+//
+// Messages of other types are called *META MESSAGE*, they have the same callback interface.
+// Use Subscriber::on_meta(MetaCallback) to set the callback function:
+// void (Subscriber::MsgType type, OptionalString channel, long long num)
+//
+// NOTE: If we haven't subscribe/psubscribe to any channel/pattern, and try to
+// unsubscribe/punsubscribe without any parameter, i.e. unsubscribe/punsubscribe all
+// channels/patterns, *channel* will be null. So the second parameter of meta callback
+// is of type *OptionalString*.
+//
+// All these callback interfaces pass std::string by value, and you can take their ownership
+// (i.e. std::move) safely.
+//
+// If you don't set callback for a specific kind of message, Subscriber::consume() will
+// receive the message, and ignore it, i.e. no callback will be called.
+class Subscriber {
+public:
+    Subscriber(const Subscriber &) = delete;
+    Subscriber& operator=(const Subscriber &) = delete;
+
+    Subscriber(Subscriber &&) = default;
+    Subscriber& operator=(Subscriber &&) = default;
+
+    ~Subscriber() = default;
+
+    enum class MsgType {
+        SUBSCRIBE,
+        UNSUBSCRIBE,
+        PSUBSCRIBE,
+        PUNSUBSCRIBE,
+        MESSAGE,
+        PMESSAGE
+    };
+
+    template <typename MsgCb>
+    void on_message(MsgCb msg_callback);
+
+    template <typename PMsgCb>
+    void on_pmessage(PMsgCb pmsg_callback);
+
+    template <typename MetaCb>
+    void on_meta(MetaCb meta_callback);
+
+    void subscribe(const StringView &channel);
+
+    template <typename Input>
+    void subscribe(Input first, Input last);
+
+    template <typename T>
+    void subscribe(std::initializer_list<T> channels) {
+        subscribe(channels.begin(), channels.end());
+    }
+
+    void unsubscribe();
+
+    void unsubscribe(const StringView &channel);
+
+    template <typename Input>
+    void unsubscribe(Input first, Input last);
+
+    template <typename T>
+    void unsubscribe(std::initializer_list<T> channels) {
+        unsubscribe(channels.begin(), channels.end());
+    }
+
+    void psubscribe(const StringView &pattern);
+
+    template <typename Input>
+    void psubscribe(Input first, Input last);
+
+    template <typename T>
+    void psubscribe(std::initializer_list<T> channels) {
+        psubscribe(channels.begin(), channels.end());
+    }
+
+    void punsubscribe();
+
+    void punsubscribe(const StringView &channel);
+
+    template <typename Input>
+    void punsubscribe(Input first, Input last);
+
+    template <typename T>
+    void punsubscribe(std::initializer_list<T> channels) {
+        punsubscribe(channels.begin(), channels.end());
+    }
+
+    void consume();
+
+private:
+    friend class Redis;
+
+    friend class RedisCluster;
+
+    explicit Subscriber(Connection connection);
+
+    MsgType _msg_type(redisReply *reply) const;
+
+    void _check_connection();
+
+    void _handle_message(redisReply &reply);
+
+    void _handle_pmessage(redisReply &reply);
+
+    void _handle_meta(MsgType type, redisReply &reply);
+
+    using MsgCallback = std::function<void (std::string channel, std::string msg)>;
+
+    using PatternMsgCallback = std::function<void (std::string pattern,
+                                                    std::string channel,
+                                                    std::string msg)>;
+
+    using MetaCallback = std::function<void (MsgType type,
+                                                OptionalString channel,
+                                                long long num)>;
+
+    using TypeIndex = std::unordered_map<std::string, MsgType>;
+    static const TypeIndex _msg_type_index;
+
+    Connection _connection;
+
+    MsgCallback _msg_callback = nullptr;
+
+    PatternMsgCallback _pmsg_callback = nullptr;
+
+    MetaCallback _meta_callback = nullptr;
+};
+
+template <typename MsgCb>
+void Subscriber::on_message(MsgCb msg_callback) {
+    _msg_callback = msg_callback;
+}
+
+template <typename PMsgCb>
+void Subscriber::on_pmessage(PMsgCb pmsg_callback) {
+    _pmsg_callback = pmsg_callback;
+}
+
+template <typename MetaCb>
+void Subscriber::on_meta(MetaCb meta_callback) {
+    _meta_callback = meta_callback;
+}
+
+template <typename Input>
+void Subscriber::subscribe(Input first, Input last) {
+    if (first == last) {
+        return;
+    }
+
+    _check_connection();
+
+    cmd::subscribe_range(_connection, first, last);
+}
+
+template <typename Input>
+void Subscriber::unsubscribe(Input first, Input last) {
+    _check_connection();
+
+    cmd::unsubscribe_range(_connection, first, last);
+}
+
+template <typename Input>
+void Subscriber::psubscribe(Input first, Input last) {
+    if (first == last) {
+        return;
+    }
+
+    _check_connection();
+
+    cmd::psubscribe_range(_connection, first, last);
+}
+
+template <typename Input>
+void Subscriber::punsubscribe(Input first, Input last) {
+    _check_connection();
+
+    cmd::punsubscribe_range(_connection, first, last);
+}
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_SUBSCRIBER_H

+ 77 - 0
ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/transaction.h

@@ -0,0 +1,77 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_TRANSACTION_H
+#define SEWENEW_REDISPLUSPLUS_TRANSACTION_H
+
+#include <cassert>
+#include <vector>
+#include "connection.h"
+#include "errors.h"
+
+namespace sw {
+
+namespace redis {
+
+class TransactionImpl {
+public:
+    explicit TransactionImpl(bool piped) : _piped(piped) {}
+
+    template <typename Cmd, typename ...Args>
+    void command(Connection &connection, Cmd cmd, Args &&...args);
+
+    std::vector<ReplyUPtr> exec(Connection &connection, std::size_t cmd_num);
+
+    void discard(Connection &connection, std::size_t cmd_num);
+
+private:
+    void _open_transaction(Connection &connection);
+
+    void _close_transaction();
+
+    void _get_queued_reply(Connection &connection);
+
+    void _get_queued_replies(Connection &connection, std::size_t cmd_num);
+
+    std::vector<ReplyUPtr> _exec(Connection &connection);
+
+    void _discard(Connection &connection);
+
+    bool _in_transaction = false;
+
+    bool _piped;
+};
+
+template <typename Cmd, typename ...Args>
+void TransactionImpl::command(Connection &connection, Cmd cmd, Args &&...args) {
+    assert(!connection.broken());
+
+    if (!_in_transaction) {
+        _open_transaction(connection);
+    }
+
+    cmd(connection, std::forward<Args>(args)...);
+
+    if (!_piped) {
+        _get_queued_reply(connection);
+    }
+}
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_TRANSACTION_H

+ 269 - 0
ext/redis-plus-plus-1.1.1/install/macos/include/sw/redis++/utils.h

@@ -0,0 +1,269 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_UTILS_H
+#define SEWENEW_REDISPLUSPLUS_UTILS_H
+
+#include <cstring>
+#include <string>
+#include <type_traits>
+
+namespace sw {
+
+namespace redis {
+
+// By now, not all compilers support std::string_view,
+// so we make our own implementation.
+class StringView {
+public:
+    constexpr StringView() noexcept = default;
+
+    constexpr StringView(const char *data, std::size_t size) : _data(data), _size(size) {}
+
+    StringView(const char *data) : _data(data), _size(std::strlen(data)) {}
+
+    StringView(const std::string &str) : _data(str.data()), _size(str.size()) {}
+
+    constexpr StringView(const StringView &) noexcept = default;
+
+    StringView& operator=(const StringView &) noexcept = default;
+
+    constexpr const char* data() const noexcept {
+        return _data;
+    }
+
+    constexpr std::size_t size() const noexcept {
+        return _size;
+    }
+
+private:
+    const char *_data = nullptr;
+    std::size_t _size = 0;
+};
+
+template <typename T>
+class Optional {
+public:
+    Optional() = default;
+
+    Optional(const Optional &) = default;
+    Optional& operator=(const Optional &) = default;
+
+    Optional(Optional &&) = default;
+    Optional& operator=(Optional &&) = default;
+
+    ~Optional() = default;
+
+    template <typename ...Args>
+    explicit Optional(Args &&...args) : _value(true, T(std::forward<Args>(args)...)) {}
+
+    explicit operator bool() const {
+        return _value.first;
+    }
+
+    T& value() {
+        return _value.second;
+    }
+
+    const T& value() const {
+        return _value.second;
+    }
+
+    T* operator->() {
+        return &(_value.second);
+    }
+
+    const T* operator->() const {
+        return &(_value.second);
+    }
+
+    T& operator*() {
+        return _value.second;
+    }
+
+    const T& operator*() const {
+        return _value.second;
+    }
+
+private:
+    std::pair<bool, T> _value;
+};
+
+using OptionalString = Optional<std::string>;
+
+using OptionalLongLong = Optional<long long>;
+
+using OptionalDouble = Optional<double>;
+
+using OptionalStringPair = Optional<std::pair<std::string, std::string>>;
+
+template <typename ...>
+struct IsKvPair : std::false_type {};
+
+template <typename T, typename U>
+struct IsKvPair<std::pair<T, U>> : std::true_type {};
+
+template <typename ...>
+using Void = void;
+
+template <typename T, typename U = Void<>>
+struct IsInserter : std::false_type {};
+
+template <typename T>
+//struct IsInserter<T, Void<typename T::container_type>> : std::true_type {};
+struct IsInserter<T,
+    typename std::enable_if<!std::is_void<typename T::container_type>::value>::type>
+        : std::true_type {};
+
+template <typename Iter, typename T = Void<>>
+struct IterType {
+    using type = typename std::iterator_traits<Iter>::value_type;
+};
+
+template <typename Iter>
+//struct IterType<Iter, Void<typename Iter::container_type>> {
+struct IterType<Iter,
+    //typename std::enable_if<std::is_void<typename Iter::value_type>::value>::type> {
+    typename std::enable_if<IsInserter<Iter>::value>::type> {
+    using type = typename std::decay<typename Iter::container_type::value_type>::type;
+};
+
+template <typename Iter, typename T = Void<>>
+struct IsIter : std::false_type {};
+
+template <typename Iter>
+struct IsIter<Iter, typename std::enable_if<IsInserter<Iter>::value>::type> : std::true_type {};
+
+template <typename Iter>
+//struct IsIter<Iter, Void<typename std::iterator_traits<Iter>::iterator_category>>
+struct IsIter<Iter,
+    typename std::enable_if<!std::is_void<
+        typename std::iterator_traits<Iter>::value_type>::value>::type>
+            : std::integral_constant<bool, !std::is_convertible<Iter, StringView>::value> {};
+
+template <typename T>
+struct IsKvPairIter : IsKvPair<typename IterType<T>::type> {};
+
+template <typename T, typename Tuple>
+struct TupleWithType : std::false_type {};
+
+template <typename T>
+struct TupleWithType<T, std::tuple<>> : std::false_type {};
+
+template <typename T, typename U, typename ...Args>
+struct TupleWithType<T, std::tuple<U, Args...>> : TupleWithType<T, std::tuple<Args...>> {};
+
+template <typename T, typename ...Args>
+struct TupleWithType<T, std::tuple<T, Args...>> : std::true_type {};
+
+template <std::size_t ...Is>
+struct IndexSequence {};
+
+template <std::size_t I, std::size_t ...Is>
+struct MakeIndexSequence : MakeIndexSequence<I - 1, I - 1, Is...> {};
+
+template <std::size_t ...Is>
+struct MakeIndexSequence<0, Is...> : IndexSequence<Is...> {};
+
+// NthType and NthValue are taken from
+// https://stackoverflow.com/questions/14261183
+template <std::size_t I, typename ...Args>
+struct NthType {};
+
+template <typename Arg, typename ...Args>
+struct NthType<0, Arg, Args...> {
+    using type = Arg;
+};
+
+template <std::size_t I, typename Arg, typename ...Args>
+struct NthType<I, Arg, Args...> {
+    using type = typename NthType<I - 1, Args...>::type;
+};
+
+template <typename ...Args>
+struct LastType {
+    using type = typename NthType<sizeof...(Args) - 1, Args...>::type;
+};
+
+struct InvalidLastType {};
+
+template <>
+struct LastType<> {
+    using type = InvalidLastType;
+};
+
+template <std::size_t I, typename Arg, typename ...Args>
+auto NthValue(Arg &&arg, Args &&...)
+    -> typename std::enable_if<(I == 0), decltype(std::forward<Arg>(arg))>::type {
+    return std::forward<Arg>(arg);
+}
+
+template <std::size_t I, typename Arg, typename ...Args>
+auto NthValue(Arg &&, Args &&...args)
+    -> typename std::enable_if<(I > 0),
+            decltype(std::forward<typename NthType<I, Arg, Args...>::type>(
+                    std::declval<typename NthType<I, Arg, Args...>::type>()))>::type {
+    return std::forward<typename NthType<I, Arg, Args...>::type>(
+            NthValue<I - 1>(std::forward<Args>(args)...));
+}
+
+template <typename ...Args>
+auto LastValue(Args &&...args)
+    -> decltype(std::forward<typename LastType<Args...>::type>(
+            std::declval<typename LastType<Args...>::type>())) {
+    return std::forward<typename LastType<Args...>::type>(
+            NthValue<sizeof...(Args) - 1>(std::forward<Args>(args)...));
+}
+
+template <typename T, typename = Void<>>
+struct HasPushBack : std::false_type {};
+
+template <typename T>
+struct HasPushBack<T,
+    typename std::enable_if<
+        std::is_void<decltype(
+            std::declval<T>().push_back(std::declval<typename T::value_type>())
+                )>::value>::type> : std::true_type {};
+
+template <typename T, typename = Void<>>
+struct HasInsert : std::false_type {};
+
+template <typename T>
+struct HasInsert<T,
+    typename std::enable_if<
+        std::is_same<
+            decltype(std::declval<T>().insert(std::declval<typename T::const_iterator>(),
+                                                std::declval<typename T::value_type>())),
+            typename T::iterator>::value>::type> : std::true_type {};
+
+template <typename T>
+struct IsSequenceContainer
+    : std::integral_constant<bool,
+        HasPushBack<T>::value
+            && !std::is_same<typename std::decay<T>::type, std::string>::value> {};
+
+template <typename T>
+struct IsAssociativeContainer
+    : std::integral_constant<bool,
+        HasInsert<T>::value && !HasPushBack<T>::value> {};
+
+uint16_t crc16(const char *buf, int len);
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_UTILS_H

+ 376 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/command.cpp

@@ -0,0 +1,376 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#include "command.h"
+#include <cassert>
+
+namespace sw {
+
+namespace redis {
+
+namespace cmd {
+
+// KEY commands.
+
+void restore(Connection &connection,
+                const StringView &key,
+                const StringView &val,
+                long long ttl,
+                bool replace) {
+    CmdArgs args;
+    args << "RESTORE" << key << ttl << val;
+
+    if (replace) {
+        args << "REPLACE";
+    }
+
+    connection.send(args);
+}
+
+// STRING commands.
+
+void bitop(Connection &connection,
+            BitOp op,
+            const StringView &destination,
+            const StringView &key) {
+    CmdArgs args;
+
+    detail::set_bitop(args, op);
+
+    args << destination << key;
+
+    connection.send(args);
+}
+
+void set(Connection &connection,
+            const StringView &key,
+            const StringView &val,
+            long long ttl,
+            UpdateType type) {
+    CmdArgs args;
+    args << "SET" << key << val;
+
+    if (ttl > 0) {
+        args << "PX" << ttl;
+    }
+
+    detail::set_update_type(args, type);
+
+    connection.send(args);
+}
+
+// LIST commands.
+
+void linsert(Connection &connection,
+                const StringView &key,
+                InsertPosition position,
+                const StringView &pivot,
+                const StringView &val) {
+    std::string pos;
+    switch (position) {
+    case InsertPosition::BEFORE:
+        pos = "BEFORE";
+        break;
+
+    case InsertPosition::AFTER:
+        pos = "AFTER";
+        break;
+
+    default:
+        assert(false);
+    }
+
+    connection.send("LINSERT %b %s %b %b",
+                    key.data(), key.size(),
+                    pos.c_str(),
+                    pivot.data(), pivot.size(),
+                    val.data(), val.size());
+}
+
+// GEO commands.
+
+void geodist(Connection &connection,
+                const StringView &key,
+                const StringView &member1,
+                const StringView &member2,
+                GeoUnit unit) {
+    CmdArgs args;
+    args << "GEODIST" << key << member1 << member2;
+
+    detail::set_geo_unit(args, unit);
+
+    connection.send(args);
+}
+
+void georadius_store(Connection &connection,
+                        const StringView &key,
+                        const std::pair<double, double> &loc,
+                        double radius,
+                        GeoUnit unit,
+                        const StringView &destination,
+                        bool store_dist,
+                        long long count) {
+    CmdArgs args;
+    args << "GEORADIUS" << key << loc.first << loc.second;
+
+    detail::set_georadius_store_parameters(args,
+                                            radius,
+                                            unit,
+                                            destination,
+                                            store_dist,
+                                            count);
+
+    connection.send(args);
+}
+
+void georadius(Connection &connection,
+                const StringView &key,
+                const std::pair<double, double> &loc,
+                double radius,
+                GeoUnit unit,
+                long long count,
+                bool asc,
+                bool with_coord,
+                bool with_dist,
+                bool with_hash) {
+    CmdArgs args;
+    args << "GEORADIUS" << key << loc.first << loc.second;
+
+    detail::set_georadius_parameters(args,
+                                        radius,
+                                        unit,
+                                        count,
+                                        asc,
+                                        with_coord,
+                                        with_dist,
+                                        with_hash);
+
+    connection.send(args);
+}
+
+void georadiusbymember(Connection &connection,
+                        const StringView &key,
+                        const StringView &member,
+                        double radius,
+                        GeoUnit unit,
+                        long long count,
+                        bool asc,
+                        bool with_coord,
+                        bool with_dist,
+                        bool with_hash) {
+    CmdArgs args;
+    args << "GEORADIUSBYMEMBER" << key << member;
+
+    detail::set_georadius_parameters(args,
+                                        radius,
+                                        unit,
+                                        count,
+                                        asc,
+                                        with_coord,
+                                        with_dist,
+                                        with_hash);
+
+    connection.send(args);
+}
+
+void georadiusbymember_store(Connection &connection,
+                                const StringView &key,
+                                const StringView &member,
+                                double radius,
+                                GeoUnit unit,
+                                const StringView &destination,
+                                bool store_dist,
+                                long long count) {
+    CmdArgs args;
+    args << "GEORADIUSBYMEMBER" << key << member;
+
+    detail::set_georadius_store_parameters(args,
+                                            radius,
+                                            unit,
+                                            destination,
+                                            store_dist,
+                                            count);
+
+    connection.send(args);
+}
+
+// Stream commands.
+
+void xtrim(Connection &connection, const StringView &key, long long count, bool approx) {
+    CmdArgs args;
+    args << "XTRIM" << key << "MAXLEN";
+
+    if (approx) {
+        args << "~";
+    }
+
+    args << count;
+
+    connection.send(args);
+}
+
+namespace detail {
+
+void set_bitop(CmdArgs &args, BitOp op) {
+    args << "BITOP";
+
+    switch (op) {
+    case BitOp::AND:
+        args << "AND";
+        break;
+
+    case BitOp::OR:
+        args << "OR";
+        break;
+
+    case BitOp::XOR:
+        args << "XOR";
+        break;
+
+    case BitOp::NOT:
+        args << "NOT";
+        break;
+
+    default:
+        throw Error("Unknown bit operations");
+    }
+}
+
+void set_update_type(CmdArgs &args, UpdateType type) {
+    switch (type) {
+    case UpdateType::EXIST:
+        args << "XX";
+        break;
+
+    case UpdateType::NOT_EXIST:
+        args << "NX";
+        break;
+
+    case UpdateType::ALWAYS:
+        // Do nothing.
+        break;
+
+    default:
+        throw Error("Unknown update type");
+    }
+}
+
+void set_aggregation_type(CmdArgs &args, Aggregation aggr) {
+    args << "AGGREGATE";
+
+    switch (aggr) {
+    case Aggregation::SUM:
+        args << "SUM";
+        break;
+
+    case Aggregation::MIN:
+        args << "MIN";
+        break;
+
+    case Aggregation::MAX:
+        args << "MAX";
+        break;
+
+    default:
+        throw Error("Unknown aggregation type");
+    }
+}
+
+void set_geo_unit(CmdArgs &args, GeoUnit unit) {
+    switch (unit) {
+    case GeoUnit::M:
+        args << "m";
+        break;
+
+    case GeoUnit::KM:
+        args << "km";
+        break;
+
+    case GeoUnit::MI:
+        args << "mi";
+        break;
+
+    case GeoUnit::FT:
+        args << "ft";
+        break;
+
+    default:
+        throw Error("Unknown geo unit type");
+        break;
+    }
+}
+
+void set_georadius_store_parameters(CmdArgs &args,
+                                    double radius,
+                                    GeoUnit unit,
+                                    const StringView &destination,
+                                    bool store_dist,
+                                    long long count) {
+    args << radius;
+
+    detail::set_geo_unit(args, unit);
+
+    args << "COUNT" << count;
+
+    if (store_dist) {
+        args << "STOREDIST";
+    } else {
+        args << "STORE";
+    }
+
+    args << destination;
+}
+
+void set_georadius_parameters(CmdArgs &args,
+                                double radius,
+                                GeoUnit unit,
+                                long long count,
+                                bool asc,
+                                bool with_coord,
+                                bool with_dist,
+                                bool with_hash) {
+    args << radius;
+
+    detail::set_geo_unit(args, unit);
+
+    if (with_coord) {
+        args << "WITHCOORD";
+    }
+
+    if (with_dist) {
+        args << "WITHDIST";
+    }
+
+    if (with_hash) {
+        args << "WITHHASH";
+    }
+
+    args << "COUNT" << count;
+
+    if (asc) {
+        args << "ASC";
+    } else {
+        args << "DESC";
+    }
+}
+
+}
+
+}
+
+}
+
+}

+ 2233 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/command.h

@@ -0,0 +1,2233 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_COMMAND_H
+#define SEWENEW_REDISPLUSPLUS_COMMAND_H
+
+#include <cassert>
+#include <ctime>
+#include <string>
+#include <chrono>
+#include "connection.h"
+#include "command_options.h"
+#include "command_args.h"
+#include "utils.h"
+
+namespace sw {
+
+namespace redis {
+
+namespace cmd {
+
+// CONNECTION command.
+inline void auth(Connection &connection, const StringView &password) {
+    connection.send("AUTH %b", password.data(), password.size());
+}
+
+inline void echo(Connection &connection, const StringView &msg) {
+    connection.send("ECHO %b", msg.data(), msg.size());
+}
+
+inline void ping(Connection &connection) {
+    connection.send("PING");
+}
+
+inline void quit(Connection &connection) {
+    connection.send("QUIT");
+}
+
+inline void ping(Connection &connection, const StringView &msg) {
+    // If *msg* is empty, Redis returns am empty reply of REDIS_REPLY_STRING type.
+    connection.send("PING %b", msg.data(), msg.size());
+}
+
+inline void select(Connection &connection, long long idx) {
+    connection.send("SELECT %lld", idx);
+}
+
+inline void swapdb(Connection &connection, long long idx1, long long idx2) {
+    connection.send("SWAPDB %lld %lld", idx1, idx2);
+}
+
+// SERVER commands.
+
+inline void bgrewriteaof(Connection &connection) {
+    connection.send("BGREWRITEAOF");
+}
+
+inline void bgsave(Connection &connection) {
+    connection.send("BGSAVE");
+}
+
+inline void dbsize(Connection &connection) {
+    connection.send("DBSIZE");
+}
+
+inline void flushall(Connection &connection, bool async) {
+    if (async) {
+        connection.send("FLUSHALL ASYNC");
+    } else {
+        connection.send("FLUSHALL");
+    }
+}
+
+inline void flushdb(Connection &connection, bool async) {
+    if (async) {
+        connection.send("FLUSHDB ASYNC");
+    } else {
+        connection.send("FLUSHDB");
+    }
+}
+
+inline void info(Connection &connection) {
+    connection.send("INFO");
+}
+
+inline void info(Connection &connection, const StringView &section) {
+    connection.send("INFO %b", section.data(), section.size());
+}
+
+inline void lastsave(Connection &connection) {
+    connection.send("LASTSAVE");
+}
+
+inline void save(Connection &connection) {
+    connection.send("SAVE");
+}
+
+// KEY commands.
+
+inline void del(Connection &connection, const StringView &key) {
+    connection.send("DEL %b", key.data(), key.size());
+}
+
+template <typename Input>
+inline void del_range(Connection &connection, Input first, Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "DEL" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void dump(Connection &connection, const StringView &key) {
+    connection.send("DUMP %b", key.data(), key.size());
+}
+
+inline void exists(Connection &connection, const StringView &key) {
+    connection.send("EXISTS %b", key.data(), key.size());
+}
+
+template <typename Input>
+inline void exists_range(Connection &connection, Input first, Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "EXISTS" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void expire(Connection &connection,
+                    const StringView &key,
+                    long long timeout) {
+    connection.send("EXPIRE %b %lld",
+                    key.data(), key.size(),
+                    timeout);
+}
+
+inline void expireat(Connection &connection,
+                        const StringView &key,
+                        long long timestamp) {
+    connection.send("EXPIREAT %b %lld",
+                    key.data(), key.size(),
+                    timestamp);
+}
+
+inline void keys(Connection &connection, const StringView &pattern) {
+    connection.send("KEYS %b", pattern.data(), pattern.size());
+}
+
+inline void move(Connection &connection, const StringView &key, long long db) {
+    connection.send("MOVE %b %lld",
+                    key.data(), key.size(),
+                    db);
+}
+
+inline void persist(Connection &connection, const StringView &key) {
+    connection.send("PERSIST %b", key.data(), key.size());
+}
+
+inline void pexpire(Connection &connection,
+                    const StringView &key,
+                    long long timeout) {
+    connection.send("PEXPIRE %b %lld",
+                    key.data(), key.size(),
+                    timeout);
+}
+
+inline void pexpireat(Connection &connection,
+                        const StringView &key,
+                        long long timestamp) {
+    connection.send("PEXPIREAT %b %lld",
+                    key.data(), key.size(),
+                    timestamp);
+}
+
+inline void pttl(Connection &connection, const StringView &key) {
+    connection.send("PTTL %b", key.data(), key.size());
+}
+
+inline void randomkey(Connection &connection) {
+    connection.send("RANDOMKEY");
+}
+
+inline void rename(Connection &connection,
+                    const StringView &key,
+                    const StringView &newkey) {
+    connection.send("RENAME %b %b",
+                    key.data(), key.size(),
+                    newkey.data(), newkey.size());
+}
+
+inline void renamenx(Connection &connection,
+                        const StringView &key,
+                        const StringView &newkey) {
+    connection.send("RENAMENX %b %b",
+                    key.data(), key.size(),
+                    newkey.data(), newkey.size());
+}
+
+void restore(Connection &connection,
+                const StringView &key,
+                const StringView &val,
+                long long ttl,
+                bool replace);
+
+inline void scan(Connection &connection,
+                    long long cursor,
+                    const StringView &pattern,
+                    long long count) {
+    connection.send("SCAN %lld MATCH %b COUNT %lld",
+                    cursor,
+                    pattern.data(), pattern.size(),
+                    count);
+}
+
+inline void touch(Connection &connection, const StringView &key) {
+    connection.send("TOUCH %b", key.data(), key.size());
+}
+
+template <typename Input>
+inline void touch_range(Connection &connection, Input first, Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "TOUCH" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void ttl(Connection &connection, const StringView &key) {
+    connection.send("TTL %b", key.data(), key.size());
+}
+
+inline void type(Connection &connection, const StringView &key) {
+    connection.send("TYPE %b", key.data(), key.size());
+}
+
+inline void unlink(Connection &connection, const StringView &key) {
+    connection.send("UNLINK %b", key.data(), key.size());
+}
+
+template <typename Input>
+inline void unlink_range(Connection &connection, Input first, Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "UNLINK" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void wait(Connection &connection, long long numslave, long long timeout) {
+    connection.send("WAIT %lld %lld", numslave, timeout);
+}
+
+// STRING commands.
+
+inline void append(Connection &connection, const StringView &key, const StringView &str) {
+    connection.send("APPEND %b %b",
+                    key.data(), key.size(),
+                    str.data(), str.size());
+}
+
+inline void bitcount(Connection &connection,
+                        const StringView &key,
+                        long long start,
+                        long long end) {
+    connection.send("BITCOUNT %b %lld %lld",
+                    key.data(), key.size(),
+                    start, end);
+}
+
+void bitop(Connection &connection,
+            BitOp op,
+            const StringView &destination,
+            const StringView &key);
+
+template <typename Input>
+void bitop_range(Connection &connection,
+                    BitOp op,
+                    const StringView &destination,
+                    Input first,
+                    Input last);
+
+inline void bitpos(Connection &connection,
+                    const StringView &key,
+                    long long bit,
+                    long long start,
+                    long long end) {
+    connection.send("BITPOS %b %lld %lld %lld",
+                    key.data(), key.size(),
+                    bit,
+                    start,
+                    end);
+}
+
+inline void decr(Connection &connection, const StringView &key) {
+    connection.send("DECR %b", key.data(), key.size());
+}
+
+inline void decrby(Connection &connection, const StringView &key, long long decrement) {
+    connection.send("DECRBY %b %lld",
+                    key.data(), key.size(),
+                    decrement);
+}
+
+inline void get(Connection &connection, const StringView &key) {
+    connection.send("GET %b",
+                    key.data(), key.size());
+}
+
+inline void getbit(Connection &connection, const StringView &key, long long offset) {
+    connection.send("GETBIT %b %lld",
+                    key.data(), key.size(),
+                    offset);
+}
+
+inline void getrange(Connection &connection,
+                        const StringView &key,
+                        long long start,
+                        long long end) {
+    connection.send("GETRANGE %b %lld %lld",
+                    key.data(), key.size(),
+                    start,
+                    end);
+}
+
+inline void getset(Connection &connection,
+                    const StringView &key,
+                    const StringView &val) {
+    connection.send("GETSET %b %b",
+                    key.data(), key.size(),
+                    val.data(), val.size());
+}
+
+inline void incr(Connection &connection, const StringView &key) {
+    connection.send("INCR %b", key.data(), key.size());
+}
+
+inline void incrby(Connection &connection, const StringView &key, long long increment) {
+    connection.send("INCRBY %b %lld",
+                    key.data(), key.size(),
+                    increment);
+}
+
+inline void incrbyfloat(Connection &connection, const StringView &key, double increment) {
+    connection.send("INCRBYFLOAT %b %f",
+                    key.data(), key.size(),
+                    increment);
+}
+
+template <typename Input>
+inline void mget(Connection &connection, Input first, Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "MGET" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+template <typename Input>
+inline void mset(Connection &connection, Input first, Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "MSET" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+template <typename Input>
+inline void msetnx(Connection &connection, Input first, Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "MSETNX" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void psetex(Connection &connection,
+                    const StringView &key,
+                    long long ttl,
+                    const StringView &val) {
+    connection.send("PSETEX %b %lld %b",
+                    key.data(), key.size(),
+                    ttl,
+                    val.data(), val.size());
+}
+
+void set(Connection &connection,
+            const StringView &key,
+            const StringView &val,
+            long long ttl,
+            UpdateType type);
+
+inline void setex(Connection &connection,
+                    const StringView &key,
+                    long long ttl,
+                    const StringView &val) {
+    connection.send("SETEX %b %lld %b",
+                    key.data(), key.size(),
+                    ttl,
+                    val.data(), val.size());
+}
+
+inline void setnx(Connection &connection,
+                    const StringView &key,
+                    const StringView &val) {
+    connection.send("SETNX %b %b",
+                    key.data(), key.size(),
+                    val.data(), val.size());
+}
+
+inline void setrange(Connection &connection,
+                        const StringView &key,
+                        long long offset,
+                        const StringView &val) {
+    connection.send("SETRANGE %b %lld %b",
+                    key.data(), key.size(),
+                    offset,
+                    val.data(), val.size());
+}
+
+inline void strlen(Connection &connection, const StringView &key) {
+    connection.send("STRLEN %b", key.data(), key.size());
+}
+
+// LIST commands.
+
+inline void blpop(Connection &connection, const StringView &key, long long timeout) {
+    connection.send("BLPOP %b %lld",
+                    key.data(), key.size(),
+                    timeout);
+}
+
+template <typename Input>
+inline void blpop_range(Connection &connection,
+                        Input first,
+                        Input last,
+                        long long timeout) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "BLPOP" << std::make_pair(first, last) << timeout;
+
+    connection.send(args);
+}
+
+inline void brpop(Connection &connection, const StringView &key, long long timeout) {
+    connection.send("BRPOP %b %lld",
+                    key.data(), key.size(),
+                    timeout);
+}
+
+template <typename Input>
+inline void brpop_range(Connection &connection,
+                        Input first,
+                        Input last,
+                        long long timeout) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "BRPOP" << std::make_pair(first, last) << timeout;
+
+    connection.send(args);
+}
+
+inline void brpoplpush(Connection &connection,
+                        const StringView &source,
+                        const StringView &destination,
+                        long long timeout) {
+    connection.send("BRPOPLPUSH %b %b %lld",
+                    source.data(), source.size(),
+                    destination.data(), destination.size(),
+                    timeout);
+}
+
+inline void lindex(Connection &connection, const StringView &key, long long index) {
+    connection.send("LINDEX %b %lld",
+                    key.data(), key.size(),
+                    index);
+}
+
+void linsert(Connection &connection,
+                const StringView &key,
+                InsertPosition position,
+                const StringView &pivot,
+                const StringView &val);
+
+inline void llen(Connection &connection,
+                    const StringView &key) {
+    connection.send("LLEN %b", key.data(), key.size());
+}
+
+inline void lpop(Connection &connection, const StringView &key) {
+    connection.send("LPOP %b",
+                    key.data(), key.size());
+}
+
+inline void lpush(Connection &connection, const StringView &key, const StringView &val) {
+    connection.send("LPUSH %b %b",
+                    key.data(), key.size(),
+                    val.data(), val.size());
+}
+
+template <typename Input>
+inline void lpush_range(Connection &connection,
+                        const StringView &key,
+                        Input first,
+                        Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "LPUSH" << key << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void lpushx(Connection &connection, const StringView &key, const StringView &val) {
+    connection.send("LPUSHX %b %b",
+                    key.data(), key.size(),
+                    val.data(), val.size());
+}
+
+inline void lrange(Connection &connection,
+                    const StringView &key,
+                    long long start,
+                    long long stop) {
+    connection.send("LRANGE %b %lld %lld",
+                    key.data(), key.size(),
+                    start,
+                    stop);
+}
+
+inline void lrem(Connection &connection,
+                    const StringView &key,
+                    long long count,
+                    const StringView &val) {
+    connection.send("LREM %b %lld %b",
+                    key.data(), key.size(),
+                    count,
+                    val.data(), val.size());
+}
+
+inline void lset(Connection &connection,
+                    const StringView &key,
+                    long long index,
+                    const StringView &val) {
+    connection.send("LSET %b %lld %b",
+                    key.data(), key.size(),
+                    index,
+                    val.data(), val.size());
+}
+
+inline void ltrim(Connection &connection,
+                    const StringView &key,
+                    long long start,
+                    long long stop) {
+    connection.send("LTRIM %b %lld %lld",
+                    key.data(), key.size(),
+                    start,
+                    stop);
+}
+
+inline void rpop(Connection &connection, const StringView &key) {
+    connection.send("RPOP %b", key.data(), key.size());
+}
+
+inline void rpoplpush(Connection &connection,
+                        const StringView &source,
+                        const StringView &destination) {
+    connection.send("RPOPLPUSH %b %b",
+                    source.data(), source.size(),
+                    destination.data(), destination.size());
+}
+
+inline void rpush(Connection &connection, const StringView &key, const StringView &val) {
+    connection.send("RPUSH %b %b",
+                    key.data(), key.size(),
+                    val.data(), val.size());
+}
+
+template <typename Input>
+inline void rpush_range(Connection &connection,
+                        const StringView &key,
+                        Input first,
+                        Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "RPUSH" << key << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void rpushx(Connection &connection, const StringView &key, const StringView &val) {
+    connection.send("RPUSHX %b %b",
+                    key.data(), key.size(),
+                    val.data(), val.size());
+}
+
+// HASH commands.
+
+inline void hdel(Connection &connection, const StringView &key, const StringView &field) {
+    connection.send("HDEL %b %b",
+                    key.data(), key.size(),
+                    field.data(), field.size());
+}
+
+template <typename Input>
+inline void hdel_range(Connection &connection,
+                        const StringView &key,
+                        Input first,
+                        Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "HDEL" << key << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void hexists(Connection &connection, const StringView &key, const StringView &field) {
+    connection.send("HEXISTS %b %b",
+                    key.data(), key.size(),
+                    field.data(), field.size());
+}
+
+inline void hget(Connection &connection, const StringView &key, const StringView &field) {
+    connection.send("HGET %b %b",
+                    key.data(), key.size(),
+                    field.data(), field.size());
+}
+
+inline void hgetall(Connection &connection, const StringView &key) {
+    connection.send("HGETALL %b", key.data(), key.size());
+}
+
+inline void hincrby(Connection &connection,
+                    const StringView &key,
+                    const StringView &field,
+                    long long increment) {
+    connection.send("HINCRBY %b %b %lld",
+                    key.data(), key.size(),
+                    field.data(), field.size(),
+                    increment);
+}
+
+inline void hincrbyfloat(Connection &connection,
+                            const StringView &key,
+                            const StringView &field,
+                            double increment) {
+    connection.send("HINCRBYFLOAT %b %b %f",
+                    key.data(), key.size(),
+                    field.data(), field.size(),
+                    increment);
+}
+
+inline void hkeys(Connection &connection, const StringView &key) {
+    connection.send("HKEYS %b", key.data(), key.size());
+}
+
+inline void hlen(Connection &connection, const StringView &key) {
+    connection.send("HLEN %b", key.data(), key.size());
+}
+
+template <typename Input>
+inline void hmget(Connection &connection,
+                    const StringView &key,
+                    Input first,
+                    Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "HMGET" << key << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+template <typename Input>
+inline void hmset(Connection &connection,
+                    const StringView &key,
+                    Input first,
+                    Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "HMSET" << key << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void hscan(Connection &connection,
+                    const StringView &key,
+                    long long cursor,
+                    const StringView &pattern,
+                    long long count) {
+    connection.send("HSCAN %b %lld MATCH %b COUNT %lld",
+                    key.data(), key.size(),
+                    cursor,
+                    pattern.data(), pattern.size(),
+                    count);
+}
+
+inline void hset(Connection &connection,
+                    const StringView &key,
+                    const StringView &field,
+                    const StringView &val) {
+    connection.send("HSET %b %b %b",
+                    key.data(), key.size(),
+                    field.data(), field.size(),
+                    val.data(), val.size());
+}
+
+inline void hsetnx(Connection &connection,
+                    const StringView &key,
+                    const StringView &field,
+                    const StringView &val) {
+    connection.send("HSETNX %b %b %b",
+                    key.data(), key.size(),
+                    field.data(), field.size(),
+                    val.data(), val.size());
+}
+
+inline void hstrlen(Connection &connection,
+                    const StringView &key,
+                    const StringView &field) {
+    connection.send("HSTRLEN %b %b",
+                    key.data(), key.size(),
+                    field.data(), field.size());
+}
+
+inline void hvals(Connection &connection, const StringView &key) {
+    connection.send("HVALS %b", key.data(), key.size());
+}
+
+// SET commands
+
+inline void sadd(Connection &connection,
+                    const StringView &key,
+                    const StringView &member) {
+    connection.send("SADD %b %b",
+                    key.data(), key.size(),
+                    member.data(), member.size());
+}
+
+template <typename Input>
+inline void sadd_range(Connection &connection,
+                        const StringView &key,
+                        Input first,
+                        Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "SADD" << key << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void scard(Connection &connection, const StringView &key) {
+    connection.send("SCARD %b", key.data(), key.size());
+}
+
+template <typename Input>
+inline void sdiff(Connection &connection, Input first, Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "SDIFF" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void sdiffstore(Connection &connection,
+                        const StringView &destination,
+                        const StringView &key) {
+    connection.send("SDIFFSTORE %b %b",
+                    destination.data(), destination.size(),
+                    key.data(), key.size());
+}
+
+template <typename Input>
+inline void sdiffstore_range(Connection &connection,
+                                const StringView &destination,
+                                Input first,
+                                Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "SDIFFSTORE" << destination << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+template <typename Input>
+inline void sinter(Connection &connection, Input first, Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "SINTER" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void sinterstore(Connection &connection,
+                        const StringView &destination,
+                        const StringView &key) {
+    connection.send("SINTERSTORE %b %b",
+                    destination.data(), destination.size(),
+                    key.data(), key.size());
+}
+
+template <typename Input>
+inline void sinterstore_range(Connection &connection,
+                                const StringView &destination,
+                                Input first,
+                                Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "SINTERSTORE" << destination << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void sismember(Connection &connection,
+                        const StringView &key,
+                        const StringView &member) {
+    connection.send("SISMEMBER %b %b",
+                    key.data(), key.size(),
+                    member.data(), member.size());
+}
+
+inline void smembers(Connection &connection, const StringView &key) {
+    connection.send("SMEMBERS %b", key.data(), key.size());
+}
+
+inline void smove(Connection &connection,
+                    const StringView &source,
+                    const StringView &destination,
+                    const StringView &member) {
+    connection.send("SMOVE %b %b %b",
+                    source.data(), source.size(),
+                    destination.data(), destination.size(),
+                    member.data(), member.size());
+}
+
+inline void spop(Connection &connection, const StringView &key) {
+    connection.send("SPOP %b", key.data(), key.size());
+}
+
+inline void spop_range(Connection &connection, const StringView &key, long long count) {
+    connection.send("SPOP %b %lld",
+                    key.data(), key.size(),
+                    count);
+}
+
+inline void srandmember(Connection &connection, const StringView &key) {
+    connection.send("SRANDMEMBER %b", key.data(), key.size());
+}
+
+inline void srandmember_range(Connection &connection,
+                                const StringView &key,
+                                long long count) {
+    connection.send("SRANDMEMBER %b %lld",
+                    key.data(), key.size(),
+                    count);
+}
+
+inline void srem(Connection &connection,
+                    const StringView &key,
+                    const StringView &member) {
+    connection.send("SREM %b %b",
+                    key.data(), key.size(),
+                    member.data(), member.size());
+}
+
+template <typename Input>
+inline void srem_range(Connection &connection,
+                    const StringView &key,
+                    Input first,
+                    Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "SREM" << key << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void sscan(Connection &connection,
+                    const StringView &key,
+                    long long cursor,
+                    const StringView &pattern,
+                    long long count) {
+    connection.send("SSCAN %b %lld MATCH %b COUNT %lld",
+                    key.data(), key.size(),
+                    cursor,
+                    pattern.data(), pattern.size(),
+                    count);
+}
+
+template <typename Input>
+inline void sunion(Connection &connection, Input first, Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "SUNION" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void sunionstore(Connection &connection,
+                        const StringView &destination,
+                        const StringView &key) {
+    connection.send("SUNIONSTORE %b %b",
+                    destination.data(), destination.size(),
+                    key.data(), key.size());
+}
+
+template <typename Input>
+inline void sunionstore_range(Connection &connection,
+                                const StringView &destination,
+                                Input first,
+                                Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "SUNIONSTORE" << destination << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+// Sorted Set commands.
+
+inline void bzpopmax(Connection &connection, const StringView &key, long long timeout) {
+    connection.send("BZPOPMAX %b %lld", key.data(), key.size(), timeout);
+}
+
+template <typename Input>
+void bzpopmax_range(Connection &connection,
+                    Input first,
+                    Input last,
+                    long long timeout) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "BZPOPMAX" << std::make_pair(first, last) << timeout;
+
+    connection.send(args);
+}
+
+inline void bzpopmin(Connection &connection, const StringView &key, long long timeout) {
+    connection.send("BZPOPMIN %b %lld", key.data(), key.size(), timeout);
+}
+
+template <typename Input>
+void bzpopmin_range(Connection &connection,
+                    Input first,
+                    Input last,
+                    long long timeout) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "BZPOPMIN" << std::make_pair(first, last) << timeout;
+
+    connection.send(args);
+}
+
+template <typename Input>
+void zadd_range(Connection &connection,
+                const StringView &key,
+                Input first,
+                Input last,
+                UpdateType type,
+                bool changed);
+
+inline void zadd(Connection &connection,
+                    const StringView &key,
+                    const StringView &member,
+                    double score,
+                    UpdateType type,
+                    bool changed) {
+    auto tmp = {std::make_pair(member, score)};
+
+    zadd_range(connection, key, tmp.begin(), tmp.end(), type, changed);
+}
+
+inline void zcard(Connection &connection, const StringView &key) {
+    connection.send("ZCARD %b", key.data(), key.size());
+}
+
+template <typename Interval>
+inline void zcount(Connection &connection,
+                    const StringView &key,
+                    const Interval &interval) {
+    connection.send("ZCOUNT %b %s %s",
+                    key.data(), key.size(),
+                    interval.min().c_str(),
+                    interval.max().c_str());
+}
+
+inline void zincrby(Connection &connection,
+                    const StringView &key,
+                    double increment,
+                    const StringView &member) {
+    connection.send("ZINCRBY %b %f %b",
+                    key.data(), key.size(),
+                    increment,
+                    member.data(), member.size());
+}
+
+inline void zinterstore(Connection &connection,
+                        const StringView &destination,
+                        const StringView &key,
+                        double weight) {
+    connection.send("ZINTERSTORE %b 1 %b WEIGHTS %f",
+                    destination.data(), destination.size(),
+                    key.data(), key.size(),
+                    weight);
+}
+
+template <typename Input>
+void zinterstore_range(Connection &connection,
+                        const StringView &destination,
+                        Input first,
+                        Input last,
+                        Aggregation aggr);
+
+template <typename Interval>
+inline void zlexcount(Connection &connection,
+                        const StringView &key,
+                        const Interval &interval) {
+    const auto &min = interval.min();
+    const auto &max = interval.max();
+
+    connection.send("ZLEXCOUNT %b %b %b",
+                    key.data(), key.size(),
+                    min.data(), min.size(),
+                    max.data(), max.size());
+}
+
+inline void zpopmax(Connection &connection, const StringView &key, long long count) {
+    connection.send("ZPOPMAX %b %lld",
+                        key.data(), key.size(),
+                        count);
+}
+
+inline void zpopmin(Connection &connection, const StringView &key, long long count) {
+    connection.send("ZPOPMIN %b %lld",
+                        key.data(), key.size(),
+                        count);
+}
+
+inline void zrange(Connection &connection,
+                    const StringView &key,
+                    long long start,
+                    long long stop,
+                    bool with_scores) {
+    if (with_scores) {
+        connection.send("ZRANGE %b %lld %lld WITHSCORES",
+                        key.data(), key.size(),
+                        start,
+                        stop);
+    } else {
+        connection.send("ZRANGE %b %lld %lld",
+                        key.data(), key.size(),
+                        start,
+                        stop);
+    }
+}
+
+template <typename Interval>
+inline void zrangebylex(Connection &connection,
+                        const StringView &key,
+                        const Interval &interval,
+                        const LimitOptions &opts) {
+    const auto &min = interval.min();
+    const auto &max = interval.max();
+
+    connection.send("ZRANGEBYLEX %b %b %b LIMIT %lld %lld",
+                    key.data(), key.size(),
+                    min.data(), min.size(),
+                    max.data(), max.size(),
+                    opts.offset,
+                    opts.count);
+}
+
+template <typename Interval>
+void zrangebyscore(Connection &connection,
+                    const StringView &key,
+                    const Interval &interval,
+                    const LimitOptions &opts,
+                    bool with_scores) {
+    const auto &min = interval.min();
+    const auto &max = interval.max();
+
+    if (with_scores) {
+        connection.send("ZRANGEBYSCORE %b %b %b WITHSCORES LIMIT %lld %lld",
+                        key.data(), key.size(),
+                        min.data(), min.size(),
+                        max.data(), max.size(),
+                        opts.offset,
+                        opts.count);
+    } else {
+        connection.send("ZRANGEBYSCORE %b %b %b LIMIT %lld %lld",
+                        key.data(), key.size(),
+                        min.data(), min.size(),
+                        max.data(), max.size(),
+                        opts.offset,
+                        opts.count);
+    }
+}
+
+inline void zrank(Connection &connection,
+                    const StringView &key,
+                    const StringView &member) {
+    connection.send("ZRANK %b %b",
+                    key.data(), key.size(),
+                    member.data(), member.size());
+}
+
+inline void zrem(Connection &connection,
+                    const StringView &key,
+                    const StringView &member) {
+    connection.send("ZREM %b %b",
+                    key.data(), key.size(),
+                    member.data(), member.size());
+}
+
+template <typename Input>
+inline void zrem_range(Connection &connection,
+                        const StringView &key,
+                        Input first,
+                        Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "ZREM" << key << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+template <typename Interval>
+inline void zremrangebylex(Connection &connection,
+                            const StringView &key,
+                            const Interval &interval) {
+    const auto &min = interval.min();
+    const auto &max = interval.max();
+
+    connection.send("ZREMRANGEBYLEX %b %b %b",
+                    key.data(), key.size(),
+                    min.data(), min.size(),
+                    max.data(), max.size());
+}
+
+inline void zremrangebyrank(Connection &connection,
+                            const StringView &key,
+                            long long start,
+                            long long stop) {
+    connection.send("zremrangebyrank %b %lld %lld",
+                    key.data(), key.size(),
+                    start,
+                    stop);
+}
+
+template <typename Interval>
+inline void zremrangebyscore(Connection &connection,
+                                const StringView &key,
+                                const Interval &interval) {
+    const auto &min = interval.min();
+    const auto &max = interval.max();
+
+    connection.send("ZREMRANGEBYSCORE %b %b %b",
+                    key.data(), key.size(),
+                    min.data(), min.size(),
+                    max.data(), max.size());
+}
+
+inline void zrevrange(Connection &connection,
+                        const StringView &key,
+                        long long start,
+                        long long stop,
+                        bool with_scores) {
+    if (with_scores) {
+        connection.send("ZREVRANGE %b %lld %lld WITHSCORES",
+                        key.data(), key.size(),
+                        start,
+                        stop);
+    } else {
+        connection.send("ZREVRANGE %b %lld %lld",
+                        key.data(), key.size(),
+                        start,
+                        stop);
+    }
+}
+
+template <typename Interval>
+inline void zrevrangebylex(Connection &connection,
+                            const StringView &key,
+                            const Interval &interval,
+                            const LimitOptions &opts) {
+    const auto &min = interval.min();
+    const auto &max = interval.max();
+
+    connection.send("ZREVRANGEBYLEX %b %b %b LIMIT %lld %lld",
+                    key.data(), key.size(),
+                    max.data(), max.size(),
+                    min.data(), min.size(),
+                    opts.offset,
+                    opts.count);
+}
+
+template <typename Interval>
+void zrevrangebyscore(Connection &connection,
+                        const StringView &key,
+                        const Interval &interval,
+                        const LimitOptions &opts,
+                        bool with_scores) {
+    const auto &min = interval.min();
+    const auto &max = interval.max();
+
+    if (with_scores) {
+        connection.send("ZREVRANGEBYSCORE %b %b %b WITHSCORES LIMIT %lld %lld",
+                        key.data(), key.size(),
+                        max.data(), max.size(),
+                        min.data(), min.size(),
+                        opts.offset,
+                        opts.count);
+    } else {
+        connection.send("ZREVRANGEBYSCORE %b %b %b LIMIT %lld %lld",
+                        key.data(), key.size(),
+                        max.data(), max.size(),
+                        min.data(), min.size(),
+                        opts.offset,
+                        opts.count);
+    }
+}
+
+inline void zrevrank(Connection &connection,
+                        const StringView &key,
+                        const StringView &member) {
+    connection.send("ZREVRANK %b %b",
+                    key.data(), key.size(),
+                    member.data(), member.size());
+}
+
+inline void zscan(Connection &connection,
+                    const StringView &key,
+                    long long cursor,
+                    const StringView &pattern,
+                    long long count) {
+    connection.send("ZSCAN %b %lld MATCH %b COUNT %lld",
+                    key.data(), key.size(),
+                    cursor,
+                    pattern.data(), pattern.size(),
+                    count);
+}
+
+inline void zscore(Connection &connection,
+                    const StringView &key,
+                    const StringView &member) {
+    connection.send("ZSCORE %b %b",
+                    key.data(), key.size(),
+                    member.data(), member.size());
+}
+
+inline void zunionstore(Connection &connection,
+                        const StringView &destination,
+                        const StringView &key,
+                        double weight) {
+    connection.send("ZUNIONSTORE %b 1 %b WEIGHTS %f",
+                    destination.data(), destination.size(),
+                    key.data(), key.size(),
+                    weight);
+}
+
+template <typename Input>
+void zunionstore_range(Connection &connection,
+                        const StringView &destination,
+                        Input first,
+                        Input last,
+                        Aggregation aggr);
+
+// HYPERLOGLOG commands.
+
+inline void pfadd(Connection &connection,
+                    const StringView &key,
+                    const StringView &element) {
+    connection.send("PFADD %b %b",
+                    key.data(), key.size(),
+                    element.data(), element.size());
+}
+
+template <typename Input>
+inline void pfadd_range(Connection &connection,
+                        const StringView &key,
+                        Input first,
+                        Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "PFADD" << key << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void pfcount(Connection &connection, const StringView &key) {
+    connection.send("PFCOUNT %b", key.data(), key.size());
+}
+
+template <typename Input>
+inline void pfcount_range(Connection &connection,
+                            Input first,
+                            Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "PFCOUNT" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void pfmerge(Connection &connection, const StringView &destination, const StringView &key) {
+    connection.send("PFMERGE %b %b",
+                    destination.data(), destination.size(),
+                    key.data(), key.size());
+}
+
+template <typename Input>
+inline void pfmerge_range(Connection &connection,
+                            const StringView &destination,
+                            Input first,
+                            Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "PFMERGE" << destination << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+// GEO commands.
+
+inline void geoadd(Connection &connection,
+                    const StringView &key,
+                    const std::tuple<StringView, double, double> &member) {
+    const auto &mem = std::get<0>(member);
+
+    connection.send("GEOADD %b %f %f %b",
+                    key.data(), key.size(),
+                    std::get<1>(member),
+                    std::get<2>(member),
+                    mem.data(), mem.size());
+}
+
+template <typename Input>
+inline void geoadd_range(Connection &connection,
+                            const StringView &key,
+                            Input first,
+                            Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "GEOADD" << key;
+
+    while (first != last) {
+        const auto &member = *first;
+        args << std::get<1>(member) << std::get<2>(member) << std::get<0>(member);
+        ++first;
+    }
+
+    connection.send(args);
+}
+
+void geodist(Connection &connection,
+                const StringView &key,
+                const StringView &member1,
+                const StringView &member2,
+                GeoUnit unit);
+
+template <typename Input>
+inline void geohash_range(Connection &connection,
+                            const StringView &key,
+                            Input first,
+                            Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "GEOHASH" << key << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+template <typename Input>
+inline void geopos_range(Connection &connection,
+                            const StringView &key,
+                            Input first,
+                            Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "GEOPOS" << key << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+void georadius(Connection &connection,
+                const StringView &key,
+                const std::pair<double, double> &loc,
+                double radius,
+                GeoUnit unit,
+                long long count,
+                bool asc,
+                bool with_coord,
+                bool with_dist,
+                bool with_hash);
+
+void georadius_store(Connection &connection,
+                        const StringView &key,
+                        const std::pair<double, double> &loc,
+                        double radius,
+                        GeoUnit unit,
+                        const StringView &destination,
+                        bool store_dist,
+                        long long count);
+
+void georadiusbymember(Connection &connection,
+                        const StringView &key,
+                        const StringView &member,
+                        double radius,
+                        GeoUnit unit,
+                        long long count,
+                        bool asc,
+                        bool with_coord,
+                        bool with_dist,
+                        bool with_hash);
+
+void georadiusbymember_store(Connection &connection,
+                                const StringView &key,
+                                const StringView &member,
+                                double radius,
+                                GeoUnit unit,
+                                const StringView &destination,
+                                bool store_dist,
+                                long long count);
+
+// SCRIPTING commands.
+
+inline void eval(Connection &connection,
+                    const StringView &script,
+                    std::initializer_list<StringView> keys,
+                    std::initializer_list<StringView> args) {
+    CmdArgs cmd_args;
+
+    cmd_args << "EVAL" << script << keys.size()
+            << std::make_pair(keys.begin(), keys.end())
+            << std::make_pair(args.begin(), args.end());
+
+    connection.send(cmd_args);
+}
+
+inline void evalsha(Connection &connection,
+                    const StringView &script,
+                    std::initializer_list<StringView> keys,
+                    std::initializer_list<StringView> args) {
+    CmdArgs cmd_args;
+
+    cmd_args << "EVALSHA" << script << keys.size()
+            << std::make_pair(keys.begin(), keys.end())
+            << std::make_pair(args.begin(), args.end());
+
+    connection.send(cmd_args);
+}
+
+inline void script_exists(Connection &connection, const StringView &sha) {
+    connection.send("SCRIPT EXISTS %b", sha.data(), sha.size());
+}
+
+template <typename Input>
+inline void script_exists_range(Connection &connection, Input first, Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+    args << "SCRIPT" << "EXISTS" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void script_flush(Connection &connection) {
+    connection.send("SCRIPT FLUSH");
+}
+
+inline void script_kill(Connection &connection) {
+    connection.send("SCRIPT KILL");
+}
+
+inline void script_load(Connection &connection, const StringView &script) {
+    connection.send("SCRIPT LOAD %b", script.data(), script.size());
+}
+
+// PUBSUB commands.
+
+inline void psubscribe(Connection &connection, const StringView &pattern) {
+    connection.send("PSUBSCRIBE %b", pattern.data(), pattern.size());
+}
+
+template <typename Input>
+inline void psubscribe_range(Connection &connection, Input first, Input last) {
+    if (first == last) {
+        throw Error("PSUBSCRIBE: no key specified");
+    }
+
+    CmdArgs args;
+    args << "PSUBSCRIBE" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void publish(Connection &connection,
+                    const StringView &channel,
+                    const StringView &message) {
+    connection.send("PUBLISH %b %b",
+                    channel.data(), channel.size(),
+                    message.data(), message.size());
+}
+
+inline void punsubscribe(Connection &connection) {
+    connection.send("PUNSUBSCRIBE");
+}
+
+inline void punsubscribe(Connection &connection, const StringView &pattern) {
+    connection.send("PUNSUBSCRIBE %b", pattern.data(), pattern.size());
+}
+
+template <typename Input>
+inline void punsubscribe_range(Connection &connection, Input first, Input last) {
+    if (first == last) {
+        throw Error("PUNSUBSCRIBE: no key specified");
+    }
+
+    CmdArgs args;
+    args << "PUNSUBSCRIBE" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void subscribe(Connection &connection, const StringView &channel) {
+    connection.send("SUBSCRIBE %b", channel.data(), channel.size());
+}
+
+template <typename Input>
+inline void subscribe_range(Connection &connection, Input first, Input last) {
+    if (first == last) {
+        throw Error("SUBSCRIBE: no key specified");
+    }
+
+    CmdArgs args;
+    args << "SUBSCRIBE" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void unsubscribe(Connection &connection) {
+    connection.send("UNSUBSCRIBE");
+}
+
+inline void unsubscribe(Connection &connection, const StringView &channel) {
+    connection.send("UNSUBSCRIBE %b", channel.data(), channel.size());
+}
+
+template <typename Input>
+inline void unsubscribe_range(Connection &connection, Input first, Input last) {
+    if (first == last) {
+        throw Error("UNSUBSCRIBE: no key specified");
+    }
+
+    CmdArgs args;
+    args << "UNSUBSCRIBE" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+// Transaction commands.
+
+inline void discard(Connection &connection) {
+    connection.send("DISCARD");
+}
+
+inline void exec(Connection &connection) {
+    connection.send("EXEC");
+}
+
+inline void multi(Connection &connection) {
+    connection.send("MULTI");
+}
+
+inline void unwatch(Connection &connection, const StringView &key) {
+    connection.send("UNWATCH %b", key.data(), key.size());
+}
+
+template <typename Input>
+inline void unwatch_range(Connection &connection, Input first, Input last) {
+    if (first == last) {
+        throw Error("UNWATCH: no key specified");
+    }
+
+    CmdArgs args;
+    args << "UNWATCH" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void watch(Connection &connection, const StringView &key) {
+    connection.send("WATCH %b", key.data(), key.size());
+}
+
+template <typename Input>
+inline void watch_range(Connection &connection, Input first, Input last) {
+    if (first == last) {
+        throw Error("WATCH: no key specified");
+    }
+
+    CmdArgs args;
+    args << "WATCH" << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+// Stream commands.
+
+inline void xack(Connection &connection,
+                    const StringView &key,
+                    const StringView &group,
+                    const StringView &id) {
+    connection.send("XACK %b %b %b",
+                    key.data(), key.size(),
+                    group.data(), group.size(),
+                    id.data(), id.size());
+}
+
+template <typename Input>
+void xack_range(Connection &connection,
+                const StringView &key,
+                const StringView &group,
+                Input first,
+                Input last) {
+    CmdArgs args;
+    args << "XACK" << key << group << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+template <typename Input>
+void xadd_range(Connection &connection,
+                const StringView &key,
+                const StringView &id,
+                Input first,
+                Input last) {
+    CmdArgs args;
+    args << "XADD" << key << id << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+template <typename Input>
+void xadd_maxlen_range(Connection &connection,
+                        const StringView &key,
+                        const StringView &id,
+                        Input first,
+                        Input last,
+                        long long count,
+                        bool approx) {
+    CmdArgs args;
+    args << "XADD" << key << "MAXLEN";
+
+    if (approx) {
+        args << "~";
+    }
+
+    args << count << id << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void xclaim(Connection &connection,
+                    const StringView &key,
+                    const StringView &group,
+                    const StringView &consumer,
+                    long long min_idle_time,
+                    const StringView &id) {
+    connection.send("XCLAIM %b %b %b %lld %b",
+                    key.data(), key.size(),
+                    group.data(), group.size(),
+                    consumer.data(), consumer.size(),
+                    min_idle_time,
+                    id.data(), id.size());
+}
+
+template <typename Input>
+void xclaim_range(Connection &connection,
+                    const StringView &key,
+                    const StringView &group,
+                    const StringView &consumer,
+                    long long min_idle_time,
+                    Input first,
+                    Input last) {
+    CmdArgs args;
+    args << "XCLAIM" << key << group << consumer << min_idle_time << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void xdel(Connection &connection, const StringView &key, const StringView &id) {
+    connection.send("XDEL %b %b", key.data(), key.size(), id.data(), id.size());
+}
+
+template <typename Input>
+void xdel_range(Connection &connection, const StringView &key, Input first, Input last) {
+    CmdArgs args;
+    args << "XDEL" << key << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+inline void xgroup_create(Connection &connection,
+                            const StringView &key,
+                            const StringView &group,
+                            const StringView &id,
+                            bool mkstream) {
+    CmdArgs args;
+    args << "XGROUP" << "CREATE" << key << group << id;
+
+    if (mkstream) {
+        args << "MKSTREAM";
+    }
+
+    connection.send(args);
+}
+
+inline void xgroup_setid(Connection &connection,
+                            const StringView &key,
+                            const StringView &group,
+                            const StringView &id) {
+    connection.send("XGROUP SETID %b %b %b",
+                    key.data(), key.size(),
+                    group.data(), group.size(),
+                    id.data(), id.size());
+}
+
+inline void xgroup_destroy(Connection &connection,
+                            const StringView &key,
+                            const StringView &group) {
+    connection.send("XGROUP DESTROY %b %b",
+                    key.data(), key.size(),
+                    group.data(), group.size());
+}
+
+inline void xgroup_delconsumer(Connection &connection,
+                                const StringView &key,
+                                const StringView &group,
+                                const StringView &consumer) {
+    connection.send("XGROUP DELCONSUMER %b %b %b",
+                    key.data(), key.size(),
+                    group.data(), group.size(),
+                    consumer.data(), consumer.size());
+}
+
+inline void xlen(Connection &connection, const StringView &key) {
+    connection.send("XLEN %b", key.data(), key.size());
+}
+
+inline void xpending(Connection &connection, const StringView &key, const StringView &group) {
+    connection.send("XPENDING %b %b",
+                    key.data(), key.size(),
+                    group.data(), group.size());
+}
+
+inline void xpending_detail(Connection &connection,
+                            const StringView &key,
+                            const StringView &group,
+                            const StringView &start,
+                            const StringView &end,
+                            long long count) {
+    connection.send("XPENDING %b %b %b %b %lld",
+                    key.data(), key.size(),
+                    group.data(), group.size(),
+                    start.data(), start.size(),
+                    end.data(), end.size(),
+                    count);
+}
+
+inline void xpending_per_consumer(Connection &connection,
+                                    const StringView &key,
+                                    const StringView &group,
+                                    const StringView &start,
+                                    const StringView &end,
+                                    long long count,
+                                    const StringView &consumer) {
+    connection.send("XPENDING %b %b %b %b %lld %b",
+                    key.data(), key.size(),
+                    group.data(), group.size(),
+                    start.data(), start.size(),
+                    end.data(), end.size(),
+                    count,
+                    consumer.data(), consumer.size());
+}
+
+inline void xrange(Connection &connection,
+                    const StringView &key,
+                    const StringView &start,
+                    const StringView &end) {
+    connection.send("XRANGE %b %b %b",
+                    key.data(), key.size(),
+                    start.data(), start.size(),
+                    end.data(), end.size());
+}
+
+inline void xrange_count(Connection &connection,
+                            const StringView &key,
+                            const StringView &start,
+                            const StringView &end,
+                            long long count) {
+    connection.send("XRANGE %b %b %b COUNT %lld",
+                    key.data(), key.size(),
+                    start.data(), start.size(),
+                    end.data(), end.size(),
+                    count);
+}
+
+inline void xread(Connection &connection,
+                    const StringView &key,
+                    const StringView &id,
+                    long long count) {
+    connection.send("XREAD COUNT %lld STREAMS %b %b",
+                    count,
+                    key.data(), key.size(),
+                    id.data(), id.size());
+}
+
+template <typename Input>
+void xread_range(Connection &connection, Input first, Input last, long long count) {
+    CmdArgs args;
+    args << "XREAD" << "COUNT" << count << "STREAMS";
+
+    for (auto iter = first; iter != last; ++iter) {
+        args << iter->first;
+    }
+
+    for (auto iter = first; iter != last; ++iter) {
+        args << iter->second;
+    }
+
+    connection.send(args);
+}
+
+inline void xread_block(Connection &connection,
+                        const StringView &key,
+                        const StringView &id,
+                        long long timeout,
+                        long long count) {
+    connection.send("XREAD COUNT %lld BLOCK %lld STREAMS %b %b",
+                    count,
+                    timeout,
+                    key.data(), key.size(),
+                    id.data(), id.size());
+}
+
+template <typename Input>
+void xread_block_range(Connection &connection,
+                        Input first,
+                        Input last,
+                        long long timeout,
+                        long long count) {
+    CmdArgs args;
+    args << "XREAD" << "COUNT" << count << "BLOCK" << timeout << "STREAMS";
+
+    for (auto iter = first; iter != last; ++iter) {
+        args << iter->first;
+    }
+
+    for (auto iter = first; iter != last; ++iter) {
+        args << iter->second;
+    }
+
+    connection.send(args);
+}
+
+inline void xreadgroup(Connection &connection,
+                        const StringView &group,
+                        const StringView &consumer,
+                        const StringView &key,
+                        const StringView &id,
+                        long long count,
+                        bool noack) {
+    CmdArgs args;
+    args << "XREADGROUP" << "GROUP" << group << consumer << "COUNT" << count;
+
+    if (noack) {
+        args << "NOACK";
+    }
+
+    args << "STREAMS" << key << id;
+
+    connection.send(args);
+}
+
+template <typename Input>
+void xreadgroup_range(Connection &connection,
+                        const StringView &group,
+                        const StringView &consumer,
+                        Input first,
+                        Input last,
+                        long long count,
+                        bool noack) {
+    CmdArgs args;
+    args << "XREADGROUP" << "GROUP" << group << consumer << "COUNT" << count;
+
+    if (noack) {
+        args << "NOACK";
+    }
+
+    args << "STREAMS";
+
+    for (auto iter = first; iter != last; ++iter) {
+        args << iter->first;
+    }
+
+    for (auto iter = first; iter != last; ++iter) {
+        args << iter->second;
+    }
+
+    connection.send(args);
+}
+
+inline void xreadgroup_block(Connection &connection,
+                                const StringView &group,
+                                const StringView &consumer,
+                                const StringView &key,
+                                const StringView &id,
+                                long long timeout,
+                                long long count,
+                                bool noack) {
+    CmdArgs args;
+    args << "XREADGROUP" << "GROUP" << group << consumer
+        << "COUNT" << count << "BLOCK" << timeout;
+
+    if (noack) {
+        args << "NOACK";
+    }
+
+    args << "STREAMS" << key << id;
+
+    connection.send(args);
+}
+
+template <typename Input>
+void xreadgroup_block_range(Connection &connection,
+                            const StringView &group,
+                            const StringView &consumer,
+                            Input first,
+                            Input last,
+                            long long timeout,
+                            long long count,
+                            bool noack) {
+    CmdArgs args;
+    args << "XREADGROUP" << "GROUP" << group << consumer
+        << "COUNT" << count << "BLOCK" << timeout;
+
+    if (noack) {
+        args << "NOACK";
+    }
+
+    args << "STREAMS";
+
+    for (auto iter = first; iter != last; ++iter) {
+        args << iter->first;
+    }
+
+    for (auto iter = first; iter != last; ++iter) {
+        args << iter->second;
+    }
+
+    connection.send(args);
+}
+
+inline void xrevrange(Connection &connection,
+                    const StringView &key,
+                    const StringView &end,
+                    const StringView &start) {
+    connection.send("XREVRANGE %b %b %b",
+                    key.data(), key.size(),
+                    end.data(), end.size(),
+                    start.data(), start.size());
+}
+
+inline void xrevrange_count(Connection &connection,
+                            const StringView &key,
+                            const StringView &end,
+                            const StringView &start,
+                            long long count) {
+    connection.send("XREVRANGE %b %b %b COUNT %lld",
+                    key.data(), key.size(),
+                    end.data(), end.size(),
+                    start.data(), start.size(),
+                    count);
+}
+
+void xtrim(Connection &connection, const StringView &key, long long count, bool approx);
+
+namespace detail {
+
+void set_bitop(CmdArgs &args, BitOp op);
+
+void set_update_type(CmdArgs &args, UpdateType type);
+
+void set_aggregation_type(CmdArgs &args, Aggregation type);
+
+template <typename Input>
+void zinterstore(std::false_type,
+                    Connection &connection,
+                    const StringView &destination,
+                    Input first,
+                    Input last,
+                    Aggregation aggr) {
+    CmdArgs args;
+    args << "ZINTERSTORE" << destination << std::distance(first, last)
+        << std::make_pair(first, last);
+
+    set_aggregation_type(args, aggr);
+
+    connection.send(args);
+}
+
+template <typename Input>
+void zinterstore(std::true_type,
+                    Connection &connection,
+                    const StringView &destination,
+                    Input first,
+                    Input last,
+                    Aggregation aggr) {
+    CmdArgs args;
+    args << "ZINTERSTORE" << destination << std::distance(first, last);
+
+    for (auto iter = first; iter != last; ++iter) {
+        args << iter->first;
+    }
+
+    args << "WEIGHTS";
+
+    for (auto iter = first; iter != last; ++iter) {
+        args << iter->second;
+    }
+
+    set_aggregation_type(args, aggr);
+
+    connection.send(args);
+}
+
+template <typename Input>
+void zunionstore(std::false_type,
+                    Connection &connection,
+                    const StringView &destination,
+                    Input first,
+                    Input last,
+                    Aggregation aggr) {
+    CmdArgs args;
+    args << "ZUNIONSTORE" << destination << std::distance(first, last)
+        << std::make_pair(first, last);
+
+    set_aggregation_type(args, aggr);
+
+    connection.send(args);
+}
+
+template <typename Input>
+void zunionstore(std::true_type,
+                    Connection &connection,
+                    const StringView &destination,
+                    Input first,
+                    Input last,
+                    Aggregation aggr) {
+    CmdArgs args;
+    args << "ZUNIONSTORE" << destination << std::distance(first, last);
+
+    for (auto iter = first; iter != last; ++iter) {
+        args << iter->first;
+    }
+
+    args << "WEIGHTS";
+
+    for (auto iter = first; iter != last; ++iter) {
+        args << iter->second;
+    }
+
+    set_aggregation_type(args, aggr);
+
+    connection.send(args);
+}
+
+void set_geo_unit(CmdArgs &args, GeoUnit unit);
+
+void set_georadius_store_parameters(CmdArgs &args,
+                                    double radius,
+                                    GeoUnit unit,
+                                    const StringView &destination,
+                                    bool store_dist,
+                                    long long count);
+
+void set_georadius_parameters(CmdArgs &args,
+                                double radius,
+                                GeoUnit unit,
+                                long long count,
+                                bool asc,
+                                bool with_coord,
+                                bool with_dist,
+                                bool with_hash);
+
+}
+
+}
+
+}
+
+}
+
+namespace sw {
+
+namespace redis {
+
+namespace cmd {
+
+template <typename Input>
+void bitop_range(Connection &connection,
+                    BitOp op,
+                    const StringView &destination,
+                    Input first,
+                    Input last) {
+    assert(first != last);
+
+    CmdArgs args;
+
+    detail::set_bitop(args, op);
+
+    args << destination << std::make_pair(first, last);
+
+    connection.send(args);
+}
+
+template <typename Input>
+void zadd_range(Connection &connection,
+                const StringView &key,
+                Input first,
+                Input last,
+                UpdateType type,
+                bool changed) {
+    assert(first != last);
+
+    CmdArgs args;
+
+    args << "ZADD" << key;
+
+    detail::set_update_type(args, type);
+
+    if (changed) {
+        args << "CH";
+    }
+
+    while (first != last) {
+        // Swap the <member, score> pair to <score, member> pair.
+        args << first->second << first->first;
+        ++first;
+    }
+
+    connection.send(args);
+}
+
+template <typename Input>
+void zinterstore_range(Connection &connection,
+                        const StringView &destination,
+                        Input first,
+                        Input last,
+                        Aggregation aggr) {
+    assert(first != last);
+
+    detail::zinterstore(typename IsKvPairIter<Input>::type(),
+                        connection,
+                        destination,
+                        first,
+                        last,
+                        aggr);
+}
+
+template <typename Input>
+void zunionstore_range(Connection &connection,
+                        const StringView &destination,
+                        Input first,
+                        Input last,
+                        Aggregation aggr) {
+    assert(first != last);
+
+    detail::zunionstore(typename IsKvPairIter<Input>::type(),
+                        connection,
+                        destination,
+                        first,
+                        last,
+                        aggr);
+}
+
+}
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_COMMAND_H

+ 180 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/command_args.h

@@ -0,0 +1,180 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_COMMAND_ARGS_H
+#define SEWENEW_REDISPLUSPLUS_COMMAND_ARGS_H
+
+#include <vector>
+#include <list>
+#include <string>
+#include <tuple>
+#include "utils.h"
+
+namespace sw {
+
+namespace redis {
+
+class CmdArgs {
+public:
+    template <typename Arg>
+    CmdArgs& append(Arg &&arg);
+
+    template <typename Arg, typename ...Args>
+    CmdArgs& append(Arg &&arg, Args &&...args);
+
+    // All overloads of operator<< are for internal use only.
+    CmdArgs& operator<<(const StringView &arg);
+
+    template <typename T,
+                 typename std::enable_if<std::is_arithmetic<typename std::decay<T>::type>::value,
+                                        int>::type = 0>
+    CmdArgs& operator<<(T &&arg);
+
+    template <typename Iter>
+    CmdArgs& operator<<(const std::pair<Iter, Iter> &range);
+
+    template <std::size_t N, typename ...Args>
+    auto operator<<(const std::tuple<Args...> &) ->
+        typename std::enable_if<N == sizeof...(Args), CmdArgs&>::type {
+        return *this;
+    }
+
+    template <std::size_t N = 0, typename ...Args>
+    auto operator<<(const std::tuple<Args...> &arg) ->
+        typename std::enable_if<N < sizeof...(Args), CmdArgs&>::type;
+
+    const char** argv() {
+        return _argv.data();
+    }
+
+    const std::size_t* argv_len() {
+        return _argv_len.data();
+    }
+
+    std::size_t size() const {
+        return _argv.size();
+    }
+
+private:
+    // Deep copy.
+    CmdArgs& _append(std::string arg);
+
+    // Shallow copy.
+    CmdArgs& _append(const StringView &arg);
+
+    // Shallow copy.
+    CmdArgs& _append(const char *arg);
+
+    template <typename T,
+                 typename std::enable_if<std::is_arithmetic<typename std::decay<T>::type>::value,
+                                        int>::type = 0>
+    CmdArgs& _append(T &&arg) {
+        return operator<<(std::forward<T>(arg));
+    }
+
+    template <typename Iter>
+    CmdArgs& _append(std::true_type, const std::pair<Iter, Iter> &range);
+
+    template <typename Iter>
+    CmdArgs& _append(std::false_type, const std::pair<Iter, Iter> &range);
+
+    std::vector<const char *> _argv;
+    std::vector<std::size_t> _argv_len;
+
+    std::list<std::string> _args;
+};
+
+template <typename Arg>
+inline CmdArgs& CmdArgs::append(Arg &&arg) {
+    return _append(std::forward<Arg>(arg));
+}
+
+template <typename Arg, typename ...Args>
+inline CmdArgs& CmdArgs::append(Arg &&arg, Args &&...args) {
+    _append(std::forward<Arg>(arg));
+
+    return append(std::forward<Args>(args)...);
+}
+
+inline CmdArgs& CmdArgs::operator<<(const StringView &arg) {
+    _argv.push_back(arg.data());
+    _argv_len.push_back(arg.size());
+
+    return *this;
+}
+
+template <typename Iter>
+inline CmdArgs& CmdArgs::operator<<(const std::pair<Iter, Iter> &range) {
+    return _append(IsKvPair<typename std::decay<decltype(*std::declval<Iter>())>::type>(), range);
+}
+
+template <typename T,
+             typename std::enable_if<std::is_arithmetic<typename std::decay<T>::type>::value,
+                                    int>::type>
+inline CmdArgs& CmdArgs::operator<<(T &&arg) {
+    return _append(std::to_string(std::forward<T>(arg)));
+}
+
+template <std::size_t N, typename ...Args>
+auto CmdArgs::operator<<(const std::tuple<Args...> &arg) ->
+    typename std::enable_if<N < sizeof...(Args), CmdArgs&>::type {
+    operator<<(std::get<N>(arg));
+
+    return operator<<<N + 1, Args...>(arg);
+}
+
+inline CmdArgs& CmdArgs::_append(std::string arg) {
+    _args.push_back(std::move(arg));
+    return operator<<(_args.back());
+}
+
+inline CmdArgs& CmdArgs::_append(const StringView &arg) {
+    return operator<<(arg);
+}
+
+inline CmdArgs& CmdArgs::_append(const char *arg) {
+    return operator<<(arg);
+}
+
+template <typename Iter>
+CmdArgs& CmdArgs::_append(std::false_type, const std::pair<Iter, Iter> &range) {
+    auto first = range.first;
+    auto last = range.second;
+    while (first != last) {
+        *this << *first;
+        ++first;
+    }
+
+    return *this;
+}
+
+template <typename Iter>
+CmdArgs& CmdArgs::_append(std::true_type, const std::pair<Iter, Iter> &range) {
+    auto first = range.first;
+    auto last = range.second;
+    while (first != last) {
+        *this << first->first << first->second;
+        ++first;
+    }
+
+    return *this;
+}
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_COMMAND_ARGS_H

+ 201 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/command_options.cpp

@@ -0,0 +1,201 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#include "command_options.h"
+#include "errors.h"
+
+namespace {
+
+const std::string NEGATIVE_INFINITY_NUMERIC = "-inf";
+const std::string POSITIVE_INFINITY_NUMERIC = "+inf";
+
+const std::string NEGATIVE_INFINITY_STRING = "-";
+const std::string POSITIVE_INFINITY_STRING = "+";
+
+std::string unbound(const std::string &bnd);
+
+std::string bound(const std::string &bnd);
+
+}
+
+namespace sw {
+
+namespace redis {
+
+const std::string& UnboundedInterval<double>::min() const {
+    return NEGATIVE_INFINITY_NUMERIC;
+}
+
+const std::string& UnboundedInterval<double>::max() const {
+    return POSITIVE_INFINITY_NUMERIC;
+}
+
+BoundedInterval<double>::BoundedInterval(double min, double max, BoundType type) :
+                                            _min(std::to_string(min)),
+                                            _max(std::to_string(max)) {
+    switch (type) {
+    case BoundType::CLOSED:
+        // Do nothing
+        break;
+
+    case BoundType::OPEN:
+        _min = unbound(_min);
+        _max = unbound(_max);
+        break;
+
+    case BoundType::LEFT_OPEN:
+        _min = unbound(_min);
+        break;
+
+    case BoundType::RIGHT_OPEN:
+        _max = unbound(_max);
+        break;
+
+    default:
+        throw Error("Unknow BoundType");
+    }
+}
+
+LeftBoundedInterval<double>::LeftBoundedInterval(double min, BoundType type) :
+                                                    _min(std::to_string(min)) {
+    switch (type) {
+    case BoundType::OPEN:
+        _min = unbound(_min);
+        break;
+
+    case BoundType::RIGHT_OPEN:
+        // Do nothing.
+        break;
+
+    default:
+        throw Error("Bound type can only be OPEN or RIGHT_OPEN");
+    }
+}
+
+const std::string& LeftBoundedInterval<double>::max() const {
+    return POSITIVE_INFINITY_NUMERIC;
+}
+
+RightBoundedInterval<double>::RightBoundedInterval(double max, BoundType type) :
+                                                    _max(std::to_string(max)) {
+    switch (type) {
+    case BoundType::OPEN:
+        _max = unbound(_max);
+        break;
+
+    case BoundType::LEFT_OPEN:
+        // Do nothing.
+        break;
+
+    default:
+        throw Error("Bound type can only be OPEN or LEFT_OPEN");
+    }
+}
+
+const std::string& RightBoundedInterval<double>::min() const {
+    return NEGATIVE_INFINITY_NUMERIC;
+}
+
+const std::string& UnboundedInterval<std::string>::min() const {
+    return NEGATIVE_INFINITY_STRING;
+}
+
+const std::string& UnboundedInterval<std::string>::max() const {
+    return POSITIVE_INFINITY_STRING;
+}
+
+BoundedInterval<std::string>::BoundedInterval(const std::string &min,
+                                                const std::string &max,
+                                                BoundType type) {
+    switch (type) {
+    case BoundType::CLOSED:
+        _min = bound(min);
+        _max = bound(max);
+        break;
+
+    case BoundType::OPEN:
+        _min = unbound(min);
+        _max = unbound(max);
+        break;
+
+    case BoundType::LEFT_OPEN:
+        _min = unbound(min);
+        _max = bound(max);
+        break;
+
+    case BoundType::RIGHT_OPEN:
+        _min = bound(min);
+        _max = unbound(max);
+        break;
+
+    default:
+        throw Error("Unknow BoundType");
+    }
+}
+
+LeftBoundedInterval<std::string>::LeftBoundedInterval(const std::string &min, BoundType type) {
+    switch (type) {
+    case BoundType::OPEN:
+        _min = unbound(min);
+        break;
+
+    case BoundType::RIGHT_OPEN:
+        _min = bound(min);
+        break;
+
+    default:
+        throw Error("Bound type can only be OPEN or RIGHT_OPEN");
+    }
+}
+
+const std::string& LeftBoundedInterval<std::string>::max() const {
+    return POSITIVE_INFINITY_STRING;
+}
+
+RightBoundedInterval<std::string>::RightBoundedInterval(const std::string &max, BoundType type) {
+    switch (type) {
+    case BoundType::OPEN:
+        _max = unbound(max);
+        break;
+
+    case BoundType::LEFT_OPEN:
+        _max = bound(max);
+        break;
+
+    default:
+        throw Error("Bound type can only be OPEN or LEFT_OPEN");
+    }
+}
+
+const std::string& RightBoundedInterval<std::string>::min() const {
+    return NEGATIVE_INFINITY_STRING;
+}
+
+}
+
+}
+
+namespace {
+
+std::string unbound(const std::string &bnd) {
+    return "(" + bnd;
+}
+
+std::string bound(const std::string &bnd) {
+    return "[" + bnd;
+}
+
+}

+ 211 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/command_options.h

@@ -0,0 +1,211 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_COMMAND_OPTIONS_H
+#define SEWENEW_REDISPLUSPLUS_COMMAND_OPTIONS_H
+
+#include <string>
+#include "utils.h"
+
+namespace sw {
+
+namespace redis {
+
+enum class UpdateType {
+    EXIST,
+    NOT_EXIST,
+    ALWAYS
+};
+
+enum class InsertPosition {
+    BEFORE,
+    AFTER
+};
+
+enum class BoundType {
+    CLOSED,
+    OPEN,
+    LEFT_OPEN,
+    RIGHT_OPEN
+};
+
+// (-inf, +inf)
+template <typename T>
+class UnboundedInterval;
+
+// [min, max], (min, max), (min, max], [min, max)
+template <typename T>
+class BoundedInterval;
+
+// [min, +inf), (min, +inf)
+template <typename T>
+class LeftBoundedInterval;
+
+// (-inf, max], (-inf, max)
+template <typename T>
+class RightBoundedInterval;
+
+template <>
+class UnboundedInterval<double> {
+public:
+    const std::string& min() const;
+
+    const std::string& max() const;
+};
+
+template <>
+class BoundedInterval<double> {
+public:
+    BoundedInterval(double min, double max, BoundType type);
+
+    const std::string& min() const {
+        return _min;
+    }
+
+    const std::string& max() const {
+        return _max;
+    }
+
+private:
+    std::string _min;
+    std::string _max;
+};
+
+template <>
+class LeftBoundedInterval<double> {
+public:
+    LeftBoundedInterval(double min, BoundType type);
+
+    const std::string& min() const {
+        return _min;
+    }
+
+    const std::string& max() const;
+
+private:
+    std::string _min;
+};
+
+template <>
+class RightBoundedInterval<double> {
+public:
+    RightBoundedInterval(double max, BoundType type);
+
+    const std::string& min() const;
+
+    const std::string& max() const {
+        return _max;
+    }
+
+private:
+    std::string _max;
+};
+
+template <>
+class UnboundedInterval<std::string> {
+public:
+    const std::string& min() const;
+
+    const std::string& max() const;
+};
+
+template <>
+class BoundedInterval<std::string> {
+public:
+    BoundedInterval(const std::string &min, const std::string &max, BoundType type);
+
+    const std::string& min() const {
+        return _min;
+    }
+
+    const std::string& max() const {
+        return _max;
+    }
+
+private:
+    std::string _min;
+    std::string _max;
+};
+
+template <>
+class LeftBoundedInterval<std::string> {
+public:
+    LeftBoundedInterval(const std::string &min, BoundType type);
+
+    const std::string& min() const {
+        return _min;
+    }
+
+    const std::string& max() const;
+
+private:
+    std::string _min;
+};
+
+template <>
+class RightBoundedInterval<std::string> {
+public:
+    RightBoundedInterval(const std::string &max, BoundType type);
+
+    const std::string& min() const;
+
+    const std::string& max() const {
+        return _max;
+    }
+
+private:
+    std::string _max;
+};
+
+struct LimitOptions {
+    long long offset = 0;
+    long long count = -1;
+};
+
+enum class Aggregation {
+    SUM,
+    MIN,
+    MAX
+};
+
+enum class BitOp {
+    AND,
+    OR,
+    XOR,
+    NOT
+};
+
+enum class GeoUnit {
+    M,
+    KM,
+    MI,
+    FT
+};
+
+template <typename T>
+struct WithCoord : TupleWithType<std::pair<double, double>, T> {};
+
+template <typename T>
+struct WithDist : TupleWithType<double, T> {};
+
+template <typename T>
+struct WithHash : TupleWithType<long long, T> {};
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_COMMAND_OPTIONS_H

+ 305 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/connection.cpp

@@ -0,0 +1,305 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#include "connection.h"
+#include <cassert>
+#include "reply.h"
+#include "command.h"
+#include "command_args.h"
+
+namespace sw {
+
+namespace redis {
+
+ConnectionOptions::ConnectionOptions(const std::string &uri) :
+                                        ConnectionOptions(_parse_options(uri)) {}
+
+ConnectionOptions ConnectionOptions::_parse_options(const std::string &uri) const {
+    std::string type;
+    std::string path;
+    std::tie(type, path) = _split_string(uri, "://");
+
+    if (path.empty()) {
+        throw Error("Invalid URI: no path");
+    }
+
+    if (type == "tcp") {
+        return _parse_tcp_options(path);
+    } else if (type == "unix") {
+        return _parse_unix_options(path);
+    } else {
+        throw Error("Invalid URI: invalid type");
+    }
+}
+
+ConnectionOptions ConnectionOptions::_parse_tcp_options(const std::string &path) const {
+    ConnectionOptions options;
+
+    options.type = ConnectionType::TCP;
+
+    std::string host;
+    std::string port;
+    std::tie(host, port) = _split_string(path, ":");
+
+    options.host = host;
+    try {
+        if (!port.empty()) {
+            options.port = std::stoi(port);
+        } // else use default port, i.e. 6379.
+    } catch (const std::exception &) {
+        throw Error("Invalid URL: invalid port");
+    }
+
+    return options;
+}
+
+ConnectionOptions ConnectionOptions::_parse_unix_options(const std::string &path) const {
+    ConnectionOptions options;
+
+    options.type = ConnectionType::UNIX;
+    options.path = path;
+
+    return options;
+}
+
+auto ConnectionOptions::_split_string(const std::string &str, const std::string &delimiter) const ->
+        std::pair<std::string, std::string> {
+    auto pos = str.rfind(delimiter);
+    if (pos == std::string::npos) {
+        return {str, ""};
+    }
+
+    return {str.substr(0, pos), str.substr(pos + delimiter.size())};
+}
+
+class Connection::Connector {
+public:
+    explicit Connector(const ConnectionOptions &opts);
+
+    ContextUPtr connect() const;
+
+private:
+    ContextUPtr _connect() const;
+
+    redisContext* _connect_tcp() const;
+
+    redisContext* _connect_unix() const;
+
+    void _set_socket_timeout(redisContext &ctx) const;
+
+    void _enable_keep_alive(redisContext &ctx) const;
+
+    timeval _to_timeval(const std::chrono::milliseconds &dur) const;
+
+    const ConnectionOptions &_opts;
+};
+
+Connection::Connector::Connector(const ConnectionOptions &opts) : _opts(opts) {}
+
+Connection::ContextUPtr Connection::Connector::connect() const {
+    auto ctx = _connect();
+
+    assert(ctx);
+
+    if (ctx->err != REDIS_OK) {
+        throw_error(*ctx, "Failed to connect to Redis");
+    }
+
+    _set_socket_timeout(*ctx);
+
+    _enable_keep_alive(*ctx);
+
+    return ctx;
+}
+
+Connection::ContextUPtr Connection::Connector::_connect() const {
+    redisContext *context = nullptr;
+    switch (_opts.type) {
+    case ConnectionType::TCP:
+        context = _connect_tcp();
+        break;
+
+    case ConnectionType::UNIX:
+        context = _connect_unix();
+        break;
+
+    default:
+        // Never goes here.
+        throw Error("Unkonw connection type");
+    }
+
+    if (context == nullptr) {
+        throw Error("Failed to allocate memory for connection.");
+    }
+
+    return ContextUPtr(context);
+}
+
+redisContext* Connection::Connector::_connect_tcp() const {
+    if (_opts.connect_timeout > std::chrono::milliseconds(0)) {
+        return redisConnectWithTimeout(_opts.host.c_str(),
+                    _opts.port,
+                    _to_timeval(_opts.connect_timeout));
+    } else {
+        return redisConnect(_opts.host.c_str(), _opts.port);
+    }
+}
+
+redisContext* Connection::Connector::_connect_unix() const {
+    if (_opts.connect_timeout > std::chrono::milliseconds(0)) {
+        return redisConnectUnixWithTimeout(
+                    _opts.path.c_str(),
+                    _to_timeval(_opts.connect_timeout));
+    } else {
+        return redisConnectUnix(_opts.path.c_str());
+    }
+}
+
+void Connection::Connector::_set_socket_timeout(redisContext &ctx) const {
+    if (_opts.socket_timeout <= std::chrono::milliseconds(0)) {
+        return;
+    }
+
+    if (redisSetTimeout(&ctx, _to_timeval(_opts.socket_timeout)) != REDIS_OK) {
+        throw_error(ctx, "Failed to set socket timeout");
+    }
+}
+
+void Connection::Connector::_enable_keep_alive(redisContext &ctx) const {
+    if (!_opts.keep_alive) {
+        return;
+    }
+
+    if (redisEnableKeepAlive(&ctx) != REDIS_OK) {
+        throw_error(ctx, "Failed to enable keep alive option");
+    }
+}
+
+timeval Connection::Connector::_to_timeval(const std::chrono::milliseconds &dur) const {
+    auto sec = std::chrono::duration_cast<std::chrono::seconds>(dur);
+    auto msec = std::chrono::duration_cast<std::chrono::microseconds>(dur - sec);
+
+    return {
+            static_cast<std::time_t>(sec.count()),
+            static_cast<suseconds_t>(msec.count())
+    };
+}
+
+void swap(Connection &lhs, Connection &rhs) noexcept {
+    std::swap(lhs._ctx, rhs._ctx);
+    std::swap(lhs._last_active, rhs._last_active);
+    std::swap(lhs._opts, rhs._opts);
+}
+
+Connection::Connection(const ConnectionOptions &opts) :
+            _ctx(Connector(opts).connect()),
+            _last_active(std::chrono::steady_clock::now()),
+            _opts(opts) {
+    assert(_ctx && !broken());
+
+    _set_options();
+}
+
+void Connection::reconnect() {
+    Connection connection(_opts);
+
+    swap(*this, connection);
+}
+
+void Connection::send(int argc, const char **argv, const std::size_t *argv_len) {
+    auto ctx = _context();
+
+    assert(ctx != nullptr);
+
+    if (redisAppendCommandArgv(ctx,
+                                argc,
+                                argv,
+                                argv_len) != REDIS_OK) {
+        throw_error(*ctx, "Failed to send command");
+    }
+
+    assert(!broken());
+}
+
+void Connection::send(CmdArgs &args) {
+    auto ctx = _context();
+
+    assert(ctx != nullptr);
+
+    if (redisAppendCommandArgv(ctx,
+                                args.size(),
+                                args.argv(),
+                                args.argv_len()) != REDIS_OK) {
+        throw_error(*ctx, "Failed to send command");
+    }
+
+    assert(!broken());
+}
+
+ReplyUPtr Connection::recv() {
+    auto *ctx = _context();
+
+    assert(ctx != nullptr);
+
+    void *r = nullptr;
+    if (redisGetReply(ctx, &r) != REDIS_OK) {
+        throw_error(*ctx, "Failed to get reply");
+    }
+
+    assert(!broken() && r != nullptr);
+
+    auto reply = ReplyUPtr(static_cast<redisReply*>(r));
+
+    if (reply::is_error(*reply)) {
+        throw_error(*reply);
+    }
+
+    return reply;
+}
+
+void Connection::_set_options() {
+    _auth();
+
+    _select_db();
+}
+
+void Connection::_auth() {
+    if (_opts.password.empty()) {
+        return;
+    }
+
+    cmd::auth(*this, _opts.password);
+
+    auto reply = recv();
+
+    reply::parse<void>(*reply);
+}
+
+void Connection::_select_db() {
+    if (_opts.db == 0) {
+        return;
+    }
+
+    cmd::select(*this, _opts.db);
+
+    auto reply = recv();
+
+    reply::parse<void>(*reply);
+}
+
+}
+
+}

+ 194 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/connection.h

@@ -0,0 +1,194 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_CONNECTION_H
+#define SEWENEW_REDISPLUSPLUS_CONNECTION_H
+
+#include <cerrno>
+#include <cstring>
+#include <memory>
+#include <string>
+#include <sstream>
+#include <chrono>
+#include <hiredis/hiredis.h>
+#include "errors.h"
+#include "reply.h"
+#include "utils.h"
+
+namespace sw {
+
+namespace redis {
+
+enum class ConnectionType {
+    TCP = 0,
+    UNIX
+};
+
+struct ConnectionOptions {
+public:
+    ConnectionOptions() = default;
+
+    explicit ConnectionOptions(const std::string &uri);
+
+    ConnectionOptions(const ConnectionOptions &) = default;
+    ConnectionOptions& operator=(const ConnectionOptions &) = default;
+
+    ConnectionOptions(ConnectionOptions &&) = default;
+    ConnectionOptions& operator=(ConnectionOptions &&) = default;
+
+    ~ConnectionOptions() = default;
+
+    ConnectionType type = ConnectionType::TCP;
+
+    std::string host;
+
+    int port = 6379;
+
+    std::string path;
+
+    std::string password;
+
+    int db = 0;
+
+    bool keep_alive = false;
+
+    std::chrono::milliseconds connect_timeout{0};
+
+    std::chrono::milliseconds socket_timeout{0};
+
+private:
+    ConnectionOptions _parse_options(const std::string &uri) const;
+
+    ConnectionOptions _parse_tcp_options(const std::string &path) const;
+
+    ConnectionOptions _parse_unix_options(const std::string &path) const;
+
+    auto _split_string(const std::string &str, const std::string &delimiter) const ->
+            std::pair<std::string, std::string>;
+};
+
+class CmdArgs;
+
+class Connection {
+public:
+    explicit Connection(const ConnectionOptions &opts);
+
+    Connection(const Connection &) = delete;
+    Connection& operator=(const Connection &) = delete;
+
+    Connection(Connection &&) = default;
+    Connection& operator=(Connection &&) = default;
+
+    ~Connection() = default;
+
+    // Check if the connection is broken. Client needs to do this check
+    // before sending some command to the connection. If it's broken,
+    // client needs to reconnect it.
+    bool broken() const noexcept {
+        return _ctx->err != REDIS_OK;
+    }
+
+    void reset() noexcept {
+        _ctx->err = 0;
+    }
+
+    void reconnect();
+
+    auto last_active() const
+        -> std::chrono::time_point<std::chrono::steady_clock> {
+        return _last_active;
+    }
+
+    template <typename ...Args>
+    void send(const char *format, Args &&...args);
+
+    void send(int argc, const char **argv, const std::size_t *argv_len);
+
+    void send(CmdArgs &args);
+
+    ReplyUPtr recv();
+
+    const ConnectionOptions& options() const {
+        return _opts;
+    }
+
+    friend void swap(Connection &lhs, Connection &rhs) noexcept;
+
+private:
+    class Connector;
+
+    struct ContextDeleter {
+        void operator()(redisContext *context) const {
+            if (context != nullptr) {
+                redisFree(context);
+            }
+        };
+    };
+
+    using ContextUPtr = std::unique_ptr<redisContext, ContextDeleter>;
+
+    void _set_options();
+
+    void _auth();
+
+    void _select_db();
+
+    redisContext* _context();
+
+    ContextUPtr _ctx;
+
+    // The time that the connection is created or the time that
+    // the connection is used, i.e. *context()* is called.
+    std::chrono::time_point<std::chrono::steady_clock> _last_active{};
+
+    ConnectionOptions _opts;
+};
+
+using ConnectionSPtr = std::shared_ptr<Connection>;
+
+enum class Role {
+    MASTER,
+    SLAVE
+};
+
+// Inline implementaions.
+
+template <typename ...Args>
+inline void Connection::send(const char *format, Args &&...args) {
+    auto ctx = _context();
+
+    assert(ctx != nullptr);
+
+    if (redisAppendCommand(ctx,
+                format,
+                std::forward<Args>(args)...) != REDIS_OK) {
+        throw_error(*ctx, "Failed to send command");
+    }
+
+    assert(!broken());
+}
+
+inline redisContext* Connection::_context() {
+    _last_active = std::chrono::steady_clock::now();
+
+    return _ctx.get();
+}
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_CONNECTION_H

+ 249 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/connection_pool.cpp

@@ -0,0 +1,249 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#include "connection_pool.h"
+#include <cassert>
+#include "errors.h"
+
+namespace sw {
+
+namespace redis {
+
+ConnectionPool::ConnectionPool(const ConnectionPoolOptions &pool_opts,
+        const ConnectionOptions &connection_opts) :
+            _opts(connection_opts),
+            _pool_opts(pool_opts) {
+    if (_pool_opts.size == 0) {
+        throw Error("CANNOT create an empty pool");
+    }
+
+    // Lazily create connections.
+}
+
+ConnectionPool::ConnectionPool(SimpleSentinel sentinel,
+                                const ConnectionPoolOptions &pool_opts,
+                                const ConnectionOptions &connection_opts) :
+                                    _opts(connection_opts),
+                                    _pool_opts(pool_opts),
+                                    _sentinel(std::move(sentinel)) {
+    // In this case, the connection must be of TCP type.
+    if (_opts.type != ConnectionType::TCP) {
+        throw Error("Sentinel only supports TCP connection");
+    }
+
+    if (_opts.connect_timeout == std::chrono::milliseconds(0)
+            || _opts.socket_timeout == std::chrono::milliseconds(0)) {
+        throw Error("With sentinel, connection timeout and socket timeout cannot be 0");
+    }
+
+    // Cleanup connection options.
+    _update_connection_opts("", -1);
+
+    assert(_sentinel);
+}
+
+ConnectionPool::ConnectionPool(ConnectionPool &&that) {
+    std::lock_guard<std::mutex> lock(that._mutex);
+
+    _move(std::move(that));
+}
+
+ConnectionPool& ConnectionPool::operator=(ConnectionPool &&that) {
+    if (this != &that) {
+        std::lock(_mutex, that._mutex);
+        std::lock_guard<std::mutex> lock_this(_mutex, std::adopt_lock);
+        std::lock_guard<std::mutex> lock_that(that._mutex, std::adopt_lock);
+
+        _move(std::move(that));
+    }
+
+    return *this;
+}
+
+Connection ConnectionPool::fetch() {
+    std::unique_lock<std::mutex> lock(_mutex);
+
+    if (_pool.empty()) {
+        if (_used_connections == _pool_opts.size) {
+            _wait_for_connection(lock);
+        } else {
+            // Lazily create a new connection.
+            auto connection = _create();
+
+            ++_used_connections;
+
+            return connection;
+        }
+    }
+
+    // _pool is NOT empty.
+    auto connection = _fetch();
+
+    auto connection_lifetime = _pool_opts.connection_lifetime;
+
+    if (_sentinel) {
+        auto opts = _opts;
+        auto role_changed = _role_changed(connection.options());
+        auto sentinel = _sentinel;
+
+        lock.unlock();
+
+        if (role_changed || _need_reconnect(connection, connection_lifetime)) {
+            try {
+                connection = _create(sentinel, opts, false);
+            } catch (const Error &e) {
+                // Failed to reconnect, return it to the pool, and retry latter.
+                release(std::move(connection));
+                throw;
+            }
+        }
+
+        return connection;
+    }
+
+    lock.unlock();
+
+    if (_need_reconnect(connection, connection_lifetime)) {
+        try {
+            connection.reconnect();
+        } catch (const Error &e) {
+            // Failed to reconnect, return it to the pool, and retry latter.
+            release(std::move(connection));
+            throw;
+        }
+    }
+
+    return connection;
+}
+
+ConnectionOptions ConnectionPool::connection_options() {
+    std::lock_guard<std::mutex> lock(_mutex);
+
+    return _opts;
+}
+
+void ConnectionPool::release(Connection connection) {
+    {
+        std::lock_guard<std::mutex> lock(_mutex);
+
+        _pool.push_back(std::move(connection));
+    }
+
+    _cv.notify_one();
+}
+
+Connection ConnectionPool::create() {
+    std::unique_lock<std::mutex> lock(_mutex);
+
+    auto opts = _opts;
+
+    if (_sentinel) {
+        auto sentinel = _sentinel;
+
+        lock.unlock();
+
+        return _create(sentinel, opts, false);
+    } else {
+        lock.unlock();
+
+        return Connection(opts);
+    }
+}
+
+void ConnectionPool::_move(ConnectionPool &&that) {
+    _opts = std::move(that._opts);
+    _pool_opts = std::move(that._pool_opts);
+    _pool = std::move(that._pool);
+    _used_connections = that._used_connections;
+    _sentinel = std::move(that._sentinel);
+}
+
+Connection ConnectionPool::_create() {
+    if (_sentinel) {
+        // Get Redis host and port info from sentinel.
+        return _create(_sentinel, _opts, true);
+    }
+
+    return Connection(_opts);
+}
+
+Connection ConnectionPool::_create(SimpleSentinel &sentinel,
+                                    const ConnectionOptions &opts,
+                                    bool locked) {
+    try {
+        auto connection = sentinel.create(opts);
+
+        std::unique_lock<std::mutex> lock(_mutex, std::defer_lock);
+        if (!locked) {
+            lock.lock();
+        }
+
+        const auto &connection_opts = connection.options();
+        if (_role_changed(connection_opts)) {
+            // Master/Slave has been changed, reconnect all connections.
+            _update_connection_opts(connection_opts.host, connection_opts.port);
+        }
+
+        return connection;
+    } catch (const StopIterError &e) {
+        throw Error("Failed to create connection with sentinel");
+    }
+}
+
+Connection ConnectionPool::_fetch() {
+    assert(!_pool.empty());
+
+    auto connection = std::move(_pool.front());
+    _pool.pop_front();
+
+    return connection;
+}
+
+void ConnectionPool::_wait_for_connection(std::unique_lock<std::mutex> &lock) {
+    auto timeout = _pool_opts.wait_timeout;
+    if (timeout > std::chrono::milliseconds(0)) {
+        // Wait until _pool is no longer empty or timeout.
+        if (!_cv.wait_for(lock,
+                    timeout,
+                    [this] { return !(this->_pool).empty(); })) {
+            throw Error("Failed to fetch a connection in "
+                    + std::to_string(timeout.count()) + " milliseconds");
+        }
+    } else {
+        // Wait forever.
+        _cv.wait(lock, [this] { return !(this->_pool).empty(); });
+    }
+}
+
+bool ConnectionPool::_need_reconnect(const Connection &connection,
+                                    const std::chrono::milliseconds &connection_lifetime) const {
+    if (connection.broken()) {
+        return true;
+    }
+
+    if (connection_lifetime > std::chrono::milliseconds(0)) {
+        auto now = std::chrono::steady_clock::now();
+        if (now - connection.last_active() > connection_lifetime) {
+            return true;
+        }
+    }
+
+    return false;
+}
+
+}
+
+}

+ 115 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/connection_pool.h

@@ -0,0 +1,115 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_CONNECTION_POOL_H
+#define SEWENEW_REDISPLUSPLUS_CONNECTION_POOL_H
+
+#include <chrono>
+#include <mutex>
+#include <memory>
+#include <condition_variable>
+#include <deque>
+#include "connection.h"
+#include "sentinel.h"
+
+namespace sw {
+
+namespace redis {
+
+struct ConnectionPoolOptions {
+    // Max number of connections, including both in-use and idle ones.
+    std::size_t size = 1;
+
+    // Max time to wait for a connection. 0ms means client waits forever.
+    std::chrono::milliseconds wait_timeout{0};
+
+    // Max lifetime of a connection. 0ms means we never expire the connection.
+    std::chrono::milliseconds connection_lifetime{0};
+};
+
+class ConnectionPool {
+public:
+    ConnectionPool(const ConnectionPoolOptions &pool_opts,
+                    const ConnectionOptions &connection_opts);
+
+    ConnectionPool(SimpleSentinel sentinel,
+                    const ConnectionPoolOptions &pool_opts,
+                    const ConnectionOptions &connection_opts);
+
+    ConnectionPool() = default;
+
+    ConnectionPool(ConnectionPool &&that);
+    ConnectionPool& operator=(ConnectionPool &&that);
+
+    ConnectionPool(const ConnectionPool &) = delete;
+    ConnectionPool& operator=(const ConnectionPool &) = delete;
+
+    ~ConnectionPool() = default;
+
+    // Fetch a connection from pool.
+    Connection fetch();
+
+    ConnectionOptions connection_options();
+
+    void release(Connection connection);
+
+    // Create a new connection.
+    Connection create();
+
+private:
+    void _move(ConnectionPool &&that);
+
+    // NOT thread-safe
+    Connection _create();
+
+    Connection _create(SimpleSentinel &sentinel, const ConnectionOptions &opts, bool locked);
+
+    Connection _fetch();
+
+    void _wait_for_connection(std::unique_lock<std::mutex> &lock);
+
+    bool _need_reconnect(const Connection &connection,
+                            const std::chrono::milliseconds &connection_lifetime) const;
+
+    void _update_connection_opts(const std::string &host, int port) {
+        _opts.host = host;
+        _opts.port = port;
+    }
+
+    bool _role_changed(const ConnectionOptions &opts) const {
+        return opts.port != _opts.port || opts.host != _opts.host;
+    }
+
+    ConnectionOptions _opts;
+
+    ConnectionPoolOptions _pool_opts;
+
+    std::deque<Connection> _pool;
+
+    std::size_t _used_connections = 0;
+
+    std::mutex _mutex;
+
+    std::condition_variable _cv;
+
+    SimpleSentinel _sentinel;
+};
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_CONNECTION_POOL_H

+ 96 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/crc16.cpp

@@ -0,0 +1,96 @@
+/*
+ * Copyright 2001-2010 Georges Menie (www.menie.org)
+ * Copyright 2010-2012 Salvatore Sanfilippo (adapted to Redis coding style)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in the
+ *       documentation and/or other materials provided with the distribution.
+ *     * Neither the name of the University of California, Berkeley nor the
+ *       names of its contributors may be used to endorse or promote products
+ *       derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/* CRC16 implementation according to CCITT standards.
+ *
+ * Note by @antirez: this is actually the XMODEM CRC 16 algorithm, using the
+ * following parameters:
+ *
+ * Name                       : "XMODEM", also known as "ZMODEM", "CRC-16/ACORN"
+ * Width                      : 16 bit
+ * Poly                       : 1021 (That is actually x^16 + x^12 + x^5 + 1)
+ * Initialization             : 0000
+ * Reflect Input byte         : False
+ * Reflect Output CRC         : False
+ * Xor constant to output CRC : 0000
+ * Output for "123456789"     : 31C3
+ */
+
+#include <cstdint>
+
+namespace sw {
+
+namespace redis {
+
+static const uint16_t crc16tab[256]= {
+    0x0000,0x1021,0x2042,0x3063,0x4084,0x50a5,0x60c6,0x70e7,
+    0x8108,0x9129,0xa14a,0xb16b,0xc18c,0xd1ad,0xe1ce,0xf1ef,
+    0x1231,0x0210,0x3273,0x2252,0x52b5,0x4294,0x72f7,0x62d6,
+    0x9339,0x8318,0xb37b,0xa35a,0xd3bd,0xc39c,0xf3ff,0xe3de,
+    0x2462,0x3443,0x0420,0x1401,0x64e6,0x74c7,0x44a4,0x5485,
+    0xa56a,0xb54b,0x8528,0x9509,0xe5ee,0xf5cf,0xc5ac,0xd58d,
+    0x3653,0x2672,0x1611,0x0630,0x76d7,0x66f6,0x5695,0x46b4,
+    0xb75b,0xa77a,0x9719,0x8738,0xf7df,0xe7fe,0xd79d,0xc7bc,
+    0x48c4,0x58e5,0x6886,0x78a7,0x0840,0x1861,0x2802,0x3823,
+    0xc9cc,0xd9ed,0xe98e,0xf9af,0x8948,0x9969,0xa90a,0xb92b,
+    0x5af5,0x4ad4,0x7ab7,0x6a96,0x1a71,0x0a50,0x3a33,0x2a12,
+    0xdbfd,0xcbdc,0xfbbf,0xeb9e,0x9b79,0x8b58,0xbb3b,0xab1a,
+    0x6ca6,0x7c87,0x4ce4,0x5cc5,0x2c22,0x3c03,0x0c60,0x1c41,
+    0xedae,0xfd8f,0xcdec,0xddcd,0xad2a,0xbd0b,0x8d68,0x9d49,
+    0x7e97,0x6eb6,0x5ed5,0x4ef4,0x3e13,0x2e32,0x1e51,0x0e70,
+    0xff9f,0xefbe,0xdfdd,0xcffc,0xbf1b,0xaf3a,0x9f59,0x8f78,
+    0x9188,0x81a9,0xb1ca,0xa1eb,0xd10c,0xc12d,0xf14e,0xe16f,
+    0x1080,0x00a1,0x30c2,0x20e3,0x5004,0x4025,0x7046,0x6067,
+    0x83b9,0x9398,0xa3fb,0xb3da,0xc33d,0xd31c,0xe37f,0xf35e,
+    0x02b1,0x1290,0x22f3,0x32d2,0x4235,0x5214,0x6277,0x7256,
+    0xb5ea,0xa5cb,0x95a8,0x8589,0xf56e,0xe54f,0xd52c,0xc50d,
+    0x34e2,0x24c3,0x14a0,0x0481,0x7466,0x6447,0x5424,0x4405,
+    0xa7db,0xb7fa,0x8799,0x97b8,0xe75f,0xf77e,0xc71d,0xd73c,
+    0x26d3,0x36f2,0x0691,0x16b0,0x6657,0x7676,0x4615,0x5634,
+    0xd94c,0xc96d,0xf90e,0xe92f,0x99c8,0x89e9,0xb98a,0xa9ab,
+    0x5844,0x4865,0x7806,0x6827,0x18c0,0x08e1,0x3882,0x28a3,
+    0xcb7d,0xdb5c,0xeb3f,0xfb1e,0x8bf9,0x9bd8,0xabbb,0xbb9a,
+    0x4a75,0x5a54,0x6a37,0x7a16,0x0af1,0x1ad0,0x2ab3,0x3a92,
+    0xfd2e,0xed0f,0xdd6c,0xcd4d,0xbdaa,0xad8b,0x9de8,0x8dc9,
+    0x7c26,0x6c07,0x5c64,0x4c45,0x3ca2,0x2c83,0x1ce0,0x0cc1,
+    0xef1f,0xff3e,0xcf5d,0xdf7c,0xaf9b,0xbfba,0x8fd9,0x9ff8,
+    0x6e17,0x7e36,0x4e55,0x5e74,0x2e93,0x3eb2,0x0ed1,0x1ef0
+};
+
+uint16_t crc16(const char *buf, int len) {
+    int counter;
+    uint16_t crc = 0;
+    for (counter = 0; counter < len; counter++)
+            crc = (crc<<8) ^ crc16tab[((crc>>8) ^ *buf++)&0x00FF];
+    return crc;
+}
+
+}
+
+}

+ 136 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/errors.cpp

@@ -0,0 +1,136 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#include "errors.h"
+#include <cassert>
+#include <cerrno>
+#include <unordered_map>
+#include <tuple>
+#include "shards.h"
+
+namespace {
+
+using namespace sw::redis;
+
+std::pair<ReplyErrorType, std::string> parse_error(const std::string &msg);
+
+std::unordered_map<std::string, ReplyErrorType> error_map = {
+    {"MOVED", ReplyErrorType::MOVED},
+    {"ASK", ReplyErrorType::ASK}
+};
+
+}
+
+namespace sw {
+
+namespace redis {
+
+void throw_error(redisContext &context, const std::string &err_info) {
+    auto err_code = context.err;
+    const auto *err_str = context.errstr;
+    if (err_str == nullptr) {
+        throw Error(err_info + ": null error message: " + std::to_string(err_code));
+    }
+
+    auto err_msg = err_info + ": " + err_str;
+
+    switch (err_code) {
+    case REDIS_ERR_IO:
+        if (errno == EAGAIN || errno == EINTR) {
+            throw TimeoutError(err_msg);
+        } else {
+            throw IoError(err_msg);
+        }
+        break;
+
+    case REDIS_ERR_EOF:
+        throw ClosedError(err_msg);
+        break;
+
+    case REDIS_ERR_PROTOCOL:
+        throw ProtoError(err_msg);
+        break;
+
+    case REDIS_ERR_OOM:
+        throw OomError(err_msg);
+        break;
+
+    case REDIS_ERR_OTHER:
+        throw Error(err_msg);
+        break;
+
+    default:
+        throw Error(err_info + ": Unknown error code");
+    }
+}
+
+void throw_error(const redisReply &reply) {
+    assert(reply.type == REDIS_REPLY_ERROR);
+
+    if (reply.str == nullptr) {
+        throw Error("Null error reply");
+    }
+
+    auto err_str = std::string(reply.str, reply.len);
+
+    auto err_type = ReplyErrorType::ERR;
+    std::string err_msg;
+    std::tie(err_type, err_msg) = parse_error(err_str);
+
+    switch (err_type) {
+    case ReplyErrorType::MOVED:
+        throw MovedError(err_msg);
+        break;
+
+    case ReplyErrorType::ASK:
+        throw AskError(err_msg);
+        break;
+
+    default:
+        throw ReplyError(err_str);
+        break;
+    }
+}
+
+}
+
+}
+
+namespace {
+
+using namespace sw::redis;
+
+std::pair<ReplyErrorType, std::string> parse_error(const std::string &err) {
+    // The error contains an Error Prefix, and an optional error message.
+    auto idx = err.find_first_of(" \n");
+
+    if (idx == std::string::npos) {
+        throw ProtoError("No Error Prefix: " + err);
+    }
+
+    auto err_prefix = err.substr(0, idx);
+    auto err_type = ReplyErrorType::ERR;
+
+    auto iter = error_map.find(err_prefix);
+    if (iter != error_map.end()) {
+        // Specific error.
+        err_type = iter->second;
+    } // else Generic error.
+
+    return {err_type, err.substr(idx + 1)};
+}
+
+}

+ 159 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/errors.h

@@ -0,0 +1,159 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_ERRORS_H
+#define SEWENEW_REDISPLUSPLUS_ERRORS_H
+
+#include <exception>
+#include <string>
+#include <hiredis/hiredis.h>
+
+namespace sw {
+
+namespace redis {
+
+enum ReplyErrorType {
+    ERR,
+    MOVED,
+    ASK
+};
+
+class Error : public std::exception {
+public:
+    explicit Error(const std::string &msg) : _msg(msg) {}
+
+    Error(const Error &) = default;
+    Error& operator=(const Error &) = default;
+
+    Error(Error &&) = default;
+    Error& operator=(Error &&) = default;
+
+    virtual ~Error() = default;
+
+    virtual const char* what() const noexcept {
+        return _msg.data();
+    }
+
+private:
+    std::string _msg;
+};
+
+class IoError : public Error {
+public:
+    explicit IoError(const std::string &msg) : Error(msg) {}
+
+    IoError(const IoError &) = default;
+    IoError& operator=(const IoError &) = default;
+
+    IoError(IoError &&) = default;
+    IoError& operator=(IoError &&) = default;
+
+    virtual ~IoError() = default;
+};
+
+class TimeoutError : public IoError {
+public:
+    explicit TimeoutError(const std::string &msg) : IoError(msg) {}
+
+    TimeoutError(const TimeoutError &) = default;
+    TimeoutError& operator=(const TimeoutError &) = default;
+
+    TimeoutError(TimeoutError &&) = default;
+    TimeoutError& operator=(TimeoutError &&) = default;
+
+    virtual ~TimeoutError() = default;
+};
+
+class ClosedError : public Error {
+public:
+    explicit ClosedError(const std::string &msg) : Error(msg) {}
+
+    ClosedError(const ClosedError &) = default;
+    ClosedError& operator=(const ClosedError &) = default;
+
+    ClosedError(ClosedError &&) = default;
+    ClosedError& operator=(ClosedError &&) = default;
+
+    virtual ~ClosedError() = default;
+};
+
+class ProtoError : public Error {
+public:
+    explicit ProtoError(const std::string &msg) : Error(msg) {}
+
+    ProtoError(const ProtoError &) = default;
+    ProtoError& operator=(const ProtoError &) = default;
+
+    ProtoError(ProtoError &&) = default;
+    ProtoError& operator=(ProtoError &&) = default;
+
+    virtual ~ProtoError() = default;
+};
+
+class OomError : public Error {
+public:
+    explicit OomError(const std::string &msg) : Error(msg) {}
+
+    OomError(const OomError &) = default;
+    OomError& operator=(const OomError &) = default;
+
+    OomError(OomError &&) = default;
+    OomError& operator=(OomError &&) = default;
+
+    virtual ~OomError() = default;
+};
+
+class ReplyError : public Error {
+public:
+    explicit ReplyError(const std::string &msg) : Error(msg) {}
+
+    ReplyError(const ReplyError &) = default;
+    ReplyError& operator=(const ReplyError &) = default;
+
+    ReplyError(ReplyError &&) = default;
+    ReplyError& operator=(ReplyError &&) = default;
+
+    virtual ~ReplyError() = default;
+};
+
+class WatchError : public Error {
+public:
+    explicit WatchError() : Error("Watched key has been modified") {}
+
+    WatchError(const WatchError &) = default;
+    WatchError& operator=(const WatchError &) = default;
+
+    WatchError(WatchError &&) = default;
+    WatchError& operator=(WatchError &&) = default;
+
+    virtual ~WatchError() = default;
+};
+
+
+// MovedError and AskError are defined in shards.h
+class MovedError;
+
+class AskError;
+
+void throw_error(redisContext &context, const std::string &err_info);
+
+void throw_error(const redisReply &reply);
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_ERRORS_H

+ 35 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/pipeline.cpp

@@ -0,0 +1,35 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#include "pipeline.h"
+
+namespace sw {
+
+namespace redis {
+
+std::vector<ReplyUPtr> PipelineImpl::exec(Connection &connection, std::size_t cmd_num) {
+    std::vector<ReplyUPtr> replies;
+    while (cmd_num > 0) {
+        replies.push_back(connection.recv());
+        --cmd_num;
+    }
+
+    return replies;
+}
+
+}
+
+}

+ 49 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/pipeline.h

@@ -0,0 +1,49 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_PIPELINE_H
+#define SEWENEW_REDISPLUSPLUS_PIPELINE_H
+
+#include <cassert>
+#include <vector>
+#include "connection.h"
+
+namespace sw {
+
+namespace redis {
+
+class PipelineImpl {
+public:
+    template <typename Cmd, typename ...Args>
+    void command(Connection &connection, Cmd cmd, Args &&...args) {
+        assert(!connection.broken());
+
+        cmd(connection, std::forward<Args>(args)...);
+    }
+
+    std::vector<ReplyUPtr> exec(Connection &connection, std::size_t cmd_num);
+
+    void discard(Connection &connection, std::size_t /*cmd_num*/) {
+        // Reconnect to Redis to discard all commands.
+        connection.reconnect();
+    }
+};
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_PIPELINE_H

+ 1844 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/queued_redis.h

@@ -0,0 +1,1844 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_QUEUED_REDIS_H
+#define SEWENEW_REDISPLUSPLUS_QUEUED_REDIS_H
+
+#include <cassert>
+#include <chrono>
+#include <initializer_list>
+#include <vector>
+#include "connection.h"
+#include "utils.h"
+#include "reply.h"
+#include "command.h"
+#include "redis.h"
+
+namespace sw {
+
+namespace redis {
+
+class QueuedReplies;
+
+// If any command throws, QueuedRedis resets the connection, and becomes invalid.
+// In this case, the only thing we can do is to destory the QueuedRedis object.
+template <typename Impl>
+class QueuedRedis {
+public:
+    QueuedRedis(QueuedRedis &&) = default;
+    QueuedRedis& operator=(QueuedRedis &&) = default;
+
+    // When it destructs, the underlying *Connection* will be closed,
+    // and any command that has NOT been executed will be ignored.
+    ~QueuedRedis() = default;
+
+    Redis redis();
+
+    template <typename Cmd, typename ...Args>
+    auto command(Cmd cmd, Args &&...args)
+        -> typename std::enable_if<!std::is_convertible<Cmd, StringView>::value,
+                                    QueuedRedis&>::type;
+
+    template <typename ...Args>
+    QueuedRedis& command(const StringView &cmd_name, Args &&...args);
+
+    template <typename Input>
+    auto command(Input first, Input last)
+        -> typename std::enable_if<IsIter<Input>::value, QueuedRedis&>::type;
+
+    QueuedReplies exec();
+
+    void discard();
+
+    // CONNECTION commands.
+
+    QueuedRedis& auth(const StringView &password) {
+        return command(cmd::auth, password);
+    }
+
+    QueuedRedis& echo(const StringView &msg) {
+        return command(cmd::echo, msg);
+    }
+
+    QueuedRedis& ping() {
+        return command<void (*)(Connection &)>(cmd::ping);
+    }
+
+    QueuedRedis& ping(const StringView &msg) {
+        return command<void (*)(Connection &, const StringView &)>(cmd::ping, msg);
+    }
+
+    // We DO NOT support the QUIT command. See *Redis::quit* doc for details.
+    //
+    // QueuedRedis& quit();
+
+    QueuedRedis& select(long long idx) {
+        return command(cmd::select, idx);
+    }
+
+    QueuedRedis& swapdb(long long idx1, long long idx2) {
+        return command(cmd::swapdb, idx1, idx2);
+    }
+
+    // SERVER commands.
+
+    QueuedRedis& bgrewriteaof() {
+        return command(cmd::bgrewriteaof);
+    }
+
+    QueuedRedis& bgsave() {
+        return command(cmd::bgsave);
+    }
+
+    QueuedRedis& dbsize() {
+        return command(cmd::dbsize);
+    }
+
+    QueuedRedis& flushall(bool async = false) {
+        return command(cmd::flushall, async);
+    }
+
+    QueuedRedis& flushdb(bool async = false) {
+        return command(cmd::flushdb, async);
+    }
+
+    QueuedRedis& info() {
+        return command<void (*)(Connection &)>(cmd::info);
+    }
+
+    QueuedRedis& info(const StringView &section) {
+        return command<void (*)(Connection &, const StringView &)>(cmd::info, section);
+    }
+
+    QueuedRedis& lastsave() {
+        return command(cmd::lastsave);
+    }
+
+    QueuedRedis& save() {
+        return command(cmd::save);
+    }
+
+    // KEY commands.
+
+    QueuedRedis& del(const StringView &key) {
+        return command(cmd::del, key);
+    }
+
+    template <typename Input>
+    QueuedRedis& del(Input first, Input last) {
+        return command(cmd::del_range<Input>, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& del(std::initializer_list<T> il) {
+        return del(il.begin(), il.end());
+    }
+
+    QueuedRedis& dump(const StringView &key) {
+        return command(cmd::dump, key);
+    }
+
+    QueuedRedis& exists(const StringView &key) {
+        return command(cmd::exists, key);
+    }
+
+    template <typename Input>
+    QueuedRedis& exists(Input first, Input last) {
+        return command(cmd::exists_range<Input>, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& exists(std::initializer_list<T> il) {
+        return exists(il.begin(), il.end());
+    }
+
+    QueuedRedis& expire(const StringView &key, long long timeout) {
+        return command(cmd::expire, key, timeout);
+    }
+
+    QueuedRedis& expire(const StringView &key,
+                        const std::chrono::seconds &timeout) {
+        return expire(key, timeout.count());
+    }
+
+    QueuedRedis& expireat(const StringView &key, long long timestamp) {
+        return command(cmd::expireat, key, timestamp);
+    }
+
+    QueuedRedis& expireat(const StringView &key,
+                            const std::chrono::time_point<std::chrono::system_clock,
+                                                            std::chrono::seconds> &tp) {
+        return expireat(key, tp.time_since_epoch().count());
+    }
+
+    QueuedRedis& keys(const StringView &pattern) {
+        return command(cmd::keys, pattern);
+    }
+
+    QueuedRedis& move(const StringView &key, long long db) {
+        return command(cmd::move, key, db);
+    }
+
+    QueuedRedis& persist(const StringView &key) {
+        return command(cmd::persist, key);
+    }
+
+    QueuedRedis& pexpire(const StringView &key, long long timeout) {
+        return command(cmd::pexpire, key, timeout);
+    }
+
+    QueuedRedis& pexpire(const StringView &key,
+                            const std::chrono::milliseconds &timeout) {
+        return pexpire(key, timeout.count());
+    }
+
+    QueuedRedis& pexpireat(const StringView &key, long long timestamp) {
+        return command(cmd::pexpireat, key, timestamp);
+    }
+
+    QueuedRedis& pexpireat(const StringView &key,
+                            const std::chrono::time_point<std::chrono::system_clock,
+                                                            std::chrono::milliseconds> &tp) {
+        return pexpireat(key, tp.time_since_epoch().count());
+    }
+
+    QueuedRedis& pttl(const StringView &key) {
+        return command(cmd::pttl, key);
+    }
+
+    QueuedRedis& randomkey() {
+        return command(cmd::randomkey);
+    }
+
+    QueuedRedis& rename(const StringView &key, const StringView &newkey) {
+        return command(cmd::rename, key, newkey);
+    }
+
+    QueuedRedis& renamenx(const StringView &key, const StringView &newkey) {
+        return command(cmd::renamenx, key, newkey);
+    }
+
+    QueuedRedis& restore(const StringView &key,
+                                const StringView &val,
+                                long long ttl,
+                                bool replace = false) {
+        return command(cmd::restore, key, val, ttl, replace);
+    }
+
+    QueuedRedis& restore(const StringView &key,
+                            const StringView &val,
+                            const std::chrono::milliseconds &ttl = std::chrono::milliseconds{0},
+                            bool replace = false) {
+        return restore(key, val, ttl.count(), replace);
+    }
+
+    // TODO: sort
+
+    QueuedRedis& scan(long long cursor,
+                        const StringView &pattern,
+                        long long count) {
+        return command(cmd::scan, cursor, pattern, count);
+    }
+
+    QueuedRedis& scan(long long cursor) {
+        return scan(cursor, "*", 10);
+    }
+
+    QueuedRedis& scan(long long cursor,
+                        const StringView &pattern) {
+        return scan(cursor, pattern, 10);
+    }
+
+    QueuedRedis& scan(long long cursor,
+                        long long count) {
+        return scan(cursor, "*", count);
+    }
+
+    QueuedRedis& touch(const StringView &key) {
+        return command(cmd::touch, key);
+    }
+
+    template <typename Input>
+    QueuedRedis& touch(Input first, Input last) {
+        return command(cmd::touch_range<Input>, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& touch(std::initializer_list<T> il) {
+        return touch(il.begin(), il.end());
+    }
+
+    QueuedRedis& ttl(const StringView &key) {
+        return command(cmd::ttl, key);
+    }
+
+    QueuedRedis& type(const StringView &key) {
+        return command(cmd::type, key);
+    }
+
+    QueuedRedis& unlink(const StringView &key) {
+        return command(cmd::unlink, key);
+    }
+
+    template <typename Input>
+    QueuedRedis& unlink(Input first, Input last) {
+        return command(cmd::unlink_range<Input>, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& unlink(std::initializer_list<T> il) {
+        return unlink(il.begin(), il.end());
+    }
+
+    QueuedRedis& wait(long long numslaves, long long timeout) {
+        return command(cmd::wait, numslaves, timeout);
+    }
+
+    QueuedRedis& wait(long long numslaves, const std::chrono::milliseconds &timeout) {
+        return wait(numslaves, timeout.count());
+    }
+
+    // STRING commands.
+
+    QueuedRedis& append(const StringView &key, const StringView &str) {
+        return command(cmd::append, key, str);
+    }
+
+    QueuedRedis& bitcount(const StringView &key,
+                            long long start = 0,
+                            long long end = -1) {
+        return command(cmd::bitcount, key, start, end);
+    }
+
+    QueuedRedis& bitop(BitOp op,
+                        const StringView &destination,
+                        const StringView &key) {
+        return command(cmd::bitop, op, destination, key);
+    }
+
+    template <typename Input>
+    QueuedRedis& bitop(BitOp op,
+                        const StringView &destination,
+                        Input first,
+                        Input last) {
+        return command(cmd::bitop_range<Input>, op, destination, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& bitop(BitOp op,
+                        const StringView &destination,
+                        std::initializer_list<T> il) {
+        return bitop(op, destination, il.begin(), il.end());
+    }
+
+    QueuedRedis& bitpos(const StringView &key,
+                        long long bit,
+                        long long start = 0,
+                        long long end = -1) {
+        return command(cmd::bitpos, key, bit, start, end);
+    }
+
+    QueuedRedis& decr(const StringView &key) {
+        return command(cmd::decr, key);
+    }
+
+    QueuedRedis& decrby(const StringView &key, long long decrement) {
+        return command(cmd::decrby, key, decrement);
+    }
+
+    QueuedRedis& get(const StringView &key) {
+        return command(cmd::get, key);
+    }
+
+    QueuedRedis& getbit(const StringView &key, long long offset) {
+        return command(cmd::getbit, key, offset);
+    }
+
+    QueuedRedis& getrange(const StringView &key, long long start, long long end) {
+        return command(cmd::getrange, key, start, end);
+    }
+
+    QueuedRedis& getset(const StringView &key, const StringView &val) {
+        return command(cmd::getset, key, val);
+    }
+
+    QueuedRedis& incr(const StringView &key) {
+        return command(cmd::incr, key);
+    }
+
+    QueuedRedis& incrby(const StringView &key, long long increment) {
+        return command(cmd::incrby, key, increment);
+    }
+
+    QueuedRedis& incrbyfloat(const StringView &key, double increment) {
+        return command(cmd::incrbyfloat, key, increment);
+    }
+
+    template <typename Input>
+    QueuedRedis& mget(Input first, Input last) {
+        return command(cmd::mget<Input>, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& mget(std::initializer_list<T> il) {
+        return mget(il.begin(), il.end());
+    }
+
+    template <typename Input>
+    QueuedRedis& mset(Input first, Input last) {
+        return command(cmd::mset<Input>, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& mset(std::initializer_list<T> il) {
+        return mset(il.begin(), il.end());
+    }
+
+    template <typename Input>
+    QueuedRedis& msetnx(Input first, Input last) {
+        return command(cmd::msetnx<Input>, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& msetnx(std::initializer_list<T> il) {
+        return msetnx(il.begin(), il.end());
+    }
+
+    QueuedRedis& psetex(const StringView &key,
+                        long long ttl,
+                        const StringView &val) {
+        return command(cmd::psetex, key, ttl, val);
+    }
+
+    QueuedRedis& psetex(const StringView &key,
+                        const std::chrono::milliseconds &ttl,
+                        const StringView &val) {
+        return psetex(key, ttl.count(), val);
+    }
+
+    QueuedRedis& set(const StringView &key,
+                        const StringView &val,
+                        const std::chrono::milliseconds &ttl = std::chrono::milliseconds(0),
+                        UpdateType type = UpdateType::ALWAYS) {
+        _set_cmd_indexes.push_back(_cmd_num);
+
+        return command(cmd::set, key, val, ttl.count(), type);
+    }
+
+    QueuedRedis& setex(const StringView &key,
+                        long long ttl,
+                        const StringView &val) {
+        return command(cmd::setex, key, ttl, val);
+    }
+
+    QueuedRedis& setex(const StringView &key,
+                        const std::chrono::seconds &ttl,
+                        const StringView &val) {
+        return setex(key, ttl.count(), val);
+    }
+
+    QueuedRedis& setnx(const StringView &key, const StringView &val) {
+        return command(cmd::setnx, key, val);
+    }
+
+    QueuedRedis& setrange(const StringView &key,
+                            long long offset,
+                            const StringView &val) {
+        return command(cmd::setrange, key, offset, val);
+    }
+
+    QueuedRedis& strlen(const StringView &key) {
+        return command(cmd::strlen, key);
+    }
+
+    // LIST commands.
+
+    QueuedRedis& blpop(const StringView &key, long long timeout) {
+        return command(cmd::blpop, key, timeout);
+    }
+
+    QueuedRedis& blpop(const StringView &key,
+                        const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
+        return blpop(key, timeout.count());
+    }
+
+    template <typename Input>
+    QueuedRedis& blpop(Input first, Input last, long long timeout) {
+        return command(cmd::blpop_range<Input>, first, last, timeout);
+    }
+
+    template <typename T>
+    QueuedRedis& blpop(std::initializer_list<T> il, long long timeout) {
+        return blpop(il.begin(), il.end(), timeout);
+    }
+
+    template <typename Input>
+    QueuedRedis& blpop(Input first,
+                        Input last,
+                        const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
+        return blpop(first, last, timeout.count());
+    }
+
+    template <typename T>
+    QueuedRedis& blpop(std::initializer_list<T> il,
+                        const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
+        return blpop(il.begin(), il.end(), timeout);
+    }
+
+    QueuedRedis& brpop(const StringView &key, long long timeout) {
+        return command(cmd::brpop, key, timeout);
+    }
+
+    QueuedRedis& brpop(const StringView &key,
+                        const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
+        return brpop(key, timeout.count());
+    }
+
+    template <typename Input>
+    QueuedRedis& brpop(Input first, Input last, long long timeout) {
+        return command(cmd::brpop_range<Input>, first, last, timeout);
+    }
+
+    template <typename T>
+    QueuedRedis& brpop(std::initializer_list<T> il, long long timeout) {
+        return brpop(il.begin(), il.end(), timeout);
+    }
+
+    template <typename Input>
+    QueuedRedis& brpop(Input first,
+                        Input last,
+                        const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
+        return brpop(first, last, timeout.count());
+    }
+
+    template <typename T>
+    QueuedRedis& brpop(std::initializer_list<T> il,
+                        const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
+        return brpop(il.begin(), il.end(), timeout);
+    }
+
+    QueuedRedis& brpoplpush(const StringView &source,
+                            const StringView &destination,
+                            long long timeout) {
+        return command(cmd::brpoplpush, source, destination, timeout);
+    }
+
+    QueuedRedis& brpoplpush(const StringView &source,
+                            const StringView &destination,
+                            const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
+        return brpoplpush(source, destination, timeout.count());
+    }
+
+    QueuedRedis& lindex(const StringView &key, long long index) {
+        return command(cmd::lindex, key, index);
+    }
+
+    QueuedRedis& linsert(const StringView &key,
+                            InsertPosition position,
+                            const StringView &pivot,
+                            const StringView &val) {
+        return command(cmd::linsert, key, position, pivot, val);
+    }
+
+    QueuedRedis& llen(const StringView &key) {
+        return command(cmd::llen, key);
+    }
+
+    QueuedRedis& lpop(const StringView &key) {
+        return command(cmd::lpop, key);
+    }
+
+    QueuedRedis& lpush(const StringView &key, const StringView &val) {
+        return command(cmd::lpush, key, val);
+    }
+
+    template <typename Input>
+    QueuedRedis& lpush(const StringView &key, Input first, Input last) {
+        return command(cmd::lpush_range<Input>, key, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& lpush(const StringView &key, std::initializer_list<T> il) {
+        return lpush(key, il.begin(), il.end());
+    }
+
+    QueuedRedis& lpushx(const StringView &key, const StringView &val) {
+        return command(cmd::lpushx, key, val);
+    }
+
+    QueuedRedis& lrange(const StringView &key,
+                        long long start,
+                        long long stop) {
+        return command(cmd::lrange, key, start, stop);
+    }
+
+    QueuedRedis& lrem(const StringView &key, long long count, const StringView &val) {
+        return command(cmd::lrem, key, count, val);
+    }
+
+    QueuedRedis& lset(const StringView &key, long long index, const StringView &val) {
+        return command(cmd::lset, key, index, val);
+    }
+
+    QueuedRedis& ltrim(const StringView &key, long long start, long long stop) {
+        return command(cmd::ltrim, key, start, stop);
+    }
+
+    QueuedRedis& rpop(const StringView &key) {
+        return command(cmd::rpop, key);
+    }
+
+    QueuedRedis& rpoplpush(const StringView &source, const StringView &destination) {
+        return command(cmd::rpoplpush, source, destination);
+    }
+
+    QueuedRedis& rpush(const StringView &key, const StringView &val) {
+        return command(cmd::rpush, key, val);
+    }
+
+    template <typename Input>
+    QueuedRedis& rpush(const StringView &key, Input first, Input last) {
+        return command(cmd::rpush_range<Input>, key, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& rpush(const StringView &key, std::initializer_list<T> il) {
+        return rpush(key, il.begin(), il.end());
+    }
+
+    QueuedRedis& rpushx(const StringView &key, const StringView &val) {
+        return command(cmd::rpushx, key, val);
+    }
+
+    // HASH commands.
+
+    QueuedRedis& hdel(const StringView &key, const StringView &field) {
+        return command(cmd::hdel, key, field);
+    }
+
+    template <typename Input>
+    QueuedRedis& hdel(const StringView &key, Input first, Input last) {
+        return command(cmd::hdel_range<Input>, key, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& hdel(const StringView &key, std::initializer_list<T> il) {
+        return hdel(key, il.begin(), il.end());
+    }
+
+    QueuedRedis& hexists(const StringView &key, const StringView &field) {
+        return command(cmd::hexists, key, field);
+    }
+
+    QueuedRedis& hget(const StringView &key, const StringView &field) {
+        return command(cmd::hget, key, field);
+    }
+
+    QueuedRedis& hgetall(const StringView &key) {
+        return command(cmd::hgetall, key);
+    }
+
+    QueuedRedis& hincrby(const StringView &key,
+                            const StringView &field,
+                            long long increment) {
+        return command(cmd::hincrby, key, field, increment);
+    }
+
+    QueuedRedis& hincrbyfloat(const StringView &key,
+                                const StringView &field,
+                                double increment) {
+        return command(cmd::hincrbyfloat, key, field, increment);
+    }
+
+    QueuedRedis& hkeys(const StringView &key) {
+        return command(cmd::hkeys, key);
+    }
+
+    QueuedRedis& hlen(const StringView &key) {
+        return command(cmd::hlen, key);
+    }
+
+    template <typename Input>
+    QueuedRedis& hmget(const StringView &key, Input first, Input last) {
+        return command(cmd::hmget<Input>, key, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& hmget(const StringView &key, std::initializer_list<T> il) {
+        return hmget(key, il.begin(), il.end());
+    }
+
+    template <typename Input>
+    QueuedRedis& hmset(const StringView &key, Input first, Input last) {
+        return command(cmd::hmset<Input>, key, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& hmset(const StringView &key, std::initializer_list<T> il) {
+        return hmset(key, il.begin(), il.end());
+    }
+
+    QueuedRedis& hscan(const StringView &key,
+                        long long cursor,
+                        const StringView &pattern,
+                        long long count) {
+        return command(cmd::hscan, key, cursor, pattern, count);
+    }
+
+    QueuedRedis& hscan(const StringView &key,
+                        long long cursor,
+                        const StringView &pattern) {
+        return hscan(key, cursor, pattern, 10);
+    }
+
+    QueuedRedis& hscan(const StringView &key,
+                        long long cursor,
+                        long long count) {
+        return hscan(key, cursor, "*", count);
+    }
+
+    QueuedRedis& hscan(const StringView &key,
+                        long long cursor) {
+        return hscan(key, cursor, "*", 10);
+    }
+
+    QueuedRedis& hset(const StringView &key, const StringView &field, const StringView &val) {
+        return command(cmd::hset, key, field, val);
+    }
+
+    QueuedRedis& hset(const StringView &key, const std::pair<StringView, StringView> &item) {
+        return hset(key, item.first, item.second);
+    }
+
+    QueuedRedis& hsetnx(const StringView &key, const StringView &field, const StringView &val) {
+        return command(cmd::hsetnx, key, field, val);
+    }
+
+    QueuedRedis& hsetnx(const StringView &key, const std::pair<StringView, StringView> &item) {
+        return hsetnx(key, item.first, item.second);
+    }
+
+    QueuedRedis& hstrlen(const StringView &key, const StringView &field) {
+        return command(cmd::hstrlen, key, field);
+    }
+
+    QueuedRedis& hvals(const StringView &key) {
+        return command(cmd::hvals, key);
+    }
+
+    // SET commands.
+
+    QueuedRedis& sadd(const StringView &key, const StringView &member) {
+        return command(cmd::sadd, key, member);
+    }
+
+    template <typename Input>
+    QueuedRedis& sadd(const StringView &key, Input first, Input last) {
+        return command(cmd::sadd_range<Input>, key, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& sadd(const StringView &key, std::initializer_list<T> il) {
+        return sadd(key, il.begin(), il.end());
+    }
+
+    QueuedRedis& scard(const StringView &key) {
+        return command(cmd::scard, key);
+    }
+
+    template <typename Input>
+    QueuedRedis& sdiff(Input first, Input last) {
+        return command(cmd::sdiff<Input>, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& sdiff(std::initializer_list<T> il) {
+        return sdiff(il.begin(), il.end());
+    }
+
+    QueuedRedis& sdiffstore(const StringView &destination, const StringView &key) {
+        return command(cmd::sdiffstore, destination, key);
+    }
+
+    template <typename Input>
+    QueuedRedis& sdiffstore(const StringView &destination,
+                            Input first,
+                            Input last) {
+        return command(cmd::sdiffstore_range<Input>, destination, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& sdiffstore(const StringView &destination, std::initializer_list<T> il) {
+        return sdiffstore(destination, il.begin(), il.end());
+    }
+
+    template <typename Input>
+    QueuedRedis& sinter(Input first, Input last) {
+        return command(cmd::sinter<Input>, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& sinter(std::initializer_list<T> il) {
+        return sinter(il.begin(), il.end());
+    }
+
+    QueuedRedis& sinterstore(const StringView &destination, const StringView &key) {
+        return command(cmd::sinterstore, destination, key);
+    }
+
+    template <typename Input>
+    QueuedRedis& sinterstore(const StringView &destination,
+                                Input first,
+                                Input last) {
+        return command(cmd::sinterstore_range<Input>, destination, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& sinterstore(const StringView &destination, std::initializer_list<T> il) {
+        return sinterstore(destination, il.begin(), il.end());
+    }
+
+    QueuedRedis& sismember(const StringView &key, const StringView &member) {
+        return command(cmd::sismember, key, member);
+    }
+
+    QueuedRedis& smembers(const StringView &key) {
+        return command(cmd::smembers, key);
+    }
+
+    QueuedRedis& smove(const StringView &source,
+                        const StringView &destination,
+                        const StringView &member) {
+        return command(cmd::smove, source, destination, member);
+    }
+
+    QueuedRedis& spop(const StringView &key) {
+        return command(cmd::spop, key);
+    }
+
+    QueuedRedis& spop(const StringView &key, long long count) {
+        return command(cmd::spop_range, key, count);
+    }
+
+    QueuedRedis& srandmember(const StringView &key) {
+        return command(cmd::srandmember, key);
+    }
+
+    QueuedRedis& srandmember(const StringView &key, long long count) {
+        return command(cmd::srandmember_range, key, count);
+    }
+
+    QueuedRedis& srem(const StringView &key, const StringView &member) {
+        return command(cmd::srem, key, member);
+    }
+
+    template <typename Input>
+    QueuedRedis& srem(const StringView &key, Input first, Input last) {
+        return command(cmd::srem_range<Input>, key, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& srem(const StringView &key, std::initializer_list<T> il) {
+        return srem(key, il.begin(), il.end());
+    }
+
+    QueuedRedis& sscan(const StringView &key,
+                        long long cursor,
+                        const StringView &pattern,
+                        long long count) {
+        return command(cmd::sscan, key, cursor, pattern, count);
+    }
+
+    QueuedRedis& sscan(const StringView &key,
+                    long long cursor,
+                    const StringView &pattern) {
+        return sscan(key, cursor, pattern, 10);
+    }
+
+    QueuedRedis& sscan(const StringView &key,
+                        long long cursor,
+                        long long count) {
+        return sscan(key, cursor, "*", count);
+    }
+
+    QueuedRedis& sscan(const StringView &key,
+                        long long cursor) {
+        return sscan(key, cursor, "*", 10);
+    }
+
+    template <typename Input>
+    QueuedRedis& sunion(Input first, Input last) {
+        return command(cmd::sunion<Input>, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& sunion(std::initializer_list<T> il) {
+        return sunion(il.begin(), il.end());
+    }
+
+    QueuedRedis& sunionstore(const StringView &destination, const StringView &key) {
+        return command(cmd::sunionstore, destination, key);
+    }
+
+    template <typename Input>
+    QueuedRedis& sunionstore(const StringView &destination, Input first, Input last) {
+        return command(cmd::sunionstore_range<Input>, destination, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& sunionstore(const StringView &destination, std::initializer_list<T> il) {
+        return sunionstore(destination, il.begin(), il.end());
+    }
+
+    // SORTED SET commands.
+
+    QueuedRedis& bzpopmax(const StringView &key, long long timeout) {
+        return command(cmd::bzpopmax, key, timeout);
+    }
+
+    QueuedRedis& bzpopmax(const StringView &key,
+                    const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
+        return bzpopmax(key, timeout.count());
+    }
+
+    template <typename Input>
+    QueuedRedis& bzpopmax(Input first, Input last, long long timeout) {
+        return command(cmd::bzpopmax_range<Input>, first, last, timeout);
+    }
+
+    template <typename Input>
+    QueuedRedis& bzpopmax(Input first,
+                            Input last,
+                            const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
+        return bzpopmax(first, last, timeout.count());
+    }
+
+    template <typename T>
+    QueuedRedis& bzpopmax(std::initializer_list<T> il, long long timeout) {
+        return bzpopmax(il.begin(), il.end(), timeout);
+    }
+
+    template <typename T>
+    QueuedRedis& bzpopmax(std::initializer_list<T> il,
+                            const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
+        return bzpopmax(il.begin(), il.end(), timeout);
+    }
+
+    QueuedRedis& bzpopmin(const StringView &key, long long timeout) {
+        return command(cmd::bzpopmin, key, timeout);
+    }
+
+    QueuedRedis& bzpopmin(const StringView &key,
+                            const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
+        return bzpopmin(key, timeout.count());
+    }
+
+    template <typename Input>
+    QueuedRedis& bzpopmin(Input first, Input last, long long timeout) {
+        return command(cmd::bzpopmin_range<Input>, first, last, timeout);
+    }
+
+    template <typename Input>
+    QueuedRedis& bzpopmin(Input first,
+                            Input last,
+                            const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
+        return bzpopmin(first, last, timeout.count());
+    }
+
+    template <typename T>
+    QueuedRedis& bzpopmin(std::initializer_list<T> il, long long timeout) {
+        return bzpopmin(il.begin(), il.end(), timeout);
+    }
+
+    template <typename T>
+    QueuedRedis& bzpopmin(std::initializer_list<T> il,
+                            const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
+        return bzpopmin(il.begin(), il.end(), timeout);
+    }
+
+    // We don't support the INCR option, since you can always use ZINCRBY instead.
+    QueuedRedis& zadd(const StringView &key,
+                        const StringView &member,
+                        double score,
+                        UpdateType type = UpdateType::ALWAYS,
+                        bool changed = false) {
+        return command(cmd::zadd, key, member, score, type, changed);
+    }
+
+    template <typename Input>
+    QueuedRedis& zadd(const StringView &key,
+                        Input first,
+                        Input last,
+                        UpdateType type = UpdateType::ALWAYS,
+                        bool changed = false) {
+        return command(cmd::zadd_range<Input>, key, first, last, type, changed);
+    }
+
+    QueuedRedis& zcard(const StringView &key) {
+        return command(cmd::zcard, key);
+    }
+
+    template <typename Interval>
+    QueuedRedis& zcount(const StringView &key, const Interval &interval) {
+        return command(cmd::zcount<Interval>, key, interval);
+    }
+
+    QueuedRedis& zincrby(const StringView &key, double increment, const StringView &member) {
+        return command(cmd::zincrby, key, increment, member);
+    }
+
+    QueuedRedis& zinterstore(const StringView &destination,
+                                const StringView &key,
+                                double weight) {
+        return command(cmd::zinterstore, destination, key, weight);
+    }
+
+    template <typename Input>
+    QueuedRedis& zinterstore(const StringView &destination,
+                                Input first,
+                                Input last,
+                                Aggregation type = Aggregation::SUM) {
+        return command(cmd::zinterstore_range<Input>, destination, first, last, type);
+    }
+
+    template <typename T>
+    QueuedRedis& zinterstore(const StringView &destination,
+                                std::initializer_list<T> il,
+                                Aggregation type = Aggregation::SUM) {
+        return zinterstore(destination, il.begin(), il.end(), type);
+    }
+
+    template <typename Interval>
+    QueuedRedis& zlexcount(const StringView &key, const Interval &interval) {
+        return command(cmd::zlexcount<Interval>, key, interval);
+    }
+
+    QueuedRedis& zpopmax(const StringView &key) {
+        return command(cmd::zpopmax, key, 1);
+    }
+
+    QueuedRedis& zpopmax(const StringView &key, long long count) {
+        return command(cmd::zpopmax, key, count);
+    }
+
+    QueuedRedis& zpopmin(const StringView &key) {
+        return command(cmd::zpopmin, key, 1);
+    }
+
+    QueuedRedis& zpopmin(const StringView &key, long long count) {
+        return command(cmd::zpopmin, key, count);
+    }
+
+    // NOTE: *QueuedRedis::zrange*'s parameters are different from *Redis::zrange*.
+    // *Redis::zrange* is overloaded by the output iterator, however, there's no such
+    // iterator in *QueuedRedis::zrange*. So we have to use an extra parameter: *with_scores*,
+    // to decide whether we should send *WITHSCORES* option to Redis. This also applies to
+    // other commands with the *WITHSCORES* option, e.g. *ZRANGEBYSCORE*, *ZREVRANGE*,
+    // *ZREVRANGEBYSCORE*.
+    QueuedRedis& zrange(const StringView &key,
+                        long long start,
+                        long long stop,
+                        bool with_scores = false) {
+        return command(cmd::zrange, key, start, stop, with_scores);
+    }
+
+    template <typename Interval>
+    QueuedRedis& zrangebylex(const StringView &key,
+                                const Interval &interval,
+                                const LimitOptions &opts) {
+        return command(cmd::zrangebylex<Interval>, key, interval, opts);
+    }
+
+    template <typename Interval>
+    QueuedRedis& zrangebylex(const StringView &key, const Interval &interval) {
+        return zrangebylex(key, interval, {});
+    }
+
+    // See comments on *ZRANGE*.
+    template <typename Interval>
+    QueuedRedis& zrangebyscore(const StringView &key,
+                                const Interval &interval,
+                                const LimitOptions &opts,
+                                bool with_scores = false) {
+        return command(cmd::zrangebyscore<Interval>, key, interval, opts, with_scores);
+    }
+
+    // See comments on *ZRANGE*.
+    template <typename Interval>
+    QueuedRedis& zrangebyscore(const StringView &key,
+                                const Interval &interval,
+                                bool with_scores = false) {
+        return zrangebyscore(key, interval, {}, with_scores);
+    }
+
+    QueuedRedis& zrank(const StringView &key, const StringView &member) {
+        return command(cmd::zrank, key, member);
+    }
+
+    QueuedRedis& zrem(const StringView &key, const StringView &member) {
+        return command(cmd::zrem, key, member);
+    }
+
+    template <typename Input>
+    QueuedRedis& zrem(const StringView &key, Input first, Input last) {
+        return command(cmd::zrem_range<Input>, key, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& zrem(const StringView &key, std::initializer_list<T> il) {
+        return zrem(key, il.begin(), il.end());
+    }
+
+    template <typename Interval>
+    QueuedRedis& zremrangebylex(const StringView &key, const Interval &interval) {
+        return command(cmd::zremrangebylex<Interval>, key, interval);
+    }
+
+    QueuedRedis& zremrangebyrank(const StringView &key, long long start, long long stop) {
+        return command(cmd::zremrangebyrank, key, start, stop);
+    }
+
+    template <typename Interval>
+    QueuedRedis& zremrangebyscore(const StringView &key, const Interval &interval) {
+        return command(cmd::zremrangebyscore<Interval>, key, interval);
+    }
+
+    // See comments on *ZRANGE*.
+    QueuedRedis& zrevrange(const StringView &key,
+                            long long start,
+                            long long stop,
+                            bool with_scores = false) {
+        return command(cmd::zrevrange, key, start, stop, with_scores);
+    }
+
+    template <typename Interval>
+    QueuedRedis& zrevrangebylex(const StringView &key,
+                                const Interval &interval,
+                                const LimitOptions &opts) {
+        return command(cmd::zrevrangebylex<Interval>, key, interval, opts);
+    }
+
+    template <typename Interval>
+    QueuedRedis& zrevrangebylex(const StringView &key, const Interval &interval) {
+        return zrevrangebylex(key, interval, {});
+    }
+
+    // See comments on *ZRANGE*.
+    template <typename Interval>
+    QueuedRedis& zrevrangebyscore(const StringView &key,
+                                    const Interval &interval,
+                                    const LimitOptions &opts,
+                                    bool with_scores = false) {
+        return command(cmd::zrevrangebyscore<Interval>, key, interval, opts, with_scores);
+    }
+
+    // See comments on *ZRANGE*.
+    template <typename Interval>
+    QueuedRedis& zrevrangebyscore(const StringView &key,
+                                    const Interval &interval,
+                                    bool with_scores = false) {
+        return zrevrangebyscore(key, interval, {}, with_scores);
+    }
+
+    QueuedRedis& zrevrank(const StringView &key, const StringView &member) {
+        return command(cmd::zrevrank, key, member);
+    }
+
+    QueuedRedis& zscan(const StringView &key,
+                        long long cursor,
+                        const StringView &pattern,
+                        long long count) {
+        return command(cmd::zscan, key, cursor, pattern, count);
+    }
+
+    QueuedRedis& zscan(const StringView &key,
+                        long long cursor,
+                        const StringView &pattern) {
+        return zscan(key, cursor, pattern, 10);
+    }
+
+    QueuedRedis& zscan(const StringView &key,
+                        long long cursor,
+                        long long count) {
+        return zscan(key, cursor, "*", count);
+    }
+
+    QueuedRedis& zscan(const StringView &key,
+                        long long cursor) {
+        return zscan(key, cursor, "*", 10);
+    }
+
+    QueuedRedis& zscore(const StringView &key, const StringView &member) {
+        return command(cmd::zscore, key, member);
+    }
+
+    QueuedRedis& zunionstore(const StringView &destination,
+                                const StringView &key,
+                                double weight) {
+        return command(cmd::zunionstore, destination, key, weight);
+    }
+
+    template <typename Input>
+    QueuedRedis& zunionstore(const StringView &destination,
+                                Input first,
+                                Input last,
+                                Aggregation type = Aggregation::SUM) {
+        return command(cmd::zunionstore_range<Input>, destination, first, last, type);
+    }
+
+    template <typename T>
+    QueuedRedis& zunionstore(const StringView &destination,
+                                std::initializer_list<T> il,
+                                Aggregation type = Aggregation::SUM) {
+        return zunionstore(destination, il.begin(), il.end(), type);
+    }
+
+    // HYPERLOGLOG commands.
+
+    QueuedRedis& pfadd(const StringView &key, const StringView &element) {
+        return command(cmd::pfadd, key, element);
+    }
+
+    template <typename Input>
+    QueuedRedis& pfadd(const StringView &key, Input first, Input last) {
+        return command(cmd::pfadd_range<Input>, key, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& pfadd(const StringView &key, std::initializer_list<T> il) {
+        return pfadd(key, il.begin(), il.end());
+    }
+
+    QueuedRedis& pfcount(const StringView &key) {
+        return command(cmd::pfcount, key);
+    }
+
+    template <typename Input>
+    QueuedRedis& pfcount(Input first, Input last) {
+        return command(cmd::pfcount_range<Input>, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& pfcount(std::initializer_list<T> il) {
+        return pfcount(il.begin(), il.end());
+    }
+
+    QueuedRedis& pfmerge(const StringView &destination, const StringView &key) {
+        return command(cmd::pfmerge, destination, key);
+    }
+
+    template <typename Input>
+    QueuedRedis& pfmerge(const StringView &destination, Input first, Input last) {
+        return command(cmd::pfmerge_range<Input>, destination, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& pfmerge(const StringView &destination, std::initializer_list<T> il) {
+        return pfmerge(destination, il.begin(), il.end());
+    }
+
+    // GEO commands.
+
+    QueuedRedis& geoadd(const StringView &key,
+                        const std::tuple<StringView, double, double> &member) {
+        return command(cmd::geoadd, key, member);
+    }
+
+    template <typename Input>
+    QueuedRedis& geoadd(const StringView &key,
+                        Input first,
+                        Input last) {
+        return command(cmd::geoadd_range<Input>, key, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& geoadd(const StringView &key, std::initializer_list<T> il) {
+        return geoadd(key, il.begin(), il.end());
+    }
+
+    QueuedRedis& geodist(const StringView &key,
+                            const StringView &member1,
+                            const StringView &member2,
+                            GeoUnit unit = GeoUnit::M) {
+        return command(cmd::geodist, key, member1, member2, unit);
+    }
+
+    template <typename Input>
+    QueuedRedis& geohash(const StringView &key, Input first, Input last) {
+        return command(cmd::geohash_range<Input>, key, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& geohash(const StringView &key, std::initializer_list<T> il) {
+        return geohash(key, il.begin(), il.end());
+    }
+
+    template <typename Input>
+    QueuedRedis& geopos(const StringView &key, Input first, Input last) {
+        return command(cmd::geopos_range<Input>, key, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& geopos(const StringView &key, std::initializer_list<T> il) {
+        return geopos(key, il.begin(), il.end());
+    }
+
+    // TODO:
+    // 1. since we have different overloads for georadius and georadius-store,
+    //    we might use the GEORADIUS_RO command in the future.
+    // 2. there're too many parameters for this method, we might refactor it.
+    QueuedRedis& georadius(const StringView &key,
+                            const std::pair<double, double> &loc,
+                            double radius,
+                            GeoUnit unit,
+                            const StringView &destination,
+                            bool store_dist,
+                            long long count) {
+        _georadius_cmd_indexes.push_back(_cmd_num);
+
+        return command(cmd::georadius_store,
+                        key,
+                        loc,
+                        radius,
+                        unit,
+                        destination,
+                        store_dist,
+                        count);
+    }
+
+    // NOTE: *QueuedRedis::georadius*'s parameters are different from *Redis::georadius*.
+    // *Redis::georadius* is overloaded by the output iterator, however, there's no such
+    // iterator in *QueuedRedis::georadius*. So we have to use extra parameters to decide
+    // whether we should send options to Redis. This also applies to *GEORADIUSBYMEMBER*.
+    QueuedRedis& georadius(const StringView &key,
+                            const std::pair<double, double> &loc,
+                            double radius,
+                            GeoUnit unit,
+                            long long count,
+                            bool asc,
+                            bool with_coord,
+                            bool with_dist,
+                            bool with_hash) {
+        return command(cmd::georadius,
+                        key,
+                        loc,
+                        radius,
+                        unit,
+                        count,
+                        asc,
+                        with_coord,
+                        with_dist,
+                        with_hash);
+    }
+
+    QueuedRedis& georadiusbymember(const StringView &key,
+                                    const StringView &member,
+                                    double radius,
+                                    GeoUnit unit,
+                                    const StringView &destination,
+                                    bool store_dist,
+                                    long long count) {
+        _georadius_cmd_indexes.push_back(_cmd_num);
+
+        return command(cmd::georadiusbymember,
+                        key,
+                        member,
+                        radius,
+                        unit,
+                        destination,
+                        store_dist,
+                        count);
+    }
+
+    // See the comments on *GEORADIUS*.
+    QueuedRedis& georadiusbymember(const StringView &key,
+                                    const StringView &member,
+                                    double radius,
+                                    GeoUnit unit,
+                                    long long count,
+                                    bool asc,
+                                    bool with_coord,
+                                    bool with_dist,
+                                    bool with_hash) {
+        return command(cmd::georadiusbymember,
+                        key,
+                        member,
+                        radius,
+                        unit,
+                        count,
+                        asc,
+                        with_coord,
+                        with_dist,
+                        with_hash);
+    }
+
+    // SCRIPTING commands.
+
+    QueuedRedis& eval(const StringView &script,
+                        std::initializer_list<StringView> keys,
+                        std::initializer_list<StringView> args) {
+        return command(cmd::eval, script, keys, args);
+    }
+
+    QueuedRedis& evalsha(const StringView &script,
+                            std::initializer_list<StringView> keys,
+                            std::initializer_list<StringView> args) {
+        return command(cmd::evalsha, script, keys, args);
+    }
+
+    template <typename Input>
+    QueuedRedis& script_exists(Input first, Input last) {
+        return command(cmd::script_exists_range<Input>, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& script_exists(std::initializer_list<T> il) {
+        return script_exists(il.begin(), il.end());
+    }
+
+    QueuedRedis& script_flush() {
+        return command(cmd::script_flush);
+    }
+
+    QueuedRedis& script_kill() {
+        return command(cmd::script_kill);
+    }
+
+    QueuedRedis& script_load(const StringView &script) {
+        return command(cmd::script_load, script);
+    }
+
+    // PUBSUB commands.
+
+    QueuedRedis& publish(const StringView &channel, const StringView &message) {
+        return command(cmd::publish, channel, message);
+    }
+
+    // Stream commands.
+
+    QueuedRedis& xack(const StringView &key, const StringView &group, const StringView &id) {
+        return command(cmd::xack, key, group, id);
+    }
+
+    template <typename Input>
+    QueuedRedis& xack(const StringView &key, const StringView &group, Input first, Input last) {
+        return command(cmd::xack_range<Input>, key, group, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& xack(const StringView &key, const StringView &group, std::initializer_list<T> il) {
+        return xack(key, group, il.begin(), il.end());
+    }
+
+    template <typename Input>
+    QueuedRedis& xadd(const StringView &key, const StringView &id, Input first, Input last) {
+        return command(cmd::xadd_range<Input>, key, id, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& xadd(const StringView &key, const StringView &id, std::initializer_list<T> il) {
+        return xadd(key, id, il.begin(), il.end());
+    }
+
+    template <typename Input>
+    QueuedRedis& xadd(const StringView &key,
+                        const StringView &id,
+                        Input first,
+                        Input last,
+                        long long count,
+                        bool approx = true) {
+        return command(cmd::xadd_maxlen_range<Input>, key, id, first, last, count, approx);
+    }
+
+    template <typename T>
+    QueuedRedis& xadd(const StringView &key,
+                        const StringView &id,
+                        std::initializer_list<T> il,
+                        long long count,
+                        bool approx = true) {
+        return xadd(key, id, il.begin(), il.end(), count, approx);
+    }
+
+    QueuedRedis& xclaim(const StringView &key,
+                        const StringView &group,
+                        const StringView &consumer,
+                        const std::chrono::milliseconds &min_idle_time,
+                        const StringView &id) {
+        return command(cmd::xclaim, key, group, consumer, min_idle_time.count(), id);
+    }
+
+    template <typename Input>
+    QueuedRedis& xclaim(const StringView &key,
+                const StringView &group,
+                const StringView &consumer,
+                const std::chrono::milliseconds &min_idle_time,
+                Input first,
+                Input last) {
+        return command(cmd::xclaim_range<Input>,
+                        key,
+                        group,
+                        consumer,
+                        min_idle_time.count(),
+                        first,
+                        last);
+    }
+
+    template <typename T>
+    QueuedRedis& xclaim(const StringView &key,
+                const StringView &group,
+                const StringView &consumer,
+                const std::chrono::milliseconds &min_idle_time,
+                std::initializer_list<T> il) {
+        return xclaim(key, group, consumer, min_idle_time, il.begin(), il.end());
+    }
+
+    QueuedRedis& xdel(const StringView &key, const StringView &id) {
+        return command(cmd::xdel, key, id);
+    }
+
+    template <typename Input>
+    QueuedRedis& xdel(const StringView &key, Input first, Input last) {
+        return command(cmd::xdel_range<Input>, key, first, last);
+    }
+
+    template <typename T>
+    QueuedRedis& xdel(const StringView &key, std::initializer_list<T> il) {
+        return xdel(key, il.begin(), il.end());
+    }
+
+    QueuedRedis& xgroup_create(const StringView &key,
+                                const StringView &group,
+                                const StringView &id,
+                                bool mkstream = false) {
+        return command(cmd::xgroup_create, key, group, id, mkstream);
+    }
+
+    QueuedRedis& xgroup_setid(const StringView &key,
+                                const StringView &group,
+                                const StringView &id) {
+        return command(cmd::xgroup_setid, key, group, id);
+    }
+
+    QueuedRedis& xgroup_destroy(const StringView &key, const StringView &group) {
+        return command(cmd::xgroup_destroy, key, group);
+    }
+
+    QueuedRedis& xgroup_delconsumer(const StringView &key,
+                                    const StringView &group,
+                                    const StringView &consumer) {
+        return command(cmd::xgroup_delconsumer, key, group, consumer);
+    }
+
+    QueuedRedis& xlen(const StringView &key) {
+        return command(cmd::xlen, key);
+    }
+
+    QueuedRedis& xpending(const StringView &key, const StringView &group) {
+        return command(cmd::xpending, key, group);
+    }
+
+    QueuedRedis& xpending(const StringView &key,
+                            const StringView &group,
+                            const StringView &start,
+                            const StringView &end,
+                            long long count) {
+        return command(cmd::xpending_detail, key, group, start, end, count);
+    }
+
+    QueuedRedis& xpending(const StringView &key,
+                            const StringView &group,
+                            const StringView &start,
+                            const StringView &end,
+                            long long count,
+                            const StringView &consumer) {
+        return command(cmd::xpending_per_consumer, key, group, start, end, count, consumer);
+    }
+
+    QueuedRedis& xrange(const StringView &key,
+                        const StringView &start,
+                        const StringView &end) {
+        return command(cmd::xrange, key, start, end);
+    }
+
+    QueuedRedis& xrange(const StringView &key,
+                        const StringView &start,
+                        const StringView &end,
+                        long long count) {
+        return command(cmd::xrange, key, start, end, count);
+    }
+
+    QueuedRedis& xread(const StringView &key, const StringView &id, long long count) {
+        return command(cmd::xread, key, id, count);
+    }
+
+    QueuedRedis& xread(const StringView &key, const StringView &id) {
+        return xread(key, id, 0);
+    }
+
+    template <typename Input>
+    auto xread(Input first, Input last, long long count)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value,
+                                    QueuedRedis&>::type {
+        return command(cmd::xread_range<Input>, first, last, count);
+    }
+
+    template <typename Input>
+    auto xread(Input first, Input last)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value,
+                                    QueuedRedis&>::type {
+        return xread(first, last, 0);
+    }
+
+    QueuedRedis& xread(const StringView &key,
+                        const StringView &id,
+                        const std::chrono::milliseconds &timeout,
+                        long long count) {
+        return command(cmd::xread_block, key, id, timeout.count(), count);
+    }
+
+    QueuedRedis& xread(const StringView &key,
+                        const StringView &id,
+                        const std::chrono::milliseconds &timeout) {
+        return xread(key, id, timeout, 0);
+    }
+
+    template <typename Input>
+    auto xread(Input first,
+                Input last,
+                const std::chrono::milliseconds &timeout,
+                long long count)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value,
+                                    QueuedRedis&>::type {
+        return command(cmd::xread_block_range<Input>, first, last, timeout.count(), count);
+    }
+
+    template <typename Input>
+    auto xread(Input first,
+                Input last,
+                const std::chrono::milliseconds &timeout)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value,
+                                    QueuedRedis&>::type {
+        return xread(first, last, timeout, 0);
+    }
+
+    QueuedRedis& xreadgroup(const StringView &group,
+                            const StringView &consumer,
+                            const StringView &key,
+                            const StringView &id,
+                            long long count,
+                            bool noack) {
+        return command(cmd::xreadgroup, group, consumer, key, id, count, noack);
+    }
+
+    QueuedRedis& xreadgroup(const StringView &group,
+                            const StringView &consumer,
+                            const StringView &key,
+                            const StringView &id,
+                            long long count) {
+        return xreadgroup(group, consumer, key, id, count, false);
+    }
+
+    template <typename Input>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last,
+                    long long count,
+                    bool noack)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value,
+                                    QueuedRedis&>::type {
+        return command(cmd::xreadgroup_range<Input>, group, consumer, first, last, count, noack);
+    }
+
+    template <typename Input>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last,
+                    long long count)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value,
+                                    QueuedRedis&>::type {
+        return xreadgroup(group, consumer, first ,last, count, false);
+    }
+
+    template <typename Input>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value,
+                                    QueuedRedis&>::type {
+        return xreadgroup(group, consumer, first ,last, 0, false);
+    }
+
+    QueuedRedis& xreadgroup(const StringView &group,
+                            const StringView &consumer,
+                            const StringView &key,
+                            const StringView &id,
+                            const std::chrono::milliseconds &timeout,
+                            long long count,
+                            bool noack) {
+        return command(cmd::xreadgroup_block,
+                        group,
+                        consumer,
+                        key,
+                        id,
+                        timeout.count(),
+                        count,
+                        noack);
+    }
+
+    QueuedRedis& xreadgroup(const StringView &group,
+                            const StringView &consumer,
+                            const StringView &key,
+                            const StringView &id,
+                            const std::chrono::milliseconds &timeout,
+                            long long count) {
+        return xreadgroup(group, consumer, key, id, timeout, count, false);
+    }
+
+    QueuedRedis& xreadgroup(const StringView &group,
+                            const StringView &consumer,
+                            const StringView &key,
+                            const StringView &id,
+                            const std::chrono::milliseconds &timeout) {
+        return xreadgroup(group, consumer, key, id, timeout, 0, false);
+    }
+
+    template <typename Input>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last,
+                    const std::chrono::milliseconds &timeout,
+                    long long count,
+                    bool noack)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value,
+                                    QueuedRedis&>::type {
+        return command(cmd::xreadgroup_block_range<Input>,
+                        group,
+                        consumer,
+                        first,
+                        last,
+                        timeout.count(),
+                        count,
+                        noack);
+    }
+
+    template <typename Input>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last,
+                    const std::chrono::milliseconds &timeout,
+                    long long count)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value,
+                                    QueuedRedis&>::type {
+        return xreadgroup(group, consumer, first, last, timeout, count, false);
+    }
+
+    template <typename Input>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last,
+                    const std::chrono::milliseconds &timeout)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value,
+                                    QueuedRedis&>::type {
+        return xreadgroup(group, consumer, first, last, timeout, 0, false);
+    }
+
+    QueuedRedis& xrevrange(const StringView &key,
+                            const StringView &end,
+                            const StringView &start) {
+        return command(cmd::xrevrange, key, end, start);
+    }
+
+    QueuedRedis& xrevrange(const StringView &key,
+                            const StringView &end,
+                            const StringView &start,
+                            long long count) {
+        return command(cmd::xrevrange, key, end, start, count);
+    }
+
+    QueuedRedis& xtrim(const StringView &key, long long count, bool approx = true) {
+        return command(cmd::xtrim, key, count, approx);
+    }
+
+private:
+    friend class Redis;
+
+    friend class RedisCluster;
+
+    template <typename ...Args>
+    QueuedRedis(const ConnectionSPtr &connection, Args &&...args);
+
+    void _sanity_check() const;
+
+    void _reset();
+
+    void _invalidate();
+
+    void _rewrite_replies(std::vector<ReplyUPtr> &replies) const;
+
+    template <typename Func>
+    void _rewrite_replies(const std::vector<std::size_t> &indexes,
+                            Func rewriter,
+                            std::vector<ReplyUPtr> &replies) const;
+
+    ConnectionSPtr _connection;
+
+    Impl _impl;
+
+    std::size_t _cmd_num = 0;
+
+    std::vector<std::size_t> _set_cmd_indexes;
+
+    std::vector<std::size_t> _georadius_cmd_indexes;
+
+    bool _valid = true;
+};
+
+class QueuedReplies {
+public:
+    std::size_t size() const;
+
+    redisReply& get(std::size_t idx);
+
+    template <typename Result>
+    Result get(std::size_t idx);
+
+    template <typename Output>
+    void get(std::size_t idx, Output output);
+
+private:
+    template <typename Impl>
+    friend class QueuedRedis;
+
+    explicit QueuedReplies(std::vector<ReplyUPtr> replies) : _replies(std::move(replies)) {}
+
+    void _index_check(std::size_t idx) const;
+
+    std::vector<ReplyUPtr> _replies;
+};
+
+}
+
+}
+
+#include "queued_redis.hpp"
+
+#endif // end SEWENEW_REDISPLUSPLUS_QUEUED_REDIS_H

+ 208 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/queued_redis.hpp

@@ -0,0 +1,208 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_QUEUED_REDIS_HPP
+#define SEWENEW_REDISPLUSPLUS_QUEUED_REDIS_HPP
+
+namespace sw {
+
+namespace redis {
+
+template <typename Impl>
+template <typename ...Args>
+QueuedRedis<Impl>::QueuedRedis(const ConnectionSPtr &connection, Args &&...args) :
+            _connection(connection),
+            _impl(std::forward<Args>(args)...) {
+    assert(_connection);
+}
+
+template <typename Impl>
+Redis QueuedRedis<Impl>::redis() {
+    return Redis(_connection);
+}
+
+template <typename Impl>
+template <typename Cmd, typename ...Args>
+auto QueuedRedis<Impl>::command(Cmd cmd, Args &&...args)
+    -> typename std::enable_if<!std::is_convertible<Cmd, StringView>::value,
+                                QueuedRedis<Impl>&>::type {
+    try {
+        _sanity_check();
+
+        _impl.command(*_connection, cmd, std::forward<Args>(args)...);
+
+        ++_cmd_num;
+    } catch (const Error &e) {
+        _invalidate();
+        throw;
+    }
+
+    return *this;
+}
+
+template <typename Impl>
+template <typename ...Args>
+QueuedRedis<Impl>& QueuedRedis<Impl>::command(const StringView &cmd_name, Args &&...args) {
+    auto cmd = [](Connection &connection, const StringView &cmd_name, Args &&...args) {
+                    CmdArgs cmd_args;
+                    cmd_args.append(cmd_name, std::forward<Args>(args)...);
+                    connection.send(cmd_args);
+    };
+
+    return command(cmd, cmd_name, std::forward<Args>(args)...);
+}
+
+template <typename Impl>
+template <typename Input>
+auto QueuedRedis<Impl>::command(Input first, Input last)
+    -> typename std::enable_if<IsIter<Input>::value, QueuedRedis<Impl>&>::type {
+    if (first == last) {
+        throw Error("command: empty range");
+    }
+
+    auto cmd = [](Connection &connection, Input first, Input last) {
+                    CmdArgs cmd_args;
+                    while (first != last) {
+                        cmd_args.append(*first);
+                        ++first;
+                    }
+                    connection.send(cmd_args);
+    };
+
+    return command(cmd, first, last);
+}
+
+template <typename Impl>
+QueuedReplies QueuedRedis<Impl>::exec() {
+    try {
+        _sanity_check();
+
+        auto replies = _impl.exec(*_connection, _cmd_num);
+
+        _rewrite_replies(replies);
+
+        _reset();
+
+        return QueuedReplies(std::move(replies));
+    } catch (const Error &e) {
+        _invalidate();
+        throw;
+    }
+}
+
+template <typename Impl>
+void QueuedRedis<Impl>::discard() {
+    try {
+        _sanity_check();
+
+        _impl.discard(*_connection, _cmd_num);
+
+        _reset();
+    } catch (const Error &e) {
+        _invalidate();
+        throw;
+    }
+}
+
+template <typename Impl>
+void QueuedRedis<Impl>::_sanity_check() const {
+    if (!_valid) {
+        throw Error("Not in valid state");
+    }
+
+    if (_connection->broken()) {
+        throw Error("Connection is broken");
+    }
+}
+
+template <typename Impl>
+inline void QueuedRedis<Impl>::_reset() {
+    _cmd_num = 0;
+
+    _set_cmd_indexes.clear();
+
+    _georadius_cmd_indexes.clear();
+}
+
+template <typename Impl>
+void QueuedRedis<Impl>::_invalidate() {
+    _valid = false;
+
+    _reset();
+}
+
+template <typename Impl>
+void QueuedRedis<Impl>::_rewrite_replies(std::vector<ReplyUPtr> &replies) const {
+    _rewrite_replies(_set_cmd_indexes, reply::rewrite_set_reply, replies);
+
+    _rewrite_replies(_georadius_cmd_indexes, reply::rewrite_georadius_reply, replies);
+}
+
+template <typename Impl>
+template <typename Func>
+void QueuedRedis<Impl>::_rewrite_replies(const std::vector<std::size_t> &indexes,
+                                            Func rewriter,
+                                            std::vector<ReplyUPtr> &replies) const {
+    for (auto idx : indexes) {
+        assert(idx < replies.size());
+
+        auto &reply = replies[idx];
+
+        assert(reply);
+
+        rewriter(*reply);
+    }
+}
+
+inline std::size_t QueuedReplies::size() const {
+    return _replies.size();
+}
+
+inline redisReply& QueuedReplies::get(std::size_t idx) {
+    _index_check(idx);
+
+    auto &reply = _replies[idx];
+
+    assert(reply);
+
+    return *reply;
+}
+
+template <typename Result>
+inline Result QueuedReplies::get(std::size_t idx) {
+    auto &reply = get(idx);
+
+    return reply::parse<Result>(reply);
+}
+
+template <typename Output>
+inline void QueuedReplies::get(std::size_t idx, Output output) {
+    auto &reply = get(idx);
+
+    reply::to_array(reply, output);
+}
+
+inline void QueuedReplies::_index_check(std::size_t idx) const {
+    if (idx >= size()) {
+        throw Error("Out of range");
+    }
+}
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_QUEUED_REDIS_HPP

+ 25 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/redis++.h

@@ -0,0 +1,25 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_REDISPLUSPLUS_H
+#define SEWENEW_REDISPLUSPLUS_REDISPLUSPLUS_H
+
+#include "redis.h"
+#include "redis_cluster.h"
+#include "queued_redis.h"
+#include "sentinel.h"
+
+#endif // end SEWENEW_REDISPLUSPLUS_REDISPLUSPLUS_H

+ 882 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/redis.cpp

@@ -0,0 +1,882 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#include "redis.h"
+#include <hiredis/hiredis.h>
+#include "command.h"
+#include "errors.h"
+#include "queued_redis.h"
+
+namespace sw {
+
+namespace redis {
+
+Redis::Redis(const std::string &uri) : Redis(ConnectionOptions(uri)) {}
+
+Redis::Redis(const ConnectionSPtr &connection) : _connection(connection) {
+    assert(_connection);
+}
+
+Pipeline Redis::pipeline() {
+    return Pipeline(std::make_shared<Connection>(_pool.create()));
+}
+
+Transaction Redis::transaction(bool piped) {
+    return Transaction(std::make_shared<Connection>(_pool.create()), piped);
+}
+
+Subscriber Redis::subscriber() {
+    return Subscriber(_pool.create());
+}
+
+// CONNECTION commands.
+
+void Redis::auth(const StringView &password) {
+    auto reply = command(cmd::auth, password);
+
+    reply::parse<void>(*reply);
+}
+
+std::string Redis::echo(const StringView &msg) {
+    auto reply = command(cmd::echo, msg);
+
+    return reply::parse<std::string>(*reply);
+}
+
+std::string Redis::ping() {
+    auto reply = command<void (*)(Connection &)>(cmd::ping);
+
+    return reply::to_status(*reply);
+}
+
+std::string Redis::ping(const StringView &msg) {
+    auto reply = command<void (*)(Connection &, const StringView &)>(cmd::ping, msg);
+
+    return reply::parse<std::string>(*reply);
+}
+
+void Redis::swapdb(long long idx1, long long idx2) {
+    auto reply = command(cmd::swapdb, idx1, idx2);
+
+    reply::parse<void>(*reply);
+}
+
+// SERVER commands.
+
+void Redis::bgrewriteaof() {
+    auto reply = command(cmd::bgrewriteaof);
+
+    reply::parse<void>(*reply);
+}
+
+void Redis::bgsave() {
+    auto reply = command(cmd::bgsave);
+
+    reply::parse<void>(*reply);
+}
+
+long long Redis::dbsize() {
+    auto reply = command(cmd::dbsize);
+
+    return reply::parse<long long>(*reply);
+}
+
+void Redis::flushall(bool async) {
+    auto reply = command(cmd::flushall, async);
+
+    reply::parse<void>(*reply);
+}
+
+void Redis::flushdb(bool async) {
+    auto reply = command(cmd::flushdb, async);
+
+    reply::parse<void>(*reply);
+}
+
+std::string Redis::info() {
+    auto reply = command<void (*)(Connection &)>(cmd::info);
+
+    return reply::parse<std::string>(*reply);
+}
+
+std::string Redis::info(const StringView &section) {
+    auto reply = command<void (*)(Connection &, const StringView &)>(cmd::info, section);
+
+    return reply::parse<std::string>(*reply);
+}
+
+long long Redis::lastsave() {
+    auto reply = command(cmd::lastsave);
+
+    return reply::parse<long long>(*reply);
+}
+
+void Redis::save() {
+    auto reply = command(cmd::save);
+
+    reply::parse<void>(*reply);
+}
+
+// KEY commands.
+
+long long Redis::del(const StringView &key) {
+    auto reply = command(cmd::del, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+OptionalString Redis::dump(const StringView &key) {
+    auto reply = command(cmd::dump, key);
+
+    return reply::parse<OptionalString>(*reply);
+}
+
+long long Redis::exists(const StringView &key) {
+    auto reply = command(cmd::exists, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+bool Redis::expire(const StringView &key, long long timeout) {
+    auto reply = command(cmd::expire, key, timeout);
+
+    return reply::parse<bool>(*reply);
+}
+
+bool Redis::expireat(const StringView &key, long long timestamp) {
+    auto reply = command(cmd::expireat, key, timestamp);
+
+    return reply::parse<bool>(*reply);
+}
+
+bool Redis::move(const StringView &key, long long db) {
+    auto reply = command(cmd::move, key, db);
+
+    return reply::parse<bool>(*reply);
+}
+
+bool Redis::persist(const StringView &key) {
+    auto reply = command(cmd::persist, key);
+
+    return reply::parse<bool>(*reply);
+}
+
+bool Redis::pexpire(const StringView &key, long long timeout) {
+    auto reply = command(cmd::pexpire, key, timeout);
+
+    return reply::parse<bool>(*reply);
+}
+
+bool Redis::pexpireat(const StringView &key, long long timestamp) {
+    auto reply = command(cmd::pexpireat, key, timestamp);
+
+    return reply::parse<bool>(*reply);
+}
+
+long long Redis::pttl(const StringView &key) {
+    auto reply = command(cmd::pttl, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+OptionalString Redis::randomkey() {
+    auto reply = command(cmd::randomkey);
+
+    return reply::parse<OptionalString>(*reply);
+}
+
+void Redis::rename(const StringView &key, const StringView &newkey) {
+    auto reply = command(cmd::rename, key, newkey);
+
+    reply::parse<void>(*reply);
+}
+
+bool Redis::renamenx(const StringView &key, const StringView &newkey) {
+    auto reply = command(cmd::renamenx, key, newkey);
+
+    return reply::parse<bool>(*reply);
+}
+
+void Redis::restore(const StringView &key,
+                    const StringView &val,
+                    long long ttl,
+                    bool replace) {
+    auto reply = command(cmd::restore, key, val, ttl, replace);
+
+    reply::parse<void>(*reply);
+}
+
+long long Redis::touch(const StringView &key) {
+    auto reply = command(cmd::touch, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long Redis::ttl(const StringView &key) {
+    auto reply = command(cmd::ttl, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+std::string Redis::type(const StringView &key) {
+    auto reply = command(cmd::type, key);
+
+    return reply::parse<std::string>(*reply);
+}
+
+long long Redis::unlink(const StringView &key) {
+    auto reply = command(cmd::unlink, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long Redis::wait(long long numslaves, long long timeout) {
+    auto reply = command(cmd::wait, numslaves, timeout);
+
+    return reply::parse<long long>(*reply);
+}
+
+// STRING commands.
+
+long long Redis::append(const StringView &key, const StringView &val) {
+    auto reply = command(cmd::append, key, val);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long Redis::bitcount(const StringView &key, long long start, long long end) {
+    auto reply = command(cmd::bitcount, key, start, end);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long Redis::bitop(BitOp op, const StringView &destination, const StringView &key) {
+    auto reply = command(cmd::bitop, op, destination, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long Redis::bitpos(const StringView &key,
+                            long long bit,
+                            long long start,
+                            long long end) {
+    auto reply = command(cmd::bitpos, key, bit, start, end);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long Redis::decr(const StringView &key) {
+    auto reply = command(cmd::decr, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long Redis::decrby(const StringView &key, long long decrement) {
+    auto reply = command(cmd::decrby, key, decrement);
+
+    return reply::parse<long long>(*reply);
+}
+
+OptionalString Redis::get(const StringView &key) {
+    auto reply = command(cmd::get, key);
+
+    return reply::parse<OptionalString>(*reply);
+}
+
+long long Redis::getbit(const StringView &key, long long offset) {
+    auto reply = command(cmd::getbit, key, offset);
+
+    return reply::parse<long long>(*reply);
+}
+
+std::string Redis::getrange(const StringView &key, long long start, long long end) {
+    auto reply = command(cmd::getrange, key, start, end);
+
+    return reply::parse<std::string>(*reply);
+}
+
+OptionalString Redis::getset(const StringView &key, const StringView &val) {
+    auto reply = command(cmd::getset, key, val);
+
+    return reply::parse<OptionalString>(*reply);
+}
+
+long long Redis::incr(const StringView &key) {
+    auto reply = command(cmd::incr, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long Redis::incrby(const StringView &key, long long increment) {
+    auto reply = command(cmd::incrby, key, increment);
+
+    return reply::parse<long long>(*reply);
+}
+
+double Redis::incrbyfloat(const StringView &key, double increment) {
+    auto reply = command(cmd::incrbyfloat, key, increment);
+
+    return reply::parse<double>(*reply);
+}
+
+void Redis::psetex(const StringView &key,
+                        long long ttl,
+                        const StringView &val) {
+    auto reply = command(cmd::psetex, key, ttl, val);
+
+    reply::parse<void>(*reply);
+}
+
+bool Redis::set(const StringView &key,
+                    const StringView &val,
+                    const std::chrono::milliseconds &ttl,
+                    UpdateType type) {
+    auto reply = command(cmd::set, key, val, ttl.count(), type);
+
+    reply::rewrite_set_reply(*reply);
+
+    return reply::parse<bool>(*reply);
+}
+
+void Redis::setex(const StringView &key,
+                    long long ttl,
+                    const StringView &val) {
+    auto reply = command(cmd::setex, key, ttl, val);
+
+    reply::parse<void>(*reply);
+}
+
+bool Redis::setnx(const StringView &key, const StringView &val) {
+    auto reply = command(cmd::setnx, key, val);
+
+    return reply::parse<bool>(*reply);
+}
+
+long long Redis::setrange(const StringView &key, long long offset, const StringView &val) {
+    auto reply = command(cmd::setrange, key, offset, val);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long Redis::strlen(const StringView &key) {
+    auto reply = command(cmd::strlen, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+// LIST commands.
+
+OptionalStringPair Redis::blpop(const StringView &key, long long timeout) {
+    auto reply = command(cmd::blpop, key, timeout);
+
+    return reply::parse<OptionalStringPair>(*reply);
+}
+
+OptionalStringPair Redis::blpop(const StringView &key, const std::chrono::seconds &timeout) {
+    return blpop(key, timeout.count());
+}
+
+OptionalStringPair Redis::brpop(const StringView &key, long long timeout) {
+    auto reply = command(cmd::brpop, key, timeout);
+
+    return reply::parse<OptionalStringPair>(*reply);
+}
+
+OptionalStringPair Redis::brpop(const StringView &key, const std::chrono::seconds &timeout) {
+    return brpop(key, timeout.count());
+}
+
+OptionalString Redis::brpoplpush(const StringView &source,
+                                    const StringView &destination,
+                                    long long timeout) {
+    auto reply = command(cmd::brpoplpush, source, destination, timeout);
+
+    return reply::parse<OptionalString>(*reply);
+}
+
+OptionalString Redis::lindex(const StringView &key, long long index) {
+    auto reply = command(cmd::lindex, key, index);
+
+    return reply::parse<OptionalString>(*reply);
+}
+
+long long Redis::linsert(const StringView &key,
+                            InsertPosition position,
+                            const StringView &pivot,
+                            const StringView &val) {
+    auto reply = command(cmd::linsert, key, position, pivot, val);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long Redis::llen(const StringView &key) {
+    auto reply = command(cmd::llen, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+OptionalString Redis::lpop(const StringView &key) {
+    auto reply = command(cmd::lpop, key);
+
+    return reply::parse<OptionalString>(*reply);
+}
+
+long long Redis::lpush(const StringView &key, const StringView &val) {
+    auto reply = command(cmd::lpush, key, val);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long Redis::lpushx(const StringView &key, const StringView &val) {
+    auto reply = command(cmd::lpushx, key, val);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long Redis::lrem(const StringView &key, long long count, const StringView &val) {
+    auto reply = command(cmd::lrem, key, count, val);
+
+    return reply::parse<long long>(*reply);
+}
+
+void Redis::lset(const StringView &key, long long index, const StringView &val) {
+    auto reply = command(cmd::lset, key, index, val);
+
+    reply::parse<void>(*reply);
+}
+
+void Redis::ltrim(const StringView &key, long long start, long long stop) {
+    auto reply = command(cmd::ltrim, key, start, stop);
+
+    reply::parse<void>(*reply);
+}
+
+OptionalString Redis::rpop(const StringView &key) {
+    auto reply = command(cmd::rpop, key);
+
+    return reply::parse<OptionalString>(*reply);
+}
+
+OptionalString Redis::rpoplpush(const StringView &source, const StringView &destination) {
+    auto reply = command(cmd::rpoplpush, source, destination);
+
+    return reply::parse<OptionalString>(*reply);
+}
+
+long long Redis::rpush(const StringView &key, const StringView &val) {
+    auto reply = command(cmd::rpush, key, val);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long Redis::rpushx(const StringView &key, const StringView &val) {
+    auto reply = command(cmd::rpushx, key, val);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long Redis::hdel(const StringView &key, const StringView &field) {
+    auto reply = command(cmd::hdel, key, field);
+
+    return reply::parse<long long>(*reply);
+}
+
+bool Redis::hexists(const StringView &key, const StringView &field) {
+    auto reply = command(cmd::hexists, key, field);
+
+    return reply::parse<bool>(*reply);
+}
+
+OptionalString Redis::hget(const StringView &key, const StringView &field) {
+    auto reply = command(cmd::hget, key, field);
+
+    return reply::parse<OptionalString>(*reply);
+}
+
+long long Redis::hincrby(const StringView &key, const StringView &field, long long increment) {
+    auto reply = command(cmd::hincrby, key, field, increment);
+
+    return reply::parse<long long>(*reply);
+}
+
+double Redis::hincrbyfloat(const StringView &key, const StringView &field, double increment) {
+    auto reply = command(cmd::hincrbyfloat, key, field, increment);
+
+    return reply::parse<double>(*reply);
+}
+
+long long Redis::hlen(const StringView &key) {
+    auto reply = command(cmd::hlen, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+bool Redis::hset(const StringView &key, const StringView &field, const StringView &val) {
+    auto reply = command(cmd::hset, key, field, val);
+
+    return reply::parse<bool>(*reply);
+}
+
+bool Redis::hset(const StringView &key, const std::pair<StringView, StringView> &item) {
+    return hset(key, item.first, item.second);
+}
+
+bool Redis::hsetnx(const StringView &key, const StringView &field, const StringView &val) {
+    auto reply = command(cmd::hsetnx, key, field, val);
+
+    return reply::parse<bool>(*reply);
+}
+
+bool Redis::hsetnx(const StringView &key, const std::pair<StringView, StringView> &item) {
+    return hsetnx(key, item.first, item.second);
+}
+
+long long Redis::hstrlen(const StringView &key, const StringView &field) {
+    auto reply = command(cmd::hstrlen, key, field);
+
+    return reply::parse<long long>(*reply);
+}
+
+// SET commands.
+
+long long Redis::sadd(const StringView &key, const StringView &member) {
+    auto reply = command(cmd::sadd, key, member);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long Redis::scard(const StringView &key) {
+    auto reply = command(cmd::scard, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long Redis::sdiffstore(const StringView &destination, const StringView &key) {
+    auto reply = command(cmd::sdiffstore, destination, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long Redis::sinterstore(const StringView &destination, const StringView &key) {
+    auto reply = command(cmd::sinterstore, destination, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+bool Redis::sismember(const StringView &key, const StringView &member) {
+    auto reply = command(cmd::sismember, key, member);
+
+    return reply::parse<bool>(*reply);
+}
+
+bool Redis::smove(const StringView &source,
+                    const StringView &destination,
+                    const StringView &member) {
+    auto reply = command(cmd::smove, source, destination, member);
+
+    return reply::parse<bool>(*reply);
+}
+
+OptionalString Redis::spop(const StringView &key) {
+    auto reply = command(cmd::spop, key);
+
+    return reply::parse<OptionalString>(*reply);
+}
+
+OptionalString Redis::srandmember(const StringView &key) {
+    auto reply = command(cmd::srandmember, key);
+
+    return reply::parse<OptionalString>(*reply);
+}
+
+long long Redis::srem(const StringView &key, const StringView &member) {
+    auto reply = command(cmd::srem, key, member);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long Redis::sunionstore(const StringView &destination, const StringView &key) {
+    auto reply = command(cmd::sunionstore, destination, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+// SORTED SET commands.
+
+auto Redis::bzpopmax(const StringView &key, long long timeout)
+    -> Optional<std::tuple<std::string, std::string, double>> {
+    auto reply = command(cmd::bzpopmax, key, timeout);
+
+    return reply::parse<Optional<std::tuple<std::string, std::string, double>>>(*reply);
+}
+
+auto Redis::bzpopmin(const StringView &key, long long timeout)
+    -> Optional<std::tuple<std::string, std::string, double>> {
+    auto reply = command(cmd::bzpopmin, key, timeout);
+
+    return reply::parse<Optional<std::tuple<std::string, std::string, double>>>(*reply);
+}
+
+long long Redis::zadd(const StringView &key,
+                        const StringView &member,
+                        double score,
+                        UpdateType type,
+                        bool changed) {
+    auto reply = command(cmd::zadd, key, member, score, type, changed);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long Redis::zcard(const StringView &key) {
+    auto reply = command(cmd::zcard, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+double Redis::zincrby(const StringView &key, double increment, const StringView &member) {
+    auto reply = command(cmd::zincrby, key, increment, member);
+
+    return reply::parse<double>(*reply);
+}
+
+long long Redis::zinterstore(const StringView &destination, const StringView &key, double weight) {
+    auto reply = command(cmd::zinterstore, destination, key, weight);
+
+    return reply::parse<long long>(*reply);
+}
+
+Optional<std::pair<std::string, double>> Redis::zpopmax(const StringView &key) {
+    auto reply = command(cmd::zpopmax, key, 1);
+
+    return reply::parse<Optional<std::pair<std::string, double>>>(*reply);
+}
+
+Optional<std::pair<std::string, double>> Redis::zpopmin(const StringView &key) {
+    auto reply = command(cmd::zpopmin, key, 1);
+
+    return reply::parse<Optional<std::pair<std::string, double>>>(*reply);
+}
+
+OptionalLongLong Redis::zrank(const StringView &key, const StringView &member) {
+    auto reply = command(cmd::zrank, key, member);
+
+    return reply::parse<OptionalLongLong>(*reply);
+}
+
+long long Redis::zrem(const StringView &key, const StringView &member) {
+    auto reply = command(cmd::zrem, key, member);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long Redis::zremrangebyrank(const StringView &key, long long start, long long stop) {
+    auto reply = command(cmd::zremrangebyrank, key, start, stop);
+
+    return reply::parse<long long>(*reply);
+}
+
+OptionalLongLong Redis::zrevrank(const StringView &key, const StringView &member) {
+    auto reply = command(cmd::zrevrank, key, member);
+
+    return reply::parse<OptionalLongLong>(*reply);
+}
+
+OptionalDouble Redis::zscore(const StringView &key, const StringView &member) {
+    auto reply = command(cmd::zscore, key, member);
+
+    return reply::parse<OptionalDouble>(*reply);
+}
+
+long long Redis::zunionstore(const StringView &destination, const StringView &key, double weight) {
+    auto reply = command(cmd::zunionstore, destination, key, weight);
+
+    return reply::parse<long long>(*reply);
+}
+
+// HYPERLOGLOG commands.
+
+bool Redis::pfadd(const StringView &key, const StringView &element) {
+    auto reply = command(cmd::pfadd, key, element);
+
+    return reply::parse<bool>(*reply);
+}
+
+long long Redis::pfcount(const StringView &key) {
+    auto reply = command(cmd::pfcount, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+void Redis::pfmerge(const StringView &destination, const StringView &key) {
+    auto reply = command(cmd::pfmerge, destination, key);
+
+    reply::parse<void>(*reply);
+}
+
+// GEO commands.
+
+long long Redis::geoadd(const StringView &key,
+                        const std::tuple<StringView, double, double> &member) {
+    auto reply = command(cmd::geoadd, key, member);
+
+    return reply::parse<long long>(*reply);
+}
+
+OptionalDouble Redis::geodist(const StringView &key,
+                                const StringView &member1,
+                                const StringView &member2,
+                                GeoUnit unit) {
+    auto reply = command(cmd::geodist, key, member1, member2, unit);
+
+    return reply::parse<OptionalDouble>(*reply);
+}
+
+OptionalLongLong Redis::georadius(const StringView &key,
+                                    const std::pair<double, double> &loc,
+                                    double radius,
+                                    GeoUnit unit,
+                                    const StringView &destination,
+                                    bool store_dist,
+                                    long long count) {
+    auto reply = command(cmd::georadius_store,
+                            key,
+                            loc,
+                            radius,
+                            unit,
+                            destination,
+                            store_dist,
+                            count);
+
+    reply::rewrite_georadius_reply(*reply);
+
+    return reply::parse<OptionalLongLong>(*reply);
+}
+
+OptionalLongLong Redis::georadiusbymember(const StringView &key,
+                                            const StringView &member,
+                                            double radius,
+                                            GeoUnit unit,
+                                            const StringView &destination,
+                                            bool store_dist,
+                                            long long count) {
+    auto reply = command(cmd::georadiusbymember_store,
+                            key,
+                            member,
+                            radius,
+                            unit,
+                            destination,
+                            store_dist,
+                            count);
+
+    reply::rewrite_georadius_reply(*reply);
+
+    return reply::parse<OptionalLongLong>(*reply);
+}
+
+// SCRIPTING commands.
+
+void Redis::script_flush() {
+    auto reply = command(cmd::script_flush);
+
+    reply::parse<void>(*reply);
+}
+
+void Redis::script_kill() {
+    auto reply = command(cmd::script_kill);
+
+    reply::parse<void>(*reply);
+}
+
+std::string Redis::script_load(const StringView &script) {
+    auto reply = command(cmd::script_load, script);
+
+    return reply::parse<std::string>(*reply);
+}
+
+// PUBSUB commands.
+
+long long Redis::publish(const StringView &channel, const StringView &message) {
+    auto reply = command(cmd::publish, channel, message);
+
+    return reply::parse<long long>(*reply);
+}
+
+// Transaction commands.
+
+void Redis::watch(const StringView &key) {
+    auto reply = command(cmd::watch, key);
+
+    reply::parse<void>(*reply);
+}
+
+// Stream commands.
+
+long long Redis::xack(const StringView &key, const StringView &group, const StringView &id) {
+    auto reply = command(cmd::xack, key, group, id);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long Redis::xdel(const StringView &key, const StringView &id) {
+    auto reply = command(cmd::xdel, key, id);
+
+    return reply::parse<long long>(*reply);
+}
+
+void Redis::xgroup_create(const StringView &key,
+                            const StringView &group,
+                            const StringView &id,
+                            bool mkstream) {
+    auto reply = command(cmd::xgroup_create, key, group, id, mkstream);
+
+    reply::parse<void>(*reply);
+}
+
+void Redis::xgroup_setid(const StringView &key, const StringView &group, const StringView &id) {
+    auto reply = command(cmd::xgroup_setid, key, group, id);
+
+    reply::parse<void>(*reply);
+}
+
+long long Redis::xgroup_destroy(const StringView &key, const StringView &group) {
+    auto reply = command(cmd::xgroup_destroy, key, group);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long Redis::xgroup_delconsumer(const StringView &key,
+                                const StringView &group,
+                                const StringView &consumer) {
+    auto reply = command(cmd::xgroup_delconsumer, key, group, consumer);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long Redis::xlen(const StringView &key) {
+    auto reply = command(cmd::xlen, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long Redis::xtrim(const StringView &key, long long count, bool approx) {
+    auto reply = command(cmd::xtrim, key, count, approx);
+
+    return reply::parse<long long>(*reply);
+}
+
+}
+
+}

+ 1523 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/redis.h

@@ -0,0 +1,1523 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_REDIS_H
+#define SEWENEW_REDISPLUSPLUS_REDIS_H
+
+#include <string>
+#include <chrono>
+#include <memory>
+#include <initializer_list>
+#include <tuple>
+#include "connection_pool.h"
+#include "reply.h"
+#include "command_options.h"
+#include "utils.h"
+#include "subscriber.h"
+#include "pipeline.h"
+#include "transaction.h"
+#include "sentinel.h"
+
+namespace sw {
+
+namespace redis {
+
+template <typename Impl>
+class QueuedRedis;
+
+using Transaction = QueuedRedis<TransactionImpl>;
+
+using Pipeline = QueuedRedis<PipelineImpl>;
+
+class Redis {
+public:
+    Redis(const ConnectionOptions &connection_opts,
+            const ConnectionPoolOptions &pool_opts = {}) : _pool(pool_opts, connection_opts) {}
+
+    // Construct Redis instance with URI:
+    // "tcp://127.0.0.1", "tcp://127.0.0.1:6379", or "unix://path/to/socket"
+    explicit Redis(const std::string &uri);
+
+    Redis(const std::shared_ptr<Sentinel> &sentinel,
+            const std::string &master_name,
+            Role role,
+            const ConnectionOptions &connection_opts,
+            const ConnectionPoolOptions &pool_opts = {}) :
+                _pool(SimpleSentinel(sentinel, master_name, role), pool_opts, connection_opts) {}
+
+    Redis(const Redis &) = delete;
+    Redis& operator=(const Redis &) = delete;
+
+    Redis(Redis &&) = default;
+    Redis& operator=(Redis &&) = default;
+
+    Pipeline pipeline();
+
+    Transaction transaction(bool piped = false);
+
+    Subscriber subscriber();
+
+    template <typename Cmd, typename ...Args>
+    auto command(Cmd cmd, Args &&...args)
+        -> typename std::enable_if<!std::is_convertible<Cmd, StringView>::value, ReplyUPtr>::type;
+
+    template <typename ...Args>
+    auto command(const StringView &cmd_name, Args &&...args)
+        -> typename std::enable_if<!IsIter<typename LastType<Args...>::type>::value,
+                                    ReplyUPtr>::type;
+
+    template <typename ...Args>
+    auto command(const StringView &cmd_name, Args &&...args)
+        -> typename std::enable_if<IsIter<typename LastType<Args...>::type>::value, void>::type;
+
+    template <typename Result, typename ...Args>
+    Result command(const StringView &cmd_name, Args &&...args);
+
+    template <typename Input>
+    auto command(Input first, Input last)
+        -> typename std::enable_if<IsIter<Input>::value, ReplyUPtr>::type;
+
+    template <typename Result, typename Input>
+    auto command(Input first, Input last)
+        -> typename std::enable_if<IsIter<Input>::value, Result>::type;
+
+    template <typename Input, typename Output>
+    auto command(Input first, Input last, Output output)
+        -> typename std::enable_if<IsIter<Input>::value, void>::type;
+
+    // CONNECTION commands.
+
+    void auth(const StringView &password);
+
+    std::string echo(const StringView &msg);
+
+    std::string ping();
+
+    std::string ping(const StringView &msg);
+
+    // After sending QUIT, only the current connection will be close, while
+    // other connections in the pool is still open. This is a strange behavior.
+    // So we DO NOT support the QUIT command. If you want to quit connection to
+    // server, just destroy the Redis object.
+    //
+    // void quit();
+
+    // We get a connection from the pool, and send the SELECT command to switch
+    // to a specified DB. However, when we try to send other commands to the
+    // given DB, we might get a different connection from the pool, and these
+    // commands, in fact, work on other DB. e.g.
+    //
+    // redis.select(1); // get a connection from the pool and switch to the 1th DB
+    // redis.get("key"); // might get another connection from the pool,
+    //                   // and try to get 'key' on the default DB
+    //
+    // Obviously, this is NOT what we expect. So we DO NOT support SELECT command.
+    // In order to select a DB, we can specify the DB index with the ConnectionOptions.
+    //
+    // However, since Pipeline and Transaction always send multiple commands on a
+    // single connection, these two classes have a *select* method.
+    //
+    // void select(long long idx);
+
+    void swapdb(long long idx1, long long idx2);
+
+    // SERVER commands.
+
+    void bgrewriteaof();
+
+    void bgsave();
+
+    long long dbsize();
+
+    void flushall(bool async = false);
+
+    void flushdb(bool async = false);
+
+    std::string info();
+
+    std::string info(const StringView &section);
+
+    long long lastsave();
+
+    void save();
+
+    // KEY commands.
+
+    long long del(const StringView &key);
+
+    template <typename Input>
+    long long del(Input first, Input last);
+
+    template <typename T>
+    long long del(std::initializer_list<T> il) {
+        return del(il.begin(), il.end());
+    }
+
+    OptionalString dump(const StringView &key);
+
+    long long exists(const StringView &key);
+
+    template <typename Input>
+    long long exists(Input first, Input last);
+
+    template <typename T>
+    long long exists(std::initializer_list<T> il) {
+        return exists(il.begin(), il.end());
+    }
+
+    bool expire(const StringView &key, long long timeout);
+
+    bool expire(const StringView &key, const std::chrono::seconds &timeout);
+
+    bool expireat(const StringView &key, long long timestamp);
+
+    bool expireat(const StringView &key,
+                    const std::chrono::time_point<std::chrono::system_clock,
+                                                    std::chrono::seconds> &tp);
+
+    template <typename Output>
+    void keys(const StringView &pattern, Output output);
+
+    bool move(const StringView &key, long long db);
+
+    bool persist(const StringView &key);
+
+    bool pexpire(const StringView &key, long long timeout);
+
+    bool pexpire(const StringView &key, const std::chrono::milliseconds &timeout);
+
+    bool pexpireat(const StringView &key, long long timestamp);
+
+    bool pexpireat(const StringView &key,
+                    const std::chrono::time_point<std::chrono::system_clock,
+                                                    std::chrono::milliseconds> &tp);
+
+    long long pttl(const StringView &key);
+
+    OptionalString randomkey();
+
+    void rename(const StringView &key, const StringView &newkey);
+
+    bool renamenx(const StringView &key, const StringView &newkey);
+
+    void restore(const StringView &key,
+                    const StringView &val,
+                    long long ttl,
+                    bool replace = false);
+
+    void restore(const StringView &key,
+                    const StringView &val,
+                    const std::chrono::milliseconds &ttl = std::chrono::milliseconds{0},
+                    bool replace = false);
+
+    // TODO: sort
+
+    template <typename Output>
+    long long scan(long long cursor,
+                    const StringView &pattern,
+                    long long count,
+                    Output output);
+
+    template <typename Output>
+    long long scan(long long cursor,
+                    Output output);
+
+    template <typename Output>
+    long long scan(long long cursor,
+                    const StringView &pattern,
+                    Output output);
+
+    template <typename Output>
+    long long scan(long long cursor,
+                    long long count,
+                    Output output);
+
+    long long touch(const StringView &key);
+
+    template <typename Input>
+    long long touch(Input first, Input last);
+
+    template <typename T>
+    long long touch(std::initializer_list<T> il) {
+        return touch(il.begin(), il.end());
+    }
+
+    long long ttl(const StringView &key);
+
+    std::string type(const StringView &key);
+
+    long long unlink(const StringView &key);
+
+    template <typename Input>
+    long long unlink(Input first, Input last);
+
+    template <typename T>
+    long long unlink(std::initializer_list<T> il) {
+        return unlink(il.begin(), il.end());
+    }
+
+    long long wait(long long numslaves, long long timeout);
+
+    long long wait(long long numslaves, const std::chrono::milliseconds &timeout);
+
+    // STRING commands.
+
+    long long append(const StringView &key, const StringView &str);
+
+    long long bitcount(const StringView &key, long long start = 0, long long end = -1);
+
+    long long bitop(BitOp op, const StringView &destination, const StringView &key);
+
+    template <typename Input>
+    long long bitop(BitOp op, const StringView &destination, Input first, Input last);
+
+    template <typename T>
+    long long bitop(BitOp op, const StringView &destination, std::initializer_list<T> il) {
+        return bitop(op, destination, il.begin(), il.end());
+    }
+
+    long long bitpos(const StringView &key,
+                        long long bit,
+                        long long start = 0,
+                        long long end = -1);
+
+    long long decr(const StringView &key);
+
+    long long decrby(const StringView &key, long long decrement);
+
+    OptionalString get(const StringView &key);
+
+    long long getbit(const StringView &key, long long offset);
+
+    std::string getrange(const StringView &key, long long start, long long end);
+
+    OptionalString getset(const StringView &key, const StringView &val);
+
+    long long incr(const StringView &key);
+
+    long long incrby(const StringView &key, long long increment);
+
+    double incrbyfloat(const StringView &key, double increment);
+
+    template <typename Input, typename Output>
+    void mget(Input first, Input last, Output output);
+
+    template <typename T, typename Output>
+    void mget(std::initializer_list<T> il, Output output) {
+        mget(il.begin(), il.end(), output);
+    }
+
+    template <typename Input>
+    void mset(Input first, Input last);
+
+    template <typename T>
+    void mset(std::initializer_list<T> il) {
+        mset(il.begin(), il.end());
+    }
+
+    template <typename Input>
+    bool msetnx(Input first, Input last);
+
+    template <typename T>
+    bool msetnx(std::initializer_list<T> il) {
+        return msetnx(il.begin(), il.end());
+    }
+
+    void psetex(const StringView &key,
+                long long ttl,
+                const StringView &val);
+
+    void psetex(const StringView &key,
+                const std::chrono::milliseconds &ttl,
+                const StringView &val);
+
+    bool set(const StringView &key,
+                const StringView &val,
+                const std::chrono::milliseconds &ttl = std::chrono::milliseconds(0),
+                UpdateType type = UpdateType::ALWAYS);
+
+    void setex(const StringView &key,
+                long long ttl,
+                const StringView &val);
+
+    void setex(const StringView &key,
+                const std::chrono::seconds &ttl,
+                const StringView &val);
+
+    bool setnx(const StringView &key, const StringView &val);
+
+    long long setrange(const StringView &key, long long offset, const StringView &val);
+
+    long long strlen(const StringView &key);
+
+    // LIST commands.
+
+    OptionalStringPair blpop(const StringView &key, long long timeout);
+
+    OptionalStringPair blpop(const StringView &key,
+                                const std::chrono::seconds &timeout = std::chrono::seconds{0});
+
+    template <typename Input>
+    OptionalStringPair blpop(Input first, Input last, long long timeout);
+
+    template <typename T>
+    OptionalStringPair blpop(std::initializer_list<T> il, long long timeout) {
+        return blpop(il.begin(), il.end(), timeout);
+    }
+
+    template <typename Input>
+    OptionalStringPair blpop(Input first,
+                                Input last,
+                                const std::chrono::seconds &timeout = std::chrono::seconds{0});
+
+    template <typename T>
+    OptionalStringPair blpop(std::initializer_list<T> il,
+                                const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
+        return blpop(il.begin(), il.end(), timeout);
+    }
+
+    OptionalStringPair brpop(const StringView &key, long long timeout);
+
+    OptionalStringPair brpop(const StringView &key,
+                                const std::chrono::seconds &timeout = std::chrono::seconds{0});
+
+    template <typename Input>
+    OptionalStringPair brpop(Input first, Input last, long long timeout);
+
+    template <typename T>
+    OptionalStringPair brpop(std::initializer_list<T> il, long long timeout) {
+        return brpop(il.begin(), il.end(), timeout);
+    }
+
+    template <typename Input>
+    OptionalStringPair brpop(Input first,
+                                Input last,
+                                const std::chrono::seconds &timeout = std::chrono::seconds{0});
+
+    template <typename T>
+    OptionalStringPair brpop(std::initializer_list<T> il,
+                                const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
+        return brpop(il.begin(), il.end(), timeout);
+    }
+
+    OptionalString brpoplpush(const StringView &source,
+                                const StringView &destination,
+                                long long timeout);
+
+    OptionalString brpoplpush(const StringView &source,
+                                const StringView &destination,
+                                const std::chrono::seconds &timeout = std::chrono::seconds{0});
+
+    OptionalString lindex(const StringView &key, long long index);
+
+    long long linsert(const StringView &key,
+                        InsertPosition position,
+                        const StringView &pivot,
+                        const StringView &val);
+
+    long long llen(const StringView &key);
+
+    OptionalString lpop(const StringView &key);
+
+    long long lpush(const StringView &key, const StringView &val);
+
+    template <typename Input>
+    long long lpush(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    long long lpush(const StringView &key, std::initializer_list<T> il) {
+        return lpush(key, il.begin(), il.end());
+    }
+
+    long long lpushx(const StringView &key, const StringView &val);
+
+    template <typename Output>
+    void lrange(const StringView &key, long long start, long long stop, Output output);
+
+    long long lrem(const StringView &key, long long count, const StringView &val);
+
+    void lset(const StringView &key, long long index, const StringView &val);
+
+    void ltrim(const StringView &key, long long start, long long stop);
+
+    OptionalString rpop(const StringView &key);
+
+    OptionalString rpoplpush(const StringView &source, const StringView &destination);
+
+    long long rpush(const StringView &key, const StringView &val);
+
+    template <typename Input>
+    long long rpush(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    long long rpush(const StringView &key, std::initializer_list<T> il) {
+        return rpush(key, il.begin(), il.end());
+    }
+
+    long long rpushx(const StringView &key, const StringView &val);
+
+    // HASH commands.
+
+    long long hdel(const StringView &key, const StringView &field);
+
+    template <typename Input>
+    long long hdel(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    long long hdel(const StringView &key, std::initializer_list<T> il) {
+        return hdel(key, il.begin(), il.end());
+    }
+
+    bool hexists(const StringView &key, const StringView &field);
+
+    OptionalString hget(const StringView &key, const StringView &field);
+
+    template <typename Output>
+    void hgetall(const StringView &key, Output output);
+
+    long long hincrby(const StringView &key, const StringView &field, long long increment);
+
+    double hincrbyfloat(const StringView &key, const StringView &field, double increment);
+
+    template <typename Output>
+    void hkeys(const StringView &key, Output output);
+
+    long long hlen(const StringView &key);
+
+    template <typename Input, typename Output>
+    void hmget(const StringView &key, Input first, Input last, Output output);
+
+    template <typename T, typename Output>
+    void hmget(const StringView &key, std::initializer_list<T> il, Output output) {
+        hmget(key, il.begin(), il.end(), output);
+    }
+
+    template <typename Input>
+    void hmset(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    void hmset(const StringView &key, std::initializer_list<T> il) {
+        hmset(key, il.begin(), il.end());
+    }
+
+    template <typename Output>
+    long long hscan(const StringView &key,
+                    long long cursor,
+                    const StringView &pattern,
+                    long long count,
+                    Output output);
+
+    template <typename Output>
+    long long hscan(const StringView &key,
+                    long long cursor,
+                    const StringView &pattern,
+                    Output output);
+
+    template <typename Output>
+    long long hscan(const StringView &key,
+                    long long cursor,
+                    long long count,
+                    Output output);
+
+    template <typename Output>
+    long long hscan(const StringView &key,
+                    long long cursor,
+                    Output output);
+
+    bool hset(const StringView &key, const StringView &field, const StringView &val);
+
+    bool hset(const StringView &key, const std::pair<StringView, StringView> &item);
+
+    bool hsetnx(const StringView &key, const StringView &field, const StringView &val);
+
+    bool hsetnx(const StringView &key, const std::pair<StringView, StringView> &item);
+
+    long long hstrlen(const StringView &key, const StringView &field);
+
+    template <typename Output>
+    void hvals(const StringView &key, Output output);
+
+    // SET commands.
+
+    long long sadd(const StringView &key, const StringView &member);
+
+    template <typename Input>
+    long long sadd(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    long long sadd(const StringView &key, std::initializer_list<T> il) {
+        return sadd(key, il.begin(), il.end());
+    }
+
+    long long scard(const StringView &key);
+
+    template <typename Input, typename Output>
+    void sdiff(Input first, Input last, Output output);
+
+    template <typename T, typename Output>
+    void sdiff(std::initializer_list<T> il, Output output) {
+        sdiff(il.begin(), il.end(), output);
+    }
+
+    long long sdiffstore(const StringView &destination, const StringView &key);
+
+    template <typename Input>
+    long long sdiffstore(const StringView &destination,
+                            Input first,
+                            Input last);
+
+    template <typename T>
+    long long sdiffstore(const StringView &destination,
+                            std::initializer_list<T> il) {
+        return sdiffstore(destination, il.begin(), il.end());
+    }
+
+    template <typename Input, typename Output>
+    void sinter(Input first, Input last, Output output);
+
+    template <typename T, typename Output>
+    void sinter(std::initializer_list<T> il, Output output) {
+        sinter(il.begin(), il.end(), output);
+    }
+
+    long long sinterstore(const StringView &destination, const StringView &key);
+
+    template <typename Input>
+    long long sinterstore(const StringView &destination,
+                            Input first,
+                            Input last);
+
+    template <typename T>
+    long long sinterstore(const StringView &destination,
+                            std::initializer_list<T> il) {
+        return sinterstore(destination, il.begin(), il.end());
+    }
+
+    bool sismember(const StringView &key, const StringView &member);
+
+    template <typename Output>
+    void smembers(const StringView &key, Output output);
+
+    bool smove(const StringView &source,
+                const StringView &destination,
+                const StringView &member);
+
+    OptionalString spop(const StringView &key);
+
+    template <typename Output>
+    void spop(const StringView &key, long long count, Output output);
+
+    OptionalString srandmember(const StringView &key);
+
+    template <typename Output>
+    void srandmember(const StringView &key, long long count, Output output);
+
+    long long srem(const StringView &key, const StringView &member);
+
+    template <typename Input>
+    long long srem(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    long long srem(const StringView &key, std::initializer_list<T> il) {
+        return srem(key, il.begin(), il.end());
+    }
+
+    template <typename Output>
+    long long sscan(const StringView &key,
+                    long long cursor,
+                    const StringView &pattern,
+                    long long count,
+                    Output output);
+
+    template <typename Output>
+    long long sscan(const StringView &key,
+                    long long cursor,
+                    const StringView &pattern,
+                    Output output);
+
+    template <typename Output>
+    long long sscan(const StringView &key,
+                    long long cursor,
+                    long long count,
+                    Output output);
+
+    template <typename Output>
+    long long sscan(const StringView &key,
+                    long long cursor,
+                    Output output);
+
+    template <typename Input, typename Output>
+    void sunion(Input first, Input last, Output output);
+
+    template <typename T, typename Output>
+    void sunion(std::initializer_list<T> il, Output output) {
+        sunion(il.begin(), il.end(), output);
+    }
+
+    long long sunionstore(const StringView &destination, const StringView &key);
+
+    template <typename Input>
+    long long sunionstore(const StringView &destination, Input first, Input last);
+
+    template <typename T>
+    long long sunionstore(const StringView &destination, std::initializer_list<T> il) {
+        return sunionstore(destination, il.begin(), il.end());
+    }
+
+    // SORTED SET commands.
+
+    auto bzpopmax(const StringView &key, long long timeout)
+        -> Optional<std::tuple<std::string, std::string, double>>;
+
+    auto bzpopmax(const StringView &key,
+                    const std::chrono::seconds &timeout = std::chrono::seconds{0})
+        -> Optional<std::tuple<std::string, std::string, double>>;
+
+    template <typename Input>
+    auto bzpopmax(Input first, Input last, long long timeout)
+        -> Optional<std::tuple<std::string, std::string, double>>;
+
+    template <typename Input>
+    auto bzpopmax(Input first,
+                    Input last,
+                    const std::chrono::seconds &timeout = std::chrono::seconds{0})
+        -> Optional<std::tuple<std::string, std::string, double>>;
+
+    template <typename T>
+    auto bzpopmax(std::initializer_list<T> il, long long timeout)
+        -> Optional<std::tuple<std::string, std::string, double>> {
+        return bzpopmax(il.begin(), il.end(), timeout);
+    }
+
+    template <typename T>
+    auto bzpopmax(std::initializer_list<T> il,
+                    const std::chrono::seconds &timeout = std::chrono::seconds{0})
+        -> Optional<std::tuple<std::string, std::string, double>> {
+        return bzpopmax(il.begin(), il.end(), timeout);
+    }
+
+    auto bzpopmin(const StringView &key, long long timeout)
+        -> Optional<std::tuple<std::string, std::string, double>>;
+
+    auto bzpopmin(const StringView &key,
+                    const std::chrono::seconds &timeout = std::chrono::seconds{0})
+        -> Optional<std::tuple<std::string, std::string, double>>;
+
+    template <typename Input>
+    auto bzpopmin(Input first, Input last, long long timeout)
+        -> Optional<std::tuple<std::string, std::string, double>>;
+
+    template <typename Input>
+    auto bzpopmin(Input first,
+                    Input last,
+                    const std::chrono::seconds &timeout = std::chrono::seconds{0})
+        -> Optional<std::tuple<std::string, std::string, double>>;
+
+    template <typename T>
+    auto bzpopmin(std::initializer_list<T> il, long long timeout)
+        -> Optional<std::tuple<std::string, std::string, double>> {
+        return bzpopmin(il.begin(), il.end(), timeout);
+    }
+
+    template <typename T>
+    auto bzpopmin(std::initializer_list<T> il,
+                    const std::chrono::seconds &timeout = std::chrono::seconds{0})
+        -> Optional<std::tuple<std::string, std::string, double>> {
+        return bzpopmin(il.begin(), il.end(), timeout);
+    }
+
+    // We don't support the INCR option, since you can always use ZINCRBY instead.
+    long long zadd(const StringView &key,
+                    const StringView &member,
+                    double score,
+                    UpdateType type = UpdateType::ALWAYS,
+                    bool changed = false);
+
+    template <typename Input>
+    long long zadd(const StringView &key,
+                    Input first,
+                    Input last,
+                    UpdateType type = UpdateType::ALWAYS,
+                    bool changed = false);
+
+    template <typename T>
+    long long zadd(const StringView &key,
+                    std::initializer_list<T> il,
+                    UpdateType type = UpdateType::ALWAYS,
+                    bool changed = false) {
+        return zadd(key, il.begin(), il.end(), type, changed);
+    }
+
+    long long zcard(const StringView &key);
+
+    template <typename Interval>
+    long long zcount(const StringView &key, const Interval &interval);
+
+    double zincrby(const StringView &key, double increment, const StringView &member);
+
+    // There's no aggregation type parameter for single key overload, since these 3 types
+    // have the same effect.
+    long long zinterstore(const StringView &destination, const StringView &key, double weight);
+
+    // If *Input* is an iterator of a container of string,
+    // we use the default weight, i.e. 1, and send
+    // *ZINTERSTORE destination numkeys key [key ...] [AGGREGATE SUM|MIN|MAX]* command.
+    // If *Input* is an iterator of a container of pair<string, double>, i.e. key-weight pair,
+    // we send the command with the given weights:
+    // *ZINTERSTORE destination numkeys key [key ...]
+    // [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]*
+    //
+    // The following code use the default weight:
+    //
+    // vector<string> keys = {"k1", "k2", "k3"};
+    // redis.zinterstore(destination, keys.begin(), keys.end());
+    //
+    // On the other hand, the following code use the given weights:
+    //
+    // vector<pair<string, double>> keys_with_weights = {{"k1", 1}, {"k2", 2}, {"k3", 3}};
+    // redis.zinterstore(destination, keys_with_weights.begin(), keys_with_weights.end());
+    //
+    // NOTE: `keys_with_weights` can also be of type `unordered_map<string, double>`.
+    // However, it will be slower than vector<pair<string, double>>, since we use
+    // `distance(first, last)` to calculate the *numkeys* parameter.
+    //
+    // This also applies to *ZUNIONSTORE* command.
+    template <typename Input>
+    long long zinterstore(const StringView &destination,
+                            Input first,
+                            Input last,
+                            Aggregation type = Aggregation::SUM);
+
+    template <typename T>
+    long long zinterstore(const StringView &destination,
+                            std::initializer_list<T> il,
+                            Aggregation type = Aggregation::SUM) {
+        return zinterstore(destination, il.begin(), il.end(), type);
+    }
+
+    template <typename Interval>
+    long long zlexcount(const StringView &key, const Interval &interval);
+
+    Optional<std::pair<std::string, double>> zpopmax(const StringView &key);
+
+    template <typename Output>
+    void zpopmax(const StringView &key, long long count, Output output);
+
+    Optional<std::pair<std::string, double>> zpopmin(const StringView &key);
+
+    template <typename Output>
+    void zpopmin(const StringView &key, long long count, Output output);
+
+    // If *output* is an iterator of a container of string,
+    // we send *ZRANGE key start stop* command.
+    // If it's an iterator of a container of pair<string, double>,
+    // we send *ZRANGE key start stop WITHSCORES* command.
+    //
+    // The following code sends *ZRANGE* without the *WITHSCORES* option:
+    //
+    // vector<string> result;
+    // redis.zrange("key", 0, -1, back_inserter(result));
+    //
+    // On the other hand, the following code sends command with *WITHSCORES* option:
+    //
+    // unordered_map<string, double> with_score;
+    // redis.zrange("key", 0, -1, inserter(with_score, with_score.end()));
+    //
+    // This also applies to other commands with the *WITHSCORES* option,
+    // e.g. *ZRANGEBYSCORE*, *ZREVRANGE*, *ZREVRANGEBYSCORE*.
+    template <typename Output>
+    void zrange(const StringView &key, long long start, long long stop, Output output);
+
+    template <typename Interval, typename Output>
+    void zrangebylex(const StringView &key, const Interval &interval, Output output);
+
+    template <typename Interval, typename Output>
+    void zrangebylex(const StringView &key,
+                        const Interval &interval,
+                        const LimitOptions &opts,
+                        Output output);
+
+    // See *zrange* comment on how to send command with *WITHSCORES* option.
+    template <typename Interval, typename Output>
+    void zrangebyscore(const StringView &key, const Interval &interval, Output output);
+
+    // See *zrange* comment on how to send command with *WITHSCORES* option.
+    template <typename Interval, typename Output>
+    void zrangebyscore(const StringView &key,
+                        const Interval &interval,
+                        const LimitOptions &opts,
+                        Output output);
+
+    OptionalLongLong zrank(const StringView &key, const StringView &member);
+
+    long long zrem(const StringView &key, const StringView &member);
+
+    template <typename Input>
+    long long zrem(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    long long zrem(const StringView &key, std::initializer_list<T> il) {
+        return zrem(key, il.begin(), il.end());
+    }
+
+    template <typename Interval>
+    long long zremrangebylex(const StringView &key, const Interval &interval);
+
+    long long zremrangebyrank(const StringView &key, long long start, long long stop);
+
+    template <typename Interval>
+    long long zremrangebyscore(const StringView &key, const Interval &interval);
+
+    // See *zrange* comment on how to send command with *WITHSCORES* option.
+    template <typename Output>
+    void zrevrange(const StringView &key, long long start, long long stop, Output output);
+
+    template <typename Interval, typename Output>
+    void zrevrangebylex(const StringView &key, const Interval &interval, Output output);
+
+    template <typename Interval, typename Output>
+    void zrevrangebylex(const StringView &key,
+                        const Interval &interval,
+                        const LimitOptions &opts,
+                        Output output);
+
+    // See *zrange* comment on how to send command with *WITHSCORES* option.
+    template <typename Interval, typename Output>
+    void zrevrangebyscore(const StringView &key, const Interval &interval, Output output);
+
+    // See *zrange* comment on how to send command with *WITHSCORES* option.
+    template <typename Interval, typename Output>
+    void zrevrangebyscore(const StringView &key,
+                            const Interval &interval,
+                            const LimitOptions &opts,
+                            Output output);
+
+    OptionalLongLong zrevrank(const StringView &key, const StringView &member);
+
+    template <typename Output>
+    long long zscan(const StringView &key,
+                    long long cursor,
+                    const StringView &pattern,
+                    long long count,
+                    Output output);
+
+    template <typename Output>
+    long long zscan(const StringView &key,
+                    long long cursor,
+                    const StringView &pattern,
+                    Output output);
+
+    template <typename Output>
+    long long zscan(const StringView &key,
+                    long long cursor,
+                    long long count,
+                    Output output);
+
+    template <typename Output>
+    long long zscan(const StringView &key,
+                    long long cursor,
+                    Output output);
+
+    OptionalDouble zscore(const StringView &key, const StringView &member);
+
+    // There's no aggregation type parameter for single key overload, since these 3 types
+    // have the same effect.
+    long long zunionstore(const StringView &destination, const StringView &key, double weight);
+
+    // See *zinterstore* comment for how to use this method.
+    template <typename Input>
+    long long zunionstore(const StringView &destination,
+                            Input first,
+                            Input last,
+                            Aggregation type = Aggregation::SUM);
+
+    template <typename T>
+    long long zunionstore(const StringView &destination,
+                            std::initializer_list<T> il,
+                            Aggregation type = Aggregation::SUM) {
+        return zunionstore(destination, il.begin(), il.end(), type);
+    }
+
+    // HYPERLOGLOG commands.
+
+    bool pfadd(const StringView &key, const StringView &element);
+
+    template <typename Input>
+    bool pfadd(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    bool pfadd(const StringView &key, std::initializer_list<T> il) {
+        return pfadd(key, il.begin(), il.end());
+    }
+
+    long long pfcount(const StringView &key);
+
+    template <typename Input>
+    long long pfcount(Input first, Input last);
+
+    template <typename T>
+    long long pfcount(std::initializer_list<T> il) {
+        return pfcount(il.begin(), il.end());
+    }
+
+    void pfmerge(const StringView &destination, const StringView &key);
+
+    template <typename Input>
+    void pfmerge(const StringView &destination, Input first, Input last);
+
+    template <typename T>
+    void pfmerge(const StringView &destination, std::initializer_list<T> il) {
+        pfmerge(destination, il.begin(), il.end());
+    }
+
+    // GEO commands.
+
+    long long geoadd(const StringView &key,
+                        const std::tuple<StringView, double, double> &member);
+
+    template <typename Input>
+    long long geoadd(const StringView &key,
+                        Input first,
+                        Input last);
+
+    template <typename T>
+    long long geoadd(const StringView &key,
+                        std::initializer_list<T> il) {
+        return geoadd(key, il.begin(), il.end());
+    }
+
+    OptionalDouble geodist(const StringView &key,
+                            const StringView &member1,
+                            const StringView &member2,
+                            GeoUnit unit = GeoUnit::M);
+
+    template <typename Input, typename Output>
+    void geohash(const StringView &key, Input first, Input last, Output output);
+
+    template <typename T, typename Output>
+    void geohash(const StringView &key, std::initializer_list<T> il, Output output) {
+        geohash(key, il.begin(), il.end(), output);
+    }
+
+    template <typename Input, typename Output>
+    void geopos(const StringView &key, Input first, Input last, Output output);
+
+    template <typename T, typename Output>
+    void geopos(const StringView &key, std::initializer_list<T> il, Output output) {
+        geopos(key, il.begin(), il.end(), output);
+    }
+
+    // TODO:
+    // 1. since we have different overloads for georadius and georadius-store,
+    //    we might use the GEORADIUS_RO command in the future.
+    // 2. there're too many parameters for this method, we might refactor it.
+    OptionalLongLong georadius(const StringView &key,
+                                const std::pair<double, double> &loc,
+                                double radius,
+                                GeoUnit unit,
+                                const StringView &destination,
+                                bool store_dist,
+                                long long count);
+
+    // If *output* is an iterator of a container of string, we send *GEORADIUS* command
+    // without any options and only get the members in the specified geo range.
+    // If *output* is an iterator of a container of a tuple, the type of the tuple decides
+    // options we send with the *GEORADIUS* command. If the tuple has an element of type
+    // double, we send the *WITHDIST* option. If it has an element of type string, we send
+    // the *WITHHASH* option. If it has an element of type pair<double, double>, we send
+    // the *WITHCOORD* option. For example:
+    //
+    // The following code only gets the members in range, i.e. without any option.
+    //
+    // vector<string> members;
+    // redis.georadius("key", make_pair(10.1, 10.2), 10, GeoUnit::KM, 10, true,
+    //                  back_inserter(members))
+    //
+    // The following code sends the command with *WITHDIST* option.
+    //
+    // vector<tuple<string, double>> with_dist;
+    // redis.georadius("key", make_pair(10.1, 10.2), 10, GeoUnit::KM, 10, true,
+    //                  back_inserter(with_dist))
+    //
+    // The following code sends the command with *WITHDIST* and *WITHHASH* options.
+    //
+    // vector<tuple<string, double, string>> with_dist_hash;
+    // redis.georadius("key", make_pair(10.1, 10.2), 10, GeoUnit::KM, 10, true,
+    //                  back_inserter(with_dist_hash))
+    //
+    // The following code sends the command with *WITHDIST*, *WITHCOORD* and *WITHHASH* options.
+    //
+    // vector<tuple<string, double, pair<double, double>, string>> with_dist_coord_hash;
+    // redis.georadius("key", make_pair(10.1, 10.2), 10, GeoUnit::KM, 10, true,
+    //                  back_inserter(with_dist_coord_hash))
+    //
+    // This also applies to *GEORADIUSBYMEMBER*.
+    template <typename Output>
+    void georadius(const StringView &key,
+                    const std::pair<double, double> &loc,
+                    double radius,
+                    GeoUnit unit,
+                    long long count,
+                    bool asc,
+                    Output output);
+
+    OptionalLongLong georadiusbymember(const StringView &key,
+                                        const StringView &member,
+                                        double radius,
+                                        GeoUnit unit,
+                                        const StringView &destination,
+                                        bool store_dist,
+                                        long long count);
+
+    // See comments on *GEORADIUS*.
+    template <typename Output>
+    void georadiusbymember(const StringView &key,
+                            const StringView &member,
+                            double radius,
+                            GeoUnit unit,
+                            long long count,
+                            bool asc,
+                            Output output);
+
+    // SCRIPTING commands.
+
+    template <typename Result>
+    Result eval(const StringView &script,
+                std::initializer_list<StringView> keys,
+                std::initializer_list<StringView> args);
+
+    template <typename Output>
+    void eval(const StringView &script,
+                std::initializer_list<StringView> keys,
+                std::initializer_list<StringView> args,
+                Output output);
+
+    template <typename Result>
+    Result evalsha(const StringView &script,
+                    std::initializer_list<StringView> keys,
+                    std::initializer_list<StringView> args);
+
+    template <typename Output>
+    void evalsha(const StringView &script,
+                    std::initializer_list<StringView> keys,
+                    std::initializer_list<StringView> args,
+                    Output output);
+
+    template <typename Input, typename Output>
+    void script_exists(Input first, Input last, Output output);
+
+    template <typename T, typename Output>
+    void script_exists(std::initializer_list<T> il, Output output) {
+        script_exists(il.begin(), il.end(), output);
+    }
+
+    void script_flush();
+
+    void script_kill();
+
+    std::string script_load(const StringView &script);
+
+    // PUBSUB commands.
+
+    long long publish(const StringView &channel, const StringView &message);
+
+    // Transaction commands.
+    void watch(const StringView &key);
+
+    template <typename Input>
+    void watch(Input first, Input last);
+
+    template <typename T>
+    void watch(std::initializer_list<T> il) {
+        watch(il.begin(), il.end());
+    }
+
+    // Stream commands.
+
+    long long xack(const StringView &key, const StringView &group, const StringView &id);
+
+    template <typename Input>
+    long long xack(const StringView &key, const StringView &group, Input first, Input last);
+
+    template <typename T>
+    long long xack(const StringView &key, const StringView &group, std::initializer_list<T> il) {
+        return xack(key, group, il.begin(), il.end());
+    }
+
+    template <typename Input>
+    std::string xadd(const StringView &key, const StringView &id, Input first, Input last);
+
+    template <typename T>
+    std::string xadd(const StringView &key, const StringView &id, std::initializer_list<T> il) {
+        return xadd(key, id, il.begin(), il.end());
+    }
+
+    template <typename Input>
+    std::string xadd(const StringView &key,
+                        const StringView &id,
+                        Input first,
+                        Input last,
+                        long long count,
+                        bool approx = true);
+
+    template <typename T>
+    std::string xadd(const StringView &key,
+                        const StringView &id,
+                        std::initializer_list<T> il,
+                        long long count,
+                        bool approx = true) {
+        return xadd(key, id, il.begin(), il.end(), count, approx);
+    }
+
+    template <typename Output>
+    void xclaim(const StringView &key,
+                const StringView &group,
+                const StringView &consumer,
+                const std::chrono::milliseconds &min_idle_time,
+                const StringView &id,
+                Output output);
+
+    template <typename Input, typename Output>
+    void xclaim(const StringView &key,
+                const StringView &group,
+                const StringView &consumer,
+                const std::chrono::milliseconds &min_idle_time,
+                Input first,
+                Input last,
+                Output output);
+
+    template <typename T, typename Output>
+    void xclaim(const StringView &key,
+                const StringView &group,
+                const StringView &consumer,
+                const std::chrono::milliseconds &min_idle_time,
+                std::initializer_list<T> il,
+                Output output) {
+        xclaim(key, group, consumer, min_idle_time, il.begin(), il.end(), output);
+    }
+
+    long long xdel(const StringView &key, const StringView &id);
+
+    template <typename Input>
+    long long xdel(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    long long xdel(const StringView &key, std::initializer_list<T> il) {
+        return xdel(key, il.begin(), il.end());
+    }
+
+    void xgroup_create(const StringView &key,
+                        const StringView &group,
+                        const StringView &id,
+                        bool mkstream = false);
+
+    void xgroup_setid(const StringView &key, const StringView &group, const StringView &id);
+
+    long long xgroup_destroy(const StringView &key, const StringView &group);
+
+    long long xgroup_delconsumer(const StringView &key,
+                                    const StringView &group,
+                                    const StringView &consumer);
+
+    long long xlen(const StringView &key);
+
+    template <typename Output>
+    auto xpending(const StringView &key, const StringView &group, Output output)
+        -> std::tuple<long long, OptionalString, OptionalString>;
+
+    template <typename Output>
+    void xpending(const StringView &key,
+                    const StringView &group,
+                    const StringView &start,
+                    const StringView &end,
+                    long long count,
+                    Output output);
+
+    template <typename Output>
+    void xpending(const StringView &key,
+                    const StringView &group,
+                    const StringView &start,
+                    const StringView &end,
+                    long long count,
+                    const StringView &consumer,
+                    Output output);
+
+    template <typename Output>
+    void xrange(const StringView &key,
+                const StringView &start,
+                const StringView &end,
+                Output output);
+
+    template <typename Output>
+    void xrange(const StringView &key,
+                const StringView &start,
+                const StringView &end,
+                long long count,
+                Output output);
+
+    template <typename Output>
+    void xread(const StringView &key,
+                const StringView &id,
+                long long count,
+                Output output);
+
+    template <typename Output>
+    void xread(const StringView &key,
+                const StringView &id,
+                Output output) {
+        xread(key, id, 0, output);
+    }
+
+    template <typename Input, typename Output>
+    auto xread(Input first, Input last, long long count, Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type;
+
+    template <typename Input, typename Output>
+    auto xread(Input first, Input last, Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+        xread(first ,last, 0, output);
+    }
+
+    template <typename Output>
+    void xread(const StringView &key,
+                const StringView &id,
+                const std::chrono::milliseconds &timeout,
+                long long count,
+                Output output);
+
+    template <typename Output>
+    void xread(const StringView &key,
+                const StringView &id,
+                const std::chrono::milliseconds &timeout,
+                Output output) {
+        xread(key, id, timeout, 0, output);
+    }
+
+    template <typename Input, typename Output>
+    auto xread(Input first,
+                Input last,
+                const std::chrono::milliseconds &timeout,
+                long long count,
+                Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type;
+
+    template <typename Input, typename Output>
+    auto xread(Input first,
+                Input last,
+                const std::chrono::milliseconds &timeout,
+                Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+        xread(first, last, timeout, 0, output);
+    }
+
+    template <typename Output>
+    void xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    const StringView &key,
+                    const StringView &id,
+                    long long count,
+                    bool noack,
+                    Output output);
+
+    template <typename Output>
+    void xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    const StringView &key,
+                    const StringView &id,
+                    long long count,
+                    Output output) {
+        xreadgroup(group, consumer, key, id, count, false, output);
+    }
+
+    template <typename Output>
+    void xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    const StringView &key,
+                    const StringView &id,
+                    Output output) {
+        xreadgroup(group, consumer, key, id, 0, false, output);
+    }
+
+    template <typename Input, typename Output>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last,
+                    long long count,
+                    bool noack,
+                    Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type;
+
+    template <typename Input, typename Output>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last,
+                    long long count,
+                    Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+        xreadgroup(group, consumer, first ,last, count, false, output);
+    }
+
+    template <typename Input, typename Output>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last,
+                    Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+        xreadgroup(group, consumer, first ,last, 0, false, output);
+    }
+
+    template <typename Output>
+    void xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    const StringView &key,
+                    const StringView &id,
+                    const std::chrono::milliseconds &timeout,
+                    long long count,
+                    bool noack,
+                    Output output);
+
+    template <typename Output>
+    void xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    const StringView &key,
+                    const StringView &id,
+                    const std::chrono::milliseconds &timeout,
+                    long long count,
+                    Output output) {
+        xreadgroup(group, consumer, key, id, timeout, count, false, output);
+    }
+
+    template <typename Output>
+    void xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    const StringView &key,
+                    const StringView &id,
+                    const std::chrono::milliseconds &timeout,
+                    Output output) {
+        xreadgroup(group, consumer, key, id, timeout, 0, false, output);
+    }
+
+    template <typename Input, typename Output>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last,
+                    const std::chrono::milliseconds &timeout,
+                    long long count,
+                    bool noack,
+                    Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type;
+
+    template <typename Input, typename Output>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last,
+                    const std::chrono::milliseconds &timeout,
+                    long long count,
+                    Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+        xreadgroup(group, consumer, first, last, timeout, count, false, output);
+    }
+
+    template <typename Input, typename Output>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last,
+                    const std::chrono::milliseconds &timeout,
+                    Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+        xreadgroup(group, consumer, first, last, timeout, 0, false, output);
+    }
+
+    template <typename Output>
+    void xrevrange(const StringView &key,
+                    const StringView &end,
+                    const StringView &start,
+                    Output output);
+
+    template <typename Output>
+    void xrevrange(const StringView &key,
+                    const StringView &end,
+                    const StringView &start,
+                    long long count,
+                    Output output);
+
+    long long xtrim(const StringView &key, long long count, bool approx = true);
+
+private:
+    class ConnectionPoolGuard {
+    public:
+        ConnectionPoolGuard(ConnectionPool &pool,
+                            Connection &connection) : _pool(pool), _connection(connection) {}
+
+        ~ConnectionPoolGuard() {
+            _pool.release(std::move(_connection));
+        }
+
+    private:
+        ConnectionPool &_pool;
+        Connection &_connection;
+    };
+
+    template <typename Impl>
+    friend class QueuedRedis;
+
+    friend class RedisCluster;
+
+    // For internal use.
+    explicit Redis(const ConnectionSPtr &connection);
+
+    template <std::size_t ...Is, typename ...Args>
+    ReplyUPtr _command(const StringView &cmd_name, const IndexSequence<Is...> &, Args &&...args) {
+        return command(cmd_name, NthValue<Is>(std::forward<Args>(args)...)...);
+    }
+
+    template <typename Cmd, typename ...Args>
+    ReplyUPtr _command(Connection &connection, Cmd cmd, Args &&...args);
+
+    template <typename Cmd, typename ...Args>
+    ReplyUPtr _score_command(std::true_type, Cmd cmd, Args &&... args);
+
+    template <typename Cmd, typename ...Args>
+    ReplyUPtr _score_command(std::false_type, Cmd cmd, Args &&... args);
+
+    template <typename Output, typename Cmd, typename ...Args>
+    ReplyUPtr _score_command(Cmd cmd, Args &&... args);
+
+    // Pool Mode.
+    // Public constructors create a *Redis* instance with a pool.
+    // In this case, *_connection* is a null pointer, and is never used.
+    ConnectionPool _pool;
+
+    // Single Connection Mode.
+    // Private constructor creats a *Redis* instance with a single connection.
+    // This is used when we create Transaction, Pipeline and Subscriber.
+    // In this case, *_pool* is empty, and is never used.
+    ConnectionSPtr _connection;
+};
+
+}
+
+}
+
+#include "redis.hpp"
+
+#endif // end SEWENEW_REDISPLUSPLUS_REDIS_H

+ 1365 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/redis.hpp

@@ -0,0 +1,1365 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_REDIS_HPP
+#define SEWENEW_REDISPLUSPLUS_REDIS_HPP
+
+#include "command.h"
+#include "reply.h"
+#include "utils.h"
+#include "errors.h"
+
+namespace sw {
+
+namespace redis {
+
+template <typename Cmd, typename ...Args>
+auto Redis::command(Cmd cmd, Args &&...args)
+    -> typename std::enable_if<!std::is_convertible<Cmd, StringView>::value, ReplyUPtr>::type {
+    if (_connection) {
+        // Single Connection Mode.
+        // TODO: In this case, should we reconnect?
+        if (_connection->broken()) {
+            throw Error("Connection is broken");
+        }
+
+        return _command(*_connection, cmd, std::forward<Args>(args)...);
+    } else {
+        // Pool Mode, i.e. get connection from pool.
+        auto connection = _pool.fetch();
+
+        assert(!connection.broken());
+
+        ConnectionPoolGuard guard(_pool, connection);
+
+        return _command(connection, cmd, std::forward<Args>(args)...);
+    }
+}
+
+template <typename ...Args>
+auto Redis::command(const StringView &cmd_name, Args &&...args)
+    -> typename std::enable_if<!IsIter<typename LastType<Args...>::type>::value, ReplyUPtr>::type {
+    auto cmd = [](Connection &connection, const StringView &cmd_name, Args &&...args) {
+                    CmdArgs cmd_args;
+                    cmd_args.append(cmd_name, std::forward<Args>(args)...);
+                    connection.send(cmd_args);
+    };
+
+    return command(cmd, cmd_name, std::forward<Args>(args)...);
+}
+
+template <typename Input>
+auto Redis::command(Input first, Input last)
+    -> typename std::enable_if<IsIter<Input>::value, ReplyUPtr>::type {
+    if (first == last) {
+        throw Error("command: empty range");
+    }
+
+    auto cmd = [](Connection &connection, Input first, Input last) {
+                    CmdArgs cmd_args;
+                    while (first != last) {
+                        cmd_args.append(*first);
+                        ++first;
+                    }
+                    connection.send(cmd_args);
+    };
+
+    return command(cmd, first, last);
+}
+
+template <typename Result, typename ...Args>
+Result Redis::command(const StringView &cmd_name, Args &&...args) {
+    auto r = command(cmd_name, std::forward<Args>(args)...);
+
+    assert(r);
+
+    return reply::parse<Result>(*r);
+}
+
+template <typename ...Args>
+auto Redis::command(const StringView &cmd_name, Args &&...args)
+    -> typename std::enable_if<IsIter<typename LastType<Args...>::type>::value, void>::type {
+    auto r = _command(cmd_name,
+                        MakeIndexSequence<sizeof...(Args) - 1>(),
+                        std::forward<Args>(args)...);
+
+    assert(r);
+
+    reply::to_array(*r, LastValue(std::forward<Args>(args)...));
+}
+
+template <typename Result, typename Input>
+auto Redis::command(Input first, Input last)
+    -> typename std::enable_if<IsIter<Input>::value, Result>::type {
+    auto r = command(first, last);
+
+    assert(r);
+
+    return reply::parse<Result>(*r);
+}
+
+template <typename Input, typename Output>
+auto Redis::command(Input first, Input last, Output output)
+    -> typename std::enable_if<IsIter<Input>::value, void>::type {
+    auto r = command(first, last);
+
+    assert(r);
+
+    reply::to_array(*r, output);
+}
+
+// KEY commands.
+
+template <typename Input>
+long long Redis::del(Input first, Input last) {
+    if (first == last) {
+        throw Error("DEL: no key specified");
+    }
+
+    auto reply = command(cmd::del_range<Input>, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input>
+long long Redis::exists(Input first, Input last) {
+    if (first == last) {
+        throw Error("EXISTS: no key specified");
+    }
+
+    auto reply = command(cmd::exists_range<Input>, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+inline bool Redis::expire(const StringView &key, const std::chrono::seconds &timeout) {
+    return expire(key, timeout.count());
+}
+
+inline bool Redis::expireat(const StringView &key,
+                                    const std::chrono::time_point<std::chrono::system_clock,
+                                                                    std::chrono::seconds> &tp) {
+    return expireat(key, tp.time_since_epoch().count());
+}
+
+template <typename Output>
+void Redis::keys(const StringView &pattern, Output output) {
+    auto reply = command(cmd::keys, pattern);
+
+    reply::to_array(*reply, output);
+}
+
+inline bool Redis::pexpire(const StringView &key, const std::chrono::milliseconds &timeout) {
+    return pexpire(key, timeout.count());
+}
+
+inline bool Redis::pexpireat(const StringView &key,
+                                const std::chrono::time_point<std::chrono::system_clock,
+                                                                std::chrono::milliseconds> &tp) {
+    return pexpireat(key, tp.time_since_epoch().count());
+}
+
+inline void Redis::restore(const StringView &key,
+                            const StringView &val,
+                            const std::chrono::milliseconds &ttl,
+                            bool replace) {
+    return restore(key, val, ttl.count(), replace);
+}
+
+template <typename Output>
+long long Redis::scan(long long cursor,
+                    const StringView &pattern,
+                    long long count,
+                    Output output) {
+    auto reply = command(cmd::scan, cursor, pattern, count);
+
+    return reply::parse_scan_reply(*reply, output);
+}
+
+template <typename Output>
+inline long long Redis::scan(long long cursor,
+                                const StringView &pattern,
+                                Output output) {
+    return scan(cursor, pattern, 10, output);
+}
+
+template <typename Output>
+inline long long Redis::scan(long long cursor,
+                                long long count,
+                                Output output) {
+    return scan(cursor, "*", count, output);
+}
+
+template <typename Output>
+inline long long Redis::scan(long long cursor,
+                                Output output) {
+    return scan(cursor, "*", 10, output);
+}
+
+template <typename Input>
+long long Redis::touch(Input first, Input last) {
+    if (first == last) {
+        throw Error("TOUCH: no key specified");
+    }
+
+    auto reply = command(cmd::touch_range<Input>, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input>
+long long Redis::unlink(Input first, Input last) {
+    if (first == last) {
+        throw Error("UNLINK: no key specified");
+    }
+
+    auto reply = command(cmd::unlink_range<Input>, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+inline long long Redis::wait(long long numslaves, const std::chrono::milliseconds &timeout) {
+    return wait(numslaves, timeout.count());
+}
+
+// STRING commands.
+
+template <typename Input>
+long long Redis::bitop(BitOp op, const StringView &destination, Input first, Input last) {
+    if (first == last) {
+        throw Error("BITOP: no key specified");
+    }
+
+    auto reply = command(cmd::bitop_range<Input>, op, destination, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input, typename Output>
+void Redis::mget(Input first, Input last, Output output) {
+    if (first == last) {
+        throw Error("MGET: no key specified");
+    }
+
+    auto reply = command(cmd::mget<Input>, first, last);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+void Redis::mset(Input first, Input last) {
+    if (first == last) {
+        throw Error("MSET: no key specified");
+    }
+
+    auto reply = command(cmd::mset<Input>, first, last);
+
+    reply::parse<void>(*reply);
+}
+
+template <typename Input>
+bool Redis::msetnx(Input first, Input last) {
+    if (first == last) {
+        throw Error("MSETNX: no key specified");
+    }
+
+    auto reply = command(cmd::msetnx<Input>, first, last);
+
+    return reply::parse<bool>(*reply);
+}
+
+inline void Redis::psetex(const StringView &key,
+                            const std::chrono::milliseconds &ttl,
+                            const StringView &val) {
+    return psetex(key, ttl.count(), val);
+}
+
+inline void Redis::setex(const StringView &key,
+                            const std::chrono::seconds &ttl,
+                            const StringView &val) {
+    setex(key, ttl.count(), val);
+}
+
+// LIST commands.
+
+template <typename Input>
+OptionalStringPair Redis::blpop(Input first, Input last, long long timeout) {
+    if (first == last) {
+        throw Error("BLPOP: no key specified");
+    }
+
+    auto reply = command(cmd::blpop_range<Input>, first, last, timeout);
+
+    return reply::parse<OptionalStringPair>(*reply);
+}
+
+template <typename Input>
+OptionalStringPair Redis::blpop(Input first,
+                                Input last,
+                                const std::chrono::seconds &timeout) {
+    return blpop(first, last, timeout.count());
+}
+
+template <typename Input>
+OptionalStringPair Redis::brpop(Input first, Input last, long long timeout) {
+    if (first == last) {
+        throw Error("BRPOP: no key specified");
+    }
+
+    auto reply = command(cmd::brpop_range<Input>, first, last, timeout);
+
+    return reply::parse<OptionalStringPair>(*reply);
+}
+
+template <typename Input>
+OptionalStringPair Redis::brpop(Input first,
+                                Input last,
+                                const std::chrono::seconds &timeout) {
+    return brpop(first, last, timeout.count());
+}
+
+inline OptionalString Redis::brpoplpush(const StringView &source,
+                                        const StringView &destination,
+                                        const std::chrono::seconds &timeout) {
+    return brpoplpush(source, destination, timeout.count());
+}
+
+template <typename Input>
+inline long long Redis::lpush(const StringView &key, Input first, Input last) {
+    if (first == last) {
+        throw Error("LPUSH: no key specified");
+    }
+
+    auto reply = command(cmd::lpush_range<Input>, key, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Output>
+inline void Redis::lrange(const StringView &key, long long start, long long stop, Output output) {
+    auto reply = command(cmd::lrange, key, start, stop);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+inline long long Redis::rpush(const StringView &key, Input first, Input last) {
+    if (first == last) {
+        throw Error("RPUSH: no key specified");
+    }
+
+    auto reply = command(cmd::rpush_range<Input>, key, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+// HASH commands.
+
+template <typename Input>
+inline long long Redis::hdel(const StringView &key, Input first, Input last) {
+    if (first == last) {
+        throw Error("HDEL: no key specified");
+    }
+
+    auto reply = command(cmd::hdel_range<Input>, key, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Output>
+inline void Redis::hgetall(const StringView &key, Output output) {
+    auto reply = command(cmd::hgetall, key);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+inline void Redis::hkeys(const StringView &key, Output output) {
+    auto reply = command(cmd::hkeys, key);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input, typename Output>
+inline void Redis::hmget(const StringView &key, Input first, Input last, Output output) {
+    if (first == last) {
+        throw Error("HMGET: no key specified");
+    }
+
+    auto reply = command(cmd::hmget<Input>, key, first, last);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+inline void Redis::hmset(const StringView &key, Input first, Input last) {
+    if (first == last) {
+        throw Error("HMSET: no key specified");
+    }
+
+    auto reply = command(cmd::hmset<Input>, key, first, last);
+
+    reply::parse<void>(*reply);
+}
+
+template <typename Output>
+long long Redis::hscan(const StringView &key,
+                        long long cursor,
+                        const StringView &pattern,
+                        long long count,
+                        Output output) {
+    auto reply = command(cmd::hscan, key, cursor, pattern, count);
+
+    return reply::parse_scan_reply(*reply, output);
+}
+
+template <typename Output>
+inline long long Redis::hscan(const StringView &key,
+                                long long cursor,
+                                const StringView &pattern,
+                                Output output) {
+    return hscan(key, cursor, pattern, 10, output);
+}
+
+template <typename Output>
+inline long long Redis::hscan(const StringView &key,
+                                long long cursor,
+                                long long count,
+                                Output output) {
+    return hscan(key, cursor, "*", count, output);
+}
+
+template <typename Output>
+inline long long Redis::hscan(const StringView &key,
+                                long long cursor,
+                                Output output) {
+    return hscan(key, cursor, "*", 10, output);
+}
+
+template <typename Output>
+inline void Redis::hvals(const StringView &key, Output output) {
+    auto reply = command(cmd::hvals, key);
+
+    reply::to_array(*reply, output);
+}
+
+// SET commands.
+
+template <typename Input>
+long long Redis::sadd(const StringView &key, Input first, Input last) {
+    if (first == last) {
+        throw Error("SADD: no key specified");
+    }
+
+    auto reply = command(cmd::sadd_range<Input>, key, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input, typename Output>
+void Redis::sdiff(Input first, Input last, Output output) {
+    if (first == last) {
+        throw Error("SDIFF: no key specified");
+    }
+
+    auto reply = command(cmd::sdiff<Input>, first, last);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+long long Redis::sdiffstore(const StringView &destination,
+                            Input first,
+                            Input last) {
+    if (first == last) {
+        throw Error("SDIFFSTORE: no key specified");
+    }
+
+    auto reply = command(cmd::sdiffstore_range<Input>, destination, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input, typename Output>
+void Redis::sinter(Input first, Input last, Output output) {
+    if (first == last) {
+        throw Error("SINTER: no key specified");
+    }
+
+    auto reply = command(cmd::sinter<Input>, first, last);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+long long Redis::sinterstore(const StringView &destination,
+                            Input first,
+                            Input last) {
+    if (first == last) {
+        throw Error("SINTERSTORE: no key specified");
+    }
+
+    auto reply = command(cmd::sinterstore_range<Input>, destination, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Output>
+void Redis::smembers(const StringView &key, Output output) {
+    auto reply = command(cmd::smembers, key);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void Redis::spop(const StringView &key, long long count, Output output) {
+    auto reply = command(cmd::spop_range, key, count);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void Redis::srandmember(const StringView &key, long long count, Output output) {
+    auto reply = command(cmd::srandmember_range, key, count);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+long long Redis::srem(const StringView &key, Input first, Input last) {
+    if (first == last) {
+        throw Error("SREM: no key specified");
+    }
+
+    auto reply = command(cmd::srem_range<Input>, key, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Output>
+long long Redis::sscan(const StringView &key,
+                        long long cursor,
+                        const StringView &pattern,
+                        long long count,
+                        Output output) {
+    auto reply = command(cmd::sscan, key, cursor, pattern, count);
+
+    return reply::parse_scan_reply(*reply, output);
+}
+
+template <typename Output>
+inline long long Redis::sscan(const StringView &key,
+                                long long cursor,
+                                const StringView &pattern,
+                                Output output) {
+    return sscan(key, cursor, pattern, 10, output);
+}
+
+template <typename Output>
+inline long long Redis::sscan(const StringView &key,
+                                long long cursor,
+                                long long count,
+                                Output output) {
+    return sscan(key, cursor, "*", count, output);
+}
+
+template <typename Output>
+inline long long Redis::sscan(const StringView &key,
+                                long long cursor,
+                                Output output) {
+    return sscan(key, cursor, "*", 10, output);
+}
+
+template <typename Input, typename Output>
+void Redis::sunion(Input first, Input last, Output output) {
+    if (first == last) {
+        throw Error("SUNION: no key specified");
+    }
+
+    auto reply = command(cmd::sunion<Input>, first, last);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+long long Redis::sunionstore(const StringView &destination, Input first, Input last) {
+    if (first == last) {
+        throw Error("SUNIONSTORE: no key specified");
+    }
+
+    auto reply = command(cmd::sunionstore_range<Input>, destination, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+// SORTED SET commands.
+
+inline auto Redis::bzpopmax(const StringView &key, const std::chrono::seconds &timeout)
+    -> Optional<std::tuple<std::string, std::string, double>> {
+    return bzpopmax(key, timeout.count());
+}
+
+template <typename Input>
+auto Redis::bzpopmax(Input first, Input last, long long timeout)
+    -> Optional<std::tuple<std::string, std::string, double>> {
+    auto reply = command(cmd::bzpopmax_range<Input>, first, last, timeout);
+
+    return reply::parse<Optional<std::tuple<std::string, std::string, double>>>(*reply);
+}
+
+template <typename Input>
+inline auto Redis::bzpopmax(Input first,
+                            Input last,
+                            const std::chrono::seconds &timeout)
+    -> Optional<std::tuple<std::string, std::string, double>> {
+    return bzpopmax(first, last, timeout.count());
+}
+
+inline auto Redis::bzpopmin(const StringView &key, const std::chrono::seconds &timeout)
+    -> Optional<std::tuple<std::string, std::string, double>> {
+    return bzpopmin(key, timeout.count());
+}
+
+template <typename Input>
+auto Redis::bzpopmin(Input first, Input last, long long timeout)
+    -> Optional<std::tuple<std::string, std::string, double>> {
+    auto reply = command(cmd::bzpopmin_range<Input>, first, last, timeout);
+
+    return reply::parse<Optional<std::tuple<std::string, std::string, double>>>(*reply);
+}
+
+template <typename Input>
+inline auto Redis::bzpopmin(Input first,
+                            Input last,
+                            const std::chrono::seconds &timeout)
+    -> Optional<std::tuple<std::string, std::string, double>> {
+    return bzpopmin(first, last, timeout.count());
+}
+
+template <typename Input>
+long long Redis::zadd(const StringView &key,
+                        Input first,
+                        Input last,
+                        UpdateType type,
+                        bool changed) {
+    if (first == last) {
+        throw Error("ZADD: no key specified");
+    }
+
+    auto reply = command(cmd::zadd_range<Input>, key, first, last, type, changed);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Interval>
+long long Redis::zcount(const StringView &key, const Interval &interval) {
+    auto reply = command(cmd::zcount<Interval>, key, interval);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input>
+long long Redis::zinterstore(const StringView &destination,
+                                Input first,
+                                Input last,
+                                Aggregation type) {
+    if (first == last) {
+        throw Error("ZINTERSTORE: no key specified");
+    }
+
+    auto reply = command(cmd::zinterstore_range<Input>,
+                            destination,
+                            first,
+                            last,
+                            type);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Interval>
+long long Redis::zlexcount(const StringView &key, const Interval &interval) {
+    auto reply = command(cmd::zlexcount<Interval>, key, interval);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Output>
+void Redis::zpopmax(const StringView &key, long long count, Output output) {
+    auto reply = command(cmd::zpopmax, key, count);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void Redis::zpopmin(const StringView &key, long long count, Output output) {
+    auto reply = command(cmd::zpopmin, key, count);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void Redis::zrange(const StringView &key, long long start, long long stop, Output output) {
+    auto reply = _score_command<Output>(cmd::zrange, key, start, stop);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Interval, typename Output>
+void Redis::zrangebylex(const StringView &key, const Interval &interval, Output output) {
+    zrangebylex(key, interval, {}, output);
+}
+
+template <typename Interval, typename Output>
+void Redis::zrangebylex(const StringView &key,
+                        const Interval &interval,
+                        const LimitOptions &opts,
+                        Output output) {
+    auto reply = command(cmd::zrangebylex<Interval>, key, interval, opts);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Interval, typename Output>
+void Redis::zrangebyscore(const StringView &key,
+                            const Interval &interval,
+                            Output output) {
+    zrangebyscore(key, interval, {}, output);
+}
+
+template <typename Interval, typename Output>
+void Redis::zrangebyscore(const StringView &key,
+                            const Interval &interval,
+                            const LimitOptions &opts,
+                            Output output) {
+    auto reply = _score_command<Output>(cmd::zrangebyscore<Interval>,
+                                        key,
+                                        interval,
+                                        opts);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+long long Redis::zrem(const StringView &key, Input first, Input last) {
+    if (first == last) {
+        throw Error("ZREM: no key specified");
+    }
+
+    auto reply = command(cmd::zrem_range<Input>, key, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Interval>
+long long Redis::zremrangebylex(const StringView &key, const Interval &interval) {
+    auto reply = command(cmd::zremrangebylex<Interval>, key, interval);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Interval>
+long long Redis::zremrangebyscore(const StringView &key, const Interval &interval) {
+    auto reply = command(cmd::zremrangebyscore<Interval>, key, interval);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Output>
+void Redis::zrevrange(const StringView &key, long long start, long long stop, Output output) {
+    auto reply = _score_command<Output>(cmd::zrevrange, key, start, stop);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Interval, typename Output>
+inline void Redis::zrevrangebylex(const StringView &key,
+                                    const Interval &interval,
+                                    Output output) {
+    zrevrangebylex(key, interval, {}, output);
+}
+
+template <typename Interval, typename Output>
+void Redis::zrevrangebylex(const StringView &key,
+                            const Interval &interval,
+                            const LimitOptions &opts,
+                            Output output) {
+    auto reply = command(cmd::zrevrangebylex<Interval>, key, interval, opts);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Interval, typename Output>
+void Redis::zrevrangebyscore(const StringView &key, const Interval &interval, Output output) {
+    zrevrangebyscore(key, interval, {}, output);
+}
+
+template <typename Interval, typename Output>
+void Redis::zrevrangebyscore(const StringView &key,
+                                const Interval &interval,
+                                const LimitOptions &opts,
+                                Output output) {
+    auto reply = _score_command<Output>(cmd::zrevrangebyscore<Interval>, key, interval, opts);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+long long Redis::zscan(const StringView &key,
+                        long long cursor,
+                        const StringView &pattern,
+                        long long count,
+                        Output output) {
+    auto reply = command(cmd::zscan, key, cursor, pattern, count);
+
+    return reply::parse_scan_reply(*reply, output);
+}
+
+template <typename Output>
+inline long long Redis::zscan(const StringView &key,
+                                long long cursor,
+                                const StringView &pattern,
+                                Output output) {
+    return zscan(key, cursor, pattern, 10, output);
+}
+
+template <typename Output>
+inline long long Redis::zscan(const StringView &key,
+                                long long cursor,
+                                long long count,
+                                Output output) {
+    return zscan(key, cursor, "*", count, output);
+}
+
+template <typename Output>
+inline long long Redis::zscan(const StringView &key,
+                                long long cursor,
+                                Output output) {
+    return zscan(key, cursor, "*", 10, output);
+}
+
+template <typename Input>
+long long Redis::zunionstore(const StringView &destination,
+                                    Input first,
+                                    Input last,
+                                    Aggregation type) {
+    if (first == last) {
+        throw Error("ZUNIONSTORE: no key specified");
+    }
+
+    auto reply = command(cmd::zunionstore_range<Input>,
+                            destination,
+                            first,
+                            last,
+                            type);
+
+    return reply::parse<long long>(*reply);
+}
+
+// HYPERLOGLOG commands.
+
+template <typename Input>
+bool Redis::pfadd(const StringView &key, Input first, Input last) {
+    if (first == last) {
+        throw Error("PFADD: no key specified");
+    }
+
+    auto reply = command(cmd::pfadd_range<Input>, key, first, last);
+
+    return reply::parse<bool>(*reply);
+}
+
+template <typename Input>
+long long Redis::pfcount(Input first, Input last) {
+    if (first == last) {
+        throw Error("PFCOUNT: no key specified");
+    }
+
+    auto reply = command(cmd::pfcount_range<Input>, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input>
+void Redis::pfmerge(const StringView &destination,
+                    Input first,
+                    Input last) {
+    if (first == last) {
+        throw Error("PFMERGE: no key specified");
+    }
+
+    auto reply = command(cmd::pfmerge_range<Input>, destination, first, last);
+
+    reply::parse<void>(*reply);
+}
+
+// GEO commands.
+
+template <typename Input>
+inline long long Redis::geoadd(const StringView &key,
+                                Input first,
+                                Input last) {
+    if (first == last) {
+        throw Error("GEOADD: no key specified");
+    }
+
+    auto reply = command(cmd::geoadd_range<Input>, key, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input, typename Output>
+void Redis::geohash(const StringView &key, Input first, Input last, Output output) {
+    if (first == last) {
+        throw Error("GEOHASH: no key specified");
+    }
+
+    auto reply = command(cmd::geohash_range<Input>, key, first, last);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input, typename Output>
+void Redis::geopos(const StringView &key, Input first, Input last, Output output) {
+    if (first == last) {
+        throw Error("GEOPOS: no key specified");
+    }
+
+    auto reply = command(cmd::geopos_range<Input>, key, first, last);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void Redis::georadius(const StringView &key,
+                        const std::pair<double, double> &loc,
+                        double radius,
+                        GeoUnit unit,
+                        long long count,
+                        bool asc,
+                        Output output) {
+    auto reply = command(cmd::georadius,
+                            key,
+                            loc,
+                            radius,
+                            unit,
+                            count,
+                            asc,
+                            WithCoord<typename IterType<Output>::type>::value,
+                            WithDist<typename IterType<Output>::type>::value,
+                            WithHash<typename IterType<Output>::type>::value);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void Redis::georadiusbymember(const StringView &key,
+                                const StringView &member,
+                                double radius,
+                                GeoUnit unit,
+                                long long count,
+                                bool asc,
+                                Output output) {
+    auto reply = command(cmd::georadiusbymember,
+                            key,
+                            member,
+                            radius,
+                            unit,
+                            count,
+                            asc,
+                            WithCoord<typename IterType<Output>::type>::value,
+                            WithDist<typename IterType<Output>::type>::value,
+                            WithHash<typename IterType<Output>::type>::value);
+
+    reply::to_array(*reply, output);
+}
+
+// SCRIPTING commands.
+
+template <typename Result>
+Result Redis::eval(const StringView &script,
+                    std::initializer_list<StringView> keys,
+                    std::initializer_list<StringView> args) {
+    auto reply = command(cmd::eval, script, keys, args);
+
+    return reply::parse<Result>(*reply);
+}
+
+template <typename Output>
+void Redis::eval(const StringView &script,
+                    std::initializer_list<StringView> keys,
+                    std::initializer_list<StringView> args,
+                    Output output) {
+    auto reply = command(cmd::eval, script, keys, args);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Result>
+Result Redis::evalsha(const StringView &script,
+                        std::initializer_list<StringView> keys,
+                        std::initializer_list<StringView> args) {
+    auto reply = command(cmd::evalsha, script, keys, args);
+
+    return reply::parse<Result>(*reply);
+}
+
+template <typename Output>
+void Redis::evalsha(const StringView &script,
+                        std::initializer_list<StringView> keys,
+                        std::initializer_list<StringView> args,
+                        Output output) {
+    auto reply = command(cmd::evalsha, script, keys, args);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input, typename Output>
+void Redis::script_exists(Input first, Input last, Output output) {
+    if (first == last) {
+        throw Error("SCRIPT EXISTS: no key specified");
+    }
+
+    auto reply = command(cmd::script_exists_range<Input>, first, last);
+
+    reply::to_array(*reply, output);
+}
+
+// Transaction commands.
+
+template <typename Input>
+void Redis::watch(Input first, Input last) {
+    auto reply = command(cmd::watch_range<Input>, first, last);
+
+    reply::parse<void>(*reply);
+}
+
+// Stream commands.
+
+template <typename Input>
+long long Redis::xack(const StringView &key, const StringView &group, Input first, Input last) {
+    auto reply = command(cmd::xack_range<Input>, key, group, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input>
+std::string Redis::xadd(const StringView &key, const StringView &id, Input first, Input last) {
+    auto reply = command(cmd::xadd_range<Input>, key, id, first, last);
+
+    return reply::parse<std::string>(*reply);
+}
+
+template <typename Input>
+std::string Redis::xadd(const StringView &key,
+                        const StringView &id,
+                        Input first,
+                        Input last,
+                        long long count,
+                        bool approx) {
+    auto reply = command(cmd::xadd_maxlen_range<Input>, key, id, first, last, count, approx);
+
+    return reply::parse<std::string>(*reply);
+}
+
+template <typename Output>
+void Redis::xclaim(const StringView &key,
+                    const StringView &group,
+                    const StringView &consumer,
+                    const std::chrono::milliseconds &min_idle_time,
+                    const StringView &id,
+                    Output output) {
+    auto reply = command(cmd::xclaim, key, group, consumer, min_idle_time.count(), id);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input, typename Output>
+void Redis::xclaim(const StringView &key,
+                    const StringView &group,
+                    const StringView &consumer,
+                    const std::chrono::milliseconds &min_idle_time,
+                    Input first,
+                    Input last,
+                    Output output) {
+    auto reply = command(cmd::xclaim_range<Input>,
+                            key,
+                            group,
+                            consumer,
+                            min_idle_time.count(),
+                            first,
+                            last);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+long long Redis::xdel(const StringView &key, Input first, Input last) {
+    auto reply = command(cmd::xdel_range<Input>, key, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Output>
+auto Redis::xpending(const StringView &key, const StringView &group, Output output)
+    -> std::tuple<long long, OptionalString, OptionalString> {
+    auto reply = command(cmd::xpending, key, group);
+
+    return reply::parse_xpending_reply(*reply, output);
+}
+
+template <typename Output>
+void Redis::xpending(const StringView &key,
+                        const StringView &group,
+                        const StringView &start,
+                        const StringView &end,
+                        long long count,
+                        Output output) {
+    auto reply = command(cmd::xpending_detail, key, group, start, end, count);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void Redis::xpending(const StringView &key,
+                        const StringView &group,
+                        const StringView &start,
+                        const StringView &end,
+                        long long count,
+                        const StringView &consumer,
+                        Output output) {
+    auto reply = command(cmd::xpending_per_consumer, key, group, start, end, count, consumer);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void Redis::xrange(const StringView &key,
+                    const StringView &start,
+                    const StringView &end,
+                    Output output) {
+    auto reply = command(cmd::xrange, key, start, end);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void Redis::xrange(const StringView &key,
+                    const StringView &start,
+                    const StringView &end,
+                    long long count,
+                    Output output) {
+    auto reply = command(cmd::xrange_count, key, start, end, count);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void Redis::xread(const StringView &key,
+                    const StringView &id,
+                    long long count,
+                    Output output) {
+    auto reply = command(cmd::xread, key, id, count);
+
+    if (!reply::is_nil(*reply)) {
+        reply::to_array(*reply, output);
+    }
+}
+
+template <typename Input, typename Output>
+auto Redis::xread(Input first, Input last, long long count, Output output)
+    -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+    if (first == last) {
+        throw Error("XREAD: no key specified");
+    }
+
+    auto reply = command(cmd::xread_range<Input>, first, last, count);
+
+    if (!reply::is_nil(*reply)) {
+        reply::to_array(*reply, output);
+    }
+}
+
+template <typename Output>
+void Redis::xread(const StringView &key,
+                    const StringView &id,
+                    const std::chrono::milliseconds &timeout,
+                    long long count,
+                    Output output) {
+    auto reply = command(cmd::xread_block, key, id, timeout.count(), count);
+
+    if (!reply::is_nil(*reply)) {
+        reply::to_array(*reply, output);
+    }
+}
+
+template <typename Input, typename Output>
+auto Redis::xread(Input first,
+                    Input last,
+                    const std::chrono::milliseconds &timeout,
+                    long long count,
+                    Output output)
+    -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+    if (first == last) {
+        throw Error("XREAD: no key specified");
+    }
+
+    auto reply = command(cmd::xread_block_range<Input>, first, last, timeout.count(), count);
+
+    if (!reply::is_nil(*reply)) {
+        reply::to_array(*reply, output);
+    }
+}
+
+template <typename Output>
+void Redis::xreadgroup(const StringView &group,
+                        const StringView &consumer,
+                        const StringView &key,
+                        const StringView &id,
+                        long long count,
+                        bool noack,
+                        Output output) {
+    auto reply = command(cmd::xreadgroup, group, consumer, key, id, count, noack);
+
+    if (!reply::is_nil(*reply)) {
+        reply::to_array(*reply, output);
+    }
+}
+
+template <typename Input, typename Output>
+auto Redis::xreadgroup(const StringView &group,
+                        const StringView &consumer,
+                        Input first,
+                        Input last,
+                        long long count,
+                        bool noack,
+                        Output output)
+    -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+    if (first == last) {
+        throw Error("XREADGROUP: no key specified");
+    }
+
+    auto reply = command(cmd::xreadgroup_range<Input>, group, consumer, first, last, count, noack);
+
+    if (!reply::is_nil(*reply)) {
+        reply::to_array(*reply, output);
+    }
+}
+
+template <typename Output>
+void Redis::xreadgroup(const StringView &group,
+                        const StringView &consumer,
+                        const StringView &key,
+                        const StringView &id,
+                        const std::chrono::milliseconds &timeout,
+                        long long count,
+                        bool noack,
+                        Output output) {
+    auto reply = command(cmd::xreadgroup_block,
+                            group,
+                            consumer,
+                            key,
+                            id,
+                            timeout.count(),
+                            count,
+                            noack);
+
+    if (!reply::is_nil(*reply)) {
+        reply::to_array(*reply, output);
+    }
+}
+
+template <typename Input, typename Output>
+auto Redis::xreadgroup(const StringView &group,
+                        const StringView &consumer,
+                        Input first,
+                        Input last,
+                        const std::chrono::milliseconds &timeout,
+                        long long count,
+                        bool noack,
+                        Output output)
+    -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+    if (first == last) {
+        throw Error("XREADGROUP: no key specified");
+    }
+
+    auto reply = command(cmd::xreadgroup_block_range<Input>,
+                            group,
+                            consumer,
+                            first,
+                            last,
+                            timeout.count(),
+                            count,
+                            noack);
+
+    if (!reply::is_nil(*reply)) {
+        reply::to_array(*reply, output);
+    }
+}
+
+template <typename Output>
+void Redis::xrevrange(const StringView &key,
+                        const StringView &end,
+                        const StringView &start,
+                        Output output) {
+    auto reply = command(cmd::xrevrange, key, end, start);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void Redis::xrevrange(const StringView &key,
+                        const StringView &end,
+                        const StringView &start,
+                        long long count,
+                        Output output) {
+    auto reply = command(cmd::xrevrange_count, key, end, start, count);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Cmd, typename ...Args>
+ReplyUPtr Redis::_command(Connection &connection, Cmd cmd, Args &&...args) {
+    assert(!connection.broken());
+
+    cmd(connection, std::forward<Args>(args)...);
+
+    auto reply = connection.recv();
+
+    return reply;
+}
+
+template <typename Cmd, typename ...Args>
+inline ReplyUPtr Redis::_score_command(std::true_type, Cmd cmd, Args &&... args) {
+    return command(cmd, std::forward<Args>(args)..., true);
+}
+
+template <typename Cmd, typename ...Args>
+inline ReplyUPtr Redis::_score_command(std::false_type, Cmd cmd, Args &&... args) {
+    return command(cmd, std::forward<Args>(args)..., false);
+}
+
+template <typename Output, typename Cmd, typename ...Args>
+inline ReplyUPtr Redis::_score_command(Cmd cmd, Args &&... args) {
+    return _score_command(typename IsKvPairIter<Output>::type(),
+                            cmd,
+                            std::forward<Args>(args)...);
+}
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_REDIS_HPP

+ 769 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/redis_cluster.cpp

@@ -0,0 +1,769 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#include "redis_cluster.h"
+#include <hiredis/hiredis.h>
+#include "command.h"
+#include "errors.h"
+#include "queued_redis.h"
+
+namespace sw {
+
+namespace redis {
+
+RedisCluster::RedisCluster(const std::string &uri) : RedisCluster(ConnectionOptions(uri)) {}
+
+Redis RedisCluster::redis(const StringView &hash_tag) {
+    auto opts = _pool.connection_options(hash_tag);
+    return Redis(std::make_shared<Connection>(opts));
+}
+
+Pipeline RedisCluster::pipeline(const StringView &hash_tag) {
+    auto opts = _pool.connection_options(hash_tag);
+    return Pipeline(std::make_shared<Connection>(opts));
+}
+
+Transaction RedisCluster::transaction(const StringView &hash_tag, bool piped) {
+    auto opts = _pool.connection_options(hash_tag);
+    return Transaction(std::make_shared<Connection>(opts), piped);
+}
+
+Subscriber RedisCluster::subscriber() {
+    auto opts = _pool.connection_options();
+    return Subscriber(Connection(opts));
+}
+
+// KEY commands.
+
+long long RedisCluster::del(const StringView &key) {
+    auto reply = command(cmd::del, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+OptionalString RedisCluster::dump(const StringView &key) {
+    auto reply = command(cmd::dump, key);
+
+    return reply::parse<OptionalString>(*reply);
+}
+
+long long RedisCluster::exists(const StringView &key) {
+    auto reply = command(cmd::exists, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+bool RedisCluster::expire(const StringView &key, long long timeout) {
+    auto reply = command(cmd::expire, key, timeout);
+
+    return reply::parse<bool>(*reply);
+}
+
+bool RedisCluster::expireat(const StringView &key, long long timestamp) {
+    auto reply = command(cmd::expireat, key, timestamp);
+
+    return reply::parse<bool>(*reply);
+}
+
+bool RedisCluster::persist(const StringView &key) {
+    auto reply = command(cmd::persist, key);
+
+    return reply::parse<bool>(*reply);
+}
+
+bool RedisCluster::pexpire(const StringView &key, long long timeout) {
+    auto reply = command(cmd::pexpire, key, timeout);
+
+    return reply::parse<bool>(*reply);
+}
+
+bool RedisCluster::pexpireat(const StringView &key, long long timestamp) {
+    auto reply = command(cmd::pexpireat, key, timestamp);
+
+    return reply::parse<bool>(*reply);
+}
+
+long long RedisCluster::pttl(const StringView &key) {
+    auto reply = command(cmd::pttl, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+void RedisCluster::rename(const StringView &key, const StringView &newkey) {
+    auto reply = command(cmd::rename, key, newkey);
+
+    reply::parse<void>(*reply);
+}
+
+bool RedisCluster::renamenx(const StringView &key, const StringView &newkey) {
+    auto reply = command(cmd::renamenx, key, newkey);
+
+    return reply::parse<bool>(*reply);
+}
+
+void RedisCluster::restore(const StringView &key,
+                    const StringView &val,
+                    long long ttl,
+                    bool replace) {
+    auto reply = command(cmd::restore, key, val, ttl, replace);
+
+    reply::parse<void>(*reply);
+}
+
+long long RedisCluster::touch(const StringView &key) {
+    auto reply = command(cmd::touch, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long RedisCluster::ttl(const StringView &key) {
+    auto reply = command(cmd::ttl, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+std::string RedisCluster::type(const StringView &key) {
+    auto reply = command(cmd::type, key);
+
+    return reply::parse<std::string>(*reply);
+}
+
+long long RedisCluster::unlink(const StringView &key) {
+    auto reply = command(cmd::unlink, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+// STRING commands.
+
+long long RedisCluster::append(const StringView &key, const StringView &val) {
+    auto reply = command(cmd::append, key, val);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long RedisCluster::bitcount(const StringView &key, long long start, long long end) {
+    auto reply = command(cmd::bitcount, key, start, end);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long RedisCluster::bitop(BitOp op, const StringView &destination, const StringView &key) {
+    auto reply = _command(cmd::bitop, destination, op, destination, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long RedisCluster::bitpos(const StringView &key,
+                            long long bit,
+                            long long start,
+                            long long end) {
+    auto reply = command(cmd::bitpos, key, bit, start, end);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long RedisCluster::decr(const StringView &key) {
+    auto reply = command(cmd::decr, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long RedisCluster::decrby(const StringView &key, long long decrement) {
+    auto reply = command(cmd::decrby, key, decrement);
+
+    return reply::parse<long long>(*reply);
+}
+
+OptionalString RedisCluster::get(const StringView &key) {
+    auto reply = command(cmd::get, key);
+
+    return reply::parse<OptionalString>(*reply);
+}
+
+long long RedisCluster::getbit(const StringView &key, long long offset) {
+    auto reply = command(cmd::getbit, key, offset);
+
+    return reply::parse<long long>(*reply);
+}
+
+std::string RedisCluster::getrange(const StringView &key, long long start, long long end) {
+    auto reply = command(cmd::getrange, key, start, end);
+
+    return reply::parse<std::string>(*reply);
+}
+
+OptionalString RedisCluster::getset(const StringView &key, const StringView &val) {
+    auto reply = command(cmd::getset, key, val);
+
+    return reply::parse<OptionalString>(*reply);
+}
+
+long long RedisCluster::incr(const StringView &key) {
+    auto reply = command(cmd::incr, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long RedisCluster::incrby(const StringView &key, long long increment) {
+    auto reply = command(cmd::incrby, key, increment);
+
+    return reply::parse<long long>(*reply);
+}
+
+double RedisCluster::incrbyfloat(const StringView &key, double increment) {
+    auto reply = command(cmd::incrbyfloat, key, increment);
+
+    return reply::parse<double>(*reply);
+}
+
+void RedisCluster::psetex(const StringView &key,
+                        long long ttl,
+                        const StringView &val) {
+    auto reply = command(cmd::psetex, key, ttl, val);
+
+    reply::parse<void>(*reply);
+}
+
+bool RedisCluster::set(const StringView &key,
+                    const StringView &val,
+                    const std::chrono::milliseconds &ttl,
+                    UpdateType type) {
+    auto reply = command(cmd::set, key, val, ttl.count(), type);
+
+    reply::rewrite_set_reply(*reply);
+
+    return reply::parse<bool>(*reply);
+}
+
+void RedisCluster::setex(const StringView &key,
+                    long long ttl,
+                    const StringView &val) {
+    auto reply = command(cmd::setex, key, ttl, val);
+
+    reply::parse<void>(*reply);
+}
+
+bool RedisCluster::setnx(const StringView &key, const StringView &val) {
+    auto reply = command(cmd::setnx, key, val);
+
+    return reply::parse<bool>(*reply);
+}
+
+long long RedisCluster::setrange(const StringView &key, long long offset, const StringView &val) {
+    auto reply = command(cmd::setrange, key, offset, val);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long RedisCluster::strlen(const StringView &key) {
+    auto reply = command(cmd::strlen, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+// LIST commands.
+
+OptionalStringPair RedisCluster::blpop(const StringView &key, long long timeout) {
+    auto reply = command(cmd::blpop, key, timeout);
+
+    return reply::parse<OptionalStringPair>(*reply);
+}
+
+OptionalStringPair RedisCluster::blpop(const StringView &key, const std::chrono::seconds &timeout) {
+    return blpop(key, timeout.count());
+}
+
+OptionalStringPair RedisCluster::brpop(const StringView &key, long long timeout) {
+    auto reply = command(cmd::brpop, key, timeout);
+
+    return reply::parse<OptionalStringPair>(*reply);
+}
+
+OptionalStringPair RedisCluster::brpop(const StringView &key, const std::chrono::seconds &timeout) {
+    return brpop(key, timeout.count());
+}
+
+OptionalString RedisCluster::brpoplpush(const StringView &source,
+                                    const StringView &destination,
+                                    long long timeout) {
+    auto reply = command(cmd::brpoplpush, source, destination, timeout);
+
+    return reply::parse<OptionalString>(*reply);
+}
+
+OptionalString RedisCluster::lindex(const StringView &key, long long index) {
+    auto reply = command(cmd::lindex, key, index);
+
+    return reply::parse<OptionalString>(*reply);
+}
+
+long long RedisCluster::linsert(const StringView &key,
+                            InsertPosition position,
+                            const StringView &pivot,
+                            const StringView &val) {
+    auto reply = command(cmd::linsert, key, position, pivot, val);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long RedisCluster::llen(const StringView &key) {
+    auto reply = command(cmd::llen, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+OptionalString RedisCluster::lpop(const StringView &key) {
+    auto reply = command(cmd::lpop, key);
+
+    return reply::parse<OptionalString>(*reply);
+}
+
+long long RedisCluster::lpush(const StringView &key, const StringView &val) {
+    auto reply = command(cmd::lpush, key, val);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long RedisCluster::lpushx(const StringView &key, const StringView &val) {
+    auto reply = command(cmd::lpushx, key, val);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long RedisCluster::lrem(const StringView &key, long long count, const StringView &val) {
+    auto reply = command(cmd::lrem, key, count, val);
+
+    return reply::parse<long long>(*reply);
+}
+
+void RedisCluster::lset(const StringView &key, long long index, const StringView &val) {
+    auto reply = command(cmd::lset, key, index, val);
+
+    reply::parse<void>(*reply);
+}
+
+void RedisCluster::ltrim(const StringView &key, long long start, long long stop) {
+    auto reply = command(cmd::ltrim, key, start, stop);
+
+    reply::parse<void>(*reply);
+}
+
+OptionalString RedisCluster::rpop(const StringView &key) {
+    auto reply = command(cmd::rpop, key);
+
+    return reply::parse<OptionalString>(*reply);
+}
+
+OptionalString RedisCluster::rpoplpush(const StringView &source, const StringView &destination) {
+    auto reply = command(cmd::rpoplpush, source, destination);
+
+    return reply::parse<OptionalString>(*reply);
+}
+
+long long RedisCluster::rpush(const StringView &key, const StringView &val) {
+    auto reply = command(cmd::rpush, key, val);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long RedisCluster::rpushx(const StringView &key, const StringView &val) {
+    auto reply = command(cmd::rpushx, key, val);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long RedisCluster::hdel(const StringView &key, const StringView &field) {
+    auto reply = command(cmd::hdel, key, field);
+
+    return reply::parse<long long>(*reply);
+}
+
+bool RedisCluster::hexists(const StringView &key, const StringView &field) {
+    auto reply = command(cmd::hexists, key, field);
+
+    return reply::parse<bool>(*reply);
+}
+
+OptionalString RedisCluster::hget(const StringView &key, const StringView &field) {
+    auto reply = command(cmd::hget, key, field);
+
+    return reply::parse<OptionalString>(*reply);
+}
+
+long long RedisCluster::hincrby(const StringView &key, const StringView &field, long long increment) {
+    auto reply = command(cmd::hincrby, key, field, increment);
+
+    return reply::parse<long long>(*reply);
+}
+
+double RedisCluster::hincrbyfloat(const StringView &key, const StringView &field, double increment) {
+    auto reply = command(cmd::hincrbyfloat, key, field, increment);
+
+    return reply::parse<double>(*reply);
+}
+
+long long RedisCluster::hlen(const StringView &key) {
+    auto reply = command(cmd::hlen, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+bool RedisCluster::hset(const StringView &key, const StringView &field, const StringView &val) {
+    auto reply = command(cmd::hset, key, field, val);
+
+    return reply::parse<bool>(*reply);
+}
+
+bool RedisCluster::hset(const StringView &key, const std::pair<StringView, StringView> &item) {
+    return hset(key, item.first, item.second);
+}
+
+bool RedisCluster::hsetnx(const StringView &key, const StringView &field, const StringView &val) {
+    auto reply = command(cmd::hsetnx, key, field, val);
+
+    return reply::parse<bool>(*reply);
+}
+
+bool RedisCluster::hsetnx(const StringView &key, const std::pair<StringView, StringView> &item) {
+    return hsetnx(key, item.first, item.second);
+}
+
+long long RedisCluster::hstrlen(const StringView &key, const StringView &field) {
+    auto reply = command(cmd::hstrlen, key, field);
+
+    return reply::parse<long long>(*reply);
+}
+
+// SET commands.
+
+long long RedisCluster::sadd(const StringView &key, const StringView &member) {
+    auto reply = command(cmd::sadd, key, member);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long RedisCluster::scard(const StringView &key) {
+    auto reply = command(cmd::scard, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long RedisCluster::sdiffstore(const StringView &destination, const StringView &key) {
+    auto reply = command(cmd::sdiffstore, destination, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long RedisCluster::sinterstore(const StringView &destination, const StringView &key) {
+    auto reply = command(cmd::sinterstore, destination, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+bool RedisCluster::sismember(const StringView &key, const StringView &member) {
+    auto reply = command(cmd::sismember, key, member);
+
+    return reply::parse<bool>(*reply);
+}
+
+bool RedisCluster::smove(const StringView &source,
+                    const StringView &destination,
+                    const StringView &member) {
+    auto reply = command(cmd::smove, source, destination, member);
+
+    return reply::parse<bool>(*reply);
+}
+
+OptionalString RedisCluster::spop(const StringView &key) {
+    auto reply = command(cmd::spop, key);
+
+    return reply::parse<OptionalString>(*reply);
+}
+
+OptionalString RedisCluster::srandmember(const StringView &key) {
+    auto reply = command(cmd::srandmember, key);
+
+    return reply::parse<OptionalString>(*reply);
+}
+
+long long RedisCluster::srem(const StringView &key, const StringView &member) {
+    auto reply = command(cmd::srem, key, member);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long RedisCluster::sunionstore(const StringView &destination, const StringView &key) {
+    auto reply = command(cmd::sunionstore, destination, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+// SORTED SET commands.
+
+auto RedisCluster::bzpopmax(const StringView &key, long long timeout)
+    -> Optional<std::tuple<std::string, std::string, double>> {
+    auto reply = command(cmd::bzpopmax, key, timeout);
+
+    return reply::parse<Optional<std::tuple<std::string, std::string, double>>>(*reply);
+}
+
+auto RedisCluster::bzpopmin(const StringView &key, long long timeout)
+    -> Optional<std::tuple<std::string, std::string, double>> {
+    auto reply = command(cmd::bzpopmin, key, timeout);
+
+    return reply::parse<Optional<std::tuple<std::string, std::string, double>>>(*reply);
+}
+
+long long RedisCluster::zadd(const StringView &key,
+                        const StringView &member,
+                        double score,
+                        UpdateType type,
+                        bool changed) {
+    auto reply = command(cmd::zadd, key, member, score, type, changed);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long RedisCluster::zcard(const StringView &key) {
+    auto reply = command(cmd::zcard, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+double RedisCluster::zincrby(const StringView &key, double increment, const StringView &member) {
+    auto reply = command(cmd::zincrby, key, increment, member);
+
+    return reply::parse<double>(*reply);
+}
+
+long long RedisCluster::zinterstore(const StringView &destination,
+                                    const StringView &key,
+                                    double weight) {
+    auto reply = command(cmd::zinterstore, destination, key, weight);
+
+    return reply::parse<long long>(*reply);
+}
+
+Optional<std::pair<std::string, double>> RedisCluster::zpopmax(const StringView &key) {
+    auto reply = command(cmd::zpopmax, key, 1);
+
+    return reply::parse<Optional<std::pair<std::string, double>>>(*reply);
+}
+
+Optional<std::pair<std::string, double>> RedisCluster::zpopmin(const StringView &key) {
+    auto reply = command(cmd::zpopmin, key, 1);
+
+    return reply::parse<Optional<std::pair<std::string, double>>>(*reply);
+}
+
+OptionalLongLong RedisCluster::zrank(const StringView &key, const StringView &member) {
+    auto reply = command(cmd::zrank, key, member);
+
+    return reply::parse<OptionalLongLong>(*reply);
+}
+
+long long RedisCluster::zrem(const StringView &key, const StringView &member) {
+    auto reply = command(cmd::zrem, key, member);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long RedisCluster::zremrangebyrank(const StringView &key, long long start, long long stop) {
+    auto reply = command(cmd::zremrangebyrank, key, start, stop);
+
+    return reply::parse<long long>(*reply);
+}
+
+OptionalLongLong RedisCluster::zrevrank(const StringView &key, const StringView &member) {
+    auto reply = command(cmd::zrevrank, key, member);
+
+    return reply::parse<OptionalLongLong>(*reply);
+}
+
+OptionalDouble RedisCluster::zscore(const StringView &key, const StringView &member) {
+    auto reply = command(cmd::zscore, key, member);
+
+    return reply::parse<OptionalDouble>(*reply);
+}
+
+long long RedisCluster::zunionstore(const StringView &destination,
+                                    const StringView &key,
+                                    double weight) {
+    auto reply = command(cmd::zunionstore, destination, key, weight);
+
+    return reply::parse<long long>(*reply);
+}
+
+// HYPERLOGLOG commands.
+
+bool RedisCluster::pfadd(const StringView &key, const StringView &element) {
+    auto reply = command(cmd::pfadd, key, element);
+
+    return reply::parse<bool>(*reply);
+}
+
+long long RedisCluster::pfcount(const StringView &key) {
+    auto reply = command(cmd::pfcount, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+void RedisCluster::pfmerge(const StringView &destination, const StringView &key) {
+    auto reply = command(cmd::pfmerge, destination, key);
+
+    reply::parse<void>(*reply);
+}
+
+// GEO commands.
+
+long long RedisCluster::geoadd(const StringView &key,
+                        const std::tuple<StringView, double, double> &member) {
+    auto reply = command(cmd::geoadd, key, member);
+
+    return reply::parse<long long>(*reply);
+}
+
+OptionalDouble RedisCluster::geodist(const StringView &key,
+                                const StringView &member1,
+                                const StringView &member2,
+                                GeoUnit unit) {
+    auto reply = command(cmd::geodist, key, member1, member2, unit);
+
+    return reply::parse<OptionalDouble>(*reply);
+}
+
+OptionalLongLong RedisCluster::georadius(const StringView &key,
+                                    const std::pair<double, double> &loc,
+                                    double radius,
+                                    GeoUnit unit,
+                                    const StringView &destination,
+                                    bool store_dist,
+                                    long long count) {
+    auto reply = command(cmd::georadius_store,
+                            key,
+                            loc,
+                            radius,
+                            unit,
+                            destination,
+                            store_dist,
+                            count);
+
+    reply::rewrite_georadius_reply(*reply);
+
+    return reply::parse<OptionalLongLong>(*reply);
+}
+
+OptionalLongLong RedisCluster::georadiusbymember(const StringView &key,
+                                            const StringView &member,
+                                            double radius,
+                                            GeoUnit unit,
+                                            const StringView &destination,
+                                            bool store_dist,
+                                            long long count) {
+    auto reply = command(cmd::georadiusbymember_store,
+                            key,
+                            member,
+                            radius,
+                            unit,
+                            destination,
+                            store_dist,
+                            count);
+
+    reply::rewrite_georadius_reply(*reply);
+
+    return reply::parse<OptionalLongLong>(*reply);
+}
+
+// PUBSUB commands.
+
+long long RedisCluster::publish(const StringView &channel, const StringView &message) {
+    auto reply = command(cmd::publish, channel, message);
+
+    return reply::parse<long long>(*reply);
+}
+
+// Stream commands.
+
+long long RedisCluster::xack(const StringView &key, const StringView &group, const StringView &id) {
+    auto reply = command(cmd::xack, key, group, id);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long RedisCluster::xdel(const StringView &key, const StringView &id) {
+    auto reply = command(cmd::xdel, key, id);
+
+    return reply::parse<long long>(*reply);
+}
+
+void RedisCluster::xgroup_create(const StringView &key,
+                                    const StringView &group,
+                                    const StringView &id,
+                                    bool mkstream) {
+    auto reply = command(cmd::xgroup_create, key, group, id, mkstream);
+
+    reply::parse<void>(*reply);
+}
+
+void RedisCluster::xgroup_setid(const StringView &key,
+                                const StringView &group,
+                                const StringView &id) {
+    auto reply = command(cmd::xgroup_setid, key, group, id);
+
+    reply::parse<void>(*reply);
+}
+
+long long RedisCluster::xgroup_destroy(const StringView &key, const StringView &group) {
+    auto reply = command(cmd::xgroup_destroy, key, group);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long RedisCluster::xgroup_delconsumer(const StringView &key,
+                                            const StringView &group,
+                                            const StringView &consumer) {
+    auto reply = command(cmd::xgroup_delconsumer, key, group, consumer);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long RedisCluster::xlen(const StringView &key) {
+    auto reply = command(cmd::xlen, key);
+
+    return reply::parse<long long>(*reply);
+}
+
+long long RedisCluster::xtrim(const StringView &key, long long count, bool approx) {
+    auto reply = command(cmd::xtrim, key, count, approx);
+
+    return reply::parse<long long>(*reply);
+}
+
+void RedisCluster::_asking(Connection &connection) {
+    // Send ASKING command.
+    connection.send("ASKING");
+
+    auto reply = connection.recv();
+
+    assert(reply);
+
+    reply::parse<void>(*reply);
+}
+
+}
+
+}

+ 1395 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/redis_cluster.h

@@ -0,0 +1,1395 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_REDIS_CLUSTER_H
+#define SEWENEW_REDISPLUSPLUS_REDIS_CLUSTER_H
+
+#include <string>
+#include <chrono>
+#include <initializer_list>
+#include <tuple>
+#include "shards_pool.h"
+#include "reply.h"
+#include "command_options.h"
+#include "utils.h"
+#include "subscriber.h"
+#include "pipeline.h"
+#include "transaction.h"
+#include "redis.h"
+
+namespace sw {
+
+namespace redis {
+
+template <typename Impl>
+class QueuedRedis;
+
+using Transaction = QueuedRedis<TransactionImpl>;
+
+using Pipeline = QueuedRedis<PipelineImpl>;
+
+class RedisCluster {
+public:
+    RedisCluster(const ConnectionOptions &connection_opts,
+                    const ConnectionPoolOptions &pool_opts = {}) :
+                        _pool(pool_opts, connection_opts) {}
+
+    // Construct RedisCluster with URI:
+    // "tcp://127.0.0.1" or "tcp://127.0.0.1:6379"
+    // Only need to specify one URI.
+    explicit RedisCluster(const std::string &uri);
+
+    RedisCluster(const RedisCluster &) = delete;
+    RedisCluster& operator=(const RedisCluster &) = delete;
+
+    RedisCluster(RedisCluster &&) = default;
+    RedisCluster& operator=(RedisCluster &&) = default;
+
+    Redis redis(const StringView &hash_tag);
+
+    Pipeline pipeline(const StringView &hash_tag);
+
+    Transaction transaction(const StringView &hash_tag, bool piped = false);
+
+    Subscriber subscriber();
+
+    template <typename Cmd, typename Key, typename ...Args>
+    auto command(Cmd cmd, Key &&key, Args &&...args)
+        -> typename std::enable_if<!std::is_convertible<Cmd, StringView>::value, ReplyUPtr>::type;
+
+    template <typename Key, typename ...Args>
+    auto command(const StringView &cmd_name, Key &&key, Args &&...args)
+        -> typename std::enable_if<(std::is_convertible<Key, StringView>::value
+                || std::is_arithmetic<typename std::decay<Key>::type>::value)
+                && !IsIter<typename LastType<Key, Args...>::type>::value, ReplyUPtr>::type;
+
+    template <typename Key, typename ...Args>
+    auto command(const StringView &cmd_name, Key &&key, Args &&...args)
+        -> typename std::enable_if<(std::is_convertible<Key, StringView>::value
+                || std::is_arithmetic<typename std::decay<Key>::type>::value)
+                && IsIter<typename LastType<Key, Args...>::type>::value, void>::type;
+
+    template <typename Result, typename Key, typename ...Args>
+    auto command(const StringView &cmd_name, Key &&key, Args &&...args)
+        -> typename std::enable_if<std::is_convertible<Key, StringView>::value
+                || std::is_arithmetic<typename std::decay<Key>::type>::value, Result>::type;
+
+    template <typename Input>
+    auto command(Input first, Input last)
+        -> typename std::enable_if<IsIter<Input>::value, ReplyUPtr>::type;
+
+    template <typename Result, typename Input>
+    auto command(Input first, Input last)
+        -> typename std::enable_if<IsIter<Input>::value, Result>::type;
+
+    template <typename Input, typename Output>
+    auto command(Input first, Input last, Output output)
+        -> typename std::enable_if<IsIter<Input>::value, void>::type;
+
+    // KEY commands.
+
+    long long del(const StringView &key);
+
+    template <typename Input>
+    long long del(Input first, Input last);
+
+    template <typename T>
+    long long del(std::initializer_list<T> il) {
+        return del(il.begin(), il.end());
+    }
+
+    OptionalString dump(const StringView &key);
+
+    long long exists(const StringView &key);
+
+    template <typename Input>
+    long long exists(Input first, Input last);
+
+    template <typename T>
+    long long exists(std::initializer_list<T> il) {
+        return exists(il.begin(), il.end());
+    }
+
+    bool expire(const StringView &key, long long timeout);
+
+    bool expire(const StringView &key, const std::chrono::seconds &timeout);
+
+    bool expireat(const StringView &key, long long timestamp);
+
+    bool expireat(const StringView &key,
+                    const std::chrono::time_point<std::chrono::system_clock,
+                                                    std::chrono::seconds> &tp);
+
+    bool persist(const StringView &key);
+
+    bool pexpire(const StringView &key, long long timeout);
+
+    bool pexpire(const StringView &key, const std::chrono::milliseconds &timeout);
+
+    bool pexpireat(const StringView &key, long long timestamp);
+
+    bool pexpireat(const StringView &key,
+                    const std::chrono::time_point<std::chrono::system_clock,
+                                                    std::chrono::milliseconds> &tp);
+
+    long long pttl(const StringView &key);
+
+    void rename(const StringView &key, const StringView &newkey);
+
+    bool renamenx(const StringView &key, const StringView &newkey);
+
+    void restore(const StringView &key,
+                    const StringView &val,
+                    long long ttl,
+                    bool replace = false);
+
+    void restore(const StringView &key,
+                    const StringView &val,
+                    const std::chrono::milliseconds &ttl = std::chrono::milliseconds{0},
+                    bool replace = false);
+
+    // TODO: sort
+
+    long long touch(const StringView &key);
+
+    template <typename Input>
+    long long touch(Input first, Input last);
+
+    template <typename T>
+    long long touch(std::initializer_list<T> il) {
+        return touch(il.begin(), il.end());
+    }
+
+    long long ttl(const StringView &key);
+
+    std::string type(const StringView &key);
+
+    long long unlink(const StringView &key);
+
+    template <typename Input>
+    long long unlink(Input first, Input last);
+
+    template <typename T>
+    long long unlink(std::initializer_list<T> il) {
+        return unlink(il.begin(), il.end());
+    }
+
+    // STRING commands.
+
+    long long append(const StringView &key, const StringView &str);
+
+    long long bitcount(const StringView &key, long long start = 0, long long end = -1);
+
+    long long bitop(BitOp op, const StringView &destination, const StringView &key);
+
+    template <typename Input>
+    long long bitop(BitOp op, const StringView &destination, Input first, Input last);
+
+    template <typename T>
+    long long bitop(BitOp op, const StringView &destination, std::initializer_list<T> il) {
+        return bitop(op, destination, il.begin(), il.end());
+    }
+
+    long long bitpos(const StringView &key,
+                        long long bit,
+                        long long start = 0,
+                        long long end = -1);
+
+    long long decr(const StringView &key);
+
+    long long decrby(const StringView &key, long long decrement);
+
+    OptionalString get(const StringView &key);
+
+    long long getbit(const StringView &key, long long offset);
+
+    std::string getrange(const StringView &key, long long start, long long end);
+
+    OptionalString getset(const StringView &key, const StringView &val);
+
+    long long incr(const StringView &key);
+
+    long long incrby(const StringView &key, long long increment);
+
+    double incrbyfloat(const StringView &key, double increment);
+
+    template <typename Input, typename Output>
+    void mget(Input first, Input last, Output output);
+
+    template <typename T, typename Output>
+    void mget(std::initializer_list<T> il, Output output) {
+        mget(il.begin(), il.end(), output);
+    }
+
+    template <typename Input>
+    void mset(Input first, Input last);
+
+    template <typename T>
+    void mset(std::initializer_list<T> il) {
+        mset(il.begin(), il.end());
+    }
+
+    template <typename Input>
+    bool msetnx(Input first, Input last);
+
+    template <typename T>
+    bool msetnx(std::initializer_list<T> il) {
+        return msetnx(il.begin(), il.end());
+    }
+
+    void psetex(const StringView &key,
+                long long ttl,
+                const StringView &val);
+
+    void psetex(const StringView &key,
+                const std::chrono::milliseconds &ttl,
+                const StringView &val);
+
+    bool set(const StringView &key,
+                const StringView &val,
+                const std::chrono::milliseconds &ttl = std::chrono::milliseconds(0),
+                UpdateType type = UpdateType::ALWAYS);
+
+    void setex(const StringView &key,
+                long long ttl,
+                const StringView &val);
+
+    void setex(const StringView &key,
+                const std::chrono::seconds &ttl,
+                const StringView &val);
+
+    bool setnx(const StringView &key, const StringView &val);
+
+    long long setrange(const StringView &key, long long offset, const StringView &val);
+
+    long long strlen(const StringView &key);
+
+    // LIST commands.
+
+    OptionalStringPair blpop(const StringView &key, long long timeout);
+
+    OptionalStringPair blpop(const StringView &key,
+                                const std::chrono::seconds &timeout = std::chrono::seconds{0});
+
+    template <typename Input>
+    OptionalStringPair blpop(Input first, Input last, long long timeout);
+
+    template <typename T>
+    OptionalStringPair blpop(std::initializer_list<T> il, long long timeout) {
+        return blpop(il.begin(), il.end(), timeout);
+    }
+
+    template <typename Input>
+    OptionalStringPair blpop(Input first,
+                                Input last,
+                                const std::chrono::seconds &timeout = std::chrono::seconds{0});
+
+    template <typename T>
+    OptionalStringPair blpop(std::initializer_list<T> il,
+                                const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
+        return blpop(il.begin(), il.end(), timeout);
+    }
+
+    OptionalStringPair brpop(const StringView &key, long long timeout);
+
+    OptionalStringPair brpop(const StringView &key,
+                                const std::chrono::seconds &timeout = std::chrono::seconds{0});
+
+    template <typename Input>
+    OptionalStringPair brpop(Input first, Input last, long long timeout);
+
+    template <typename T>
+    OptionalStringPair brpop(std::initializer_list<T> il, long long timeout) {
+        return brpop(il.begin(), il.end(), timeout);
+    }
+
+    template <typename Input>
+    OptionalStringPair brpop(Input first,
+                                Input last,
+                                const std::chrono::seconds &timeout = std::chrono::seconds{0});
+
+    template <typename T>
+    OptionalStringPair brpop(std::initializer_list<T> il,
+                                const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
+        return brpop(il.begin(), il.end(), timeout);
+    }
+
+    OptionalString brpoplpush(const StringView &source,
+                                const StringView &destination,
+                                long long timeout);
+
+    OptionalString brpoplpush(const StringView &source,
+                                const StringView &destination,
+                                const std::chrono::seconds &timeout = std::chrono::seconds{0});
+
+    OptionalString lindex(const StringView &key, long long index);
+
+    long long linsert(const StringView &key,
+                        InsertPosition position,
+                        const StringView &pivot,
+                        const StringView &val);
+
+    long long llen(const StringView &key);
+
+    OptionalString lpop(const StringView &key);
+
+    long long lpush(const StringView &key, const StringView &val);
+
+    template <typename Input>
+    long long lpush(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    long long lpush(const StringView &key, std::initializer_list<T> il) {
+        return lpush(key, il.begin(), il.end());
+    }
+
+    long long lpushx(const StringView &key, const StringView &val);
+
+    template <typename Output>
+    void lrange(const StringView &key, long long start, long long stop, Output output);
+
+    long long lrem(const StringView &key, long long count, const StringView &val);
+
+    void lset(const StringView &key, long long index, const StringView &val);
+
+    void ltrim(const StringView &key, long long start, long long stop);
+
+    OptionalString rpop(const StringView &key);
+
+    OptionalString rpoplpush(const StringView &source, const StringView &destination);
+
+    long long rpush(const StringView &key, const StringView &val);
+
+    template <typename Input>
+    long long rpush(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    long long rpush(const StringView &key, std::initializer_list<T> il) {
+        return rpush(key, il.begin(), il.end());
+    }
+
+    long long rpushx(const StringView &key, const StringView &val);
+
+    // HASH commands.
+
+    long long hdel(const StringView &key, const StringView &field);
+
+    template <typename Input>
+    long long hdel(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    long long hdel(const StringView &key, std::initializer_list<T> il) {
+        return hdel(key, il.begin(), il.end());
+    }
+
+    bool hexists(const StringView &key, const StringView &field);
+
+    OptionalString hget(const StringView &key, const StringView &field);
+
+    template <typename Output>
+    void hgetall(const StringView &key, Output output);
+
+    long long hincrby(const StringView &key, const StringView &field, long long increment);
+
+    double hincrbyfloat(const StringView &key, const StringView &field, double increment);
+
+    template <typename Output>
+    void hkeys(const StringView &key, Output output);
+
+    long long hlen(const StringView &key);
+
+    template <typename Input, typename Output>
+    void hmget(const StringView &key, Input first, Input last, Output output);
+
+    template <typename T, typename Output>
+    void hmget(const StringView &key, std::initializer_list<T> il, Output output) {
+        hmget(key, il.begin(), il.end(), output);
+    }
+
+    template <typename Input>
+    void hmset(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    void hmset(const StringView &key, std::initializer_list<T> il) {
+        hmset(key, il.begin(), il.end());
+    }
+
+    template <typename Output>
+    long long hscan(const StringView &key,
+                    long long cursor,
+                    const StringView &pattern,
+                    long long count,
+                    Output output);
+
+    template <typename Output>
+    long long hscan(const StringView &key,
+                    long long cursor,
+                    const StringView &pattern,
+                    Output output);
+
+    template <typename Output>
+    long long hscan(const StringView &key,
+                    long long cursor,
+                    long long count,
+                    Output output);
+
+    template <typename Output>
+    long long hscan(const StringView &key,
+                    long long cursor,
+                    Output output);
+
+    bool hset(const StringView &key, const StringView &field, const StringView &val);
+
+    bool hset(const StringView &key, const std::pair<StringView, StringView> &item);
+
+    bool hsetnx(const StringView &key, const StringView &field, const StringView &val);
+
+    bool hsetnx(const StringView &key, const std::pair<StringView, StringView> &item);
+
+    long long hstrlen(const StringView &key, const StringView &field);
+
+    template <typename Output>
+    void hvals(const StringView &key, Output output);
+
+    // SET commands.
+
+    long long sadd(const StringView &key, const StringView &member);
+
+    template <typename Input>
+    long long sadd(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    long long sadd(const StringView &key, std::initializer_list<T> il) {
+        return sadd(key, il.begin(), il.end());
+    }
+
+    long long scard(const StringView &key);
+
+    template <typename Input, typename Output>
+    void sdiff(Input first, Input last, Output output);
+
+    template <typename T, typename Output>
+    void sdiff(std::initializer_list<T> il, Output output) {
+        sdiff(il.begin(), il.end(), output);
+    }
+
+    long long sdiffstore(const StringView &destination, const StringView &key);
+
+    template <typename Input>
+    long long sdiffstore(const StringView &destination,
+                            Input first,
+                            Input last);
+
+    template <typename T>
+    long long sdiffstore(const StringView &destination,
+                            std::initializer_list<T> il) {
+        return sdiffstore(destination, il.begin(), il.end());
+    }
+
+    template <typename Input, typename Output>
+    void sinter(Input first, Input last, Output output);
+
+    template <typename T, typename Output>
+    void sinter(std::initializer_list<T> il, Output output) {
+        sinter(il.begin(), il.end(), output);
+    }
+
+    long long sinterstore(const StringView &destination, const StringView &key);
+
+    template <typename Input>
+    long long sinterstore(const StringView &destination,
+                            Input first,
+                            Input last);
+
+    template <typename T>
+    long long sinterstore(const StringView &destination,
+                            std::initializer_list<T> il) {
+        return sinterstore(destination, il.begin(), il.end());
+    }
+
+    bool sismember(const StringView &key, const StringView &member);
+
+    template <typename Output>
+    void smembers(const StringView &key, Output output);
+
+    bool smove(const StringView &source,
+                const StringView &destination,
+                const StringView &member);
+
+    OptionalString spop(const StringView &key);
+
+    template <typename Output>
+    void spop(const StringView &key, long long count, Output output);
+
+    OptionalString srandmember(const StringView &key);
+
+    template <typename Output>
+    void srandmember(const StringView &key, long long count, Output output);
+
+    long long srem(const StringView &key, const StringView &member);
+
+    template <typename Input>
+    long long srem(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    long long srem(const StringView &key, std::initializer_list<T> il) {
+        return srem(key, il.begin(), il.end());
+    }
+
+    template <typename Output>
+    long long sscan(const StringView &key,
+                    long long cursor,
+                    const StringView &pattern,
+                    long long count,
+                    Output output);
+
+    template <typename Output>
+    long long sscan(const StringView &key,
+                    long long cursor,
+                    const StringView &pattern,
+                    Output output);
+
+    template <typename Output>
+    long long sscan(const StringView &key,
+                    long long cursor,
+                    long long count,
+                    Output output);
+
+    template <typename Output>
+    long long sscan(const StringView &key,
+                    long long cursor,
+                    Output output);
+
+    template <typename Input, typename Output>
+    void sunion(Input first, Input last, Output output);
+
+    template <typename T, typename Output>
+    void sunion(std::initializer_list<T> il, Output output) {
+        sunion(il.begin(), il.end(), output);
+    }
+
+    long long sunionstore(const StringView &destination, const StringView &key);
+
+    template <typename Input>
+    long long sunionstore(const StringView &destination, Input first, Input last);
+
+    template <typename T>
+    long long sunionstore(const StringView &destination, std::initializer_list<T> il) {
+        return sunionstore(destination, il.begin(), il.end());
+    }
+
+    // SORTED SET commands.
+
+    auto bzpopmax(const StringView &key, long long timeout)
+        -> Optional<std::tuple<std::string, std::string, double>>;
+
+    auto bzpopmax(const StringView &key,
+                    const std::chrono::seconds &timeout = std::chrono::seconds{0})
+        -> Optional<std::tuple<std::string, std::string, double>>;
+
+    template <typename Input>
+    auto bzpopmax(Input first, Input last, long long timeout)
+        -> Optional<std::tuple<std::string, std::string, double>>;
+
+    template <typename Input>
+    auto bzpopmax(Input first,
+                    Input last,
+                    const std::chrono::seconds &timeout = std::chrono::seconds{0})
+        -> Optional<std::tuple<std::string, std::string, double>>;
+
+    template <typename T>
+    auto bzpopmax(std::initializer_list<T> il, long long timeout)
+        -> Optional<std::tuple<std::string, std::string, double>> {
+        return bzpopmax(il.begin(), il.end(), timeout);
+    }
+
+    template <typename T>
+    auto bzpopmax(std::initializer_list<T> il,
+                    const std::chrono::seconds &timeout = std::chrono::seconds{0})
+        -> Optional<std::tuple<std::string, std::string, double>> {
+        return bzpopmax(il.begin(), il.end(), timeout);
+    }
+
+    auto bzpopmin(const StringView &key, long long timeout)
+        -> Optional<std::tuple<std::string, std::string, double>>;
+
+    auto bzpopmin(const StringView &key,
+                    const std::chrono::seconds &timeout = std::chrono::seconds{0})
+        -> Optional<std::tuple<std::string, std::string, double>>;
+
+    template <typename Input>
+    auto bzpopmin(Input first, Input last, long long timeout)
+        -> Optional<std::tuple<std::string, std::string, double>>;
+
+    template <typename Input>
+    auto bzpopmin(Input first,
+                    Input last,
+                    const std::chrono::seconds &timeout = std::chrono::seconds{0})
+        -> Optional<std::tuple<std::string, std::string, double>>;
+
+    template <typename T>
+    auto bzpopmin(std::initializer_list<T> il, long long timeout)
+        -> Optional<std::tuple<std::string, std::string, double>> {
+        return bzpopmin(il.begin(), il.end(), timeout);
+    }
+
+    template <typename T>
+    auto bzpopmin(std::initializer_list<T> il,
+                    const std::chrono::seconds &timeout = std::chrono::seconds{0})
+        -> Optional<std::tuple<std::string, std::string, double>> {
+        return bzpopmin(il.begin(), il.end(), timeout);
+    }
+
+    // We don't support the INCR option, since you can always use ZINCRBY instead.
+    long long zadd(const StringView &key,
+                    const StringView &member,
+                    double score,
+                    UpdateType type = UpdateType::ALWAYS,
+                    bool changed = false);
+
+    template <typename Input>
+    long long zadd(const StringView &key,
+                    Input first,
+                    Input last,
+                    UpdateType type = UpdateType::ALWAYS,
+                    bool changed = false);
+
+    template <typename T>
+    long long zadd(const StringView &key,
+                    std::initializer_list<T> il,
+                    UpdateType type = UpdateType::ALWAYS,
+                    bool changed = false) {
+        return zadd(key, il.begin(), il.end(), type, changed);
+    }
+
+    long long zcard(const StringView &key);
+
+    template <typename Interval>
+    long long zcount(const StringView &key, const Interval &interval);
+
+    double zincrby(const StringView &key, double increment, const StringView &member);
+
+    long long zinterstore(const StringView &destination, const StringView &key, double weight);
+
+    template <typename Input>
+    long long zinterstore(const StringView &destination,
+                            Input first,
+                            Input last,
+                            Aggregation type = Aggregation::SUM);
+
+    template <typename T>
+    long long zinterstore(const StringView &destination,
+                            std::initializer_list<T> il,
+                            Aggregation type = Aggregation::SUM) {
+        return zinterstore(destination, il.begin(), il.end(), type);
+    }
+
+    template <typename Interval>
+    long long zlexcount(const StringView &key, const Interval &interval);
+
+    Optional<std::pair<std::string, double>> zpopmax(const StringView &key);
+
+    template <typename Output>
+    void zpopmax(const StringView &key, long long count, Output output);
+
+    Optional<std::pair<std::string, double>> zpopmin(const StringView &key);
+
+    template <typename Output>
+    void zpopmin(const StringView &key, long long count, Output output);
+
+    // If *output* is an iterator of a container of string,
+    // we send *ZRANGE key start stop* command.
+    // If it's an iterator of a container of pair<string, double>,
+    // we send *ZRANGE key start stop WITHSCORES* command.
+    //
+    // The following code sends *ZRANGE* without the *WITHSCORES* option:
+    //
+    // vector<string> result;
+    // redis.zrange("key", 0, -1, back_inserter(result));
+    //
+    // On the other hand, the following code sends command with *WITHSCORES* option:
+    //
+    // unordered_map<string, double> with_score;
+    // redis.zrange("key", 0, -1, inserter(with_score, with_score.end()));
+    //
+    // This also applies to other commands with the *WITHSCORES* option,
+    // e.g. *ZRANGEBYSCORE*, *ZREVRANGE*, *ZREVRANGEBYSCORE*.
+    template <typename Output>
+    void zrange(const StringView &key, long long start, long long stop, Output output);
+
+    template <typename Interval, typename Output>
+    void zrangebylex(const StringView &key, const Interval &interval, Output output);
+
+    template <typename Interval, typename Output>
+    void zrangebylex(const StringView &key,
+                        const Interval &interval,
+                        const LimitOptions &opts,
+                        Output output);
+
+    // See *zrange* comment on how to send command with *WITHSCORES* option.
+    template <typename Interval, typename Output>
+    void zrangebyscore(const StringView &key, const Interval &interval, Output output);
+
+    // See *zrange* comment on how to send command with *WITHSCORES* option.
+    template <typename Interval, typename Output>
+    void zrangebyscore(const StringView &key,
+                        const Interval &interval,
+                        const LimitOptions &opts,
+                        Output output);
+
+    OptionalLongLong zrank(const StringView &key, const StringView &member);
+
+    long long zrem(const StringView &key, const StringView &member);
+
+    template <typename Input>
+    long long zrem(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    long long zrem(const StringView &key, std::initializer_list<T> il) {
+        return zrem(key, il.begin(), il.end());
+    }
+
+    template <typename Interval>
+    long long zremrangebylex(const StringView &key, const Interval &interval);
+
+    long long zremrangebyrank(const StringView &key, long long start, long long stop);
+
+    template <typename Interval>
+    long long zremrangebyscore(const StringView &key, const Interval &interval);
+
+    // See *zrange* comment on how to send command with *WITHSCORES* option.
+    template <typename Output>
+    void zrevrange(const StringView &key, long long start, long long stop, Output output);
+
+    template <typename Interval, typename Output>
+    void zrevrangebylex(const StringView &key, const Interval &interval, Output output);
+
+    template <typename Interval, typename Output>
+    void zrevrangebylex(const StringView &key,
+                        const Interval &interval,
+                        const LimitOptions &opts,
+                        Output output);
+
+    // See *zrange* comment on how to send command with *WITHSCORES* option.
+    template <typename Interval, typename Output>
+    void zrevrangebyscore(const StringView &key, const Interval &interval, Output output);
+
+    // See *zrange* comment on how to send command with *WITHSCORES* option.
+    template <typename Interval, typename Output>
+    void zrevrangebyscore(const StringView &key,
+                            const Interval &interval,
+                            const LimitOptions &opts,
+                            Output output);
+
+    OptionalLongLong zrevrank(const StringView &key, const StringView &member);
+
+    template <typename Output>
+    long long zscan(const StringView &key,
+                    long long cursor,
+                    const StringView &pattern,
+                    long long count,
+                    Output output);
+
+    template <typename Output>
+    long long zscan(const StringView &key,
+                    long long cursor,
+                    const StringView &pattern,
+                    Output output);
+
+    template <typename Output>
+    long long zscan(const StringView &key,
+                    long long cursor,
+                    long long count,
+                    Output output);
+
+    template <typename Output>
+    long long zscan(const StringView &key,
+                    long long cursor,
+                    Output output);
+
+    OptionalDouble zscore(const StringView &key, const StringView &member);
+
+    long long zunionstore(const StringView &destination, const StringView &key, double weight);
+
+    template <typename Input>
+    long long zunionstore(const StringView &destination,
+                            Input first,
+                            Input last,
+                            Aggregation type = Aggregation::SUM);
+
+    template <typename T>
+    long long zunionstore(const StringView &destination,
+                            std::initializer_list<T> il,
+                            Aggregation type = Aggregation::SUM) {
+        return zunionstore(destination, il.begin(), il.end(), type);
+    }
+
+    // HYPERLOGLOG commands.
+
+    bool pfadd(const StringView &key, const StringView &element);
+
+    template <typename Input>
+    bool pfadd(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    bool pfadd(const StringView &key, std::initializer_list<T> il) {
+        return pfadd(key, il.begin(), il.end());
+    }
+
+    long long pfcount(const StringView &key);
+
+    template <typename Input>
+    long long pfcount(Input first, Input last);
+
+    template <typename T>
+    long long pfcount(std::initializer_list<T> il) {
+        return pfcount(il.begin(), il.end());
+    }
+
+    void pfmerge(const StringView &destination, const StringView &key);
+
+    template <typename Input>
+    void pfmerge(const StringView &destination, Input first, Input last);
+
+    template <typename T>
+    void pfmerge(const StringView &destination, std::initializer_list<T> il) {
+        pfmerge(destination, il.begin(), il.end());
+    }
+
+    // GEO commands.
+
+    long long geoadd(const StringView &key,
+                        const std::tuple<StringView, double, double> &member);
+
+    template <typename Input>
+    long long geoadd(const StringView &key,
+                        Input first,
+                        Input last);
+
+    template <typename T>
+    long long geoadd(const StringView &key,
+                        std::initializer_list<T> il) {
+        return geoadd(key, il.begin(), il.end());
+    }
+
+    OptionalDouble geodist(const StringView &key,
+                            const StringView &member1,
+                            const StringView &member2,
+                            GeoUnit unit = GeoUnit::M);
+
+    template <typename Input, typename Output>
+    void geohash(const StringView &key, Input first, Input last, Output output);
+
+    template <typename T, typename Output>
+    void geohash(const StringView &key, std::initializer_list<T> il, Output output) {
+        geohash(key, il.begin(), il.end(), output);
+    }
+
+    template <typename Input, typename Output>
+    void geopos(const StringView &key, Input first, Input last, Output output);
+
+    template <typename T, typename Output>
+    void geopos(const StringView &key, std::initializer_list<T> il, Output output) {
+        geopos(key, il.begin(), il.end(), output);
+    }
+
+    // TODO:
+    // 1. since we have different overloads for georadius and georadius-store,
+    //    we might use the GEORADIUS_RO command in the future.
+    // 2. there're too many parameters for this method, we might refactor it.
+    OptionalLongLong georadius(const StringView &key,
+                                const std::pair<double, double> &loc,
+                                double radius,
+                                GeoUnit unit,
+                                const StringView &destination,
+                                bool store_dist,
+                                long long count);
+
+    // If *output* is an iterator of a container of string, we send *GEORADIUS* command
+    // without any options and only get the members in the specified geo range.
+    // If *output* is an iterator of a container of a tuple, the type of the tuple decides
+    // options we send with the *GEORADIUS* command. If the tuple has an element of type
+    // double, we send the *WITHDIST* option. If it has an element of type string, we send
+    // the *WITHHASH* option. If it has an element of type pair<double, double>, we send
+    // the *WITHCOORD* option. For example:
+    //
+    // The following code only gets the members in range, i.e. without any option.
+    //
+    // vector<string> members;
+    // redis.georadius("key", make_pair(10.1, 10.2), 10, GeoUnit::KM, 10, true,
+    //                  back_inserter(members))
+    //
+    // The following code sends the command with *WITHDIST* option.
+    //
+    // vector<tuple<string, double>> with_dist;
+    // redis.georadius("key", make_pair(10.1, 10.2), 10, GeoUnit::KM, 10, true,
+    //                  back_inserter(with_dist))
+    //
+    // The following code sends the command with *WITHDIST* and *WITHHASH* options.
+    //
+    // vector<tuple<string, double, string>> with_dist_hash;
+    // redis.georadius("key", make_pair(10.1, 10.2), 10, GeoUnit::KM, 10, true,
+    //                  back_inserter(with_dist_hash))
+    //
+    // The following code sends the command with *WITHDIST*, *WITHCOORD* and *WITHHASH* options.
+    //
+    // vector<tuple<string, double, pair<double, double>, string>> with_dist_coord_hash;
+    // redis.georadius("key", make_pair(10.1, 10.2), 10, GeoUnit::KM, 10, true,
+    //                  back_inserter(with_dist_coord_hash))
+    //
+    // This also applies to *GEORADIUSBYMEMBER*.
+    template <typename Output>
+    void georadius(const StringView &key,
+                    const std::pair<double, double> &loc,
+                    double radius,
+                    GeoUnit unit,
+                    long long count,
+                    bool asc,
+                    Output output);
+
+    OptionalLongLong georadiusbymember(const StringView &key,
+                                        const StringView &member,
+                                        double radius,
+                                        GeoUnit unit,
+                                        const StringView &destination,
+                                        bool store_dist,
+                                        long long count);
+
+    // See comments on *GEORADIUS*.
+    template <typename Output>
+    void georadiusbymember(const StringView &key,
+                            const StringView &member,
+                            double radius,
+                            GeoUnit unit,
+                            long long count,
+                            bool asc,
+                            Output output);
+
+    // SCRIPTING commands.
+
+    template <typename Result>
+    Result eval(const StringView &script,
+                std::initializer_list<StringView> keys,
+                std::initializer_list<StringView> args);
+
+    template <typename Output>
+    void eval(const StringView &script,
+                std::initializer_list<StringView> keys,
+                std::initializer_list<StringView> args,
+                Output output);
+
+    template <typename Result>
+    Result evalsha(const StringView &script,
+                    std::initializer_list<StringView> keys,
+                    std::initializer_list<StringView> args);
+
+    template <typename Output>
+    void evalsha(const StringView &script,
+                    std::initializer_list<StringView> keys,
+                    std::initializer_list<StringView> args,
+                    Output output);
+
+    // PUBSUB commands.
+
+    long long publish(const StringView &channel, const StringView &message);
+
+    // Stream commands.
+
+    long long xack(const StringView &key, const StringView &group, const StringView &id);
+
+    template <typename Input>
+    long long xack(const StringView &key, const StringView &group, Input first, Input last);
+
+    template <typename T>
+    long long xack(const StringView &key, const StringView &group, std::initializer_list<T> il) {
+        return xack(key, group, il.begin(), il.end());
+    }
+
+    template <typename Input>
+    std::string xadd(const StringView &key, const StringView &id, Input first, Input last);
+
+    template <typename T>
+    std::string xadd(const StringView &key, const StringView &id, std::initializer_list<T> il) {
+        return xadd(key, id, il.begin(), il.end());
+    }
+
+    template <typename Input>
+    std::string xadd(const StringView &key,
+                        const StringView &id,
+                        Input first,
+                        Input last,
+                        long long count,
+                        bool approx = true);
+
+    template <typename T>
+    std::string xadd(const StringView &key,
+                        const StringView &id,
+                        std::initializer_list<T> il,
+                        long long count,
+                        bool approx = true) {
+        return xadd(key, id, il.begin(), il.end(), count, approx);
+    }
+
+    template <typename Output>
+    void xclaim(const StringView &key,
+                const StringView &group,
+                const StringView &consumer,
+                const std::chrono::milliseconds &min_idle_time,
+                const StringView &id,
+                Output output);
+
+    template <typename Input, typename Output>
+    void xclaim(const StringView &key,
+                const StringView &group,
+                const StringView &consumer,
+                const std::chrono::milliseconds &min_idle_time,
+                Input first,
+                Input last,
+                Output output);
+
+    template <typename T, typename Output>
+    void xclaim(const StringView &key,
+                const StringView &group,
+                const StringView &consumer,
+                const std::chrono::milliseconds &min_idle_time,
+                std::initializer_list<T> il,
+                Output output) {
+        xclaim(key, group, consumer, min_idle_time, il.begin(), il.end(), output);
+    }
+
+    long long xdel(const StringView &key, const StringView &id);
+
+    template <typename Input>
+    long long xdel(const StringView &key, Input first, Input last);
+
+    template <typename T>
+    long long xdel(const StringView &key, std::initializer_list<T> il) {
+        return xdel(key, il.begin(), il.end());
+    }
+
+    void xgroup_create(const StringView &key,
+                        const StringView &group,
+                        const StringView &id,
+                        bool mkstream = false);
+
+    void xgroup_setid(const StringView &key, const StringView &group, const StringView &id);
+
+    long long xgroup_destroy(const StringView &key, const StringView &group);
+
+    long long xgroup_delconsumer(const StringView &key,
+                                    const StringView &group,
+                                    const StringView &consumer);
+
+    long long xlen(const StringView &key);
+
+    template <typename Output>
+    auto xpending(const StringView &key, const StringView &group, Output output)
+        -> std::tuple<long long, OptionalString, OptionalString>;
+
+    template <typename Output>
+    void xpending(const StringView &key,
+                    const StringView &group,
+                    const StringView &start,
+                    const StringView &end,
+                    long long count,
+                    Output output);
+
+    template <typename Output>
+    void xpending(const StringView &key,
+                    const StringView &group,
+                    const StringView &start,
+                    const StringView &end,
+                    long long count,
+                    const StringView &consumer,
+                    Output output);
+
+    template <typename Output>
+    void xrange(const StringView &key,
+                const StringView &start,
+                const StringView &end,
+                Output output);
+
+    template <typename Output>
+    void xrange(const StringView &key,
+                const StringView &start,
+                const StringView &end,
+                long long count,
+                Output output);
+
+    template <typename Output>
+    void xread(const StringView &key,
+                const StringView &id,
+                long long count,
+                Output output);
+
+    template <typename Output>
+    void xread(const StringView &key,
+                const StringView &id,
+                Output output) {
+        xread(key, id, 0, output);
+    }
+
+    template <typename Input, typename Output>
+    auto xread(Input first, Input last, long long count, Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type;
+
+    template <typename Input, typename Output>
+    auto xread(Input first, Input last, Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+        xread(first, last, 0, output);
+    }
+
+    template <typename Output>
+    void xread(const StringView &key,
+                const StringView &id,
+                const std::chrono::milliseconds &timeout,
+                long long count,
+                Output output);
+
+    template <typename Output>
+    void xread(const StringView &key,
+                const StringView &id,
+                const std::chrono::milliseconds &timeout,
+                Output output) {
+        xread(key, id, timeout, 0, output);
+    }
+
+    template <typename Input, typename Output>
+    auto xread(Input first,
+                Input last,
+                const std::chrono::milliseconds &timeout,
+                long long count,
+                Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type;
+
+    template <typename Input, typename Output>
+    auto xread(Input first,
+                Input last,
+                const std::chrono::milliseconds &timeout,
+                Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+        xread(first, last, timeout, 0, output);
+    }
+
+    template <typename Output>
+    void xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    const StringView &key,
+                    const StringView &id,
+                    long long count,
+                    bool noack,
+                    Output output);
+
+    template <typename Output>
+    void xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    const StringView &key,
+                    const StringView &id,
+                    long long count,
+                    Output output) {
+        xreadgroup(group, consumer, key, id, count, false, output);
+    }
+
+    template <typename Output>
+    void xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    const StringView &key,
+                    const StringView &id,
+                    Output output) {
+        xreadgroup(group, consumer, key, id, 0, false, output);
+    }
+
+    template <typename Input, typename Output>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last,
+                    long long count,
+                    bool noack,
+                    Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type;
+
+    template <typename Input, typename Output>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last,
+                    long long count,
+                    Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+        xreadgroup(group, consumer, first ,last, count, false, output);
+    }
+
+    template <typename Input, typename Output>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last,
+                    Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+        xreadgroup(group, consumer, first ,last, 0, false, output);
+    }
+
+    template <typename Output>
+    void xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    const StringView &key,
+                    const StringView &id,
+                    const std::chrono::milliseconds &timeout,
+                    long long count,
+                    bool noack,
+                    Output output);
+
+    template <typename Output>
+    void xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    const StringView &key,
+                    const StringView &id,
+                    const std::chrono::milliseconds &timeout,
+                    long long count,
+                    Output output) {
+        return xreadgroup(group, consumer, key, id, timeout, count, false, output);
+    }
+
+    template <typename Output>
+    void xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    const StringView &key,
+                    const StringView &id,
+                    const std::chrono::milliseconds &timeout,
+                    Output output) {
+        return xreadgroup(group, consumer, key, id, timeout, 0, false, output);
+    }
+
+    template <typename Input, typename Output>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last,
+                    const std::chrono::milliseconds &timeout,
+                    long long count,
+                    bool noack,
+                    Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type;
+
+    template <typename Input, typename Output>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last,
+                    const std::chrono::milliseconds &timeout,
+                    long long count,
+                    Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+        xreadgroup(group, consumer, first, last, timeout, count, false, output);
+    }
+
+    template <typename Input, typename Output>
+    auto xreadgroup(const StringView &group,
+                    const StringView &consumer,
+                    Input first,
+                    Input last,
+                    const std::chrono::milliseconds &timeout,
+                    Output output)
+        -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+        xreadgroup(group, consumer, first, last, timeout, 0, false, output);
+    }
+
+    template <typename Output>
+    void xrevrange(const StringView &key,
+                    const StringView &end,
+                    const StringView &start,
+                    Output output);
+
+    template <typename Output>
+    void xrevrange(const StringView &key,
+                    const StringView &end,
+                    const StringView &start,
+                    long long count,
+                    Output output);
+
+    long long xtrim(const StringView &key, long long count, bool approx = true);
+
+private:
+    class Command {
+    public:
+        explicit Command(const StringView &cmd_name) : _cmd_name(cmd_name) {}
+
+        template <typename ...Args>
+        void operator()(Connection &connection, Args &&...args) const {
+            CmdArgs cmd_args;
+            cmd_args.append(_cmd_name, std::forward<Args>(args)...);
+            connection.send(cmd_args);
+        }
+
+    private:
+        StringView _cmd_name;
+    };
+
+    template <typename Cmd, typename Key, typename ...Args>
+    auto _generic_command(Cmd cmd, Key &&key, Args &&...args)
+        -> typename std::enable_if<std::is_convertible<Key, StringView>::value,
+                                    ReplyUPtr>::type;
+
+    template <typename Cmd, typename Key, typename ...Args>
+    auto _generic_command(Cmd cmd, Key &&key, Args &&...args)
+        -> typename std::enable_if<std::is_arithmetic<typename std::decay<Key>::type>::value,
+                                    ReplyUPtr>::type;
+
+    template <typename Cmd, typename ...Args>
+    ReplyUPtr _command(Cmd cmd, Connection &connection, Args &&...args);
+
+    template <typename Cmd, typename ...Args>
+    ReplyUPtr _command(Cmd cmd, const StringView &key, Args &&...args);
+
+    template <typename Cmd, typename ...Args>
+    ReplyUPtr _command(Cmd cmd, std::true_type, const StringView &key, Args &&...args);
+
+    template <typename Cmd, typename Input, typename ...Args>
+    ReplyUPtr _command(Cmd cmd, std::false_type, Input &&first, Args &&...args);
+
+    template <std::size_t ...Is, typename ...Args>
+    ReplyUPtr _command(const StringView &cmd_name, const IndexSequence<Is...> &, Args &&...args) {
+        return command(cmd_name, NthValue<Is>(std::forward<Args>(args)...)...);
+    }
+
+    template <typename Cmd, typename Input, typename ...Args>
+    ReplyUPtr _range_command(Cmd cmd, std::true_type, Input input, Args &&...args);
+
+    template <typename Cmd, typename Input, typename ...Args>
+    ReplyUPtr _range_command(Cmd cmd, std::false_type, Input input, Args &&...args);
+
+    void _asking(Connection &connection);
+
+    template <typename Cmd, typename ...Args>
+    ReplyUPtr _score_command(std::true_type, Cmd cmd, Args &&... args);
+
+    template <typename Cmd, typename ...Args>
+    ReplyUPtr _score_command(std::false_type, Cmd cmd, Args &&... args);
+
+    template <typename Output, typename Cmd, typename ...Args>
+    ReplyUPtr _score_command(Cmd cmd, Args &&... args);
+
+    ShardsPool _pool;
+};
+
+}
+
+}
+
+#include "redis_cluster.hpp"
+
+#endif // end SEWENEW_REDISPLUSPLUS_REDIS_CLUSTER_H

+ 1415 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/redis_cluster.hpp

@@ -0,0 +1,1415 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_REDIS_CLUSTER_HPP
+#define SEWENEW_REDISPLUSPLUS_REDIS_CLUSTER_HPP
+
+#include <utility>
+#include "command.h"
+#include "reply.h"
+#include "utils.h"
+#include "errors.h"
+#include "shards_pool.h"
+
+namespace sw {
+
+namespace redis {
+
+template <typename Cmd, typename Key, typename ...Args>
+auto RedisCluster::command(Cmd cmd, Key &&key, Args &&...args)
+    -> typename std::enable_if<!std::is_convertible<Cmd, StringView>::value, ReplyUPtr>::type {
+    return _command(cmd,
+                    std::is_convertible<typename std::decay<Key>::type, StringView>(),
+                    std::forward<Key>(key),
+                    std::forward<Args>(args)...);
+}
+
+template <typename Key, typename ...Args>
+auto RedisCluster::command(const StringView &cmd_name, Key &&key, Args &&...args)
+    -> typename std::enable_if<(std::is_convertible<Key, StringView>::value
+        || std::is_arithmetic<typename std::decay<Key>::type>::value)
+        && !IsIter<typename LastType<Key, Args...>::type>::value, ReplyUPtr>::type {
+    auto cmd = Command(cmd_name);
+
+    return _generic_command(cmd, std::forward<Key>(key), std::forward<Args>(args)...);
+}
+
+template <typename Result, typename Key, typename ...Args>
+auto RedisCluster::command(const StringView &cmd_name, Key &&key, Args &&...args)
+    -> typename std::enable_if<std::is_convertible<Key, StringView>::value
+            || std::is_arithmetic<typename std::decay<Key>::type>::value, Result>::type {
+    auto r = command(cmd_name, std::forward<Key>(key), std::forward<Args>(args)...);
+
+    assert(r);
+
+    return reply::parse<Result>(*r);
+}
+
+template <typename Key, typename ...Args>
+auto RedisCluster::command(const StringView &cmd_name, Key &&key, Args &&...args)
+    -> typename std::enable_if<(std::is_convertible<Key, StringView>::value
+            || std::is_arithmetic<typename std::decay<Key>::type>::value)
+            && IsIter<typename LastType<Key, Args...>::type>::value, void>::type {
+    auto r = _command(cmd_name,
+                        MakeIndexSequence<sizeof...(Args)>(),
+                        std::forward<Key>(key),
+                        std::forward<Args>(args)...);
+
+    assert(r);
+
+    reply::to_array(*r, LastValue(std::forward<Args>(args)...));
+}
+
+template <typename Input>
+auto RedisCluster::command(Input first, Input last)
+    -> typename std::enable_if<IsIter<Input>::value, ReplyUPtr>::type {
+    if (first == last || std::next(first) == last) {
+        throw Error("command: invalid range");
+    }
+
+    const auto &key = *first;
+    ++first;
+
+    auto cmd = [&key](Connection &connection, Input first, Input last) {
+                        CmdArgs cmd_args;
+                        cmd_args.append(key);
+                        while (first != last) {
+                            cmd_args.append(*first);
+                            ++first;
+                        }
+                        connection.send(cmd_args);
+    };
+
+    return command(cmd, first, last);
+}
+
+template <typename Result, typename Input>
+auto RedisCluster::command(Input first, Input last)
+    -> typename std::enable_if<IsIter<Input>::value, Result>::type {
+    auto r = command(first, last);
+
+    assert(r);
+
+    return reply::parse<Result>(*r);
+}
+
+template <typename Input, typename Output>
+auto RedisCluster::command(Input first, Input last, Output output)
+    -> typename std::enable_if<IsIter<Input>::value, void>::type {
+    auto r = command(first, last);
+
+    assert(r);
+
+    reply::to_array(*r, output);
+}
+
+// KEY commands.
+
+template <typename Input>
+long long RedisCluster::del(Input first, Input last) {
+    if (first == last) {
+        throw Error("DEL: no key specified");
+    }
+
+    auto reply = command(cmd::del_range<Input>, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input>
+long long RedisCluster::exists(Input first, Input last) {
+    if (first == last) {
+        throw Error("EXISTS: no key specified");
+    }
+
+    auto reply = command(cmd::exists_range<Input>, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+inline bool RedisCluster::expire(const StringView &key, const std::chrono::seconds &timeout) {
+    return expire(key, timeout.count());
+}
+
+inline bool RedisCluster::expireat(const StringView &key,
+                                    const std::chrono::time_point<std::chrono::system_clock,
+                                                                    std::chrono::seconds> &tp) {
+    return expireat(key, tp.time_since_epoch().count());
+}
+
+inline bool RedisCluster::pexpire(const StringView &key, const std::chrono::milliseconds &timeout) {
+    return pexpire(key, timeout.count());
+}
+
+inline bool RedisCluster::pexpireat(const StringView &key,
+                                const std::chrono::time_point<std::chrono::system_clock,
+                                                                std::chrono::milliseconds> &tp) {
+    return pexpireat(key, tp.time_since_epoch().count());
+}
+
+inline void RedisCluster::restore(const StringView &key,
+                            const StringView &val,
+                            const std::chrono::milliseconds &ttl,
+                            bool replace) {
+    return restore(key, val, ttl.count(), replace);
+}
+
+template <typename Input>
+long long RedisCluster::touch(Input first, Input last) {
+    if (first == last) {
+        throw Error("TOUCH: no key specified");
+    }
+
+    auto reply = command(cmd::touch_range<Input>, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input>
+long long RedisCluster::unlink(Input first, Input last) {
+    if (first == last) {
+        throw Error("UNLINK: no key specified");
+    }
+
+    auto reply = command(cmd::unlink_range<Input>, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+// STRING commands.
+
+template <typename Input>
+long long RedisCluster::bitop(BitOp op, const StringView &destination, Input first, Input last) {
+    if (first == last) {
+        throw Error("BITOP: no key specified");
+    }
+
+    auto reply = _command(cmd::bitop_range<Input>, destination, op, destination, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input, typename Output>
+void RedisCluster::mget(Input first, Input last, Output output) {
+    if (first == last) {
+        throw Error("MGET: no key specified");
+    }
+
+    auto reply = command(cmd::mget<Input>, first, last);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+void RedisCluster::mset(Input first, Input last) {
+    if (first == last) {
+        throw Error("MSET: no key specified");
+    }
+
+    auto reply = command(cmd::mset<Input>, first, last);
+
+    reply::parse<void>(*reply);
+}
+
+template <typename Input>
+bool RedisCluster::msetnx(Input first, Input last) {
+    if (first == last) {
+        throw Error("MSETNX: no key specified");
+    }
+
+    auto reply = command(cmd::msetnx<Input>, first, last);
+
+    return reply::parse<bool>(*reply);
+}
+
+inline void RedisCluster::psetex(const StringView &key,
+                            const std::chrono::milliseconds &ttl,
+                            const StringView &val) {
+    return psetex(key, ttl.count(), val);
+}
+
+inline void RedisCluster::setex(const StringView &key,
+                            const std::chrono::seconds &ttl,
+                            const StringView &val) {
+    setex(key, ttl.count(), val);
+}
+
+// LIST commands.
+
+template <typename Input>
+OptionalStringPair RedisCluster::blpop(Input first, Input last, long long timeout) {
+    if (first == last) {
+        throw Error("BLPOP: no key specified");
+    }
+
+    auto reply = command(cmd::blpop_range<Input>, first, last, timeout);
+
+    return reply::parse<OptionalStringPair>(*reply);
+}
+
+template <typename Input>
+OptionalStringPair RedisCluster::blpop(Input first,
+                                Input last,
+                                const std::chrono::seconds &timeout) {
+    return blpop(first, last, timeout.count());
+}
+
+template <typename Input>
+OptionalStringPair RedisCluster::brpop(Input first, Input last, long long timeout) {
+    if (first == last) {
+        throw Error("BRPOP: no key specified");
+    }
+
+    auto reply = command(cmd::brpop_range<Input>, first, last, timeout);
+
+    return reply::parse<OptionalStringPair>(*reply);
+}
+
+template <typename Input>
+OptionalStringPair RedisCluster::brpop(Input first,
+                                Input last,
+                                const std::chrono::seconds &timeout) {
+    return brpop(first, last, timeout.count());
+}
+
+inline OptionalString RedisCluster::brpoplpush(const StringView &source,
+                                        const StringView &destination,
+                                        const std::chrono::seconds &timeout) {
+    return brpoplpush(source, destination, timeout.count());
+}
+
+template <typename Input>
+inline long long RedisCluster::lpush(const StringView &key, Input first, Input last) {
+    if (first == last) {
+        throw Error("LPUSH: no key specified");
+    }
+
+    auto reply = command(cmd::lpush_range<Input>, key, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Output>
+inline void RedisCluster::lrange(const StringView &key, long long start, long long stop, Output output) {
+    auto reply = command(cmd::lrange, key, start, stop);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+inline long long RedisCluster::rpush(const StringView &key, Input first, Input last) {
+    if (first == last) {
+        throw Error("RPUSH: no key specified");
+    }
+
+    auto reply = command(cmd::rpush_range<Input>, key, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+// HASH commands.
+
+template <typename Input>
+inline long long RedisCluster::hdel(const StringView &key, Input first, Input last) {
+    if (first == last) {
+        throw Error("HDEL: no key specified");
+    }
+
+    auto reply = command(cmd::hdel_range<Input>, key, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Output>
+inline void RedisCluster::hgetall(const StringView &key, Output output) {
+    auto reply = command(cmd::hgetall, key);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+inline void RedisCluster::hkeys(const StringView &key, Output output) {
+    auto reply = command(cmd::hkeys, key);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input, typename Output>
+inline void RedisCluster::hmget(const StringView &key, Input first, Input last, Output output) {
+    if (first == last) {
+        throw Error("HMGET: no key specified");
+    }
+
+    auto reply = command(cmd::hmget<Input>, key, first, last);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+inline void RedisCluster::hmset(const StringView &key, Input first, Input last) {
+    if (first == last) {
+        throw Error("HMSET: no key specified");
+    }
+
+    auto reply = command(cmd::hmset<Input>, key, first, last);
+
+    reply::parse<void>(*reply);
+}
+
+template <typename Output>
+long long RedisCluster::hscan(const StringView &key,
+                        long long cursor,
+                        const StringView &pattern,
+                        long long count,
+                        Output output) {
+    auto reply = command(cmd::hscan, key, cursor, pattern, count);
+
+    return reply::parse_scan_reply(*reply, output);
+}
+
+template <typename Output>
+inline long long RedisCluster::hscan(const StringView &key,
+                                long long cursor,
+                                const StringView &pattern,
+                                Output output) {
+    return hscan(key, cursor, pattern, 10, output);
+}
+
+template <typename Output>
+inline long long RedisCluster::hscan(const StringView &key,
+                                long long cursor,
+                                long long count,
+                                Output output) {
+    return hscan(key, cursor, "*", count, output);
+}
+
+template <typename Output>
+inline long long RedisCluster::hscan(const StringView &key,
+                                long long cursor,
+                                Output output) {
+    return hscan(key, cursor, "*", 10, output);
+}
+
+template <typename Output>
+inline void RedisCluster::hvals(const StringView &key, Output output) {
+    auto reply = command(cmd::hvals, key);
+
+    reply::to_array(*reply, output);
+}
+
+// SET commands.
+
+template <typename Input>
+long long RedisCluster::sadd(const StringView &key, Input first, Input last) {
+    if (first == last) {
+        throw Error("SADD: no key specified");
+    }
+
+    auto reply = command(cmd::sadd_range<Input>, key, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input, typename Output>
+void RedisCluster::sdiff(Input first, Input last, Output output) {
+    if (first == last) {
+        throw Error("SDIFF: no key specified");
+    }
+
+    auto reply = command(cmd::sdiff<Input>, first, last);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+long long RedisCluster::sdiffstore(const StringView &destination,
+                                    Input first,
+                                    Input last) {
+    if (first == last) {
+        throw Error("SDIFFSTORE: no key specified");
+    }
+
+    auto reply = command(cmd::sdiffstore_range<Input>, destination, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input, typename Output>
+void RedisCluster::sinter(Input first, Input last, Output output) {
+    if (first == last) {
+        throw Error("SINTER: no key specified");
+    }
+
+    auto reply = command(cmd::sinter<Input>, first, last);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+long long RedisCluster::sinterstore(const StringView &destination,
+                                    Input first,
+                                    Input last) {
+    if (first == last) {
+        throw Error("SINTERSTORE: no key specified");
+    }
+
+    auto reply = command(cmd::sinterstore_range<Input>, destination, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Output>
+void RedisCluster::smembers(const StringView &key, Output output) {
+    auto reply = command(cmd::smembers, key);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void RedisCluster::spop(const StringView &key, long long count, Output output) {
+    auto reply = command(cmd::spop_range, key, count);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void RedisCluster::srandmember(const StringView &key, long long count, Output output) {
+    auto reply = command(cmd::srandmember_range, key, count);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+long long RedisCluster::srem(const StringView &key, Input first, Input last) {
+    if (first == last) {
+        throw Error("SREM: no key specified");
+    }
+
+    auto reply = command(cmd::srem_range<Input>, key, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Output>
+long long RedisCluster::sscan(const StringView &key,
+                        long long cursor,
+                        const StringView &pattern,
+                        long long count,
+                        Output output) {
+    auto reply = command(cmd::sscan, key, cursor, pattern, count);
+
+    return reply::parse_scan_reply(*reply, output);
+}
+
+template <typename Output>
+inline long long RedisCluster::sscan(const StringView &key,
+                                long long cursor,
+                                const StringView &pattern,
+                                Output output) {
+    return sscan(key, cursor, pattern, 10, output);
+}
+
+template <typename Output>
+inline long long RedisCluster::sscan(const StringView &key,
+                                long long cursor,
+                                long long count,
+                                Output output) {
+    return sscan(key, cursor, "*", count, output);
+}
+
+template <typename Output>
+inline long long RedisCluster::sscan(const StringView &key,
+                                long long cursor,
+                                Output output) {
+    return sscan(key, cursor, "*", 10, output);
+}
+
+template <typename Input, typename Output>
+void RedisCluster::sunion(Input first, Input last, Output output) {
+    if (first == last) {
+        throw Error("SUNION: no key specified");
+    }
+
+    auto reply = command(cmd::sunion<Input>, first, last);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+long long RedisCluster::sunionstore(const StringView &destination, Input first, Input last) {
+    if (first == last) {
+        throw Error("SUNIONSTORE: no key specified");
+    }
+
+    auto reply = command(cmd::sunionstore_range<Input>, destination, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+// SORTED SET commands.
+
+inline auto RedisCluster::bzpopmax(const StringView &key, const std::chrono::seconds &timeout)
+    -> Optional<std::tuple<std::string, std::string, double>> {
+    return bzpopmax(key, timeout.count());
+}
+
+template <typename Input>
+auto RedisCluster::bzpopmax(Input first, Input last, long long timeout)
+    -> Optional<std::tuple<std::string, std::string, double>> {
+    auto reply = command(cmd::bzpopmax_range<Input>, first, last, timeout);
+
+    return reply::parse<Optional<std::tuple<std::string, std::string, double>>>(*reply);
+}
+
+template <typename Input>
+inline auto RedisCluster::bzpopmax(Input first,
+                                    Input last,
+                                    const std::chrono::seconds &timeout)
+    -> Optional<std::tuple<std::string, std::string, double>> {
+    return bzpopmax(first, last, timeout.count());
+}
+
+inline auto RedisCluster::bzpopmin(const StringView &key, const std::chrono::seconds &timeout)
+    -> Optional<std::tuple<std::string, std::string, double>> {
+    return bzpopmin(key, timeout.count());
+}
+
+template <typename Input>
+auto RedisCluster::bzpopmin(Input first, Input last, long long timeout)
+    -> Optional<std::tuple<std::string, std::string, double>> {
+    auto reply = command(cmd::bzpopmin_range<Input>, first, last, timeout);
+
+    return reply::parse<Optional<std::tuple<std::string, std::string, double>>>(*reply);
+}
+
+template <typename Input>
+inline auto RedisCluster::bzpopmin(Input first,
+                                    Input last,
+                                    const std::chrono::seconds &timeout)
+    -> Optional<std::tuple<std::string, std::string, double>> {
+    return bzpopmin(first, last, timeout.count());
+}
+
+template <typename Input>
+long long RedisCluster::zadd(const StringView &key,
+                        Input first,
+                        Input last,
+                        UpdateType type,
+                        bool changed) {
+    if (first == last) {
+        throw Error("ZADD: no key specified");
+    }
+
+    auto reply = command(cmd::zadd_range<Input>, key, first, last, type, changed);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Interval>
+long long RedisCluster::zcount(const StringView &key, const Interval &interval) {
+    auto reply = command(cmd::zcount<Interval>, key, interval);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input>
+long long RedisCluster::zinterstore(const StringView &destination,
+                                Input first,
+                                Input last,
+                                Aggregation type) {
+    if (first == last) {
+        throw Error("ZINTERSTORE: no key specified");
+    }
+
+    auto reply = command(cmd::zinterstore_range<Input>,
+                            destination,
+                            first,
+                            last,
+                            type);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Interval>
+long long RedisCluster::zlexcount(const StringView &key, const Interval &interval) {
+    auto reply = command(cmd::zlexcount<Interval>, key, interval);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Output>
+void RedisCluster::zpopmax(const StringView &key, long long count, Output output) {
+    auto reply = command(cmd::zpopmax, key, count);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void RedisCluster::zpopmin(const StringView &key, long long count, Output output) {
+    auto reply = command(cmd::zpopmin, key, count);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void RedisCluster::zrange(const StringView &key, long long start, long long stop, Output output) {
+    auto reply = _score_command<Output>(cmd::zrange, key, start, stop);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Interval, typename Output>
+void RedisCluster::zrangebylex(const StringView &key, const Interval &interval, Output output) {
+    zrangebylex(key, interval, {}, output);
+}
+
+template <typename Interval, typename Output>
+void RedisCluster::zrangebylex(const StringView &key,
+                        const Interval &interval,
+                        const LimitOptions &opts,
+                        Output output) {
+    auto reply = command(cmd::zrangebylex<Interval>, key, interval, opts);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Interval, typename Output>
+void RedisCluster::zrangebyscore(const StringView &key,
+                            const Interval &interval,
+                            Output output) {
+    zrangebyscore(key, interval, {}, output);
+}
+
+template <typename Interval, typename Output>
+void RedisCluster::zrangebyscore(const StringView &key,
+                            const Interval &interval,
+                            const LimitOptions &opts,
+                            Output output) {
+    auto reply = _score_command<Output>(cmd::zrangebyscore<Interval>,
+                                        key,
+                                        interval,
+                                        opts);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+long long RedisCluster::zrem(const StringView &key, Input first, Input last) {
+    if (first == last) {
+        throw Error("ZREM: no key specified");
+    }
+
+    auto reply = command(cmd::zrem_range<Input>, key, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Interval>
+long long RedisCluster::zremrangebylex(const StringView &key, const Interval &interval) {
+    auto reply = command(cmd::zremrangebylex<Interval>, key, interval);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Interval>
+long long RedisCluster::zremrangebyscore(const StringView &key, const Interval &interval) {
+    auto reply = command(cmd::zremrangebyscore<Interval>, key, interval);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Output>
+void RedisCluster::zrevrange(const StringView &key, long long start, long long stop, Output output) {
+    auto reply = _score_command<Output>(cmd::zrevrange, key, start, stop);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Interval, typename Output>
+inline void RedisCluster::zrevrangebylex(const StringView &key,
+                                    const Interval &interval,
+                                    Output output) {
+    zrevrangebylex(key, interval, {}, output);
+}
+
+template <typename Interval, typename Output>
+void RedisCluster::zrevrangebylex(const StringView &key,
+                            const Interval &interval,
+                            const LimitOptions &opts,
+                            Output output) {
+    auto reply = command(cmd::zrevrangebylex<Interval>, key, interval, opts);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Interval, typename Output>
+void RedisCluster::zrevrangebyscore(const StringView &key, const Interval &interval, Output output) {
+    zrevrangebyscore(key, interval, {}, output);
+}
+
+template <typename Interval, typename Output>
+void RedisCluster::zrevrangebyscore(const StringView &key,
+                                const Interval &interval,
+                                const LimitOptions &opts,
+                                Output output) {
+    auto reply = _score_command<Output>(cmd::zrevrangebyscore<Interval>, key, interval, opts);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+long long RedisCluster::zscan(const StringView &key,
+                        long long cursor,
+                        const StringView &pattern,
+                        long long count,
+                        Output output) {
+    auto reply = command(cmd::zscan, key, cursor, pattern, count);
+
+    return reply::parse_scan_reply(*reply, output);
+}
+
+template <typename Output>
+inline long long RedisCluster::zscan(const StringView &key,
+                                long long cursor,
+                                const StringView &pattern,
+                                Output output) {
+    return zscan(key, cursor, pattern, 10, output);
+}
+
+template <typename Output>
+inline long long RedisCluster::zscan(const StringView &key,
+                                long long cursor,
+                                long long count,
+                                Output output) {
+    return zscan(key, cursor, "*", count, output);
+}
+
+template <typename Output>
+inline long long RedisCluster::zscan(const StringView &key,
+                                long long cursor,
+                                Output output) {
+    return zscan(key, cursor, "*", 10, output);
+}
+
+template <typename Input>
+long long RedisCluster::zunionstore(const StringView &destination,
+                                    Input first,
+                                    Input last,
+                                    Aggregation type) {
+    if (first == last) {
+        throw Error("ZUNIONSTORE: no key specified");
+    }
+
+    auto reply = command(cmd::zunionstore_range<Input>,
+                            destination,
+                            first,
+                            last,
+                            type);
+
+    return reply::parse<long long>(*reply);
+}
+
+// HYPERLOGLOG commands.
+
+template <typename Input>
+bool RedisCluster::pfadd(const StringView &key, Input first, Input last) {
+    if (first == last) {
+        throw Error("PFADD: no key specified");
+    }
+
+    auto reply = command(cmd::pfadd_range<Input>, key, first, last);
+
+    return reply::parse<bool>(*reply);
+}
+
+template <typename Input>
+long long RedisCluster::pfcount(Input first, Input last) {
+    if (first == last) {
+        throw Error("PFCOUNT: no key specified");
+    }
+
+    auto reply = command(cmd::pfcount_range<Input>, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input>
+void RedisCluster::pfmerge(const StringView &destination,
+                    Input first,
+                    Input last) {
+    if (first == last) {
+        throw Error("PFMERGE: no key specified");
+    }
+
+    auto reply = command(cmd::pfmerge_range<Input>, destination, first, last);
+
+    reply::parse<void>(*reply);
+}
+
+// GEO commands.
+
+template <typename Input>
+inline long long RedisCluster::geoadd(const StringView &key,
+                                Input first,
+                                Input last) {
+    if (first == last) {
+        throw Error("GEOADD: no key specified");
+    }
+
+    auto reply = command(cmd::geoadd_range<Input>, key, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input, typename Output>
+void RedisCluster::geohash(const StringView &key, Input first, Input last, Output output) {
+    if (first == last) {
+        throw Error("GEOHASH: no key specified");
+    }
+
+    auto reply = command(cmd::geohash_range<Input>, key, first, last);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input, typename Output>
+void RedisCluster::geopos(const StringView &key, Input first, Input last, Output output) {
+    if (first == last) {
+        throw Error("GEOPOS: no key specified");
+    }
+
+    auto reply = command(cmd::geopos_range<Input>, key, first, last);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void RedisCluster::georadius(const StringView &key,
+                        const std::pair<double, double> &loc,
+                        double radius,
+                        GeoUnit unit,
+                        long long count,
+                        bool asc,
+                        Output output) {
+    auto reply = command(cmd::georadius,
+                            key,
+                            loc,
+                            radius,
+                            unit,
+                            count,
+                            asc,
+                            WithCoord<typename IterType<Output>::type>::value,
+                            WithDist<typename IterType<Output>::type>::value,
+                            WithHash<typename IterType<Output>::type>::value);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void RedisCluster::georadiusbymember(const StringView &key,
+                                const StringView &member,
+                                double radius,
+                                GeoUnit unit,
+                                long long count,
+                                bool asc,
+                                Output output) {
+    auto reply = command(cmd::georadiusbymember,
+                            key,
+                            member,
+                            radius,
+                            unit,
+                            count,
+                            asc,
+                            WithCoord<typename IterType<Output>::type>::value,
+                            WithDist<typename IterType<Output>::type>::value,
+                            WithHash<typename IterType<Output>::type>::value);
+
+    reply::to_array(*reply, output);
+}
+
+// SCRIPTING commands.
+
+template <typename Result>
+Result RedisCluster::eval(const StringView &script,
+                            std::initializer_list<StringView> keys,
+                            std::initializer_list<StringView> args) {
+    if (keys.size() == 0) {
+        throw Error("DO NOT support Lua script without key");
+    }
+
+    auto reply = _command(cmd::eval, *keys.begin(), script, keys, args);
+
+    return reply::parse<Result>(*reply);
+}
+
+template <typename Output>
+void RedisCluster::eval(const StringView &script,
+                        std::initializer_list<StringView> keys,
+                        std::initializer_list<StringView> args,
+                        Output output) {
+    if (keys.size() == 0) {
+        throw Error("DO NOT support Lua script without key");
+    }
+
+    auto reply = _command(cmd::eval, *keys.begin(), script, keys, args);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Result>
+Result RedisCluster::evalsha(const StringView &script,
+                                std::initializer_list<StringView> keys,
+                                std::initializer_list<StringView> args) {
+    if (keys.size() == 0) {
+        throw Error("DO NOT support Lua script without key");
+    }
+
+    auto reply = _command(cmd::evalsha, *keys.begin(), script, keys, args);
+
+    return reply::parse<Result>(*reply);
+}
+
+template <typename Output>
+void RedisCluster::evalsha(const StringView &script,
+                            std::initializer_list<StringView> keys,
+                            std::initializer_list<StringView> args,
+                            Output output) {
+    if (keys.size() == 0) {
+        throw Error("DO NOT support Lua script without key");
+    }
+
+    auto reply = command(cmd::evalsha, *keys.begin(), script, keys, args);
+
+    reply::to_array(*reply, output);
+}
+
+// Stream commands.
+
+template <typename Input>
+long long RedisCluster::xack(const StringView &key,
+                                const StringView &group,
+                                Input first,
+                                Input last) {
+    auto reply = command(cmd::xack_range<Input>, key, group, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Input>
+std::string RedisCluster::xadd(const StringView &key,
+                                const StringView &id,
+                                Input first,
+                                Input last) {
+    auto reply = command(cmd::xadd_range<Input>, key, id, first, last);
+
+    return reply::parse<std::string>(*reply);
+}
+
+template <typename Input>
+std::string RedisCluster::xadd(const StringView &key,
+                                const StringView &id,
+                                Input first,
+                                Input last,
+                                long long count,
+                                bool approx) {
+    auto reply = command(cmd::xadd_maxlen_range<Input>, key, id, first, last, count, approx);
+
+    return reply::parse<std::string>(*reply);
+}
+
+template <typename Output>
+void RedisCluster::xclaim(const StringView &key,
+                            const StringView &group,
+                            const StringView &consumer,
+                            const std::chrono::milliseconds &min_idle_time,
+                            const StringView &id,
+                            Output output) {
+    auto reply = command(cmd::xclaim, key, group, consumer, min_idle_time.count(), id);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input, typename Output>
+void RedisCluster::xclaim(const StringView &key,
+                            const StringView &group,
+                            const StringView &consumer,
+                            const std::chrono::milliseconds &min_idle_time,
+                            Input first,
+                            Input last,
+                            Output output) {
+    auto reply = command(cmd::xclaim_range<Input>,
+                            key,
+                            group,
+                            consumer,
+                            min_idle_time.count(),
+                            first,
+                            last);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Input>
+long long RedisCluster::xdel(const StringView &key, Input first, Input last) {
+    auto reply = command(cmd::xdel_range<Input>, key, first, last);
+
+    return reply::parse<long long>(*reply);
+}
+
+template <typename Output>
+auto RedisCluster::xpending(const StringView &key, const StringView &group, Output output)
+    -> std::tuple<long long, OptionalString, OptionalString> {
+    auto reply = command(cmd::xpending, key, group);
+
+    return reply::parse_xpending_reply(*reply, output);
+}
+
+template <typename Output>
+void RedisCluster::xpending(const StringView &key,
+                            const StringView &group,
+                            const StringView &start,
+                            const StringView &end,
+                            long long count,
+                            Output output) {
+    auto reply = command(cmd::xpending_detail, key, group, start, end, count);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void RedisCluster::xpending(const StringView &key,
+                            const StringView &group,
+                            const StringView &start,
+                            const StringView &end,
+                            long long count,
+                            const StringView &consumer,
+                            Output output) {
+    auto reply = command(cmd::xpending_per_consumer, key, group, start, end, count, consumer);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void RedisCluster::xrange(const StringView &key,
+                            const StringView &start,
+                            const StringView &end,
+                            Output output) {
+    auto reply = command(cmd::xrange, key, start, end);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void RedisCluster::xrange(const StringView &key,
+                            const StringView &start,
+                            const StringView &end,
+                            long long count,
+                            Output output) {
+    auto reply = command(cmd::xrange_count, key, start, end, count);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void RedisCluster::xread(const StringView &key,
+                            const StringView &id,
+                            long long count,
+                            Output output) {
+    auto reply = command(cmd::xread, key, id, count);
+
+    if (!reply::is_nil(*reply)) {
+        reply::to_array(*reply, output);
+    }
+}
+
+template <typename Input, typename Output>
+auto RedisCluster::xread(Input first, Input last, long long count, Output output)
+    -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+    if (first == last) {
+        throw Error("XREAD: no key specified");
+    }
+
+    auto reply = command(cmd::xread_range<Input>, first, last, count);
+
+    if (!reply::is_nil(*reply)) {
+        reply::to_array(*reply, output);
+    }
+}
+
+template <typename Output>
+void RedisCluster::xread(const StringView &key,
+                            const StringView &id,
+                            const std::chrono::milliseconds &timeout,
+                            long long count,
+                            Output output) {
+    auto reply = command(cmd::xread_block, key, id, timeout.count(), count);
+
+    if (!reply::is_nil(*reply)) {
+        reply::to_array(*reply, output);
+    }
+}
+
+template <typename Input, typename Output>
+auto RedisCluster::xread(Input first,
+                            Input last,
+                            const std::chrono::milliseconds &timeout,
+                            long long count,
+                            Output output)
+    -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+    if (first == last) {
+        throw Error("XREAD: no key specified");
+    }
+
+    auto reply = command(cmd::xread_block_range<Input>, first, last, timeout.count(), count);
+
+    if (!reply::is_nil(*reply)) {
+        reply::to_array(*reply, output);
+    }
+}
+
+template <typename Output>
+void RedisCluster::xreadgroup(const StringView &group,
+                                const StringView &consumer,
+                                const StringView &key,
+                                const StringView &id,
+                                long long count,
+                                bool noack,
+                                Output output) {
+    auto reply = _command(cmd::xreadgroup, key, group, consumer, key, id, count, noack);
+
+    if (!reply::is_nil(*reply)) {
+        reply::to_array(*reply, output);
+    }
+}
+
+template <typename Input, typename Output>
+auto RedisCluster::xreadgroup(const StringView &group,
+                                const StringView &consumer,
+                                Input first,
+                                Input last,
+                                long long count,
+                                bool noack,
+                                Output output)
+    -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+    if (first == last) {
+        throw Error("XREADGROUP: no key specified");
+    }
+
+    auto reply = _command(cmd::xreadgroup_range<Input>,
+                            first->first,
+                            group,
+                            consumer,
+                            first,
+                            last,
+                            count,
+                            noack);
+
+    if (!reply::is_nil(*reply)) {
+        reply::to_array(*reply, output);
+    }
+}
+
+template <typename Output>
+void RedisCluster::xreadgroup(const StringView &group,
+                                const StringView &consumer,
+                                const StringView &key,
+                                const StringView &id,
+                                const std::chrono::milliseconds &timeout,
+                                long long count,
+                                bool noack,
+                                Output output) {
+    auto reply = _command(cmd::xreadgroup_block,
+                            key,
+                            group,
+                            consumer,
+                            key,
+                            id,
+                            timeout.count(),
+                            count,
+                            noack);
+
+    if (!reply::is_nil(*reply)) {
+        reply::to_array(*reply, output);
+    }
+}
+
+template <typename Input, typename Output>
+auto RedisCluster::xreadgroup(const StringView &group,
+                                const StringView &consumer,
+                                Input first,
+                                Input last,
+                                const std::chrono::milliseconds &timeout,
+                                long long count,
+                                bool noack,
+                                Output output)
+    -> typename std::enable_if<!std::is_convertible<Input, StringView>::value>::type {
+    if (first == last) {
+        throw Error("XREADGROUP: no key specified");
+    }
+
+    auto reply = _command(cmd::xreadgroup_block_range<Input>,
+                            first->first,
+                            group,
+                            consumer,
+                            first,
+                            last,
+                            timeout.count(),
+                            count,
+                            noack);
+
+    if (!reply::is_nil(*reply)) {
+        reply::to_array(*reply, output);
+    }
+}
+
+template <typename Output>
+void RedisCluster::xrevrange(const StringView &key,
+                            const StringView &end,
+                            const StringView &start,
+                            Output output) {
+    auto reply = command(cmd::xrevrange, key, end, start);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Output>
+void RedisCluster::xrevrange(const StringView &key,
+                                const StringView &end,
+                                const StringView &start,
+                                long long count,
+                                Output output) {
+    auto reply = command(cmd::xrevrange_count, key, end, start, count);
+
+    reply::to_array(*reply, output);
+}
+
+template <typename Cmd, typename Key, typename ...Args>
+auto RedisCluster::_generic_command(Cmd cmd, Key &&key, Args &&...args)
+    -> typename std::enable_if<std::is_convertible<Key, StringView>::value,
+                                ReplyUPtr>::type {
+    return command(cmd, std::forward<Key>(key), std::forward<Args>(args)...);
+}
+
+template <typename Cmd, typename Key, typename ...Args>
+auto RedisCluster::_generic_command(Cmd cmd, Key &&key, Args &&...args)
+    -> typename std::enable_if<std::is_arithmetic<typename std::decay<Key>::type>::value,
+                                ReplyUPtr>::type {
+    auto k = std::to_string(std::forward<Key>(key));
+    return command(cmd, k, std::forward<Args>(args)...);
+}
+
+template <typename Cmd, typename ...Args>
+ReplyUPtr RedisCluster::_command(Cmd cmd, std::true_type, const StringView &key, Args &&...args) {
+    return _command(cmd, key, key, std::forward<Args>(args)...);
+}
+
+template <typename Cmd, typename Input, typename ...Args>
+ReplyUPtr RedisCluster::_command(Cmd cmd, std::false_type, Input &&first, Args &&...args) {
+    return _range_command(cmd,
+                            std::is_convertible<
+                                typename std::decay<
+                                    decltype(*std::declval<Input>())>::type, StringView>(),
+                            std::forward<Input>(first),
+                            std::forward<Args>(args)...);
+}
+
+template <typename Cmd, typename Input, typename ...Args>
+ReplyUPtr RedisCluster::_range_command(Cmd cmd, std::true_type, Input input, Args &&...args) {
+    return _command(cmd, *input, input, std::forward<Args>(args)...);
+}
+
+template <typename Cmd, typename Input, typename ...Args>
+ReplyUPtr RedisCluster::_range_command(Cmd cmd, std::false_type, Input input, Args &&...args) {
+    return _command(cmd, std::get<0>(*input), input, std::forward<Args>(args)...);
+}
+
+template <typename Cmd, typename ...Args>
+ReplyUPtr RedisCluster::_command(Cmd cmd, Connection &connection, Args &&...args) {
+    assert(!connection.broken());
+
+    cmd(connection, std::forward<Args>(args)...);
+
+    return connection.recv();
+}
+
+template <typename Cmd, typename ...Args>
+ReplyUPtr RedisCluster::_command(Cmd cmd, const StringView &key, Args &&...args) {
+    for (auto idx = 0; idx < 2; ++idx) {
+        try {
+            auto guarded_connection = _pool.fetch(key);
+
+            return _command(cmd, guarded_connection.connection(), std::forward<Args>(args)...);
+        } catch (const IoError &err) {
+            // When master is down, one of its replicas will be promoted to be the new master.
+            // If we try to send command to the old master, we'll get an *IoError*.
+            // In this case, we need to update the slots mapping.
+            _pool.update();
+        } catch (const ClosedError &err) {
+            // Node might be removed.
+            // 1. Get up-to-date slot mapping to check if the node still exists.
+            _pool.update();
+
+            // TODO:
+            // 2. If it's NOT exist, update slot mapping, and retry.
+            // 3. If it's still exist, that means the node is down, NOT removed, throw exception.
+        } catch (const MovedError &err) {
+            // Slot mapping has been changed, update it and try again.
+            _pool.update();
+        } catch (const AskError &err) {
+            auto guarded_connection = _pool.fetch(err.node());
+            auto &connection = guarded_connection.connection();
+
+            // 1. send ASKING command.
+            _asking(connection);
+
+            // 2. resend last command.
+            try {
+                return _command(cmd, connection, std::forward<Args>(args)...);
+            } catch (const MovedError &err) {
+                throw Error("Slot migrating... ASKING node hasn't been set to IMPORTING state");
+            }
+        } // For other exceptions, just throw it.
+    }
+
+    // Possible failures:
+    // 1. Source node has already run 'CLUSTER SETSLOT xxx NODE xxx',
+    //    while the destination node has NOT run it.
+    //    In this case, client will be redirected by both nodes with MovedError.
+    // 2. Other failures...
+    throw Error("Failed to send command with key: " + std::string(key.data(), key.size()));
+}
+
+template <typename Cmd, typename ...Args>
+inline ReplyUPtr RedisCluster::_score_command(std::true_type, Cmd cmd, Args &&... args) {
+    return command(cmd, std::forward<Args>(args)..., true);
+}
+
+template <typename Cmd, typename ...Args>
+inline ReplyUPtr RedisCluster::_score_command(std::false_type, Cmd cmd, Args &&... args) {
+    return command(cmd, std::forward<Args>(args)..., false);
+}
+
+template <typename Output, typename Cmd, typename ...Args>
+inline ReplyUPtr RedisCluster::_score_command(Cmd cmd, Args &&... args) {
+    return _score_command(typename IsKvPairIter<Output>::type(),
+                            cmd,
+                            std::forward<Args>(args)...);
+}
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_REDIS_CLUSTER_HPP

+ 150 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/reply.cpp

@@ -0,0 +1,150 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#include "reply.h"
+
+namespace sw {
+
+namespace redis {
+
+namespace reply {
+
+std::string to_status(redisReply &reply) {
+    if (!reply::is_status(reply)) {
+        throw ProtoError("Expect STATUS reply");
+    }
+
+    if (reply.str == nullptr) {
+        throw ProtoError("A null status reply");
+    }
+
+    // Old version hiredis' *redisReply::len* is of type int.
+    // So we CANNOT have something like: *return {reply.str, reply.len}*.
+    return std::string(reply.str, reply.len);
+}
+
+std::string parse(ParseTag<std::string>, redisReply &reply) {
+    if (!reply::is_string(reply) && !reply::is_status(reply)) {
+        throw ProtoError("Expect STRING reply");
+    }
+
+    if (reply.str == nullptr) {
+        throw ProtoError("A null string reply");
+    }
+
+    // Old version hiredis' *redisReply::len* is of type int.
+    // So we CANNOT have something like: *return {reply.str, reply.len}*.
+    return std::string(reply.str, reply.len);
+}
+
+long long parse(ParseTag<long long>, redisReply &reply) {
+    if (!reply::is_integer(reply)) {
+        throw ProtoError("Expect INTEGER reply");
+    }
+
+    return reply.integer;
+}
+
+double parse(ParseTag<double>, redisReply &reply) {
+    return std::stod(parse<std::string>(reply));
+}
+
+bool parse(ParseTag<bool>, redisReply &reply) {
+    auto ret = parse<long long>(reply);
+
+    if (ret == 1) {
+        return true;
+    } else if (ret == 0) {
+        return false;
+    } else {
+        throw ProtoError("Invalid bool reply: " + std::to_string(ret));
+    }
+}
+
+void parse(ParseTag<void>, redisReply &reply) {
+    if (!reply::is_status(reply)) {
+        throw ProtoError("Expect STATUS reply");
+    }
+
+    if (reply.str == nullptr) {
+        throw ProtoError("A null status reply");
+    }
+
+    static const std::string OK = "OK";
+
+    // Old version hiredis' *redisReply::len* is of type int.
+    // So we have to cast it to an unsigned int.
+    if (static_cast<std::size_t>(reply.len) != OK.size()
+            || OK.compare(0, OK.size(), reply.str, reply.len) != 0) {
+        throw ProtoError("NOT ok status reply: " + reply::to_status(reply));
+    }
+}
+
+void rewrite_set_reply(redisReply &reply) {
+    if (is_nil(reply)) {
+        // Failed to set, and make it a FALSE reply.
+        reply.type = REDIS_REPLY_INTEGER;
+        reply.integer = 0;
+
+        return;
+    }
+
+    // Check if it's a "OK" status reply.
+    reply::parse<void>(reply);
+
+    assert(is_status(reply) && reply.str != nullptr);
+
+    free(reply.str);
+
+    // Make it a TRUE reply.
+    reply.type = REDIS_REPLY_INTEGER;
+    reply.integer = 1;
+}
+
+void rewrite_georadius_reply(redisReply &reply) {
+    if (is_array(reply) && reply.element == nullptr) {
+        // Make it a nil reply.
+        reply.type = REDIS_REPLY_NIL;
+    }
+}
+
+namespace detail {
+
+bool is_flat_array(redisReply &reply) {
+    assert(reply::is_array(reply));
+
+    // Empty array reply.
+    if (reply.element == nullptr || reply.elements == 0) {
+        return false;
+    }
+
+    auto *sub_reply = reply.element[0];
+
+    // Null element.
+    if (sub_reply == nullptr) {
+        return false;
+    }
+
+    return !reply::is_array(*sub_reply);
+}
+
+}
+
+}
+
+}
+
+}

+ 363 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/reply.h

@@ -0,0 +1,363 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_REPLY_H
+#define SEWENEW_REDISPLUSPLUS_REPLY_H
+
+#include <cassert>
+#include <string>
+#include <memory>
+#include <functional>
+#include <tuple>
+#include <hiredis/hiredis.h>
+#include "errors.h"
+#include "utils.h"
+
+namespace sw {
+
+namespace redis {
+
+struct ReplyDeleter {
+    void operator()(redisReply *reply) const {
+        if (reply != nullptr) {
+            freeReplyObject(reply);
+        }
+    }
+};
+
+using ReplyUPtr = std::unique_ptr<redisReply, ReplyDeleter>;
+
+namespace reply {
+
+template <typename T>
+struct ParseTag {};
+
+template <typename T>
+inline T parse(redisReply &reply) {
+    return parse(ParseTag<T>(), reply);
+}
+
+void parse(ParseTag<void>, redisReply &reply);
+
+std::string parse(ParseTag<std::string>, redisReply &reply);
+
+long long parse(ParseTag<long long>, redisReply &reply);
+
+double parse(ParseTag<double>, redisReply &reply);
+
+bool parse(ParseTag<bool>, redisReply &reply);
+
+template <typename T>
+Optional<T> parse(ParseTag<Optional<T>>, redisReply &reply);
+
+template <typename T, typename U>
+std::pair<T, U> parse(ParseTag<std::pair<T, U>>, redisReply &reply);
+
+template <typename ...Args>
+std::tuple<Args...> parse(ParseTag<std::tuple<Args...>>, redisReply &reply);
+
+template <typename T, typename std::enable_if<IsSequenceContainer<T>::value, int>::type = 0>
+T parse(ParseTag<T>, redisReply &reply);
+
+template <typename T, typename std::enable_if<IsAssociativeContainer<T>::value, int>::type = 0>
+T parse(ParseTag<T>, redisReply &reply);
+
+template <typename Output>
+long long parse_scan_reply(redisReply &reply, Output output);
+
+inline bool is_error(redisReply &reply) {
+    return reply.type == REDIS_REPLY_ERROR;
+}
+
+inline bool is_nil(redisReply &reply) {
+    return reply.type == REDIS_REPLY_NIL;
+}
+
+inline bool is_string(redisReply &reply) {
+    return reply.type == REDIS_REPLY_STRING;
+}
+
+inline bool is_status(redisReply &reply) {
+    return reply.type == REDIS_REPLY_STATUS;
+}
+
+inline bool is_integer(redisReply &reply) {
+    return reply.type == REDIS_REPLY_INTEGER;
+}
+
+inline bool is_array(redisReply &reply) {
+    return reply.type == REDIS_REPLY_ARRAY;
+}
+
+std::string to_status(redisReply &reply);
+
+template <typename Output>
+void to_array(redisReply &reply, Output output);
+
+// Rewrite set reply to bool type
+void rewrite_set_reply(redisReply &reply);
+
+// Rewrite georadius reply to OptionalLongLong type
+void rewrite_georadius_reply(redisReply &reply);
+
+template <typename Output>
+auto parse_xpending_reply(redisReply &reply, Output output)
+    -> std::tuple<long long, OptionalString, OptionalString>;
+
+}
+
+// Inline implementations.
+
+namespace reply {
+
+namespace detail {
+
+template <typename Output>
+void to_array(redisReply &reply, Output output) {
+    if (!is_array(reply)) {
+        throw ProtoError("Expect ARRAY reply");
+    }
+
+    if (reply.element == nullptr) {
+        // Empty array.
+        return;
+    }
+
+    for (std::size_t idx = 0; idx != reply.elements; ++idx) {
+        auto *sub_reply = reply.element[idx];
+        if (sub_reply == nullptr) {
+            throw ProtoError("Null array element reply");
+        }
+
+        *output = parse<typename IterType<Output>::type>(*sub_reply);
+
+        ++output;
+    }
+}
+
+bool is_flat_array(redisReply &reply);
+
+template <typename Output>
+void to_flat_array(redisReply &reply, Output output) {
+    if (reply.element == nullptr) {
+        // Empty array.
+        return;
+    }
+
+    if (reply.elements % 2 != 0) {
+        throw ProtoError("Not string pair array reply");
+    }
+
+    for (std::size_t idx = 0; idx != reply.elements; idx += 2) {
+        auto *key_reply = reply.element[idx];
+        auto *val_reply = reply.element[idx + 1];
+        if (key_reply == nullptr || val_reply == nullptr) {
+            throw ProtoError("Null string array reply");
+        }
+
+        using Pair = typename IterType<Output>::type;
+        using FirstType = typename std::decay<typename Pair::first_type>::type;
+        using SecondType = typename std::decay<typename Pair::second_type>::type;
+        *output = std::make_pair(parse<FirstType>(*key_reply),
+                                    parse<SecondType>(*val_reply));
+
+        ++output;
+    }
+}
+
+template <typename Output>
+void to_array(std::true_type, redisReply &reply, Output output) {
+    if (is_flat_array(reply)) {
+        to_flat_array(reply, output);
+    } else {
+        to_array(reply, output);
+    }
+}
+
+template <typename Output>
+void to_array(std::false_type, redisReply &reply, Output output) {
+    to_array(reply, output);
+}
+
+template <typename T>
+std::tuple<T> parse_tuple(redisReply **reply, std::size_t idx) {
+    assert(reply != nullptr);
+
+    auto *sub_reply = reply[idx];
+    if (sub_reply == nullptr) {
+        throw ProtoError("Null reply");
+    }
+
+    return std::make_tuple(parse<T>(*sub_reply));
+}
+
+template <typename T, typename ...Args>
+auto parse_tuple(redisReply **reply, std::size_t idx) ->
+    typename std::enable_if<sizeof...(Args) != 0, std::tuple<T, Args...>>::type {
+    assert(reply != nullptr);
+
+    return std::tuple_cat(parse_tuple<T>(reply, idx),
+                            parse_tuple<Args...>(reply, idx + 1));
+}
+
+}
+
+template <typename T>
+Optional<T> parse(ParseTag<Optional<T>>, redisReply &reply) {
+    if (reply::is_nil(reply)) {
+        return {};
+    }
+
+    return Optional<T>(parse<T>(reply));
+}
+
+template <typename T, typename U>
+std::pair<T, U> parse(ParseTag<std::pair<T, U>>, redisReply &reply) {
+    if (!is_array(reply)) {
+        throw ProtoError("Expect ARRAY reply");
+    }
+
+    if (reply.elements != 2) {
+        throw ProtoError("NOT key-value PAIR reply");
+    }
+
+    if (reply.element == nullptr) {
+        throw ProtoError("Null PAIR reply");
+    }
+
+    auto *first = reply.element[0];
+    auto *second = reply.element[1];
+    if (first == nullptr || second == nullptr) {
+        throw ProtoError("Null pair reply");
+    }
+
+    return std::make_pair(parse<typename std::decay<T>::type>(*first),
+                            parse<typename std::decay<U>::type>(*second));
+}
+
+template <typename ...Args>
+std::tuple<Args...> parse(ParseTag<std::tuple<Args...>>, redisReply &reply) {
+    constexpr auto size = sizeof...(Args);
+
+    static_assert(size > 0, "DO NOT support parsing tuple with 0 element");
+
+    if (!is_array(reply)) {
+        throw ProtoError("Expect ARRAY reply");
+    }
+
+    if (reply.elements != size) {
+        throw ProtoError("Expect tuple reply with " + std::to_string(size) + "elements");
+    }
+
+    if (reply.element == nullptr) {
+        throw ProtoError("Null TUPLE reply");
+    }
+
+    return detail::parse_tuple<Args...>(reply.element, 0);
+}
+
+template <typename T, typename std::enable_if<IsSequenceContainer<T>::value, int>::type>
+T parse(ParseTag<T>, redisReply &reply) {
+    if (!is_array(reply)) {
+        throw ProtoError("Expect ARRAY reply");
+    }
+
+    T container;
+
+    to_array(reply, std::back_inserter(container));
+
+    return container;
+}
+
+template <typename T, typename std::enable_if<IsAssociativeContainer<T>::value, int>::type>
+T parse(ParseTag<T>, redisReply &reply) {
+    if (!is_array(reply)) {
+        throw ProtoError("Expect ARRAY reply");
+    }
+
+    T container;
+
+    to_array(reply, std::inserter(container, container.end()));
+
+    return container;
+}
+
+template <typename Output>
+long long parse_scan_reply(redisReply &reply, Output output) {
+    if (reply.elements != 2 || reply.element == nullptr) {
+        throw ProtoError("Invalid scan reply");
+    }
+
+    auto *cursor_reply = reply.element[0];
+    auto *data_reply = reply.element[1];
+    if (cursor_reply == nullptr || data_reply == nullptr) {
+        throw ProtoError("Invalid cursor reply or data reply");
+    }
+
+    auto cursor_str = reply::parse<std::string>(*cursor_reply);
+    auto new_cursor = 0;
+    try {
+        new_cursor = std::stoll(cursor_str);
+    } catch (const std::exception &e) {
+        throw ProtoError("Invalid cursor reply: " + cursor_str);
+    }
+
+    reply::to_array(*data_reply, output);
+
+    return new_cursor;
+}
+
+template <typename Output>
+void to_array(redisReply &reply, Output output) {
+    if (!is_array(reply)) {
+        throw ProtoError("Expect ARRAY reply");
+    }
+
+    detail::to_array(typename IsKvPairIter<Output>::type(), reply, output);
+}
+
+template <typename Output>
+auto parse_xpending_reply(redisReply &reply, Output output)
+    -> std::tuple<long long, OptionalString, OptionalString> {
+    if (!is_array(reply) || reply.elements != 4) {
+        throw ProtoError("expect array reply with 4 elements");
+    }
+
+    for (std::size_t idx = 0; idx != reply.elements; ++idx) {
+        if (reply.element[idx] == nullptr) {
+            throw ProtoError("null array reply");
+        }
+    }
+
+    auto num = parse<long long>(*(reply.element[0]));
+    auto start = parse<OptionalString>(*(reply.element[1]));
+    auto end = parse<OptionalString>(*(reply.element[2]));
+
+    auto &entry_reply = *(reply.element[3]);
+    if (!is_nil(entry_reply)) {
+        to_array(entry_reply, output);
+    }
+
+    return std::make_tuple(num, std::move(start), std::move(end));
+}
+
+}
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_REPLY_H

+ 361 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/sentinel.cpp

@@ -0,0 +1,361 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#include "sentinel.h"
+#include <cassert>
+#include <thread>
+#include <random>
+#include <algorithm>
+#include "redis.h"
+#include "errors.h"
+
+namespace sw {
+
+namespace redis {
+
+class Sentinel::Iterator {
+public:
+    Iterator(std::list<Connection> &healthy_sentinels,
+                std::list<ConnectionOptions> &broken_sentinels) :
+                    _healthy_sentinels(healthy_sentinels),
+                    _broken_sentinels(broken_sentinels) {
+        reset();
+    }
+
+    Connection& next();
+
+    void reset();
+
+private:
+    std::list<Connection> &_healthy_sentinels;
+
+    std::size_t _healthy_size = 0;
+
+    std::list<ConnectionOptions> &_broken_sentinels;
+
+    std::size_t _broken_size = 0;
+};
+
+Connection& Sentinel::Iterator::next() {
+    while (_healthy_size > 0) {
+        assert(_healthy_sentinels.size() >= _healthy_size);
+
+        --_healthy_size;
+
+        auto &connection = _healthy_sentinels.front();
+        if (connection.broken()) {
+            _broken_sentinels.push_front(connection.options());
+            ++_broken_size;
+
+            _healthy_sentinels.pop_front();
+        } else {
+            _healthy_sentinels.splice(_healthy_sentinels.end(),
+                                        _healthy_sentinels,
+                                        _healthy_sentinels.begin());
+
+            return _healthy_sentinels.back();
+        }
+    }
+
+    while (_broken_size > 0) {
+        assert(_broken_sentinels.size() >= _broken_size);
+
+        --_broken_size;
+
+        try {
+            const auto &opt = _broken_sentinels.front();
+            Connection connection(opt);
+            _healthy_sentinels.push_back(std::move(connection));
+
+            _broken_sentinels.pop_front();
+
+            return _healthy_sentinels.back();
+        } catch (const Error &e) {
+            // Failed to connect to sentinel.
+            _broken_sentinels.splice(_broken_sentinels.end(),
+                                        _broken_sentinels,
+                                        _broken_sentinels.begin());
+        }
+    }
+
+    throw StopIterError();
+}
+
+void Sentinel::Iterator::reset() {
+    _healthy_size = _healthy_sentinels.size();
+    _broken_size = _broken_sentinels.size();
+}
+
+Sentinel::Sentinel(const SentinelOptions &sentinel_opts) :
+                    _broken_sentinels(_parse_options(sentinel_opts)),
+                    _sentinel_opts(sentinel_opts) {
+    if (_sentinel_opts.connect_timeout == std::chrono::milliseconds(0)
+            || _sentinel_opts.socket_timeout == std::chrono::milliseconds(0)) {
+        throw Error("With sentinel, connection timeout and socket timeout cannot be 0");
+    }
+}
+
+Connection Sentinel::master(const std::string &master_name, const ConnectionOptions &opts) {
+    std::lock_guard<std::mutex> lock(_mutex);
+
+    Iterator iter(_healthy_sentinels, _broken_sentinels);
+    std::size_t retries = 0;
+    while (true) {
+        try {
+            auto &sentinel = iter.next();
+
+            auto master = _get_master_addr_by_name(sentinel, master_name);
+            if (!master) {
+                // Try the next sentinel.
+                continue;
+            }
+
+            auto connection = _connect_redis(*master, opts);
+            if (_get_role(connection) != Role::MASTER) {
+                // Retry the whole process at most SentinelOptions::max_retry times.
+                ++retries;
+                if (retries > _sentinel_opts.max_retry) {
+                    throw Error("Failed to get master from sentinel");
+                }
+
+                std::this_thread::sleep_for(_sentinel_opts.retry_interval);
+
+                // Restart the iteration.
+                iter.reset();
+                continue;
+            }
+
+            return connection;
+        } catch (const StopIterError &e) {
+            throw;
+        } catch (const Error &e) {
+            continue;
+        }
+    }
+}
+
+Connection Sentinel::slave(const std::string &master_name, const ConnectionOptions &opts) {
+    std::lock_guard<std::mutex> lock(_mutex);
+
+    Iterator iter(_healthy_sentinels, _broken_sentinels);
+    std::size_t retries = 0;
+    while (true) {
+        try {
+            auto &sentinel = iter.next();
+
+            auto slaves = _get_slave_addr_by_name(sentinel, master_name);
+            if (slaves.empty()) {
+                // Try the next sentinel.
+                continue;
+            }
+
+            // Normally slaves list is NOT very large, so there won't be a performance problem.
+            auto slave_iter = std::find(slaves.begin(),
+                                        slaves.end(),
+                                        Node{opts.host, opts.port});
+            if (slave_iter != slaves.end() && slave_iter != slaves.begin()) {
+                // The given node is still a valid slave. Try it first.
+                std::swap(*(slaves.begin()), *slave_iter);
+            }
+
+            for (const auto &slave : slaves) {
+                try {
+                    auto connection = _connect_redis(slave, opts);
+                    if (_get_role(connection) != Role::SLAVE) {
+                        // Retry the whole process at most SentinelOptions::max_retry times.
+                        ++retries;
+                        if (retries > _sentinel_opts.max_retry) {
+                            throw Error("Failed to get slave from sentinel");
+                        }
+
+                        std::this_thread::sleep_for(_sentinel_opts.retry_interval);
+
+                        // Restart the iteration.
+                        iter.reset();
+                        break;
+                    }
+
+                    return connection;
+                } catch (const Error &e) {
+                    // Try the next slave.
+                    continue;
+                }
+            }
+        } catch (const StopIterError &e) {
+            throw;
+        } catch (const Error &e) {
+            continue;
+        }
+    }
+}
+
+Optional<Node> Sentinel::_get_master_addr_by_name(Connection &connection, const StringView &name) {
+    connection.send("SENTINEL GET-MASTER-ADDR-BY-NAME %b", name.data(), name.size());
+
+    auto reply = connection.recv();
+
+    assert(reply);
+
+    auto master = reply::parse<Optional<std::pair<std::string, std::string>>>(*reply);
+    if (!master) {
+        return {};
+    }
+
+    int port = 0;
+    try {
+        port = std::stoi(master->second);
+    } catch (const std::exception &) {
+        throw ProtoError("Master port is invalid: " + master->second);
+    }
+
+    return Optional<Node>{Node{master->first, port}};
+}
+
+std::vector<Node> Sentinel::_get_slave_addr_by_name(Connection &connection,
+                                                    const StringView &name) {
+    try {
+        connection.send("SENTINEL SLAVES %b", name.data(), name.size());
+
+        auto reply = connection.recv();
+
+        assert(reply);
+
+        auto slaves = _parse_slave_info(*reply);
+
+        // Make slave list random.
+        std::mt19937 gen(std::random_device{}());
+        std::shuffle(slaves.begin(), slaves.end(), gen);
+
+        return slaves;
+    } catch (const ReplyError &e) {
+        // Unknown master name.
+        return {};
+    }
+}
+
+std::vector<Node> Sentinel::_parse_slave_info(redisReply &reply) const {
+    using SlaveInfo = std::unordered_map<std::string, std::string>;
+
+    auto slaves = reply::parse<std::vector<SlaveInfo>>(reply);
+
+    std::vector<Node> nodes;
+    for (const auto &slave : slaves) {
+        auto flags_iter = slave.find("flags");
+        auto ip_iter = slave.find("ip");
+        auto port_iter = slave.find("port");
+        if (flags_iter == slave.end() || ip_iter == slave.end() || port_iter == slave.end()) {
+            throw ProtoError("Invalid slave info");
+        }
+
+        // This slave is down, e.g. 's_down,slave,disconnected'
+        if (flags_iter->second != "slave") {
+            continue;
+        }
+
+        int port = 0;
+        try {
+            port = std::stoi(port_iter->second);
+        } catch (const std::exception &) {
+            throw ProtoError("Slave port is invalid: " + port_iter->second);
+        }
+
+        nodes.push_back(Node{ip_iter->second, port});
+    }
+
+    return nodes;
+}
+
+Connection Sentinel::_connect_redis(const Node &node, ConnectionOptions opts) {
+    opts.host = node.host;
+    opts.port = node.port;
+
+    return Connection(opts);
+}
+
+Role Sentinel::_get_role(Connection &connection) {
+    connection.send("INFO REPLICATION");
+    auto reply = connection.recv();
+
+    assert(reply);
+    auto info = reply::parse<std::string>(*reply);
+
+    auto start = info.find("role:");
+    if (start == std::string::npos) {
+        throw ProtoError("Invalid INFO REPLICATION reply");
+    }
+    start += 5;
+    auto stop = info.find("\r\n", start);
+    if (stop == std::string::npos) {
+        throw ProtoError("Invalid INFO REPLICATION reply");
+    }
+
+    auto role = info.substr(start, stop - start);
+    if (role == "master") {
+        return Role::MASTER;
+    } else if (role == "slave") {
+        return Role::SLAVE;
+    } else {
+        throw Error("Invalid role: " + role);
+    }
+}
+
+std::list<ConnectionOptions> Sentinel::_parse_options(const SentinelOptions &opts) const {
+    std::list<ConnectionOptions> options;
+    for (const auto &node : opts.nodes) {
+        ConnectionOptions opt;
+        opt.host = node.first;
+        opt.port = node.second;
+        opt.password = opts.password;
+        opt.keep_alive = opts.keep_alive;
+        opt.connect_timeout = opts.connect_timeout;
+        opt.socket_timeout = opts.socket_timeout;
+
+        options.push_back(opt);
+    }
+
+    return options;
+}
+
+SimpleSentinel::SimpleSentinel(const std::shared_ptr<Sentinel> &sentinel,
+                                const std::string &master_name,
+                                Role role) :
+                                    _sentinel(sentinel),
+                                    _master_name(master_name),
+                                    _role(role) {
+    if (!_sentinel) {
+        throw Error("Sentinel cannot be null");
+    }
+
+    if (_role != Role::MASTER && _role != Role::SLAVE) {
+        throw Error("Role must be Role::MASTER or Role::SLAVE");
+    }
+}
+
+Connection SimpleSentinel::create(const ConnectionOptions &opts) {
+    assert(_sentinel);
+
+    if (_role == Role::MASTER) {
+        return _sentinel->master(_master_name, opts);
+    }
+
+    assert(_role == Role::SLAVE);
+
+    return _sentinel->slave(_master_name, opts);
+}
+
+}
+
+}

+ 138 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/sentinel.h

@@ -0,0 +1,138 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_SENTINEL_H
+#define SEWENEW_REDISPLUSPLUS_SENTINEL_H
+
+#include <string>
+#include <list>
+#include <vector>
+#include <memory>
+#include <mutex>
+#include "connection.h"
+#include "shards.h"
+#include "reply.h"
+
+namespace sw {
+
+namespace redis {
+
+struct SentinelOptions {
+    std::vector<std::pair<std::string, int>> nodes;
+
+    std::string password;
+
+    bool keep_alive = true;
+
+    std::chrono::milliseconds connect_timeout{100};
+
+    std::chrono::milliseconds socket_timeout{100};
+
+    std::chrono::milliseconds retry_interval{100};
+
+    std::size_t max_retry = 2;
+};
+
+class Sentinel {
+public:
+    explicit Sentinel(const SentinelOptions &sentinel_opts);
+
+    Sentinel(const Sentinel &) = delete;
+    Sentinel& operator=(const Sentinel &) = delete;
+
+    Sentinel(Sentinel &&) = delete;
+    Sentinel& operator=(Sentinel &&) = delete;
+
+    ~Sentinel() = default;
+
+private:
+    Connection master(const std::string &master_name, const ConnectionOptions &opts);
+
+    Connection slave(const std::string &master_name, const ConnectionOptions &opts);
+
+    class Iterator;
+
+    friend class SimpleSentinel;
+
+    std::list<ConnectionOptions> _parse_options(const SentinelOptions &opts) const;
+
+    Optional<Node> _get_master_addr_by_name(Connection &connection, const StringView &name);
+
+    std::vector<Node> _get_slave_addr_by_name(Connection &connection, const StringView &name);
+
+    Connection _connect_redis(const Node &node, ConnectionOptions opts);
+
+    Role _get_role(Connection &connection);
+
+    std::vector<Node> _parse_slave_info(redisReply &reply) const;
+
+    std::list<Connection> _healthy_sentinels;
+
+    std::list<ConnectionOptions> _broken_sentinels;
+
+    SentinelOptions _sentinel_opts;
+
+    std::mutex _mutex;
+};
+
+class SimpleSentinel {
+public:
+    SimpleSentinel(const std::shared_ptr<Sentinel> &sentinel,
+                    const std::string &master_name,
+                    Role role);
+
+    SimpleSentinel() = default;
+
+    SimpleSentinel(const SimpleSentinel &) = default;
+    SimpleSentinel& operator=(const SimpleSentinel &) = default;
+
+    SimpleSentinel(SimpleSentinel &&) = default;
+    SimpleSentinel& operator=(SimpleSentinel &&) = default;
+
+    ~SimpleSentinel() = default;
+
+    explicit operator bool() const {
+        return bool(_sentinel);
+    }
+
+    Connection create(const ConnectionOptions &opts);
+
+private:
+    std::shared_ptr<Sentinel> _sentinel;
+
+    std::string _master_name;
+
+    Role _role = Role::MASTER;
+};
+
+class StopIterError : public Error {
+public:
+    StopIterError() : Error("StopIterError") {}
+
+    StopIterError(const StopIterError &) = default;
+    StopIterError& operator=(const StopIterError &) = default;
+
+    StopIterError(StopIterError &&) = default;
+    StopIterError& operator=(StopIterError &&) = default;
+
+    virtual ~StopIterError() = default;
+};
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_SENTINEL_H

+ 50 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/shards.cpp

@@ -0,0 +1,50 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#include "shards.h"
+
+namespace sw {
+
+namespace redis {
+
+RedirectionError::RedirectionError(const std::string &msg): ReplyError(msg) {
+    std::tie(_slot, _node) = _parse_error(msg);
+}
+
+std::pair<Slot, Node> RedirectionError::_parse_error(const std::string &msg) const {
+    // "slot ip:port"
+    auto space_pos = msg.find(" ");
+    auto colon_pos = msg.find(":");
+    if (space_pos == std::string::npos
+            || colon_pos == std::string::npos
+            || colon_pos < space_pos) {
+        throw ProtoError("Invalid ASK error message: " + msg);
+    }
+
+    try {
+        auto slot = std::stoull(msg.substr(0, space_pos));
+        auto host = msg.substr(space_pos + 1, colon_pos - space_pos - 1);
+        auto port = std::stoi(msg.substr(colon_pos + 1));
+
+        return {slot, {host, port}};
+    } catch (const std::exception &e) {
+        throw ProtoError("Invalid ASK error message: " + msg);
+    }
+}
+
+}
+
+}

+ 115 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/shards.h

@@ -0,0 +1,115 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_SHARDS_H
+#define SEWENEW_REDISPLUSPLUS_SHARDS_H
+
+#include <string>
+#include <map>
+#include "errors.h"
+
+namespace sw {
+
+namespace redis {
+
+using Slot = std::size_t;
+
+struct SlotRange {
+    Slot min;
+    Slot max;
+};
+
+inline bool operator<(const SlotRange &lhs, const SlotRange &rhs) {
+    return lhs.max < rhs.max;
+}
+
+struct Node {
+    std::string host;
+    int port;
+};
+
+inline bool operator==(const Node &lhs, const Node &rhs) {
+    return lhs.host == rhs.host && lhs.port == rhs.port;
+}
+
+struct NodeHash {
+    std::size_t operator()(const Node &node) const noexcept {
+        auto host_hash = std::hash<std::string>{}(node.host);
+        auto port_hash = std::hash<int>{}(node.port);
+        return host_hash ^ (port_hash << 1);
+    }
+};
+
+using Shards = std::map<SlotRange, Node>;
+
+class RedirectionError : public ReplyError {
+public:
+    RedirectionError(const std::string &msg);
+
+    RedirectionError(const RedirectionError &) = default;
+    RedirectionError& operator=(const RedirectionError &) = default;
+
+    RedirectionError(RedirectionError &&) = default;
+    RedirectionError& operator=(RedirectionError &&) = default;
+
+    virtual ~RedirectionError() = default;
+
+    Slot slot() const {
+        return _slot;
+    }
+
+    const Node& node() const {
+        return _node;
+    }
+
+private:
+    std::pair<Slot, Node> _parse_error(const std::string &msg) const;
+
+    Slot _slot = 0;
+    Node _node;
+};
+
+class MovedError : public RedirectionError {
+public:
+    explicit MovedError(const std::string &msg) : RedirectionError(msg) {}
+
+    MovedError(const MovedError &) = default;
+    MovedError& operator=(const MovedError &) = default;
+
+    MovedError(MovedError &&) = default;
+    MovedError& operator=(MovedError &&) = default;
+
+    virtual ~MovedError() = default;
+};
+
+class AskError : public RedirectionError {
+public:
+    explicit AskError(const std::string &msg) : RedirectionError(msg) {}
+
+    AskError(const AskError &) = default;
+    AskError& operator=(const AskError &) = default;
+
+    AskError(AskError &&) = default;
+    AskError& operator=(AskError &&) = default;
+
+    virtual ~AskError() = default;
+};
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_SHARDS_H

+ 319 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/shards_pool.cpp

@@ -0,0 +1,319 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#include "shards_pool.h"
+#include <unordered_set>
+#include "errors.h"
+
+namespace sw {
+
+namespace redis {
+
+const std::size_t ShardsPool::SHARDS;
+
+ShardsPool::ShardsPool(const ConnectionPoolOptions &pool_opts,
+                        const ConnectionOptions &connection_opts) :
+                            _pool_opts(pool_opts),
+                            _connection_opts(connection_opts) {
+    if (_connection_opts.type != ConnectionType::TCP) {
+        throw Error("Only support TCP connection for Redis Cluster");
+    }
+
+    Connection connection(_connection_opts);
+
+    _shards = _cluster_slots(connection);
+
+    _init_pool(_shards);
+}
+
+ShardsPool::ShardsPool(ShardsPool &&that) {
+    std::lock_guard<std::mutex> lock(that._mutex);
+
+    _move(std::move(that));
+}
+
+ShardsPool& ShardsPool::operator=(ShardsPool &&that) {
+    if (this != &that) {
+        std::lock(_mutex, that._mutex);
+        std::lock_guard<std::mutex> lock_this(_mutex, std::adopt_lock);
+        std::lock_guard<std::mutex> lock_that(that._mutex, std::adopt_lock);
+
+        _move(std::move(that));
+    }
+
+    return *this;
+}
+
+GuardedConnection ShardsPool::fetch(const StringView &key) {
+    auto slot = _slot(key);
+
+    return _fetch(slot);
+}
+
+GuardedConnection ShardsPool::fetch() {
+    auto slot = _slot();
+
+    return _fetch(slot);
+}
+
+GuardedConnection ShardsPool::fetch(const Node &node) {
+    std::lock_guard<std::mutex> lock(_mutex);
+
+    auto iter = _pools.find(node);
+    if (iter == _pools.end()) {
+        // Node doesn't exist, and it should be a newly created node.
+        // So add a new connection pool.
+        iter = _add_node(node);
+    }
+
+    assert(iter != _pools.end());
+
+    return GuardedConnection(iter->second);
+}
+
+void ShardsPool::update() {
+    // My might send command to a removed node.
+    // Try at most 3 times.
+    for (auto idx = 0; idx < 3; ++idx) {
+        try {
+            // Randomly pick a connection.
+            auto guarded_connection = fetch();
+            auto shards = _cluster_slots(guarded_connection.connection());
+
+            std::unordered_set<Node, NodeHash> nodes;
+            for (const auto &shard : shards) {
+                nodes.insert(shard.second);
+            }
+
+            std::lock_guard<std::mutex> lock(_mutex);
+
+            // TODO: If shards is unchanged, no need to update, and return immediately.
+
+            _shards = std::move(shards);
+
+            // Remove non-existent nodes.
+            for (auto iter = _pools.begin(); iter != _pools.end(); ) {
+                if (nodes.find(iter->first) == nodes.end()) {
+                    // Node has been removed.
+                    _pools.erase(iter++);
+                } else {
+                    ++iter;
+                }
+            }
+
+            // Add connection pool for new nodes.
+            // In fact, connections will be created lazily.
+            for (const auto &node : nodes) {
+                if (_pools.find(node) == _pools.end()) {
+                    _add_node(node);
+                }
+            }
+
+            // Update successfully.
+            return;
+        } catch (const Error &) {
+            // continue;
+        }
+    }
+
+    throw Error("Failed to update shards info");
+}
+
+ConnectionOptions ShardsPool::connection_options(const StringView &key) {
+    auto slot = _slot(key);
+
+    return _connection_options(slot);
+}
+
+ConnectionOptions ShardsPool::connection_options() {
+    auto slot = _slot();
+
+    return _connection_options(slot);
+}
+void ShardsPool::_move(ShardsPool &&that) {
+    _pool_opts = that._pool_opts;
+    _connection_opts = that._connection_opts;
+    _shards = std::move(that._shards);
+    _pools = std::move(that._pools);
+}
+
+void ShardsPool::_init_pool(const Shards &shards) {
+    for (const auto &shard : shards) {
+        _add_node(shard.second);
+    }
+}
+
+Shards ShardsPool::_cluster_slots(Connection &connection) const {
+    auto reply = _cluster_slots_command(connection);
+
+    assert(reply);
+
+    return _parse_reply(*reply);
+}
+
+ReplyUPtr ShardsPool::_cluster_slots_command(Connection &connection) const {
+    connection.send("CLUSTER SLOTS");
+
+    return connection.recv();
+}
+
+Shards ShardsPool::_parse_reply(redisReply &reply) const {
+    if (!reply::is_array(reply)) {
+        throw ProtoError("Expect ARRAY reply");
+    }
+
+    if (reply.element == nullptr || reply.elements == 0) {
+        throw Error("Empty slots");
+    }
+
+    Shards shards;
+    for (std::size_t idx = 0; idx != reply.elements; ++idx) {
+        auto *sub_reply = reply.element[idx];
+        if (sub_reply == nullptr) {
+            throw ProtoError("Null slot info");
+        }
+
+        shards.emplace(_parse_slot_info(*sub_reply));
+    }
+
+    return shards;
+}
+
+std::pair<SlotRange, Node> ShardsPool::_parse_slot_info(redisReply &reply) const {
+    if (reply.elements < 3 || reply.element == nullptr) {
+        throw ProtoError("Invalid slot info");
+    }
+
+    // Min slot id
+    auto *min_slot_reply = reply.element[0];
+    if (min_slot_reply == nullptr) {
+        throw ProtoError("Invalid min slot");
+    }
+    std::size_t min_slot = reply::parse<long long>(*min_slot_reply);
+
+    // Max slot id
+    auto *max_slot_reply = reply.element[1];
+    if (max_slot_reply == nullptr) {
+        throw ProtoError("Invalid max slot");
+    }
+    std::size_t max_slot = reply::parse<long long>(*max_slot_reply);
+
+    if (min_slot > max_slot) {
+        throw ProtoError("Invalid slot range");
+    }
+
+    // Master node info
+    auto *node_reply = reply.element[2];
+    if (node_reply == nullptr
+            || !reply::is_array(*node_reply)
+            || node_reply->element == nullptr
+            || node_reply->elements < 2) {
+        throw ProtoError("Invalid node info");
+    }
+
+    auto master_host = reply::parse<std::string>(*(node_reply->element[0]));
+    int master_port = reply::parse<long long>(*(node_reply->element[1]));
+
+    // By now, we ignore node id and other replicas' info.
+
+    return {SlotRange{min_slot, max_slot}, Node{master_host, master_port}};
+}
+
+Slot ShardsPool::_slot(const StringView &key) const {
+    // The following code is copied from: https://redis.io/topics/cluster-spec
+    // And I did some minor changes.
+
+    const auto *k = key.data();
+    auto keylen = key.size();
+
+    // start-end indexes of { and }.
+    std::size_t s = 0;
+    std::size_t e = 0;
+
+    // Search the first occurrence of '{'.
+    for (s = 0; s < keylen; s++)
+        if (k[s] == '{') break;
+
+    // No '{' ? Hash the whole key. This is the base case.
+    if (s == keylen) return crc16(k, keylen) & SHARDS;
+
+    // '{' found? Check if we have the corresponding '}'.
+    for (e = s + 1; e < keylen; e++)
+        if (k[e] == '}') break;
+
+    // No '}' or nothing between {} ? Hash the whole key.
+    if (e == keylen || e == s + 1) return crc16(k, keylen) & SHARDS;
+
+    // If we are here there is both a { and a } on its right. Hash
+    // what is in the middle between { and }.
+    return crc16(k + s + 1, e - s - 1) & SHARDS;
+}
+
+Slot ShardsPool::_slot() const {
+    static thread_local std::default_random_engine engine;
+
+    std::uniform_int_distribution<std::size_t> uniform_dist(0, SHARDS);
+
+    return uniform_dist(engine);
+}
+
+ConnectionPoolSPtr& ShardsPool::_get_pool(Slot slot) {
+    auto shards_iter = _shards.lower_bound(SlotRange{slot, slot});
+    if (shards_iter == _shards.end() || slot < shards_iter->first.min) {
+        throw Error("Slot is out of range: " + std::to_string(slot));
+    }
+
+    const auto &node = shards_iter->second;
+
+    auto node_iter = _pools.find(node);
+    if (node_iter == _pools.end()) {
+        throw Error("Slot is NOT covered: " + std::to_string(slot));
+    }
+
+    return node_iter->second;
+}
+
+GuardedConnection ShardsPool::_fetch(Slot slot) {
+    std::lock_guard<std::mutex> lock(_mutex);
+
+    auto &pool = _get_pool(slot);
+
+    assert(pool);
+
+    return GuardedConnection(pool);
+}
+
+ConnectionOptions ShardsPool::_connection_options(Slot slot) {
+    std::lock_guard<std::mutex> lock(_mutex);
+
+    auto &pool = _get_pool(slot);
+
+    assert(pool);
+
+    return pool->connection_options();
+}
+
+auto ShardsPool::_add_node(const Node &node) -> NodeMap::iterator {
+    auto opts = _connection_opts;
+    opts.host = node.host;
+    opts.port = node.port;
+
+    return _pools.emplace(node, std::make_shared<ConnectionPool>(_pool_opts, opts)).first;
+}
+
+}
+
+}

+ 137 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/shards_pool.h

@@ -0,0 +1,137 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_SHARDS_POOL_H
+#define SEWENEW_REDISPLUSPLUS_SHARDS_POOL_H
+
+#include <cassert>
+#include <unordered_map>
+#include <string>
+#include <random>
+#include <memory>
+#include "reply.h"
+#include "connection_pool.h"
+#include "shards.h"
+
+namespace sw {
+
+namespace redis {
+
+using ConnectionPoolSPtr = std::shared_ptr<ConnectionPool>;
+
+class GuardedConnection {
+public:
+    GuardedConnection(const ConnectionPoolSPtr &pool) : _pool(pool),
+                                                        _connection(_pool->fetch()) {
+        assert(!_connection.broken());
+    }
+
+    GuardedConnection(const GuardedConnection &) = delete;
+    GuardedConnection& operator=(const GuardedConnection &) = delete;
+
+    GuardedConnection(GuardedConnection &&) = default;
+    GuardedConnection& operator=(GuardedConnection &&) = default;
+
+    ~GuardedConnection() {
+        _pool->release(std::move(_connection));
+    }
+
+    Connection& connection() {
+        return _connection;
+    }
+
+private:
+    ConnectionPoolSPtr _pool;
+    Connection _connection;
+};
+
+class ShardsPool {
+public:
+    ShardsPool() = default;
+
+    ShardsPool(const ShardsPool &that) = delete;
+    ShardsPool& operator=(const ShardsPool &that) = delete;
+
+    ShardsPool(ShardsPool &&that);
+    ShardsPool& operator=(ShardsPool &&that);
+
+    ~ShardsPool() = default;
+
+    ShardsPool(const ConnectionPoolOptions &pool_opts,
+                const ConnectionOptions &connection_opts);
+
+    // Fetch a connection by key.
+    GuardedConnection fetch(const StringView &key);
+
+    // Randomly pick a connection.
+    GuardedConnection fetch();
+
+    // Fetch a connection by node.
+    GuardedConnection fetch(const Node &node);
+
+    void update();
+
+    ConnectionOptions connection_options(const StringView &key);
+
+    ConnectionOptions connection_options();
+
+private:
+    void _move(ShardsPool &&that);
+
+    void _init_pool(const Shards &shards);
+
+    Shards _cluster_slots(Connection &connection) const;
+
+    ReplyUPtr _cluster_slots_command(Connection &connection) const;
+
+    Shards _parse_reply(redisReply &reply) const;
+
+    std::pair<SlotRange, Node> _parse_slot_info(redisReply &reply) const;
+
+    // Get slot by key.
+    std::size_t _slot(const StringView &key) const;
+
+    // Randomly pick a slot.
+    std::size_t _slot() const;
+
+    ConnectionPoolSPtr& _get_pool(Slot slot);
+
+    GuardedConnection _fetch(Slot slot);
+
+    ConnectionOptions _connection_options(Slot slot);
+
+    using NodeMap = std::unordered_map<Node, ConnectionPoolSPtr, NodeHash>;
+
+    NodeMap::iterator _add_node(const Node &node);
+
+    ConnectionPoolOptions _pool_opts;
+
+    ConnectionOptions _connection_opts;
+
+    Shards _shards;
+
+    NodeMap _pools;
+
+    std::mutex _mutex;
+
+    static const std::size_t SHARDS = 16383;
+};
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_SHARDS_POOL_H

+ 222 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/subscriber.cpp

@@ -0,0 +1,222 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#include "subscriber.h"
+#include <cassert>
+
+namespace sw {
+
+namespace redis {
+
+const Subscriber::TypeIndex Subscriber::_msg_type_index = {
+    {"message", MsgType::MESSAGE},
+    {"pmessage", MsgType::PMESSAGE},
+    {"subscribe", MsgType::SUBSCRIBE},
+    {"unsubscribe", MsgType::UNSUBSCRIBE},
+    {"psubscribe", MsgType::PSUBSCRIBE},
+    {"punsubscribe", MsgType::PUNSUBSCRIBE}
+};
+
+Subscriber::Subscriber(Connection connection) : _connection(std::move(connection)) {}
+
+void Subscriber::subscribe(const StringView &channel) {
+    _check_connection();
+
+    // TODO: cmd::subscribe DOES NOT send the subscribe message to Redis.
+    // In fact, it puts the command to network buffer.
+    // So we need a queue to record these sub or unsub commands, and
+    // ensure that before stopping the subscriber, all these commands
+    // have really been sent to Redis.
+    cmd::subscribe(_connection, channel);
+}
+
+void Subscriber::unsubscribe() {
+    _check_connection();
+
+    cmd::unsubscribe(_connection);
+}
+
+void Subscriber::unsubscribe(const StringView &channel) {
+    _check_connection();
+
+    cmd::unsubscribe(_connection, channel);
+}
+
+void Subscriber::psubscribe(const StringView &pattern) {
+    _check_connection();
+
+    cmd::psubscribe(_connection, pattern);
+}
+
+void Subscriber::punsubscribe() {
+    _check_connection();
+
+    cmd::punsubscribe(_connection);
+}
+
+void Subscriber::punsubscribe(const StringView &pattern) {
+    _check_connection();
+
+    cmd::punsubscribe(_connection, pattern);
+}
+
+void Subscriber::consume() {
+    _check_connection();
+
+    ReplyUPtr reply;
+    try {
+        reply = _connection.recv();
+    } catch (const TimeoutError &) {
+        _connection.reset();
+        throw;
+    }
+
+    assert(reply);
+
+    if (!reply::is_array(*reply) || reply->elements < 1 || reply->element == nullptr) {
+        throw ProtoError("Invalid subscribe message");
+    }
+
+    auto type = _msg_type(reply->element[0]);
+    switch (type) {
+    case MsgType::MESSAGE:
+        _handle_message(*reply);
+        break;
+
+    case MsgType::PMESSAGE:
+        _handle_pmessage(*reply);
+        break;
+
+    case MsgType::SUBSCRIBE:
+    case MsgType::UNSUBSCRIBE:
+    case MsgType::PSUBSCRIBE:
+    case MsgType::PUNSUBSCRIBE:
+        _handle_meta(type, *reply);
+        break;
+
+    default:
+        assert(false);
+    }
+}
+
+Subscriber::MsgType Subscriber::_msg_type(redisReply *reply) const {
+    if (reply == nullptr) {
+        throw ProtoError("Null type reply.");
+    }
+
+    auto type = reply::parse<std::string>(*reply);
+
+    auto iter = _msg_type_index.find(type);
+    if (iter == _msg_type_index.end()) {
+        throw ProtoError("Invalid message type.");
+    }
+
+    return iter->second;
+}
+
+void Subscriber::_check_connection() {
+    if (_connection.broken()) {
+        throw Error("Connection is broken");
+    }
+}
+
+void Subscriber::_handle_message(redisReply &reply) {
+    if (_msg_callback == nullptr) {
+        return;
+    }
+
+    if (reply.elements != 3) {
+        throw ProtoError("Expect 3 sub replies");
+    }
+
+    assert(reply.element != nullptr);
+
+    auto *channel_reply = reply.element[1];
+    if (channel_reply == nullptr) {
+        throw ProtoError("Null channel reply");
+    }
+    auto channel = reply::parse<std::string>(*channel_reply);
+
+    auto *msg_reply = reply.element[2];
+    if (msg_reply == nullptr) {
+        throw ProtoError("Null message reply");
+    }
+    auto msg = reply::parse<std::string>(*msg_reply);
+
+    _msg_callback(std::move(channel), std::move(msg));
+}
+
+void Subscriber::_handle_pmessage(redisReply &reply) {
+    if (_pmsg_callback == nullptr) {
+        return;
+    }
+
+    if (reply.elements != 4) {
+        throw ProtoError("Expect 4 sub replies");
+    }
+
+    assert(reply.element != nullptr);
+
+    auto *pattern_reply = reply.element[1];
+    if (pattern_reply == nullptr) {
+        throw ProtoError("Null pattern reply");
+    }
+    auto pattern = reply::parse<std::string>(*pattern_reply);
+
+    auto *channel_reply = reply.element[2];
+    if (channel_reply == nullptr) {
+        throw ProtoError("Null channel reply");
+    }
+    auto channel = reply::parse<std::string>(*channel_reply);
+
+    auto *msg_reply = reply.element[3];
+    if (msg_reply == nullptr) {
+        throw ProtoError("Null message reply");
+    }
+    auto msg = reply::parse<std::string>(*msg_reply);
+
+    _pmsg_callback(std::move(pattern), std::move(channel), std::move(msg));
+}
+
+void Subscriber::_handle_meta(MsgType type, redisReply &reply) {
+    if (_meta_callback == nullptr) {
+        return;
+    }
+
+    if (reply.elements != 3) {
+        throw ProtoError("Expect 3 sub replies");
+    }
+
+    assert(reply.element != nullptr);
+
+    auto *channel_reply = reply.element[1];
+    if (channel_reply == nullptr) {
+        throw ProtoError("Null channel reply");
+    }
+    auto channel = reply::parse<OptionalString>(*channel_reply);
+
+    auto *num_reply = reply.element[2];
+    if (num_reply == nullptr) {
+        throw ProtoError("Null num reply");
+    }
+    auto num = reply::parse<long long>(*num_reply);
+
+    _meta_callback(type, std::move(channel), num);
+}
+
+}
+
+}

+ 231 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/subscriber.h

@@ -0,0 +1,231 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_SUBSCRIBER_H
+#define SEWENEW_REDISPLUSPLUS_SUBSCRIBER_H
+
+#include <unordered_map>
+#include <string>
+#include <functional>
+#include "connection.h"
+#include "reply.h"
+#include "command.h"
+#include "utils.h"
+
+namespace sw {
+
+namespace redis {
+
+// @NOTE: Subscriber is NOT thread-safe.
+// Subscriber uses callbacks to handle messages. There are 6 kinds of messages:
+// 1) MESSAGE: message sent to a channel.
+// 2) PMESSAGE: message sent to channels of a given pattern.
+// 3) SUBSCRIBE: meta message sent when we successfully subscribe to a channel.
+// 4) UNSUBSCRIBE: meta message sent when we successfully unsubscribe to a channel.
+// 5) PSUBSCRIBE: meta message sent when we successfully subscribe to a channel pattern.
+// 6) PUNSUBSCRIBE: meta message sent when we successfully unsubscribe to a channel pattern.
+//
+// Use Subscriber::on_message(MsgCallback) to set the callback function for message of
+// *MESSAGE* type, and the callback interface is:
+// void (std::string channel, std::string msg)
+//
+// Use Subscriber::on_pmessage(PatternMsgCallback) to set the callback function for message of
+// *PMESSAGE* type, and the callback interface is:
+// void (std::string pattern, std::string channel, std::string msg)
+//
+// Messages of other types are called *META MESSAGE*, they have the same callback interface.
+// Use Subscriber::on_meta(MetaCallback) to set the callback function:
+// void (Subscriber::MsgType type, OptionalString channel, long long num)
+//
+// NOTE: If we haven't subscribe/psubscribe to any channel/pattern, and try to
+// unsubscribe/punsubscribe without any parameter, i.e. unsubscribe/punsubscribe all
+// channels/patterns, *channel* will be null. So the second parameter of meta callback
+// is of type *OptionalString*.
+//
+// All these callback interfaces pass std::string by value, and you can take their ownership
+// (i.e. std::move) safely.
+//
+// If you don't set callback for a specific kind of message, Subscriber::consume() will
+// receive the message, and ignore it, i.e. no callback will be called.
+class Subscriber {
+public:
+    Subscriber(const Subscriber &) = delete;
+    Subscriber& operator=(const Subscriber &) = delete;
+
+    Subscriber(Subscriber &&) = default;
+    Subscriber& operator=(Subscriber &&) = default;
+
+    ~Subscriber() = default;
+
+    enum class MsgType {
+        SUBSCRIBE,
+        UNSUBSCRIBE,
+        PSUBSCRIBE,
+        PUNSUBSCRIBE,
+        MESSAGE,
+        PMESSAGE
+    };
+
+    template <typename MsgCb>
+    void on_message(MsgCb msg_callback);
+
+    template <typename PMsgCb>
+    void on_pmessage(PMsgCb pmsg_callback);
+
+    template <typename MetaCb>
+    void on_meta(MetaCb meta_callback);
+
+    void subscribe(const StringView &channel);
+
+    template <typename Input>
+    void subscribe(Input first, Input last);
+
+    template <typename T>
+    void subscribe(std::initializer_list<T> channels) {
+        subscribe(channels.begin(), channels.end());
+    }
+
+    void unsubscribe();
+
+    void unsubscribe(const StringView &channel);
+
+    template <typename Input>
+    void unsubscribe(Input first, Input last);
+
+    template <typename T>
+    void unsubscribe(std::initializer_list<T> channels) {
+        unsubscribe(channels.begin(), channels.end());
+    }
+
+    void psubscribe(const StringView &pattern);
+
+    template <typename Input>
+    void psubscribe(Input first, Input last);
+
+    template <typename T>
+    void psubscribe(std::initializer_list<T> channels) {
+        psubscribe(channels.begin(), channels.end());
+    }
+
+    void punsubscribe();
+
+    void punsubscribe(const StringView &channel);
+
+    template <typename Input>
+    void punsubscribe(Input first, Input last);
+
+    template <typename T>
+    void punsubscribe(std::initializer_list<T> channels) {
+        punsubscribe(channels.begin(), channels.end());
+    }
+
+    void consume();
+
+private:
+    friend class Redis;
+
+    friend class RedisCluster;
+
+    explicit Subscriber(Connection connection);
+
+    MsgType _msg_type(redisReply *reply) const;
+
+    void _check_connection();
+
+    void _handle_message(redisReply &reply);
+
+    void _handle_pmessage(redisReply &reply);
+
+    void _handle_meta(MsgType type, redisReply &reply);
+
+    using MsgCallback = std::function<void (std::string channel, std::string msg)>;
+
+    using PatternMsgCallback = std::function<void (std::string pattern,
+                                                    std::string channel,
+                                                    std::string msg)>;
+
+    using MetaCallback = std::function<void (MsgType type,
+                                                OptionalString channel,
+                                                long long num)>;
+
+    using TypeIndex = std::unordered_map<std::string, MsgType>;
+    static const TypeIndex _msg_type_index;
+
+    Connection _connection;
+
+    MsgCallback _msg_callback = nullptr;
+
+    PatternMsgCallback _pmsg_callback = nullptr;
+
+    MetaCallback _meta_callback = nullptr;
+};
+
+template <typename MsgCb>
+void Subscriber::on_message(MsgCb msg_callback) {
+    _msg_callback = msg_callback;
+}
+
+template <typename PMsgCb>
+void Subscriber::on_pmessage(PMsgCb pmsg_callback) {
+    _pmsg_callback = pmsg_callback;
+}
+
+template <typename MetaCb>
+void Subscriber::on_meta(MetaCb meta_callback) {
+    _meta_callback = meta_callback;
+}
+
+template <typename Input>
+void Subscriber::subscribe(Input first, Input last) {
+    if (first == last) {
+        return;
+    }
+
+    _check_connection();
+
+    cmd::subscribe_range(_connection, first, last);
+}
+
+template <typename Input>
+void Subscriber::unsubscribe(Input first, Input last) {
+    _check_connection();
+
+    cmd::unsubscribe_range(_connection, first, last);
+}
+
+template <typename Input>
+void Subscriber::psubscribe(Input first, Input last) {
+    if (first == last) {
+        return;
+    }
+
+    _check_connection();
+
+    cmd::psubscribe_range(_connection, first, last);
+}
+
+template <typename Input>
+void Subscriber::punsubscribe(Input first, Input last) {
+    _check_connection();
+
+    cmd::punsubscribe_range(_connection, first, last);
+}
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_SUBSCRIBER_H

+ 123 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/transaction.cpp

@@ -0,0 +1,123 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#include "transaction.h"
+#include "command.h"
+
+namespace sw {
+
+namespace redis {
+
+std::vector<ReplyUPtr> TransactionImpl::exec(Connection &connection, std::size_t cmd_num) {
+    _close_transaction();
+
+    _get_queued_replies(connection, cmd_num);
+
+    return _exec(connection);
+}
+
+void TransactionImpl::discard(Connection &connection, std::size_t cmd_num) {
+    _close_transaction();
+
+    _get_queued_replies(connection, cmd_num);
+
+    _discard(connection);
+}
+
+void TransactionImpl::_open_transaction(Connection &connection) {
+    assert(!_in_transaction);
+
+    cmd::multi(connection);
+    auto reply = connection.recv();
+    auto status = reply::to_status(*reply);
+    if (status != "OK") {
+        throw Error("Failed to open transaction: " + status);
+    }
+
+    _in_transaction = true;
+}
+
+void TransactionImpl::_close_transaction() {
+    if (!_in_transaction) {
+        throw Error("No command in transaction");
+    }
+
+    _in_transaction = false;
+}
+
+void TransactionImpl::_get_queued_reply(Connection &connection) {
+    auto reply = connection.recv();
+    auto status = reply::to_status(*reply);
+    if (status != "QUEUED") {
+        throw Error("Invalid QUEUED reply: " + status);
+    }
+}
+
+void TransactionImpl::_get_queued_replies(Connection &connection, std::size_t cmd_num) {
+    if (_piped) {
+        // Get all QUEUED reply
+        while (cmd_num > 0) {
+            _get_queued_reply(connection);
+
+            --cmd_num;
+        }
+    }
+}
+
+std::vector<ReplyUPtr> TransactionImpl::_exec(Connection &connection) {
+    cmd::exec(connection);
+
+    auto reply = connection.recv();
+
+    if (reply::is_nil(*reply)) {
+        // Execution has been aborted, i.e. watched key has been modified.
+        throw WatchError();
+    }
+
+    if (!reply::is_array(*reply)) {
+        throw ProtoError("Expect ARRAY reply");
+    }
+
+    if (reply->element == nullptr || reply->elements == 0) {
+        // Since we don't allow EXEC without any command, this ARRAY reply
+        // should NOT be null or empty.
+        throw ProtoError("Null ARRAY reply");
+    }
+
+    std::vector<ReplyUPtr> replies;
+    for (std::size_t idx = 0; idx != reply->elements; ++idx) {
+        auto *sub_reply = reply->element[idx];
+        if (sub_reply == nullptr) {
+            throw ProtoError("Null sub reply");
+        }
+
+        auto r = ReplyUPtr(sub_reply);
+        reply->element[idx] = nullptr;
+        replies.push_back(std::move(r));
+    }
+
+    return replies;
+}
+
+void TransactionImpl::_discard(Connection &connection) {
+    cmd::discard(connection);
+    auto reply = connection.recv();
+    reply::parse<void>(*reply);
+}
+
+}
+
+}

+ 77 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/transaction.h

@@ -0,0 +1,77 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_TRANSACTION_H
+#define SEWENEW_REDISPLUSPLUS_TRANSACTION_H
+
+#include <cassert>
+#include <vector>
+#include "connection.h"
+#include "errors.h"
+
+namespace sw {
+
+namespace redis {
+
+class TransactionImpl {
+public:
+    explicit TransactionImpl(bool piped) : _piped(piped) {}
+
+    template <typename Cmd, typename ...Args>
+    void command(Connection &connection, Cmd cmd, Args &&...args);
+
+    std::vector<ReplyUPtr> exec(Connection &connection, std::size_t cmd_num);
+
+    void discard(Connection &connection, std::size_t cmd_num);
+
+private:
+    void _open_transaction(Connection &connection);
+
+    void _close_transaction();
+
+    void _get_queued_reply(Connection &connection);
+
+    void _get_queued_replies(Connection &connection, std::size_t cmd_num);
+
+    std::vector<ReplyUPtr> _exec(Connection &connection);
+
+    void _discard(Connection &connection);
+
+    bool _in_transaction = false;
+
+    bool _piped;
+};
+
+template <typename Cmd, typename ...Args>
+void TransactionImpl::command(Connection &connection, Cmd cmd, Args &&...args) {
+    assert(!connection.broken());
+
+    if (!_in_transaction) {
+        _open_transaction(connection);
+    }
+
+    cmd(connection, std::forward<Args>(args)...);
+
+    if (!_piped) {
+        _get_queued_reply(connection);
+    }
+}
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_TRANSACTION_H

+ 269 - 0
ext/redis-plus-plus-1.1.1/src/sw/redis++/utils.h

@@ -0,0 +1,269 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_UTILS_H
+#define SEWENEW_REDISPLUSPLUS_UTILS_H
+
+#include <cstring>
+#include <string>
+#include <type_traits>
+
+namespace sw {
+
+namespace redis {
+
+// By now, not all compilers support std::string_view,
+// so we make our own implementation.
+class StringView {
+public:
+    constexpr StringView() noexcept = default;
+
+    constexpr StringView(const char *data, std::size_t size) : _data(data), _size(size) {}
+
+    StringView(const char *data) : _data(data), _size(std::strlen(data)) {}
+
+    StringView(const std::string &str) : _data(str.data()), _size(str.size()) {}
+
+    constexpr StringView(const StringView &) noexcept = default;
+
+    StringView& operator=(const StringView &) noexcept = default;
+
+    constexpr const char* data() const noexcept {
+        return _data;
+    }
+
+    constexpr std::size_t size() const noexcept {
+        return _size;
+    }
+
+private:
+    const char *_data = nullptr;
+    std::size_t _size = 0;
+};
+
+template <typename T>
+class Optional {
+public:
+    Optional() = default;
+
+    Optional(const Optional &) = default;
+    Optional& operator=(const Optional &) = default;
+
+    Optional(Optional &&) = default;
+    Optional& operator=(Optional &&) = default;
+
+    ~Optional() = default;
+
+    template <typename ...Args>
+    explicit Optional(Args &&...args) : _value(true, T(std::forward<Args>(args)...)) {}
+
+    explicit operator bool() const {
+        return _value.first;
+    }
+
+    T& value() {
+        return _value.second;
+    }
+
+    const T& value() const {
+        return _value.second;
+    }
+
+    T* operator->() {
+        return &(_value.second);
+    }
+
+    const T* operator->() const {
+        return &(_value.second);
+    }
+
+    T& operator*() {
+        return _value.second;
+    }
+
+    const T& operator*() const {
+        return _value.second;
+    }
+
+private:
+    std::pair<bool, T> _value;
+};
+
+using OptionalString = Optional<std::string>;
+
+using OptionalLongLong = Optional<long long>;
+
+using OptionalDouble = Optional<double>;
+
+using OptionalStringPair = Optional<std::pair<std::string, std::string>>;
+
+template <typename ...>
+struct IsKvPair : std::false_type {};
+
+template <typename T, typename U>
+struct IsKvPair<std::pair<T, U>> : std::true_type {};
+
+template <typename ...>
+using Void = void;
+
+template <typename T, typename U = Void<>>
+struct IsInserter : std::false_type {};
+
+template <typename T>
+//struct IsInserter<T, Void<typename T::container_type>> : std::true_type {};
+struct IsInserter<T,
+    typename std::enable_if<!std::is_void<typename T::container_type>::value>::type>
+        : std::true_type {};
+
+template <typename Iter, typename T = Void<>>
+struct IterType {
+    using type = typename std::iterator_traits<Iter>::value_type;
+};
+
+template <typename Iter>
+//struct IterType<Iter, Void<typename Iter::container_type>> {
+struct IterType<Iter,
+    //typename std::enable_if<std::is_void<typename Iter::value_type>::value>::type> {
+    typename std::enable_if<IsInserter<Iter>::value>::type> {
+    using type = typename std::decay<typename Iter::container_type::value_type>::type;
+};
+
+template <typename Iter, typename T = Void<>>
+struct IsIter : std::false_type {};
+
+template <typename Iter>
+struct IsIter<Iter, typename std::enable_if<IsInserter<Iter>::value>::type> : std::true_type {};
+
+template <typename Iter>
+//struct IsIter<Iter, Void<typename std::iterator_traits<Iter>::iterator_category>>
+struct IsIter<Iter,
+    typename std::enable_if<!std::is_void<
+        typename std::iterator_traits<Iter>::value_type>::value>::type>
+            : std::integral_constant<bool, !std::is_convertible<Iter, StringView>::value> {};
+
+template <typename T>
+struct IsKvPairIter : IsKvPair<typename IterType<T>::type> {};
+
+template <typename T, typename Tuple>
+struct TupleWithType : std::false_type {};
+
+template <typename T>
+struct TupleWithType<T, std::tuple<>> : std::false_type {};
+
+template <typename T, typename U, typename ...Args>
+struct TupleWithType<T, std::tuple<U, Args...>> : TupleWithType<T, std::tuple<Args...>> {};
+
+template <typename T, typename ...Args>
+struct TupleWithType<T, std::tuple<T, Args...>> : std::true_type {};
+
+template <std::size_t ...Is>
+struct IndexSequence {};
+
+template <std::size_t I, std::size_t ...Is>
+struct MakeIndexSequence : MakeIndexSequence<I - 1, I - 1, Is...> {};
+
+template <std::size_t ...Is>
+struct MakeIndexSequence<0, Is...> : IndexSequence<Is...> {};
+
+// NthType and NthValue are taken from
+// https://stackoverflow.com/questions/14261183
+template <std::size_t I, typename ...Args>
+struct NthType {};
+
+template <typename Arg, typename ...Args>
+struct NthType<0, Arg, Args...> {
+    using type = Arg;
+};
+
+template <std::size_t I, typename Arg, typename ...Args>
+struct NthType<I, Arg, Args...> {
+    using type = typename NthType<I - 1, Args...>::type;
+};
+
+template <typename ...Args>
+struct LastType {
+    using type = typename NthType<sizeof...(Args) - 1, Args...>::type;
+};
+
+struct InvalidLastType {};
+
+template <>
+struct LastType<> {
+    using type = InvalidLastType;
+};
+
+template <std::size_t I, typename Arg, typename ...Args>
+auto NthValue(Arg &&arg, Args &&...)
+    -> typename std::enable_if<(I == 0), decltype(std::forward<Arg>(arg))>::type {
+    return std::forward<Arg>(arg);
+}
+
+template <std::size_t I, typename Arg, typename ...Args>
+auto NthValue(Arg &&, Args &&...args)
+    -> typename std::enable_if<(I > 0),
+            decltype(std::forward<typename NthType<I, Arg, Args...>::type>(
+                    std::declval<typename NthType<I, Arg, Args...>::type>()))>::type {
+    return std::forward<typename NthType<I, Arg, Args...>::type>(
+            NthValue<I - 1>(std::forward<Args>(args)...));
+}
+
+template <typename ...Args>
+auto LastValue(Args &&...args)
+    -> decltype(std::forward<typename LastType<Args...>::type>(
+            std::declval<typename LastType<Args...>::type>())) {
+    return std::forward<typename LastType<Args...>::type>(
+            NthValue<sizeof...(Args) - 1>(std::forward<Args>(args)...));
+}
+
+template <typename T, typename = Void<>>
+struct HasPushBack : std::false_type {};
+
+template <typename T>
+struct HasPushBack<T,
+    typename std::enable_if<
+        std::is_void<decltype(
+            std::declval<T>().push_back(std::declval<typename T::value_type>())
+                )>::value>::type> : std::true_type {};
+
+template <typename T, typename = Void<>>
+struct HasInsert : std::false_type {};
+
+template <typename T>
+struct HasInsert<T,
+    typename std::enable_if<
+        std::is_same<
+            decltype(std::declval<T>().insert(std::declval<typename T::const_iterator>(),
+                                                std::declval<typename T::value_type>())),
+            typename T::iterator>::value>::type> : std::true_type {};
+
+template <typename T>
+struct IsSequenceContainer
+    : std::integral_constant<bool,
+        HasPushBack<T>::value
+            && !std::is_same<typename std::decay<T>::type, std::string>::value> {};
+
+template <typename T>
+struct IsAssociativeContainer
+    : std::integral_constant<bool,
+        HasInsert<T>::value && !HasPushBack<T>::value> {};
+
+uint16_t crc16(const char *buf, int len);
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_UTILS_H

+ 33 - 0
ext/redis-plus-plus-1.1.1/test/CMakeLists.txt

@@ -0,0 +1,33 @@
+project(test_redis++)
+
+if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
+    cmake_minimum_required(VERSION 3.0.0)
+else()
+    cmake_minimum_required(VERSION 2.8.0)
+endif()
+
+set(PROJECT_SOURCE_DIR ${PROJECT_SOURCE_DIR}/src/sw/redis++)
+
+file(GLOB PROJECT_SOURCE_FILES "${PROJECT_SOURCE_DIR}/*.cpp")
+
+add_executable(${PROJECT_NAME} ${PROJECT_SOURCE_FILES})
+
+# hiredis dependency
+find_path(HIREDIS_HEADER hiredis)
+target_include_directories(${PROJECT_NAME} PUBLIC ${HIREDIS_HEADER})
+
+find_library(HIREDIS_STATIC_LIB libhiredis.a)
+target_link_libraries(${PROJECT_NAME} ${HIREDIS_STATIC_LIB})
+
+# redis++ dependency
+target_include_directories(${PROJECT_NAME} PUBLIC ../src)
+set(REDIS_PLUS_PLUS_LIB ${CMAKE_CURRENT_BINARY_DIR}/../lib/libredis++.a)
+
+## solaris socket dependency
+IF (CMAKE_SYSTEM_NAME MATCHES "(Solaris|SunOS)" )
+    target_link_libraries(${PROJECT_NAME} -lsocket)
+ENDIF(CMAKE_SYSTEM_NAME MATCHES "(Solaris|SunOS)" )
+
+find_package(Threads REQUIRED)
+
+target_link_libraries(${PROJECT_NAME} ${REDIS_PLUS_PLUS_LIB} ${CMAKE_THREAD_LIBS_INIT})

+ 83 - 0
ext/redis-plus-plus-1.1.1/test/src/sw/redis++/benchmark_test.h

@@ -0,0 +1,83 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_TEST_BENCHMARK_TEST_H
+#define SEWENEW_REDISPLUSPLUS_TEST_BENCHMARK_TEST_H
+
+#include <sw/redis++/redis++.h>
+
+namespace sw {
+
+namespace redis {
+
+namespace test {
+
+struct BenchmarkOptions {
+    std::size_t pool_size = 5;
+    std::size_t thread_num = 10;
+    std::size_t total_request_num = 100000;
+    std::size_t key_len = 10;
+    std::size_t val_len = 10;
+};
+
+template <typename RedisInstance>
+class BenchmarkTest {
+public:
+    BenchmarkTest(const BenchmarkOptions &opts, RedisInstance &instance);
+
+    ~BenchmarkTest() {
+        _cleanup();
+    }
+
+    void run();
+
+private:
+    template <typename Func>
+    void _run(const std::string &title, Func &&func);
+
+    template <typename Func>
+    std::size_t _run(Func &&func, std::size_t request_num);
+
+    void _test_get();
+
+    std::vector<std::string> _gen_keys() const;
+
+    std::string _gen_value() const;
+
+    void _cleanup();
+
+    const std::string& _key(std::size_t idx) const {
+        return _keys[idx % _keys.size()];
+    }
+
+    BenchmarkOptions _opts;
+
+    RedisInstance &_redis;
+
+    std::vector<std::string> _keys;
+
+    std::string _value;
+};
+
+}
+
+}
+
+}
+
+#include "benchmark_test.hpp"
+
+#endif // end SEWENEW_REDISPLUSPLUS_TEST_BENCHMARK_TEST_H

+ 178 - 0
ext/redis-plus-plus-1.1.1/test/src/sw/redis++/benchmark_test.hpp

@@ -0,0 +1,178 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_TEST_BENCHMARK_TEST_HPP
+#define SEWENEW_REDISPLUSPLUS_TEST_BENCHMARK_TEST_HPP
+
+#include <chrono>
+#include <random>
+#include <future>
+#include <algorithm>
+#include "utils.h"
+
+namespace sw {
+
+namespace redis {
+
+namespace test {
+
+template <typename RedisInstance>
+BenchmarkTest<RedisInstance>::BenchmarkTest(const BenchmarkOptions &opts,
+        RedisInstance &instance) : _opts(opts), _redis(instance) {
+    REDIS_ASSERT(_opts.pool_size > 0
+            && _opts.thread_num > 0
+            && _opts.total_request_num > 0
+            && _opts.key_len > 0
+            && _opts.val_len > 0,
+                "Invalid benchmark test options.");
+
+    _keys = _gen_keys();
+    _value = _gen_value();
+}
+
+template <typename RedisInstance>
+void BenchmarkTest<RedisInstance>::run() {
+    _cleanup();
+
+    _run("SET key value", [this](std::size_t idx) { this->_redis.set(this->_key(idx), _value); });
+
+    _run("GET key", [this](std::size_t idx) {
+                        auto res = this->_redis.get(this->_key(idx));
+                        (void)res;
+                    });
+
+    _cleanup();
+
+    _run("LPUSH key value", [this](std::size_t idx) {
+                                this->_redis.lpush(this->_key(idx), _value);
+                            });
+
+    _run("LRANGE key 0 10", [this](std::size_t idx) {
+                std::vector<std::string> res;
+                res.reserve(10);
+                this->_redis.lrange(this->_key(idx), 0, 10, std::back_inserter(res));
+            });
+
+    _run("LPOP key", [this](std::size_t idx) {
+                        auto res = this->_redis.lpop(this->_key(idx));
+                        (void)res;
+                     });
+
+    _cleanup();
+
+    _run("INCR key", [this](std::size_t idx) {
+                        auto num = this->_redis.incr(this->_key(idx));
+                        (void)num;
+                     });
+
+    _cleanup();
+
+    _run("SADD key member", [this](std::size_t idx) {
+                                auto num = this->_redis.sadd(this->_key(idx), _value);
+                                (void)num;
+                            });
+
+    _run("SPOP key", [this](std::size_t idx) {
+                        auto res = this->_redis.spop(this->_key(idx));
+                        (void)res;
+                     });
+
+    _cleanup();
+}
+
+template <typename RedisInstance>
+template <typename Func>
+void BenchmarkTest<RedisInstance>::_run(const std::string &title, Func &&func) {
+    auto thread_num = _opts.thread_num;
+    auto requests_per_thread = _opts.total_request_num / thread_num;
+    auto total_request_num = requests_per_thread * thread_num;
+    std::vector<std::future<std::size_t>> res;
+    res.reserve(thread_num);
+    res.push_back(std::async(std::launch::async,
+                    [this](Func &&func, std::size_t request_num) {
+                        return this->_run(std::forward<Func>(func), request_num);
+                    },
+                    std::forward<Func>(func),
+                    requests_per_thread));
+
+    auto total_in_msec = 0;
+    for (auto &fut : res) {
+        total_in_msec += fut.get();
+    }
+
+    auto total_in_sec = total_in_msec * 1.0 / 1000;
+
+    auto avg = total_in_msec * 1.0 / total_request_num;
+
+    auto ops = static_cast<std::size_t>(1000 / avg);
+
+    std::cout << "-----" << title << "-----" << std::endl;
+    std::cout << total_request_num << " requests cost " << total_in_sec << " seconds" << std::endl;
+    std::cout << ops << " requests per second" << std::endl;
+}
+
+template <typename RedisInstance>
+template <typename Func>
+std::size_t BenchmarkTest<RedisInstance>::_run(Func &&func, std::size_t request_num) {
+    auto start = std::chrono::steady_clock::now();
+
+    for (auto idx = 0U; idx != request_num; ++idx) {
+        func(idx);
+    }
+
+    auto stop = std::chrono::steady_clock::now();
+
+    return std::chrono::duration_cast<std::chrono::milliseconds>(stop - start).count();
+}
+
+template <typename RedisInstance>
+std::vector<std::string> BenchmarkTest<RedisInstance>::_gen_keys() const {
+    const auto KEY_NUM = 100;
+    std::vector<std::string> res;
+    res.reserve(KEY_NUM);
+    std::default_random_engine engine(std::random_device{}());
+    std::uniform_int_distribution<int> uniform_dist(0, 255);
+    for (auto i = 0; i != KEY_NUM; ++i) {
+        std::string str;
+        str.reserve(_opts.key_len);
+        for (std::size_t j = 0; j != _opts.key_len; ++j) {
+            str.push_back(static_cast<char>(uniform_dist(engine)));
+        }
+        res.push_back(str);
+    }
+
+    return res;
+}
+
+template <typename RedisInstance>
+std::string BenchmarkTest<RedisInstance>::_gen_value() const {
+    return std::string(_opts.val_len, 'x');
+}
+
+template <typename RedisInstance>
+void BenchmarkTest<RedisInstance>::_cleanup() {
+    for (const auto &key : _keys) {
+        _redis.del(key);
+    }
+}
+
+}
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_TEST_BENCHMARK_TEST_HPP

+ 49 - 0
ext/redis-plus-plus-1.1.1/test/src/sw/redis++/connection_cmds_test.h

@@ -0,0 +1,49 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_TEST_CONNECTION_CMDS_TEST_H
+#define SEWENEW_REDISPLUSPLUS_TEST_CONNECTION_CMDS_TEST_H
+
+#include <sw/redis++/redis++.h>
+
+namespace sw {
+
+namespace redis {
+
+namespace test {
+
+template <typename RedisInstance>
+class ConnectionCmdTest {
+public:
+    explicit ConnectionCmdTest(RedisInstance &instance) : _redis(instance) {}
+
+    void run();
+
+private:
+    void _run(Redis &redis);
+
+    RedisInstance &_redis;
+};
+
+}
+
+}
+
+}
+
+#include "connection_cmds_test.hpp"
+
+#endif // end SEWENEW_REDISPLUSPLUS_TEST_CONNECTION_CMDS_TEST_H

+ 50 - 0
ext/redis-plus-plus-1.1.1/test/src/sw/redis++/connection_cmds_test.hpp

@@ -0,0 +1,50 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_TEST_CONNECTION_CMDS_TEST_HPP
+#define SEWENEW_REDISPLUSPLUS_TEST_CONNECTION_CMDS_TEST_HPP
+
+#include "utils.h"
+
+namespace sw {
+
+namespace redis {
+
+namespace test {
+
+template <typename RedisInstance>
+void ConnectionCmdTest<RedisInstance>::run() {
+    cluster_specializing_test(*this, &ConnectionCmdTest<RedisInstance>::_run, _redis);
+}
+
+template <typename RedisInstance>
+void ConnectionCmdTest<RedisInstance>::_run(Redis &instance) {
+    auto message = std::string("hello");
+
+    REDIS_ASSERT(instance.echo(message) == message, "failed to test echo");
+
+    REDIS_ASSERT(instance.ping() == "PONG", "failed to test ping");
+
+    REDIS_ASSERT(instance.ping(message) == message, "failed to test ping");
+}
+
+}
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_TEST_CONNECTION_CMDS_TEST_HPP

+ 47 - 0
ext/redis-plus-plus-1.1.1/test/src/sw/redis++/geo_cmds_test.h

@@ -0,0 +1,47 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_TEST_GEO_CMDS_TEST_H
+#define SEWENEW_REDISPLUSPLUS_TEST_GEO_CMDS_TEST_H
+
+#include <sw/redis++/redis++.h>
+
+namespace sw {
+
+namespace redis {
+
+namespace test {
+
+template <typename RedisInstance>
+class GeoCmdTest {
+public:
+    explicit GeoCmdTest(RedisInstance &instance) : _redis(instance) {}
+
+    void run();
+
+private:
+    RedisInstance &_redis;
+};
+
+}
+
+}
+
+}
+
+#include "geo_cmds_test.hpp"
+
+#endif // end SEWENEW_REDISPLUSPLUS_TEST_GEO_CMDS_TEST_H

+ 149 - 0
ext/redis-plus-plus-1.1.1/test/src/sw/redis++/geo_cmds_test.hpp

@@ -0,0 +1,149 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_TEST_GEO_CMDS_TEST_HPP
+#define SEWENEW_REDISPLUSPLUS_TEST_GEO_CMDS_TEST_HPP
+
+#include <vector>
+#include <tuple>
+#include "utils.h"
+
+namespace sw {
+
+namespace redis {
+
+namespace test {
+
+template <typename RedisInstance>
+void GeoCmdTest<RedisInstance>::run() {
+    auto key = test_key("geo");
+    auto dest = test_key("dest");
+
+    KeyDeleter<RedisInstance> deleter(_redis, {key, dest});
+
+    auto members = {
+        std::make_tuple("m1", 10.0, 11.0),
+        std::make_tuple("m2", 10.1, 11.1),
+        std::make_tuple("m3", 10.2, 11.2)
+    };
+
+    REDIS_ASSERT(_redis.geoadd(key, std::make_tuple("m1", 10.0, 11.0)) == 1,
+            "failed to test geoadd");
+    REDIS_ASSERT(_redis.geoadd(key, members) == 2, "failed to test geoadd");
+
+    auto dist = _redis.geodist(key, "m1", "m4", GeoUnit::KM);
+    REDIS_ASSERT(!dist, "failed to test geodist with nonexistent member");
+
+    std::vector<OptionalString> hashes;
+    _redis.geohash(key, {"m1", "m4"}, std::back_inserter(hashes));
+    REDIS_ASSERT(hashes.size() == 2, "failed to test geohash");
+    REDIS_ASSERT(bool(hashes[0]) && *(hashes[0]) == "s1zned3z8u0" && !(hashes[1]),
+            "failed to test geohash");
+    hashes.clear();
+    _redis.geohash(key, {"m4"}, std::back_inserter(hashes));
+    REDIS_ASSERT(hashes.size() == 1 && !(hashes[0]), "failed to test geohash");
+
+    std::vector<Optional<std::pair<double, double>>> pos;
+    _redis.geopos(key, {"m4"}, std::back_inserter(pos));
+    REDIS_ASSERT(pos.size() == 1 && !(pos[0]), "failed to test geopos");
+
+    auto num = _redis.georadius(key,
+                                std::make_pair(10.1, 11.1),
+                                100,
+                                GeoUnit::KM,
+                                dest,
+                                false,
+                                10);
+    REDIS_ASSERT(bool(num) && *num == 3, "failed to test georadius with store option");
+
+    std::vector<std::string> mems;
+    _redis.georadius(key,
+                    std::make_pair(10.1, 11.1),
+                    100,
+                    GeoUnit::KM,
+                    10,
+                    true,
+                    std::back_inserter(mems));
+    REDIS_ASSERT(mems.size() == 3, "failed to test georadius with no option");
+
+    std::vector<std::tuple<std::string, double>> with_dist;
+    _redis.georadius(key,
+                    std::make_pair(10.1, 11.1),
+                    100,
+                    GeoUnit::KM,
+                    10,
+                    true,
+                    std::back_inserter(with_dist));
+    REDIS_ASSERT(with_dist.size() == 3, "failed to test georadius with dist");
+
+    std::vector<std::tuple<std::string, double, std::pair<double, double>>> with_dist_coord;
+    _redis.georadius(key,
+                    std::make_pair(10.1, 11.1),
+                    100,
+                    GeoUnit::KM,
+                    10,
+                    true,
+                    std::back_inserter(with_dist_coord));
+    REDIS_ASSERT(with_dist_coord.size() == 3, "failed to test georadius with dist and coord");
+
+    num = _redis.georadiusbymember(key,
+                                    "m1",
+                                    100,
+                                    GeoUnit::KM,
+                                    dest,
+                                    false,
+                                    10);
+    REDIS_ASSERT(bool(num) && *num == 3, "failed to test georadiusbymember with store option");
+
+    mems.clear();
+    _redis.georadiusbymember(key,
+                            "m1",
+                            100,
+                            GeoUnit::KM,
+                            10,
+                            true,
+                            std::back_inserter(mems));
+    REDIS_ASSERT(mems.size() == 3, "failed to test georadiusbymember with no option");
+
+    with_dist.clear();
+    _redis.georadiusbymember(key,
+                            "m1",
+                            100,
+                            GeoUnit::KM,
+                            10,
+                            true,
+                            std::back_inserter(with_dist));
+    REDIS_ASSERT(with_dist.size() == 3, "failed to test georadiusbymember with dist");
+
+    with_dist_coord.clear();
+    _redis.georadiusbymember(key,
+                            "m1",
+                            100,
+                            GeoUnit::KM,
+                            10,
+                            true,
+                            std::back_inserter(with_dist_coord));
+    REDIS_ASSERT(with_dist_coord.size() == 3,
+            "failed to test georadiusbymember with dist and coord");
+}
+
+}
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_TEST_GEO_CMDS_TEST_HPP

+ 55 - 0
ext/redis-plus-plus-1.1.1/test/src/sw/redis++/hash_cmds_test.h

@@ -0,0 +1,55 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_TEST_HASH_CMDS_TEST_H
+#define SEWENEW_REDISPLUSPLUS_TEST_HASH_CMDS_TEST_H
+
+#include <sw/redis++/redis++.h>
+
+namespace sw {
+
+namespace redis {
+
+namespace test {
+
+template <typename RedisInstance>
+class HashCmdTest {
+public:
+    explicit HashCmdTest(RedisInstance &instance) : _redis(instance) {}
+
+    void run();
+
+private:
+    void _test_hash();
+
+    void _test_hash_batch();
+
+    void _test_numeric();
+
+    void _test_hscan();
+
+    RedisInstance &_redis;
+};
+
+}
+
+}
+
+}
+
+#include "hash_cmds_test.hpp"
+
+#endif // end SEWENEW_REDISPLUSPLUS_TEST_HASH_CMDS_TEST_H

+ 177 - 0
ext/redis-plus-plus-1.1.1/test/src/sw/redis++/hash_cmds_test.hpp

@@ -0,0 +1,177 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_TEST_HASH_CMDS_TEST_HPP
+#define SEWENEW_REDISPLUSPLUS_TEST_HASH_CMDS_TEST_HPP
+
+#include <unordered_map>
+#include "utils.h"
+
+namespace sw {
+
+namespace redis {
+
+namespace test {
+
+template <typename RedisInstance>
+void HashCmdTest<RedisInstance>::run() {
+    _test_hash();
+
+    _test_hash_batch();
+
+    _test_numeric();
+
+    _test_hscan();
+}
+
+template <typename RedisInstance>
+void HashCmdTest<RedisInstance>::_test_hash() {
+    auto key = test_key("hash");
+
+    KeyDeleter<RedisInstance> deleter(_redis, key);
+
+    auto f1 = std::string("f1");
+    auto v1 = std::string("v1");
+    auto f2 = std::string("f2");
+    auto v2 = std::string("v2");
+    auto f3 = std::string("f3");
+    auto v3 = std::string("v3");
+
+    REDIS_ASSERT(_redis.hset(key, f1, v1), "failed to test hset");
+    REDIS_ASSERT(!_redis.hset(key, f1, v2), "failed to test hset with exist field");
+
+    auto res = _redis.hget(key, f1);
+    REDIS_ASSERT(res && *res == v2, "failed to test hget");
+
+    REDIS_ASSERT(_redis.hsetnx(key, f2, v1), "failed to test hsetnx");
+    REDIS_ASSERT(!_redis.hsetnx(key, f2, v2), "failed to test hsetnx with exist field");
+
+    res = _redis.hget(key, f2);
+    REDIS_ASSERT(res && *res == v1, "failed to test hget");
+
+    REDIS_ASSERT(!_redis.hexists(key, f3), "failed to test hexists");
+    REDIS_ASSERT(_redis.hset(key, std::make_pair(f3, v3)), "failed to test hset");
+    REDIS_ASSERT(_redis.hexists(key, f3), "failed to test hexists");
+
+    REDIS_ASSERT(_redis.hlen(key) == 3, "failed to test hlen");
+    REDIS_ASSERT(_redis.hstrlen(key, f1) == static_cast<long long>(v1.size()),
+            "failed to test hstrlen");
+
+    REDIS_ASSERT(_redis.hdel(key, f1) == 1, "failed to test hdel");
+    REDIS_ASSERT(_redis.hdel(key, {f1, f2, f3}) == 2, "failed to test hdel range");
+}
+
+template <typename RedisInstance>
+void HashCmdTest<RedisInstance>::_test_hash_batch() {
+    auto key = test_key("hash");
+
+    KeyDeleter<RedisInstance> deleter(_redis, key);
+
+    auto f1 = std::string("f1");
+    auto v1 = std::string("v1");
+    auto f2 = std::string("f2");
+    auto v2 = std::string("v2");
+    auto f3 = std::string("f3");
+
+    _redis.hmset(key, {std::make_pair(f1, v1),
+                        std::make_pair(f2, v2)});
+
+    std::vector<std::string> fields;
+    _redis.hkeys(key, std::back_inserter(fields));
+    REDIS_ASSERT(fields.size() == 2, "failed to test hkeys");
+
+    std::vector<std::string> vals;
+    _redis.hvals(key, std::back_inserter(vals));
+    REDIS_ASSERT(vals.size() == 2, "failed to test hvals");
+
+    std::unordered_map<std::string, std::string> items;
+    _redis.hgetall(key, std::inserter(items, items.end()));
+    REDIS_ASSERT(items.size() == 2 && items[f1] == v1 && items[f2] == v2,
+            "failed to test hgetall");
+
+    std::vector<std::pair<std::string, std::string>> item_vec;
+    _redis.hgetall(key, std::back_inserter(item_vec));
+    REDIS_ASSERT(item_vec.size() == 2, "failed to test hgetall");
+
+    std::vector<OptionalString> res;
+    _redis.hmget(key, {f1, f2, f3}, std::back_inserter(res));
+    REDIS_ASSERT(res.size() == 3
+            && bool(res[0]) && *(res[0]) == v1
+            && bool(res[1]) && *(res[1]) == v2
+            && !res[2],
+                "failed to test hmget");
+}
+
+template <typename RedisInstance>
+void HashCmdTest<RedisInstance>::_test_numeric() {
+    auto key = test_key("numeric");
+
+    KeyDeleter<RedisInstance> deleter(_redis, key);
+
+    auto field = "field";
+
+    REDIS_ASSERT(_redis.hincrby(key, field, 1) == 1, "failed to test hincrby");
+    REDIS_ASSERT(_redis.hincrby(key, field, -1) == 0, "failed to test hincrby");
+    REDIS_ASSERT(_redis.hincrbyfloat(key, field, 1.5) == 1.5, "failed to test hincrbyfloat");
+}
+
+template <typename RedisInstance>
+void HashCmdTest<RedisInstance>::_test_hscan() {
+    auto key = test_key("hscan");
+
+    KeyDeleter<RedisInstance> deleter(_redis, key);
+
+    auto items = std::unordered_map<std::string, std::string>{
+        std::make_pair("f1", "v1"),
+        std::make_pair("f2", "v2"),
+        std::make_pair("f3", "v3"),
+    };
+
+    _redis.hmset(key, items.begin(), items.end());
+
+    std::unordered_map<std::string, std::string> item_map;
+    auto cursor = 0;
+    while (true) {
+        cursor = _redis.hscan(key, cursor, "f*", 2, std::inserter(item_map, item_map.end()));
+        if (cursor == 0) {
+            break;
+        }
+    }
+
+    REDIS_ASSERT(item_map == items, "failed to test hscan with pattern and count");
+
+    std::vector<std::pair<std::string, std::string>> item_vec;
+    cursor = 0;
+    while (true) {
+        cursor = _redis.hscan(key, cursor, std::back_inserter(item_vec));
+        if (cursor == 0) {
+            break;
+        }
+    }
+
+    REDIS_ASSERT(item_vec.size() == items.size(), "failed to test hscan");
+    for (const auto &ele : item_vec) {
+        REDIS_ASSERT(items.find(ele.first) != items.end(), "failed to test hscan");
+    }
+}
+
+}
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_TEST_HASH_CMDS_TEST_HPP

+ 47 - 0
ext/redis-plus-plus-1.1.1/test/src/sw/redis++/hyperloglog_cmds_test.h

@@ -0,0 +1,47 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_TEST_HYPERLOGLOG_CMDS_TEST_H
+#define SEWENEW_REDISPLUSPLUS_TEST_HYPERLOGLOG_CMDS_TEST_H
+
+#include <sw/redis++/redis++.h>
+
+namespace sw {
+
+namespace redis {
+
+namespace test {
+
+template <typename RedisInstance>
+class HyperloglogCmdTest {
+public:
+    explicit HyperloglogCmdTest(RedisInstance &instance) : _redis(instance) {}
+
+    void run();
+
+private:
+    RedisInstance &_redis;
+};
+
+}
+
+}
+
+}
+
+#include "hyperloglog_cmds_test.hpp"
+
+#endif // end SEWENEW_REDISPLUSPLUS_TEST_HYPERLOGLOG_CMDS_TEST_H

+ 67 - 0
ext/redis-plus-plus-1.1.1/test/src/sw/redis++/hyperloglog_cmds_test.hpp

@@ -0,0 +1,67 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_TEST_HYPERLOGLOG_CMDS_TEST_HPP
+#define SEWENEW_REDISPLUSPLUS_TEST_HYPERLOGLOG_CMDS_TEST_HPP
+
+#include "utils.h"
+
+namespace sw {
+
+namespace redis {
+
+namespace test {
+
+template <typename RedisInstance>
+void HyperloglogCmdTest<RedisInstance>::run() {
+    auto k1 = test_key("k1");
+    auto k2 = test_key("k2");
+    auto k3 = test_key("k3");
+
+    KeyDeleter<RedisInstance> deleter(_redis, {k1, k2, k3});
+
+    _redis.pfadd(k1, "a");
+    auto members1 = {"b", "c", "d", "e", "f", "g"};
+    _redis.pfadd(k1, members1);
+
+    auto cnt = _redis.pfcount(k1);
+    auto err = cnt * 1.0 / (1 + members1.size());
+    REDIS_ASSERT(err < 1.02 && err > 0.98, "failed to test pfadd and pfcount");
+
+    auto members2 = {"a", "b", "c", "h", "i", "j", "k"};
+    _redis.pfadd(k2, members2);
+    auto total = 1 + members1.size() + members2.size() - 3;
+
+    cnt = _redis.pfcount({k1, k2});
+    err = cnt * 1.0 / total;
+    REDIS_ASSERT(err < 1.02 && err > 0.98, "failed to test pfcount");
+
+    _redis.pfmerge(k3, {k1, k2});
+    cnt = _redis.pfcount(k3);
+    err = cnt * 1.0 / total;
+    REDIS_ASSERT(err < 1.02 && err > 0.98, "failed to test pfcount");
+
+    _redis.pfmerge(k3, k1);
+    REDIS_ASSERT(cnt == _redis.pfcount(k3), "failed to test pfmerge");
+}
+
+}
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_TEST_HYPERLOGLOG_CMDS_TEST_HPP

+ 55 - 0
ext/redis-plus-plus-1.1.1/test/src/sw/redis++/keys_cmds_test.h

@@ -0,0 +1,55 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_TEST_KEYS_CMDS_TEST_H
+#define SEWENEW_REDISPLUSPLUS_TEST_KEYS_CMDS_TEST_H
+
+#include <sw/redis++/redis++.h>
+
+namespace sw {
+
+namespace redis {
+
+namespace test {
+
+template <typename RedisInstance>
+class KeysCmdTest {
+public:
+    explicit KeysCmdTest(RedisInstance &instance) : _redis(instance) {}
+
+    void run();
+
+private:
+    void _test_key();
+
+    void _test_randomkey(Redis &instance);
+
+    void _test_ttl();
+
+    void _test_scan(Redis &instance);
+
+    RedisInstance &_redis;
+};
+
+}
+
+}
+
+}
+
+#include "keys_cmds_test.hpp"
+
+#endif // end SEWENEW_REDISPLUSPLUS_TEST_KEYS_CMDS_TEST_H

+ 166 - 0
ext/redis-plus-plus-1.1.1/test/src/sw/redis++/keys_cmds_test.hpp

@@ -0,0 +1,166 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_TEST_KEYS_CMDS_TEST_HPP
+#define SEWENEW_REDISPLUSPLUS_TEST_KEYS_CMDS_TEST_HPP
+
+#include <vector>
+#include <unordered_set>
+#include "utils.h"
+
+namespace sw {
+
+namespace redis {
+
+namespace test {
+
+template <typename RedisInstance>
+void KeysCmdTest<RedisInstance>::run() {
+    _test_key();
+
+    cluster_specializing_test(*this, &KeysCmdTest<RedisInstance>::_test_randomkey, _redis);
+
+    _test_ttl();
+
+    cluster_specializing_test(*this, &KeysCmdTest<RedisInstance>::_test_scan, _redis);
+}
+
+template <typename RedisInstance>
+void KeysCmdTest<RedisInstance>::_test_key() {
+    auto key = test_key("key");
+    auto dest = test_key("dest");
+    auto new_key_name = test_key("new-key");
+    auto not_exist_key = test_key("not-exist");
+
+    KeyDeleter<RedisInstance> deleter(_redis, {key, dest, new_key_name});
+
+    REDIS_ASSERT(_redis.exists(key) == 0, "failed to test exists");
+
+    auto val = std::string("val");
+    _redis.set(key, val);
+
+    REDIS_ASSERT(_redis.exists({key, not_exist_key}) == 1, "failed to test exists");
+
+    auto new_val = _redis.dump(key);
+    REDIS_ASSERT(bool(new_val), "failed to test dump");
+
+    _redis.restore(dest, *new_val, std::chrono::seconds(1000));
+
+    new_val = _redis.get(dest);
+    REDIS_ASSERT(bool(new_val) && *new_val == val, "failed to test dump and restore");
+
+    _redis.rename(dest, new_key_name);
+
+    bool not_exist = false;
+    try {
+        _redis.rename(not_exist_key, new_key_name);
+    } catch (const Error &e) {
+        not_exist = true;
+    }
+    REDIS_ASSERT(not_exist, "failed to test rename with nonexistent key");
+
+    REDIS_ASSERT(_redis.renamenx(new_key_name, dest), "failed to test renamenx");
+
+    REDIS_ASSERT(_redis.touch(not_exist_key) == 0, "failed to test touch");
+    REDIS_ASSERT(_redis.touch({key, dest, new_key_name}) == 2, "failed to test touch");
+
+    REDIS_ASSERT(_redis.type(key) == "string", "failed to test type");
+
+    REDIS_ASSERT(_redis.del({new_key_name, dest}) == 1, "failed to test del");
+    REDIS_ASSERT(_redis.unlink({new_key_name, key}) == 1, "failed to test unlink");
+}
+
+template <typename RedisInstance>
+void KeysCmdTest<RedisInstance>::_test_randomkey(Redis &instance) {
+    auto key = test_key("randomkey");
+
+    KeyDeleter<Redis> deleter(instance, key);
+
+    instance.set(key, "value");
+
+    auto rand_key = instance.randomkey();
+    REDIS_ASSERT(bool(rand_key), "failed to test randomkey");
+}
+
+template <typename RedisInstance>
+void KeysCmdTest<RedisInstance>::_test_ttl() {
+    using namespace std::chrono;
+
+    auto key = test_key("ttl");
+
+    KeyDeleter<RedisInstance> deleter(_redis, key);
+
+    _redis.set(key, "val", seconds(100));
+    auto ttl = _redis.ttl(key);
+    REDIS_ASSERT(ttl > 0 && ttl <= 100, "failed to test ttl");
+
+    REDIS_ASSERT(_redis.persist(key), "failed to test persist");
+    ttl = _redis.ttl(key);
+    REDIS_ASSERT(ttl == -1, "failed to test ttl");
+
+    REDIS_ASSERT(_redis.expire(key, seconds(100)),
+            "failed to test expire");
+
+    auto tp = time_point_cast<seconds>(system_clock::now() + seconds(100));
+    REDIS_ASSERT(_redis.expireat(key, tp), "failed to test expireat");
+    ttl = _redis.ttl(key);
+    REDIS_ASSERT(ttl > 0, "failed to test expireat");
+
+    REDIS_ASSERT(_redis.pexpire(key, milliseconds(100000)), "failed to test expire");
+
+    auto pttl = _redis.pttl(key);
+    REDIS_ASSERT(pttl > 0 && pttl <= 100000, "failed to test pttl");
+
+    auto tp_milli = time_point_cast<milliseconds>(system_clock::now() + milliseconds(100000));
+    REDIS_ASSERT(_redis.pexpireat(key, tp_milli), "failed to test pexpireat");
+    pttl = _redis.pttl(key);
+    REDIS_ASSERT(pttl > 0, "failed to test pexpireat");
+}
+
+template <typename RedisInstance>
+void KeysCmdTest<RedisInstance>::_test_scan(Redis &instance) {
+    std::string key_pattern = "!@#$%^&()_+alseufoawhnlkszd";
+    auto k1 = test_key(key_pattern + "k1");
+    auto k2 = test_key(key_pattern + "k2");
+    auto k3 = test_key(key_pattern + "k3");
+
+    auto keys = {k1, k2, k3};
+
+    KeyDeleter<Redis> deleter(instance, keys);
+
+    instance.set(k1, "v");
+    instance.set(k2, "v");
+    instance.set(k3, "v");
+
+    auto cursor = 0;
+    std::unordered_set<std::string> res;
+    while (true) {
+        cursor = instance.scan(cursor, "*" + key_pattern + "*", 2, std::inserter(res, res.end()));
+        if (cursor == 0) {
+            break;
+        }
+    }
+    REDIS_ASSERT(res == std::unordered_set<std::string>(keys),
+            "failed to test scan");
+}
+
+}
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_TEST_KEYS_CMDS_TEST_HPP

+ 55 - 0
ext/redis-plus-plus-1.1.1/test/src/sw/redis++/list_cmds_test.h

@@ -0,0 +1,55 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_TEST_LIST_CMDS_TEST_H
+#define SEWENEW_REDISPLUSPLUS_TEST_LIST_CMDS_TEST_H
+
+#include <sw/redis++/redis++.h>
+
+namespace sw {
+
+namespace redis {
+
+namespace test {
+
+template <typename RedisInstance>
+class ListCmdTest {
+public:
+    explicit ListCmdTest(RedisInstance &instance) : _redis(instance) {}
+
+    void run();
+
+private:
+    void _test_lpoppush();
+
+    void _test_rpoppush();
+
+    void _test_list();
+
+    void _test_blocking();
+
+    RedisInstance &_redis;
+};
+
+}
+
+}
+
+}
+
+#include "list_cmds_test.hpp"
+
+#endif // end SEWENEW_REDISPLUSPLUS_TEST_LIST_CMDS_TEST_H

+ 154 - 0
ext/redis-plus-plus-1.1.1/test/src/sw/redis++/list_cmds_test.hpp

@@ -0,0 +1,154 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_TEST_LIST_CMDS_TEST_HPP
+#define SEWENEW_REDISPLUSPLUS_TEST_LIST_CMDS_TEST_HPP
+
+#include "utils.h"
+
+namespace sw {
+
+namespace redis {
+
+namespace test {
+
+template <typename RedisInstance>
+void ListCmdTest<RedisInstance>::run() {
+    _test_lpoppush();
+
+    _test_rpoppush();
+
+    _test_list();
+
+    _test_blocking();
+}
+
+template <typename RedisInstance>
+void ListCmdTest<RedisInstance>::_test_lpoppush() {
+    auto key = test_key("lpoppush");
+
+    KeyDeleter<RedisInstance> deleter(_redis, key);
+
+    auto item = _redis.lpop(key);
+    REDIS_ASSERT(!item, "failed to test lpop");
+
+    REDIS_ASSERT(_redis.lpushx(key, "1") == 0, "failed to test lpushx");
+    REDIS_ASSERT(_redis.lpush(key, "1") == 1, "failed to test lpush");
+    REDIS_ASSERT(_redis.lpushx(key, "2") == 2, "failed to test lpushx");
+    REDIS_ASSERT(_redis.lpush(key, {"3", "4", "5"}) == 5, "failed to test lpush");
+
+    item = _redis.lpop(key);
+    REDIS_ASSERT(item && *item == "5", "failed to test lpop");
+}
+
+template <typename RedisInstance>
+void ListCmdTest<RedisInstance>::_test_rpoppush() {
+    auto key = test_key("rpoppush");
+
+    KeyDeleter<RedisInstance> deleter(_redis, key);
+
+    auto item = _redis.rpop(key);
+    REDIS_ASSERT(!item, "failed to test rpop");
+
+    REDIS_ASSERT(_redis.rpushx(key, "1") == 0, "failed to test rpushx");
+    REDIS_ASSERT(_redis.rpush(key, "1") == 1, "failed to test rpush");
+    REDIS_ASSERT(_redis.rpushx(key, "2") == 2, "failed to test rpushx");
+    REDIS_ASSERT(_redis.rpush(key, {"3", "4", "5"}) == 5, "failed to test rpush");
+
+    item = _redis.rpop(key);
+    REDIS_ASSERT(item && *item == "5", "failed to test rpop");
+}
+
+template <typename RedisInstance>
+void ListCmdTest<RedisInstance>::_test_list() {
+    auto key = test_key("list");
+
+    KeyDeleter<RedisInstance> deleter(_redis, key);
+
+    auto item = _redis.lindex(key, 0);
+    REDIS_ASSERT(!item, "failed to test lindex");
+
+    _redis.lpush(key, {"1", "2", "3", "4", "5"});
+
+    REDIS_ASSERT(_redis.lrem(key, 0, "3") == 1, "failed to test lrem");
+
+    REDIS_ASSERT(_redis.linsert(key, InsertPosition::BEFORE, "2", "3") == 5,
+            "failed to test lindex");
+
+    REDIS_ASSERT(_redis.llen(key) == 5, "failed to test llen");
+
+    _redis.lset(key, 0, "6");
+    item = _redis.lindex(key, 0);
+    REDIS_ASSERT(item && *item == "6", "failed to test lindex");
+
+    _redis.ltrim(key, 0, 2);
+
+    std::vector<std::string> res;
+    _redis.lrange(key, 0, -1, std::back_inserter(res));
+    REDIS_ASSERT(res == std::vector<std::string>({"6", "4", "3"}), "failed to test ltrim");
+}
+
+template <typename RedisInstance>
+void ListCmdTest<RedisInstance>::_test_blocking() {
+    auto k1 = test_key("k1");
+    auto k2 = test_key("k2");
+    auto k3 = test_key("k3");
+
+    auto keys = {k1, k2, k3};
+
+    KeyDeleter<RedisInstance> deleter(_redis, keys);
+
+    std::string val("value");
+    _redis.lpush(k1, val);
+
+    auto res = _redis.blpop(keys.begin(), keys.end());
+    REDIS_ASSERT(res && *res == std::make_pair(k1, val), "failed to test blpop");
+
+    res = _redis.brpop(keys, std::chrono::seconds(1));
+    REDIS_ASSERT(!res, "failed to test brpop with timeout");
+
+    _redis.lpush(k1, val);
+    res = _redis.blpop(k1);
+    REDIS_ASSERT(res && *res == std::make_pair(k1, val), "failed to test blpop");
+
+    res = _redis.blpop(k1, std::chrono::seconds(1));
+    REDIS_ASSERT(!res, "failed to test blpop with timeout");
+
+    _redis.lpush(k1, val);
+    res = _redis.brpop(k1);
+    REDIS_ASSERT(res && *res == std::make_pair(k1, val), "failed to test brpop");
+
+    res = _redis.brpop(k1, std::chrono::seconds(1));
+    REDIS_ASSERT(!res, "failed to test brpop with timeout");
+
+    auto str = _redis.brpoplpush(k2, k3, std::chrono::seconds(1));
+    REDIS_ASSERT(!str, "failed to test brpoplpush with timeout");
+
+    _redis.lpush(k2, val);
+    str = _redis.brpoplpush(k2, k3);
+    REDIS_ASSERT(str && *str == val, "failed to test brpoplpush");
+
+    str = _redis.rpoplpush(k3, k2);
+    REDIS_ASSERT(str && *str == val, "failed to test rpoplpush");
+}
+
+}
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_TEST_LIST_CMDS_TEST_HPP

+ 57 - 0
ext/redis-plus-plus-1.1.1/test/src/sw/redis++/pipeline_transaction_test.h

@@ -0,0 +1,57 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_TEST_PIPELINE_TRANSACTION_TEST_H
+#define SEWENEW_REDISPLUSPLUS_TEST_PIPELINE_TRANSACTION_TEST_H
+
+#include <sw/redis++/redis++.h>
+
+namespace sw {
+
+namespace redis {
+
+namespace test {
+
+template <typename RedisInstance>
+class PipelineTransactionTest {
+public:
+    explicit PipelineTransactionTest(RedisInstance &instance) : _redis(instance) {}
+
+    void run();
+
+private:
+    Pipeline _pipeline(const StringView &key);
+
+    Transaction _transaction(const StringView &key, bool piped);
+
+    void _test_pipeline(const StringView &key, Pipeline &pipe);
+
+    void _test_transaction(const StringView &key, Transaction &tx);
+
+    void _test_watch();
+
+    RedisInstance &_redis;
+};
+
+}
+
+}
+
+}
+
+#include "pipeline_transaction_test.hpp"
+
+#endif // end SEWENEW_REDISPLUSPLUS_TEST_PIPELINE_TRANSACTION_TEST_H

+ 184 - 0
ext/redis-plus-plus-1.1.1/test/src/sw/redis++/pipeline_transaction_test.hpp

@@ -0,0 +1,184 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_TEST_PIPELINE_TRANSACTION_TEST_HPP
+#define SEWENEW_REDISPLUSPLUS_TEST_PIPELINE_TRANSACTION_TEST_HPP
+
+#include <string>
+#include "utils.h"
+
+namespace sw {
+
+namespace redis {
+
+namespace test {
+
+template <typename RedisInstance>
+void PipelineTransactionTest<RedisInstance>::run() {
+    {
+        auto key = test_key("pipeline");
+        KeyDeleter<RedisInstance> deleter(_redis, key);
+        auto pipe = _pipeline(key);
+        _test_pipeline(key, pipe);
+    }
+
+    {
+        auto key = test_key("transaction");
+        KeyDeleter<RedisInstance> deleter(_redis, key);
+        auto tx = _transaction(key, true);
+        _test_transaction(key, tx);
+    }
+
+    {
+        auto key = test_key("transaction");
+        KeyDeleter<RedisInstance> deleter(_redis, key);
+        auto tx = _transaction(key, false);
+        _test_transaction(key, tx);
+    }
+
+    _test_watch();
+}
+
+template <typename RedisInstance>
+Pipeline PipelineTransactionTest<RedisInstance>::_pipeline(const StringView &) {
+    return _redis.pipeline();
+}
+
+template <>
+inline Pipeline PipelineTransactionTest<RedisCluster>::_pipeline(const StringView &key) {
+    return _redis.pipeline(key);
+}
+
+template <typename RedisInstance>
+Transaction PipelineTransactionTest<RedisInstance>::_transaction(const StringView &, bool piped) {
+    return _redis.transaction(piped);
+}
+
+template <>
+inline Transaction PipelineTransactionTest<RedisCluster>::_transaction(const StringView &key,
+        bool piped) {
+    return _redis.transaction(key, piped);
+}
+
+template <typename RedisInstance>
+void PipelineTransactionTest<RedisInstance>::_test_pipeline(const StringView &key,
+        Pipeline &pipe) {
+    std::string val("value");
+    auto replies = pipe.set(key, val)
+                        .get(key)
+                        .strlen(key)
+                        .exec();
+
+    REDIS_ASSERT(replies.get<bool>(0), "failed to test pipeline with set operation");
+
+    auto new_val = replies.get<OptionalString>(1);
+    std::size_t len = replies.get<long long>(2);
+    REDIS_ASSERT(bool(new_val) && *new_val == val && len == val.size(),
+            "failed to test pipeline with string operations");
+
+    REDIS_ASSERT(reply::parse<bool>(replies.get(0)), "failed to test pipeline with set operation");
+
+    new_val = reply::parse<OptionalString>(replies.get(1));
+    len = reply::parse<long long>(replies.get(2));
+    REDIS_ASSERT(bool(new_val) && *new_val == val && len == val.size(),
+            "failed to test pipeline with string operations");
+}
+
+template <typename RedisInstance>
+void PipelineTransactionTest<RedisInstance>::_test_transaction(const StringView &key,
+        Transaction &tx) {
+    std::unordered_map<std::string, std::string> m = {
+        std::make_pair("f1", "v1"),
+        std::make_pair("f2", "v2"),
+        std::make_pair("f3", "v3")
+    };
+    auto replies = tx.hmset(key, m.begin(), m.end())
+                        .hgetall(key)
+                        .hdel(key, "f1")
+                        .exec();
+
+    replies.get<void>(0);
+
+    decltype(m) mm;
+    replies.get(1, std::inserter(mm, mm.end()));
+    REDIS_ASSERT(mm == m, "failed to test transaction");
+
+    REDIS_ASSERT(replies.get<long long>(2) == 1, "failed to test transaction");
+
+    tx.set(key, "value")
+        .get(key)
+        .incr(key);
+
+    tx.discard();
+
+    replies = tx.del(key)
+                .set(key, "value")
+                .exec();
+
+    REDIS_ASSERT(replies.get<long long>(0) == 1, "failed to test transaction");
+
+    REDIS_ASSERT(replies.get<bool>(1), "failed to test transaction");
+}
+
+template <typename RedisInstance>
+void PipelineTransactionTest<RedisInstance>::_test_watch() {
+    auto key = test_key("watch");
+
+    KeyDeleter<RedisInstance> deleter(_redis, key);
+
+    {
+        auto tx = _transaction(key, false);
+
+        auto redis = tx.redis();
+
+        redis.watch(key);
+
+        auto replies = tx.set(key, "1").get(key).exec();
+
+        REDIS_ASSERT(replies.size() == 2
+                && replies.template get<bool>(0) == true, "failed to test watch");
+
+        auto val = replies.template get<sw::redis::OptionalString>(1);
+
+        REDIS_ASSERT(val && *val == "1", "failed to test watch");
+    }
+
+    try {
+        auto tx = _transaction(key, false);
+
+        auto redis = tx.redis();
+
+        redis.watch(key);
+
+        // Key has been modified by other client.
+        _redis.set(key, "val");
+
+        // Transaction should fail, and throw WatchError
+        tx.set(key, "1").exec();
+
+        REDIS_ASSERT(false, "failed to test watch");
+    } catch (const sw::redis::WatchError &err) {
+        // Catch the error.
+    }
+}
+
+}
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_TEST_PIPELINE_TRANSACTION_TEST_HPP

+ 53 - 0
ext/redis-plus-plus-1.1.1/test/src/sw/redis++/pubsub_test.h

@@ -0,0 +1,53 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_TEST_SUBPUB_TEST_H
+#define SEWENEW_REDISPLUSPLUS_TEST_SUBPUB_TEST_H
+
+#include <sw/redis++/redis++.h>
+
+namespace sw {
+
+namespace redis {
+
+namespace test {
+
+template <typename RedisInstance>
+class PubSubTest {
+public:
+    explicit PubSubTest(RedisInstance &instance) : _redis(instance) {}
+
+    void run();
+
+private:
+    void _test_sub_channel();
+
+    void _test_sub_pattern();
+
+    void _test_unsubscribe();
+
+    RedisInstance &_redis;
+};
+
+}
+
+}
+
+}
+
+#include "pubsub_test.hpp"
+
+#endif // end SEWENEW_REDISPLUSPLUS_TEST_SUBPUB_TEST_H

+ 244 - 0
ext/redis-plus-plus-1.1.1/test/src/sw/redis++/pubsub_test.hpp

@@ -0,0 +1,244 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_TEST_SUBPUB_TEST_HPP
+#define SEWENEW_REDISPLUSPLUS_TEST_SUBPUB_TEST_HPP
+
+#include <unordered_map>
+#include <unordered_set>
+#include "utils.h"
+
+namespace sw {
+
+namespace redis {
+
+namespace test {
+
+template <typename RedisInstance>
+void PubSubTest<RedisInstance>::run() {
+    _test_sub_channel();
+
+    _test_sub_pattern();
+
+    _test_unsubscribe();
+}
+
+template <typename RedisInstance>
+void PubSubTest<RedisInstance>::_test_sub_channel() {
+    auto sub = _redis.subscriber();
+
+    auto msgs = {"msg1", "msg2"};
+    auto channel1 = test_key("c1");
+    sub.on_message([&msgs, &channel1](std::string channel, std::string msg) {
+                        static std::size_t idx = 0;
+                        REDIS_ASSERT(channel == channel1 && msg == *(msgs.begin() + idx),
+                                        "failed to test subscribe");
+                        ++idx;
+                    });
+
+    sub.subscribe(channel1);
+
+    // Consume the SUBSCRIBE message.
+    sub.consume();
+
+    for (const auto &msg : msgs) {
+        _redis.publish(channel1, msg);
+        sub.consume();
+    }
+
+    sub.unsubscribe(channel1);
+
+    // Consume the UNSUBSCRIBE message.
+    sub.consume();
+
+    auto channel2 = test_key("c2");
+    auto channel3 = test_key("c3");
+    auto channel4 = test_key("c4");
+    std::unordered_set<std::string> channels;
+    sub.on_meta([&channels](Subscriber::MsgType type,
+                            OptionalString channel,
+                            long long num) {
+                    REDIS_ASSERT(bool(channel), "failed to test subscribe");
+
+                    if (type == Subscriber::MsgType::SUBSCRIBE) {
+                        auto iter = channels.find(*channel);
+                        REDIS_ASSERT(iter == channels.end(), "failed to test subscribe");
+                        channels.insert(*channel);
+                        REDIS_ASSERT(static_cast<std::size_t>(num) == channels.size(),
+                                        "failed to test subscribe");
+                    } else if (type == Subscriber::MsgType::UNSUBSCRIBE) {
+                        auto iter = channels.find(*channel);
+                        REDIS_ASSERT(iter != channels.end(), "failed to test subscribe");
+                        channels.erase(*channel);
+                        REDIS_ASSERT(static_cast<std::size_t>(num) == channels.size(),
+                                        "failed to test subscribe");
+                    } else {
+                        REDIS_ASSERT(false, "Unknown message type");
+                    }
+                });
+
+    std::unordered_map<std::string, std::string> messages = {
+        {channel2, "msg2"},
+        {channel3, "msg3"},
+        {channel4, "msg4"},
+    };
+    sub.on_message([&messages](std::string channel, std::string msg) {
+                        REDIS_ASSERT(messages.find(channel) != messages.end(),
+                                        "failed to test subscribe");
+                        REDIS_ASSERT(messages[channel] == msg, "failed to test subscribe");
+                   });
+
+    sub.subscribe({channel2, channel3, channel4});
+
+    for (std::size_t idx = 0; idx != channels.size(); ++idx) {
+        sub.consume();
+    }
+
+    for (const auto &ele : messages) {
+        _redis.publish(ele.first, ele.second);
+        sub.consume();
+    }
+
+    auto tmp = {channel2, channel3, channel4};
+    sub.unsubscribe(tmp);
+
+    for (std::size_t idx = 0; idx != tmp.size(); ++idx) {
+        sub.consume();
+    }
+}
+
+template <typename RedisInstance>
+void PubSubTest<RedisInstance>::_test_sub_pattern() {
+    auto sub = _redis.subscriber();
+
+    auto msgs = {"msg1", "msg2"};
+    auto pattern1 = test_key("pattern*");
+    std::string channel1 = test_key("pattern1");
+    sub.on_pmessage([&msgs, &pattern1, &channel1](std::string pattern,
+                                                    std::string channel,
+                                                    std::string msg) {
+                        static std::size_t idx = 0;
+                        REDIS_ASSERT(pattern == pattern1
+                                        && channel == channel1
+                                        && msg == *(msgs.begin() + idx),
+                                        "failed to test psubscribe");
+                        ++idx;
+                    });
+
+    sub.psubscribe(pattern1);
+
+    // Consume the PSUBSCRIBE message.
+    sub.consume();
+
+    for (const auto &msg : msgs) {
+        _redis.publish(channel1, msg);
+        sub.consume();
+    }
+
+    sub.punsubscribe(pattern1);
+
+    // Consume the PUNSUBSCRIBE message.
+    sub.consume();
+
+    auto channel2 = test_key("pattern22");
+    auto channel3 = test_key("pattern33");
+    auto channel4 = test_key("pattern44");
+    std::unordered_set<std::string> channels;
+    sub.on_meta([&channels](Subscriber::MsgType type,
+                            OptionalString channel,
+                            long long num) {
+                    REDIS_ASSERT(bool(channel), "failed to test psubscribe");
+
+                    if (type == Subscriber::MsgType::PSUBSCRIBE) {
+                        auto iter = channels.find(*channel);
+                        REDIS_ASSERT(iter == channels.end(), "failed to test psubscribe");
+                        channels.insert(*channel);
+                        REDIS_ASSERT(static_cast<std::size_t>(num) == channels.size(),
+                                        "failed to test psubscribe");
+                    } else if (type == Subscriber::MsgType::PUNSUBSCRIBE) {
+                        auto iter = channels.find(*channel);
+                        REDIS_ASSERT(iter != channels.end(), "failed to test psubscribe");
+                        channels.erase(*channel);
+                        REDIS_ASSERT(static_cast<std::size_t>(num) == channels.size(),
+                                        "failed to test psubscribe");
+                    } else {
+                        REDIS_ASSERT(false, "Unknown message type");
+                    }
+                });
+
+    auto pattern2 = test_key("pattern2*");
+    auto pattern3 = test_key("pattern3*");
+    auto pattern4 = test_key("pattern4*");
+    std::unordered_set<std::string> patterns = {pattern2, pattern3, pattern4};
+
+    std::unordered_map<std::string, std::string> messages = {
+        {channel2, "msg2"},
+        {channel3, "msg3"},
+        {channel4, "msg4"},
+    };
+    sub.on_pmessage([&patterns, &messages](std::string pattern,
+                                            std::string channel,
+                                            std::string msg) {
+                        REDIS_ASSERT(patterns.find(pattern) != patterns.end(),
+                                        "failed to test psubscribe");
+                        REDIS_ASSERT(messages[channel] == msg, "failed to test psubscribe");
+                    });
+
+    sub.psubscribe({pattern2, pattern3, pattern4});
+
+    for (std::size_t idx = 0; idx != channels.size(); ++idx) {
+        sub.consume();
+    }
+
+    for (const auto &ele : messages) {
+        _redis.publish(ele.first, ele.second);
+        sub.consume();
+    }
+
+    auto tmp = {pattern2, pattern3, pattern4};
+    sub.punsubscribe(tmp);
+
+    for (std::size_t idx = 0; idx != tmp.size(); ++idx) {
+        sub.consume();
+    }
+}
+
+template <typename RedisInstance>
+void PubSubTest<RedisInstance>::_test_unsubscribe() {
+    auto sub = _redis.subscriber();
+
+    sub.on_meta([](Subscriber::MsgType type,
+                        OptionalString channel,
+                        long long num) {
+                        REDIS_ASSERT(type == Subscriber::MsgType::UNSUBSCRIBE,
+                                        "failed to test unsub");
+
+                        REDIS_ASSERT(!channel, "failed to test unsub");
+
+                        REDIS_ASSERT(num == 0, "failed to test unsub");
+                    });
+
+    sub.unsubscribe();
+    sub.consume();
+}
+
+}
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_TEST_SUBPUB_TEST_HPP

+ 76 - 0
ext/redis-plus-plus-1.1.1/test/src/sw/redis++/sanity_test.h

@@ -0,0 +1,76 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_TEST_SANITY_TEST_H
+#define SEWENEW_REDISPLUSPLUS_TEST_SANITY_TEST_H
+
+#include <sw/redis++/redis++.h>
+
+namespace sw {
+
+namespace redis {
+
+namespace test {
+
+template <typename RedisInstance>
+class SanityTest {
+public:
+    SanityTest(const ConnectionOptions &opts, RedisInstance &instance)
+        : _opts(opts), _redis(instance) {}
+
+    void run();
+
+private:
+    void _test_uri_ctor();
+
+    void _ping(Redis &instance);
+
+    void _test_move_ctor();
+
+    void _test_cmdargs();
+
+    void _test_generic_command();
+
+    void _test_hash_tag();
+
+    void _test_hash_tag(std::initializer_list<std::string> keys);
+
+    std::string _test_key(const std::string &key);
+
+    void _test_ping(Redis &instance);
+
+    void _test_pipeline(const StringView &key, Pipeline &pipeline);
+
+    void _test_transaction(const StringView &key, Transaction &transaction);
+
+    Pipeline _pipeline(const StringView &key);
+
+    Transaction _transaction(const StringView &key);
+
+    ConnectionOptions _opts;
+
+    RedisInstance &_redis;
+};
+
+}
+
+}
+
+}
+
+#include "sanity_test.hpp"
+
+#endif // end SEWENEW_REDISPLUSPLUS_TEST_SANITY_TEST_H

+ 299 - 0
ext/redis-plus-plus-1.1.1/test/src/sw/redis++/sanity_test.hpp

@@ -0,0 +1,299 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_TEST_SANITY_TEST_HPP
+#define SEWENEW_REDISPLUSPLUS_TEST_SANITY_TEST_HPP
+
+#include "utils.h"
+
+namespace sw {
+
+namespace redis {
+
+namespace test {
+
+template <typename RedisInstance>
+void SanityTest<RedisInstance>::run() {
+    _test_uri_ctor();
+
+    _test_move_ctor();
+
+    cluster_specializing_test(*this, &SanityTest<RedisInstance>::_test_ping, _redis);
+
+    auto pipe_key = test_key("pipeline");
+    auto tx_key = test_key("transaction");
+
+    KeyDeleter<RedisInstance> deleter(_redis, {pipe_key, tx_key});
+
+    auto pipeline = _pipeline(pipe_key);
+    _test_pipeline(pipe_key, pipeline);
+
+    auto transaction = _transaction(tx_key);
+    _test_transaction(tx_key, transaction);
+
+    _test_cmdargs();
+
+    _test_generic_command();
+}
+
+template <typename RedisInstance>
+void SanityTest<RedisInstance>::_test_uri_ctor() {
+    std::string uri;
+    switch (_opts.type) {
+    case sw::redis::ConnectionType::TCP:
+        uri = "tcp://" + _opts.host + ":" + std::to_string(_opts.port);
+        break;
+
+    case sw::redis::ConnectionType::UNIX:
+        REDIS_ASSERT(false, "NO test for UNIX Domain Socket");
+        break;
+
+    default:
+        REDIS_ASSERT(false, "Unknown connection type");
+    }
+
+    auto instance = RedisInstance(uri);
+
+    cluster_specializing_test(*this, &SanityTest<RedisInstance>::_ping, instance);
+}
+
+template <typename RedisInstance>
+void SanityTest<RedisInstance>::_ping(Redis &instance) {
+    try {
+        auto pong = instance.ping();
+        REDIS_ASSERT(pong == "PONG", "Failed to test constructing Redis with uri");
+    } catch (const sw::redis::ReplyError &e) {
+        REDIS_ASSERT(e.what() == std::string("NOAUTH Authentication required."),
+                "Failed to test constructing Redis with uri");
+    }
+}
+
+template <typename RedisInstance>
+void SanityTest<RedisInstance>::_test_move_ctor() {
+    auto test_move_ctor = std::move(_redis);
+
+    _redis = std::move(test_move_ctor);
+}
+
+template <typename RedisInstance>
+void SanityTest<RedisInstance>::_test_cmdargs() {
+    auto lpush_num = [](Connection &connection, const StringView &key, long long num) {
+        connection.send("LPUSH %b %lld",
+                        key.data(), key.size(),
+                        num);
+    };
+
+    auto lpush_nums = [](Connection &connection,
+                            const StringView &key,
+                            const std::vector<long long> &nums) {
+        CmdArgs args;
+        args.append("LPUSH").append(key);
+        for (auto num : nums) {
+            args.append(std::to_string(num));
+        }
+
+        connection.send(args);
+    };
+
+    auto key = test_key("lpush_num");
+
+    KeyDeleter<RedisInstance> deleter(_redis, key);
+
+    auto reply = _redis.command(lpush_num, key, 1);
+    REDIS_ASSERT(reply::parse<long long>(*reply) == 1, "failed to test cmdargs");
+
+    std::vector<long long> nums = {2, 3, 4, 5};
+    reply = _redis.command(lpush_nums, key, nums);
+    REDIS_ASSERT(reply::parse<long long>(*reply) == 5, "failed to test cmdargs");
+
+    std::vector<std::string> res;
+    _redis.lrange(key, 0, -1, std::back_inserter(res));
+    REDIS_ASSERT((res == std::vector<std::string>{"5", "4", "3", "2", "1"}),
+            "failed to test cmdargs");
+}
+
+template <typename RedisInstance>
+void SanityTest<RedisInstance>::_test_generic_command() {
+    auto key = test_key("key");
+    auto not_exist_key = test_key("not_exist_key");
+    auto k1 = test_key("k1");
+    auto k2 = test_key("k2");
+
+    KeyDeleter<RedisInstance> deleter(_redis, {key, not_exist_key, k1, k2});
+
+    std::string cmd("set");
+    _redis.command(cmd, key, 123);
+    auto reply = _redis.command("get", key);
+    auto val = reply::parse<OptionalString>(*reply);
+    REDIS_ASSERT(val && *val == "123", "failed to test generic command");
+
+    val = _redis.template command<OptionalString>("get", key);
+    REDIS_ASSERT(val && *val == "123", "failed to test generic command");
+
+    std::vector<OptionalString> res;
+    _redis.command("mget", key, not_exist_key, std::back_inserter(res));
+    REDIS_ASSERT(res.size() == 2 && res[0] && *res[0] == "123" && !res[1],
+            "failed to test generic command");
+
+    reply = _redis.command("incr", key);
+    REDIS_ASSERT(reply::parse<long long>(*reply) == 124, "failed to test generic command");
+
+    _redis.command("mset", k1.c_str(), "v", k2.c_str(), "v");
+    reply = _redis.command("mget", k1, k2);
+    res.clear();
+    reply::to_array(*reply, std::back_inserter(res));
+    REDIS_ASSERT(res.size() == 2 && res[0] && *(res[0]) == "v" && res[1] && *(res[1]) == "v",
+            "failed to test generic command");
+
+    res = _redis.template command<std::vector<OptionalString>>("mget", k1, k2);
+    REDIS_ASSERT(res.size() == 2 && res[0] && *(res[0]) == "v" && res[1] && *(res[1]) == "v",
+            "failed to test generic command");
+
+    res.clear();
+    _redis.command("mget", k1, k2, std::back_inserter(res));
+    REDIS_ASSERT(res.size() == 2 && res[0] && *(res[0]) == "v" && res[1] && *(res[1]) == "v",
+            "failed to test generic command");
+
+    auto set_cmd_str = {"set", key.c_str(), "new_value"};
+    _redis.command(set_cmd_str.begin(), set_cmd_str.end());
+
+    auto get_cmd_str = {"get", key.c_str()};
+    reply = _redis.command(get_cmd_str.begin(), get_cmd_str.end());
+    val = reply::parse<OptionalString>(*reply);
+    REDIS_ASSERT(val && *val == "new_value", "failed to test generic command");
+
+    val = _redis.template command<OptionalString>(get_cmd_str.begin(), get_cmd_str.end());
+    REDIS_ASSERT(val && *val == "new_value", "failed to test generic command");
+
+    auto mget_cmd_str = {"mget", key.c_str(), not_exist_key.c_str()};
+    res.clear();
+    _redis.command(mget_cmd_str.begin(), mget_cmd_str.end(), std::back_inserter(res));
+    REDIS_ASSERT(res.size() == 2 && res[0] && *res[0] == "new_value" && !res[1],
+            "failed to test generic command");
+}
+
+template <typename RedisInstance>
+void SanityTest<RedisInstance>::_test_hash_tag() {
+    _test_hash_tag({_test_key("{tag}postfix1"),
+                    _test_key("{tag}postfix2"),
+                    _test_key("{tag}postfix3")});
+
+    _test_hash_tag({_test_key("prefix1{tag}postfix1"),
+                    _test_key("prefix2{tag}postfix2"),
+                    _test_key("prefix3{tag}postfix3")});
+
+    _test_hash_tag({_test_key("prefix1{tag}"),
+                    _test_key("prefix2{tag}"),
+                    _test_key("prefix3{tag}")});
+
+    _test_hash_tag({_test_key("prefix{}postfix"),
+                    _test_key("prefix{}postfix"),
+                    _test_key("prefix{}postfix")});
+
+    _test_hash_tag({_test_key("prefix1{tag}post}fix1"),
+                    _test_key("prefix2{tag}pos}tfix2"),
+                    _test_key("prefix3{tag}postfi}x3")});
+
+    _test_hash_tag({_test_key("prefix1{t{ag}postfix1"),
+                    _test_key("prefix2{t{ag}postfix2"),
+                    _test_key("prefix3{t{ag}postfix3")});
+
+    _test_hash_tag({_test_key("prefix1{t{ag}postfi}x1"),
+                    _test_key("prefix2{t{ag}post}fix2"),
+                    _test_key("prefix3{t{ag}po}stfix3")});
+}
+
+template <typename RedisInstance>
+void SanityTest<RedisInstance>::_test_hash_tag(std::initializer_list<std::string> keys) {
+    KeyDeleter<RedisInstance> deleter(_redis, keys.begin(), keys.end());
+
+    std::string value = "value";
+    std::vector<std::pair<std::string, std::string>> kvs;
+    for (const auto &key : keys) {
+        kvs.emplace_back(key, value);
+    }
+
+    _redis.mset(kvs.begin(), kvs.end());
+
+    std::vector<OptionalString> res;
+    res.reserve(keys.size());
+    _redis.mget(keys.begin(), keys.end(), std::back_inserter(res));
+
+    REDIS_ASSERT(res.size() == keys.size(), "failed to test hash tag");
+
+    for (const auto &ele : res) {
+        REDIS_ASSERT(ele && *ele == value, "failed to test hash tag");
+    }
+}
+
+template <typename RedisInstance>
+std::string SanityTest<RedisInstance>::_test_key(const std::string &key) {
+    REDIS_ASSERT(key.size() > 1, "failed to generate key");
+
+    // Ensure that key prefix has NO hash tag. Also see the implementation of test_key.
+    return key.substr(1);
+}
+
+template <typename RedisInstance>
+void SanityTest<RedisInstance>::_test_ping(Redis &instance) {
+    auto reply = instance.command("ping");
+    REDIS_ASSERT(reply && reply::parse<std::string>(*reply) == "PONG",
+            "failed to test generic command");
+
+    auto pong = instance.command<std::string>("ping");
+    REDIS_ASSERT(pong == "PONG", "failed to test generic command");
+}
+
+template <typename RedisInstance>
+void SanityTest<RedisInstance>::_test_pipeline(const StringView &key, Pipeline &pipeline) {
+    auto pipe_replies = pipeline.command("set", key, "value").command("get", key).exec();
+    auto val = pipe_replies.get<OptionalString>(1);
+    REDIS_ASSERT(val && *val == "value", "failed to test generic command");
+}
+
+template <typename RedisInstance>
+void SanityTest<RedisInstance>::_test_transaction(const StringView &key, Transaction &transaction) {
+    auto tx_replies = transaction.command("set", key, 456).command("incr", key).exec();
+    REDIS_ASSERT(tx_replies.get<long long>(1) == 457, "failed to test generic command");
+}
+
+template <typename RedisInstance>
+Pipeline SanityTest<RedisInstance>::_pipeline(const StringView &) {
+    return _redis.pipeline();
+}
+
+template <>
+inline Pipeline SanityTest<RedisCluster>::_pipeline(const StringView &key) {
+    return _redis.pipeline(key);
+}
+
+template <typename RedisInstance>
+Transaction SanityTest<RedisInstance>::_transaction(const StringView &) {
+    return _redis.transaction();
+}
+
+template <>
+inline Transaction SanityTest<RedisCluster>::_transaction(const StringView &key) {
+    return _redis.transaction(key);
+}
+
+}
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_TEST_SANITY_TEST_HPP

+ 49 - 0
ext/redis-plus-plus-1.1.1/test/src/sw/redis++/script_cmds_test.h

@@ -0,0 +1,49 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_TEST_SCRIPT_CMDS_TEST_H
+#define SEWENEW_REDISPLUSPLUS_TEST_SCRIPT_CMDS_TEST_H
+
+#include <sw/redis++/redis++.h>
+
+namespace sw {
+
+namespace redis {
+
+namespace test {
+
+template <typename RedisInstance>
+class ScriptCmdTest {
+public:
+    explicit ScriptCmdTest(RedisInstance &instance) : _redis(instance) {}
+
+    void run();
+
+private:
+    void _run(Redis &instance);
+
+    RedisInstance &_redis;
+};
+
+}
+
+}
+
+}
+
+#include "script_cmds_test.hpp"
+
+#endif // end SEWENEW_REDISPLUSPLUS_TEST_SCRIPT_CMDS_TEST_H

+ 97 - 0
ext/redis-plus-plus-1.1.1/test/src/sw/redis++/script_cmds_test.hpp

@@ -0,0 +1,97 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_TEST_SCRIPT_CMDS_TEST_HPP
+#define SEWENEW_REDISPLUSPLUS_TEST_SCRIPT_CMDS_TEST_HPP
+
+#include <list>
+#include <vector>
+#include "utils.h"
+
+namespace sw {
+
+namespace redis {
+
+namespace test {
+
+template <typename RedisInstance>
+void ScriptCmdTest<RedisInstance>::run() {
+    cluster_specializing_test(*this,
+            &ScriptCmdTest<RedisInstance>::_run,
+            _redis);
+}
+
+template <typename RedisInstance>
+void ScriptCmdTest<RedisInstance>::_run(Redis &instance) {
+    auto key1 = test_key("k1");
+    auto key2 = test_key("k2");
+
+    KeyDeleter<Redis> deleter(instance, {key1, key2});
+
+    std::string script = "redis.call('set', KEYS[1], 1);"
+                    "redis.call('set', KEYS[2], 2);"
+                    "local first = redis.call('get', KEYS[1]);"
+                    "local second = redis.call('get', KEYS[2]);"
+                    "return first + second";
+
+    auto num = instance.eval<long long>(script, {key1, key2}, {});
+    REDIS_ASSERT(num == 3, "failed to test scripting for cluster");
+
+    script = "return 1";
+    num = instance.eval<long long>(script, {}, {});
+    REDIS_ASSERT(num == 1, "failed to test eval");
+
+    auto script_with_args = "return {ARGV[1] + 1, ARGV[2] + 2, ARGV[3] + 3}";
+    std::vector<long long> res;
+    instance.eval(script_with_args,
+                {"k"},
+                {"1", "2", "3"},
+                std::back_inserter(res));
+    REDIS_ASSERT(res == std::vector<long long>({2, 4, 6}),
+            "failed to test eval with array reply");
+
+    auto sha1 = instance.script_load(script);
+    num = instance.evalsha<long long>(sha1, {}, {});
+    REDIS_ASSERT(num == 1, "failed to test evalsha");
+
+    auto sha2 = instance.script_load(script_with_args);
+    res.clear();
+    instance.evalsha(sha2,
+                    {"k"},
+                    {"1", "2", "3"},
+                    std::back_inserter(res));
+    REDIS_ASSERT(res == std::vector<long long>({2, 4, 6}),
+            "failed to test evalsha with array reply");
+
+    std::list<bool> exist_res;
+    instance.script_exists({sha1, sha2, std::string("not exist")}, std::back_inserter(exist_res));
+    REDIS_ASSERT(exist_res == std::list<bool>({true, true, false}),
+            "failed to test script exists");
+
+    instance.script_flush();
+    exist_res.clear();
+    instance.script_exists({sha1, sha2, std::string("not exist")}, std::back_inserter(exist_res));
+    REDIS_ASSERT(exist_res == std::list<bool>({false, false, false}),
+            "failed to test script flush");
+}
+
+}
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_TEST_SCRIPT_CMDS_TEST_HPP

+ 53 - 0
ext/redis-plus-plus-1.1.1/test/src/sw/redis++/set_cmds_test.h

@@ -0,0 +1,53 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_TEST_SET_CMDS_TEST_H
+#define SEWENEW_REDISPLUSPLUS_TEST_SET_CMDS_TEST_H
+
+#include <sw/redis++/redis++.h>
+
+namespace sw {
+
+namespace redis {
+
+namespace test {
+
+template <typename RedisInstance>
+class SetCmdTest {
+public:
+    explicit SetCmdTest(RedisInstance &instance) : _redis(instance) {}
+
+    void run();
+
+private:
+    void _test_set();
+
+    void _test_multi_set();
+
+    void _test_sscan();
+
+    RedisInstance &_redis;
+};
+
+}
+
+}
+
+}
+
+#include "set_cmds_test.hpp"
+
+#endif // end SEWENEW_REDISPLUSPLUS_TEST_SET_CMDS_TEST_H

+ 184 - 0
ext/redis-plus-plus-1.1.1/test/src/sw/redis++/set_cmds_test.hpp

@@ -0,0 +1,184 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_TEST_SET_CMDS_TEST_HPP
+#define SEWENEW_REDISPLUSPLUS_TEST_SET_CMDS_TEST_HPP
+
+#include <unordered_set>
+#include <vector>
+#include "utils.h"
+
+namespace sw {
+
+namespace redis {
+
+namespace test {
+
+template <typename RedisInstance>
+void SetCmdTest<RedisInstance>::run() {
+    _test_set();
+
+    _test_multi_set();
+
+    _test_sscan();
+}
+
+template <typename RedisInstance>
+void SetCmdTest<RedisInstance>::_test_set() {
+    auto key = test_key("set");
+
+    KeyDeleter<RedisInstance> deleter(_redis, key);
+
+    std::string m1("m1");
+    std::string m2("m2");
+    std::string m3("m3");
+
+    REDIS_ASSERT(_redis.sadd(key, m1) == 1, "failed to test sadd");
+
+    auto members = {m1, m2, m3};
+    REDIS_ASSERT(_redis.sadd(key, members) == 2, "failed to test sadd with multiple members");
+
+    REDIS_ASSERT(_redis.scard(key) == 3, "failed to test scard");
+
+    REDIS_ASSERT(_redis.sismember(key, m1), "failed to test sismember");
+
+    std::unordered_set<std::string> res;
+    _redis.smembers(key, std::inserter(res, res.end()));
+    REDIS_ASSERT(res.find(m1) != res.end()
+            && res.find(m2) != res.end()
+            && res.find(m3) != res.end(),
+                "failed to test smembers");
+
+    auto ele = _redis.srandmember(key);
+    REDIS_ASSERT(bool(ele) && res.find(*ele) != res.end(), "failed to test srandmember");
+
+    std::vector<std::string> rand_members;
+    _redis.srandmember(key, 2, std::back_inserter(rand_members));
+    REDIS_ASSERT(rand_members.size() == 2, "failed to test srandmember");
+
+    ele = _redis.spop(key);
+    REDIS_ASSERT(bool(ele) && res.find(*ele) != res.end(), "failed to test spop");
+
+    rand_members.clear();
+    _redis.spop(key, 3, std::back_inserter(rand_members));
+    REDIS_ASSERT(rand_members.size() == 2, "failed to test srandmember");
+
+    rand_members.clear();
+    _redis.srandmember(key, 2, std::back_inserter(rand_members));
+    REDIS_ASSERT(rand_members.empty(), "failed to test srandmember with empty set");
+
+    _redis.spop(key, 2, std::back_inserter(rand_members));
+    REDIS_ASSERT(rand_members.empty(), "failed to test spop with empty set");
+
+    _redis.sadd(key, members);
+    REDIS_ASSERT(_redis.srem(key, m1) == 1, "failed to test srem");
+    REDIS_ASSERT(_redis.srem(key, members) == 2, "failed to test srem with mulitple members");
+    REDIS_ASSERT(_redis.srem(key, members) == 0, "failed to test srem with mulitple members");
+}
+
+template <typename RedisInstance>
+void SetCmdTest<RedisInstance>::_test_multi_set() {
+    auto k1 = test_key("s1");
+    auto k2 = test_key("s2");
+    auto k3 = test_key("s3");
+    auto k4 = test_key("s4");
+    auto k5 = test_key("s5");
+    auto k6 = test_key("s6");
+
+    KeyDeleter<RedisInstance> keys(_redis, {k1, k2, k3, k4, k5, k6});
+
+    _redis.sadd(k1, {"a", "c"});
+    _redis.sadd(k2, {"a", "b"});
+    std::vector<std::string> sdiff;
+    _redis.sdiff({k1, k1}, std::back_inserter(sdiff));
+    REDIS_ASSERT(sdiff.empty(), "failed to test sdiff");
+
+    _redis.sdiff({k1, k2}, std::back_inserter(sdiff));
+    REDIS_ASSERT(sdiff == std::vector<std::string>({"c"}), "failed to test sdiff");
+
+    _redis.sdiffstore(k3, {k1, k2});
+    sdiff.clear();
+    _redis.smembers(k3, std::back_inserter(sdiff));
+    REDIS_ASSERT(sdiff == std::vector<std::string>({"c"}), "failed to test sdiffstore");
+
+    REDIS_ASSERT(_redis.sdiffstore(k3, k1) == 2, "failed to test sdiffstore");
+
+    REDIS_ASSERT(_redis.sinterstore(k3, k1) == 2, "failed to test sinterstore");
+
+    REDIS_ASSERT(_redis.sunionstore(k3, k1) == 2, "failed to test sunionstore");
+
+    std::vector<std::string> sinter;
+    _redis.sinter({k1, k2}, std::back_inserter(sinter));
+    REDIS_ASSERT(sinter == std::vector<std::string>({"a"}), "failed to test sinter");
+
+    _redis.sinterstore(k4, {k1, k2});
+    sinter.clear();
+    _redis.smembers(k4, std::back_inserter(sinter));
+    REDIS_ASSERT(sinter == std::vector<std::string>({"a"}), "failed to test sinterstore");
+
+    std::unordered_set<std::string> sunion;
+    _redis.sunion({k1, k2}, std::inserter(sunion, sunion.end()));
+    REDIS_ASSERT(sunion == std::unordered_set<std::string>({"a", "b", "c"}),
+            "failed to test sunion");
+
+    _redis.sunionstore(k5, {k1, k2});
+    sunion.clear();
+    _redis.smembers(k5, std::inserter(sunion, sunion.end()));
+    REDIS_ASSERT(sunion == std::unordered_set<std::string>({"a", "b", "c"}),
+            "failed to test sunionstore");
+
+    REDIS_ASSERT(_redis.smove(k5, k6, "a"), "failed to test smove");
+}
+
+template <typename RedisInstance>
+void SetCmdTest<RedisInstance>::_test_sscan() {
+    auto key = test_key("sscan");
+
+    KeyDeleter<RedisInstance> deleter(_redis, key);
+
+    std::unordered_set<std::string> members = {"m1", "m2", "m3"};
+    _redis.sadd(key, members.begin(), members.end());
+
+    std::unordered_set<std::string> res;
+    long long cursor = 0;
+    while (true) {
+        cursor = _redis.sscan(key, cursor, "m*", 1, std::inserter(res, res.end()));
+        if (cursor == 0) {
+            break;
+        }
+    }
+
+    REDIS_ASSERT(res == members, "failed to test sscan");
+
+    res.clear();
+    cursor = 0;
+    while (true) {
+        cursor = _redis.sscan(key, cursor, std::inserter(res, res.end()));
+        if (cursor == 0) {
+            break;
+        }
+    }
+
+    REDIS_ASSERT(res == members, "failed to test sscan");
+}
+
+}
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_TEST_SET_CMDS_TEST_HPP

+ 54 - 0
ext/redis-plus-plus-1.1.1/test/src/sw/redis++/stream_cmds_test.h

@@ -0,0 +1,54 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_TEST_STREAM_CMDS_TEST_H
+#define SEWENEW_REDISPLUSPLUS_TEST_STREAM_CMDS_TEST_H
+
+#include <sw/redis++/redis++.h>
+
+namespace sw {
+
+namespace redis {
+
+namespace test {
+
+template <typename RedisInstance>
+class StreamCmdsTest {
+public:
+    explicit StreamCmdsTest(RedisInstance &instance) : _redis(instance) {}
+
+    void run();
+
+private:
+    using Item = std::pair<std::string, std::unordered_map<std::string, std::string>>;
+    using Result = std::unordered_map<std::string, std::vector<Item>>;
+
+    void _test_stream_cmds();
+
+    void _test_group_cmds();
+
+    RedisInstance &_redis;
+};
+
+}
+
+}
+
+}
+
+#include "stream_cmds_test.hpp"
+
+#endif // end SEWENEW_REDISPLUSPLUS_TEST_STREAM_CMDS_TEST_H

+ 225 - 0
ext/redis-plus-plus-1.1.1/test/src/sw/redis++/stream_cmds_test.hpp

@@ -0,0 +1,225 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_TEST_STREAM_CMDS_TEST_HPP
+#define SEWENEW_REDISPLUSPLUS_TEST_STREAM_CMDS_TEST_HPP
+
+#include <vector>
+#include <string>
+#include <thread>
+#include <chrono>
+#include <unordered_map>
+#include "utils.h"
+
+namespace sw {
+
+namespace redis {
+
+namespace test {
+
+template <typename RedisInstance>
+void StreamCmdsTest<RedisInstance>::run() {
+    _test_stream_cmds();
+
+    _test_group_cmds();
+}
+
+template <typename RedisInstance>
+void StreamCmdsTest<RedisInstance>::_test_stream_cmds() {
+    auto key = test_key("stream");
+
+    KeyDeleter<RedisInstance> deleter(_redis, key);
+
+    std::vector<std::pair<std::string, std::string>> attrs = {
+        {"f1", "v1"},
+        {"f2", "v2"}
+    };
+    auto id = "1565427842-0";
+    REDIS_ASSERT(_redis.xadd(key, id, attrs.begin(), attrs.end()) == id,
+            "failed to test xadd");
+
+    std::vector<std::pair<std::string, std::string>> keys = {std::make_pair(key, "0-0")};
+    Result result;
+    _redis.xread(keys.begin(), keys.end(), 1, std::inserter(result, result.end()));
+
+    REDIS_ASSERT(result.size() == 1
+            && result.find(key) != result.end()
+            && result[key].size() == 1
+            && result[key][0].first == id
+            && result[key][0].second.size() == 2,
+            "failed to test xread");
+
+    result.clear();
+    _redis.xread(key, std::string("0-0"), 1, std::inserter(result, result.end()));
+
+    REDIS_ASSERT(result.size() == 1
+            && result.find(key) != result.end()
+            && result[key].size() == 1
+            && result[key][0].first == id
+            && result[key][0].second.size() == 2,
+            "failed to test xread");
+
+    result.clear();
+    keys = {std::make_pair(key, id)};
+    _redis.xread(keys.begin(),
+                    keys.end(),
+                    std::chrono::seconds(1),
+                    2,
+                    std::inserter(result, result.end()));
+    REDIS_ASSERT(result.size() == 0, "failed to test xread");
+
+    _redis.xread(key,
+                    id,
+                    std::chrono::seconds(1),
+                    2,
+                    std::inserter(result, result.end()));
+    REDIS_ASSERT(result.size() == 0, "failed to test xread");
+
+    id = "1565427842-1";
+    REDIS_ASSERT(_redis.xadd(key, id, attrs.begin(), attrs.end()) == id,
+            "failed to test xadd");
+
+    REDIS_ASSERT(_redis.xlen(key) == 2, "failed to test xlen");
+
+    REDIS_ASSERT(_redis.xtrim(key, 1, false) == 1, "failed to test xtrim");
+
+    std::vector<Item> items;
+    _redis.xrange(key, "-", "+", std::back_inserter(items));
+    REDIS_ASSERT(items.size() == 1 && items[0].first == id, "failed to test xrange");
+
+    items.clear();
+    _redis.xrevrange(key, "+", "-", std::back_inserter(items));
+    REDIS_ASSERT(items.size() == 1 && items[0].first == id, "failed to test xrevrange");
+
+    REDIS_ASSERT(_redis.xdel(key, {id, "111-111"}) == 1, "failed to test xdel");
+}
+
+template <typename RedisInstance>
+void StreamCmdsTest<RedisInstance>::_test_group_cmds() {
+    auto key = test_key("stream");
+
+    KeyDeleter<RedisInstance> deleter(_redis, key);
+
+    auto group = "group";
+    auto consumer1 = "consumer1";
+
+    _redis.xgroup_create(key, group, "$", true);
+
+    std::vector<std::pair<std::string, std::string>> attrs = {
+        {"f1", "v1"},
+        {"f2", "v2"}
+    };
+    auto id = _redis.xadd(key, "*", attrs.begin(), attrs.end());
+    auto keys = {std::make_pair(key, ">")};
+
+    Result result;
+    _redis.xreadgroup(group,
+            consumer1,
+            keys.begin(),
+            keys.end(),
+            1,
+            std::inserter(result, result.end()));
+    REDIS_ASSERT(result.size() == 1
+            && result.find(key) != result.end()
+            && result[key].size() == 1
+            && result[key][0].first == id,
+            "failed to test xreadgroup");
+
+    result.clear();
+    _redis.xreadgroup(group,
+            consumer1,
+            key,
+            std::string(">"),
+            1,
+            std::inserter(result, result.end()));
+    REDIS_ASSERT(result.size() == 0, "failed to test xreadgroup");
+
+    result.clear();
+    _redis.xreadgroup(group,
+            "not-exist-consumer",
+            keys.begin(),
+            keys.end(),
+            1,
+            std::inserter(result, result.end()));
+    REDIS_ASSERT(result.size() == 0, "failed to test xreadgroup");
+
+    result.clear();
+    _redis.xreadgroup(group,
+            consumer1,
+            keys.begin(),
+            keys.end(),
+            std::chrono::seconds(1),
+            1,
+            std::inserter(result, result.end()));
+    REDIS_ASSERT(result.size() == 0, "failed to test xreadgroup");
+
+    result.clear();
+    _redis.xreadgroup(group,
+            consumer1,
+            key,
+            ">",
+            std::chrono::seconds(1),
+            1,
+            std::inserter(result, result.end()));
+    REDIS_ASSERT(result.size() == 0, "failed to test xreadgroup");
+
+    using PendingResult = std::vector<std::tuple<std::string, std::string, long long, long long>>;
+    PendingResult pending_result;
+    _redis.xpending(key, group, "-", "+", 1, consumer1, std::back_inserter(pending_result));
+
+    REDIS_ASSERT(pending_result.size() == 1
+            && std::get<0>(pending_result[0]) == id
+            && std::get<1>(pending_result[0]) == consumer1,
+            "failed to test xpending");
+
+    std::this_thread::sleep_for(std::chrono::seconds(1));
+
+    auto consumer2 = "consumer2";
+    std::vector<Item> items;
+    auto ids = {id};
+    _redis.xclaim(key,
+            group,
+            consumer2,
+            std::chrono::milliseconds(10),
+            ids,
+            std::back_inserter(items));
+    REDIS_ASSERT(items.size() == 1 && items[0].first == id, "failed to test xclaim");
+
+    std::this_thread::sleep_for(std::chrono::seconds(1));
+
+    items.clear();
+    _redis.xclaim(key, group, consumer1, std::chrono::milliseconds(10), id, std::back_inserter(items));
+    REDIS_ASSERT(items.size() == 1 && items[0].first == id, "failed to test xclaim: " + std::to_string(items.size()));
+
+    _redis.xack(key, group, id);
+
+    REDIS_ASSERT(_redis.xgroup_delconsumer(key, group, consumer1) == 0,
+            "failed to test xgroup_delconsumer");
+
+    REDIS_ASSERT(_redis.xgroup_delconsumer(key, group, consumer2) == 0,
+            "failed to test xgroup_delconsumer");
+
+    REDIS_ASSERT(_redis.xgroup_destroy(key, group) == 1,
+            "failed to test xgroup_destroy");
+}
+
+}
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_TEST_STREAM_CMDS_TEST_HPP

+ 57 - 0
ext/redis-plus-plus-1.1.1/test/src/sw/redis++/string_cmds_test.h

@@ -0,0 +1,57 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_TEST_STRING_CMDS_TEST_H
+#define SEWENEW_REDISPLUSPLUS_TEST_STRING_CMDS_TEST_H
+
+#include <sw/redis++/redis++.h>
+
+namespace sw {
+
+namespace redis {
+
+namespace test {
+
+template <typename RedisInstance>
+class StringCmdTest {
+public:
+    explicit StringCmdTest(RedisInstance &instance) : _redis(instance) {}
+
+    void run();
+
+private:
+    void _test_str();
+
+    void _test_bit();
+
+    void _test_numeric();
+
+    void _test_getset();
+
+    void _test_mgetset();
+
+    RedisInstance &_redis;
+};
+
+}
+
+}
+
+}
+
+#include "string_cmds_test.hpp"
+
+#endif // end SEWENEW_REDISPLUSPLUS_TEST_STRING_CMDS_TEST_H

+ 247 - 0
ext/redis-plus-plus-1.1.1/test/src/sw/redis++/string_cmds_test.hpp

@@ -0,0 +1,247 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_TEST_STRING_CMDS_TEST_HPP
+#define SEWENEW_REDISPLUSPLUS_TEST_STRING_CMDS_TEST_HPP
+
+#include <vector>
+#include "utils.h"
+
+namespace sw {
+
+namespace redis {
+
+namespace test {
+
+template <typename RedisInstance>
+void StringCmdTest<RedisInstance>::run() {
+    _test_str();
+
+    _test_bit();
+
+    _test_numeric();
+
+    _test_getset();
+
+    _test_mgetset();
+}
+
+template <typename RedisInstance>
+void StringCmdTest<RedisInstance>::_test_str() {
+    auto key = test_key("str");
+
+    KeyDeleter<RedisInstance> deleter(_redis, key);
+
+    std::string val("value");
+
+    long long val_size = val.size();
+
+    auto len1 = _redis.append(key, val);
+    REDIS_ASSERT(len1 == val_size, "failed to append to non-existent key");
+
+    auto len2 = _redis.append(key, val);
+    REDIS_ASSERT(len2 == len1 + val_size, "failed to append to non-empty string");
+
+    auto len3 = _redis.append(key, {});
+    REDIS_ASSERT(len3 == len2, "failed to append empty string");
+
+    auto len4 = _redis.strlen(key);
+    REDIS_ASSERT(len4 == len3, "failed to test strlen");
+
+    REDIS_ASSERT(_redis.del(key) == 1, "failed to remove key");
+
+    auto len5 = _redis.append(key, {});
+    REDIS_ASSERT(len5 == 0, "failed to append empty string to non-existent key");
+
+    _redis.del(key);
+
+    REDIS_ASSERT(_redis.getrange(key, 0, 2) == "", "failed to test getrange on non-existent key");
+
+    _redis.set(key, val);
+
+    REDIS_ASSERT(_redis.getrange(key, 1, 2) == val.substr(1, 2), "failed to test getrange");
+
+    long long new_size = val.size() * 2;
+    REDIS_ASSERT(_redis.setrange(key, val.size(), val) == new_size, "failed to test setrange");
+    REDIS_ASSERT(_redis.getrange(key, 0, -1) == val + val, "failed to test setrange");
+}
+
+template <typename RedisInstance>
+void StringCmdTest<RedisInstance>::_test_bit() {
+    auto key = test_key("bit");
+
+    KeyDeleter<RedisInstance> deleter(_redis, key);
+
+    REDIS_ASSERT(_redis.bitcount(key) == 0, "failed to test bitcount on non-existent key");
+
+    REDIS_ASSERT(_redis.getbit(key, 5) == 0, "failed to test getbit");
+
+    REDIS_ASSERT(_redis.template command<long long>("SETBIT", key, 1, 1) == 0,
+            "failed to test setbit");
+    REDIS_ASSERT(_redis.template command<long long>("SETBIT", key, 3, 1) == 0,
+            "failed to test setbit");
+    REDIS_ASSERT(_redis.template command<long long>("SETBIT", key, 7, 1) == 0,
+            "failed to test setbit");
+    REDIS_ASSERT(_redis.template command<long long>("SETBIT", key, 10, 1) == 0,
+            "failed to test setbit");
+    REDIS_ASSERT(_redis.template command<long long>("SETBIT", key, 10, 0) == 1,
+            "failed to test setbit");
+    REDIS_ASSERT(_redis.template command<long long>("SETBIT", key, 11, 1) == 0,
+            "failed to test setbit");
+    REDIS_ASSERT(_redis.template command<long long>("SETBIT", key, 21, 1) == 0,
+            "failed to test setbit");
+
+    // key -> 01010001, 00010000, 00000100
+
+    REDIS_ASSERT(_redis.getbit(key, 1) == 1, "failed to test getbit");
+    REDIS_ASSERT(_redis.getbit(key, 2) == 0, "failed to test getbit");
+    REDIS_ASSERT(_redis.getbit(key, 7) == 1, "failed to test getbit");
+    REDIS_ASSERT(_redis.getbit(key, 10) == 0, "failed to test getbit");
+    REDIS_ASSERT(_redis.getbit(key, 100) == 0, "failed to test getbit");
+
+    REDIS_ASSERT(_redis.bitcount(key) == 5, "failed to test bitcount");
+    REDIS_ASSERT(_redis.bitcount(key, 0, 0) == 3, "failed to test bitcount");
+    REDIS_ASSERT(_redis.bitcount(key, 0, 1) == 4, "failed to test bitcount");
+    REDIS_ASSERT(_redis.bitcount(key, -2, -1) == 2, "failed to test bitcount");
+
+    REDIS_ASSERT(_redis.bitpos(key, 1) == 1, "failed to test bitpos");
+    REDIS_ASSERT(_redis.bitpos(key, 0) == 0, "failed to test bitpos");
+    REDIS_ASSERT(_redis.bitpos(key, 1, 1, 1) == 11, "failed to test bitpos");
+    REDIS_ASSERT(_redis.bitpos(key, 0, 1, 1) == 8, "failed to test bitpos");
+    REDIS_ASSERT(_redis.bitpos(key, 1, -1, -1) == 21, "failed to test bitpos");
+    REDIS_ASSERT(_redis.bitpos(key, 0, -1, -1) == 16, "failed to test bitpos");
+
+    auto dest_key = test_key("bitop_dest");
+    auto src_key1 = test_key("bitop_src1");
+    auto src_key2 = test_key("bitop_src2");
+
+    KeyDeleter<RedisInstance> deleters(_redis, {dest_key, src_key1, src_key2});
+
+    // src_key1 -> 00010000
+    _redis.template command<long long>("SETBIT", src_key1, 3, 1);
+
+    // src_key2 -> 00000000, 00001000
+    _redis.template command<long long>("SETBIT", src_key2, 12, 1);
+
+    REDIS_ASSERT(_redis.bitop(BitOp::AND, dest_key, {src_key1, src_key2}) == 2,
+            "failed to test bitop");
+
+    // dest_key -> 00000000, 00000000
+    auto v = _redis.get(dest_key);
+    REDIS_ASSERT(v && *v == std::string(2, 0), "failed to test bitop");
+
+    REDIS_ASSERT(_redis.bitop(BitOp::NOT, dest_key, src_key1) == 1,
+            "failed to test bitop");
+
+    // dest_key -> 11101111
+    v = _redis.get(dest_key);
+    REDIS_ASSERT(v && *v == std::string(1, 0xEF), "failed to test bitop");
+}
+
+template <typename RedisInstance>
+void StringCmdTest<RedisInstance>::_test_numeric() {
+    auto key = test_key("numeric");
+
+    KeyDeleter<RedisInstance> deleter(_redis, key);
+
+    REDIS_ASSERT(_redis.incr(key) == 1, "failed to test incr");
+    REDIS_ASSERT(_redis.decr(key) == 0, "failed to test decr");
+    REDIS_ASSERT(_redis.incrby(key, 3) == 3, "failed to test incrby");
+    REDIS_ASSERT(_redis.decrby(key, 3) == 0, "failed to test decrby");
+    REDIS_ASSERT(_redis.incrby(key, -3) == -3, "failed to test incrby");
+    REDIS_ASSERT(_redis.decrby(key, -3) == 0, "failed to test incrby");
+    REDIS_ASSERT(_redis.incrbyfloat(key, 1.5) == 1.5, "failed to test incrbyfloat");
+}
+
+template <typename RedisInstance>
+void StringCmdTest<RedisInstance>::_test_getset() {
+    auto key = test_key("getset");
+    auto non_exist_key = test_key("non-existent");
+
+    KeyDeleter<RedisInstance> deleter(_redis, {key, non_exist_key});
+
+    std::string val("value");
+    REDIS_ASSERT(_redis.set(key, val), "failed to test set");
+
+    auto v = _redis.get(key);
+    REDIS_ASSERT(v && *v == val, "failed to test get");
+
+    v = _redis.getset(key, val + val);
+    REDIS_ASSERT(v && *v == val, "failed to test get");
+
+    REDIS_ASSERT(!_redis.set(key, val, std::chrono::milliseconds(0), UpdateType::NOT_EXIST),
+            "failed to test set with NOT_EXIST type");
+    REDIS_ASSERT(!_redis.set(non_exist_key, val, std::chrono::milliseconds(0), UpdateType::EXIST),
+            "failed to test set with EXIST type");
+
+    REDIS_ASSERT(!_redis.setnx(key, val), "failed to test setnx");
+    REDIS_ASSERT(_redis.setnx(non_exist_key, val), "failed to test setnx");
+
+    auto ttl = std::chrono::seconds(10);
+
+    _redis.set(key, val, ttl);
+    REDIS_ASSERT(_redis.ttl(key) <= ttl.count(), "failed to test set key with ttl");
+
+    _redis.setex(key, ttl, val);
+    REDIS_ASSERT(_redis.ttl(key) <= ttl.count(), "failed to test setex");
+
+    auto pttl = std::chrono::milliseconds(10000);
+
+    _redis.psetex(key, ttl, val);
+    REDIS_ASSERT(_redis.pttl(key) <= pttl.count(), "failed to test psetex");
+}
+
+template <typename RedisInstance>
+void StringCmdTest<RedisInstance>::_test_mgetset() {
+    auto kvs = {std::make_pair(test_key("k1"), "v1"),
+                std::make_pair(test_key("k2"), "v2"),
+                std::make_pair(test_key("k3"), "v3")};
+
+    std::vector<std::string> keys;
+    std::vector<std::string> vals;
+    for (const auto &kv : kvs) {
+        keys.push_back(kv.first);
+        vals.push_back(kv.second);
+    }
+
+    KeyDeleter<RedisInstance> deleter(_redis, keys.begin(), keys.end());
+
+    _redis.mset(kvs);
+
+    std::vector<OptionalString> res;
+    _redis.mget(keys.begin(), keys.end(), std::back_inserter(res));
+
+    REDIS_ASSERT(res.size() == kvs.size(), "failed to test mget");
+
+    std::vector<std::string> res_vals;
+    for (const auto &ele : res) {
+        REDIS_ASSERT(bool(ele), "failed to test mget");
+
+        res_vals.push_back(*ele);
+    }
+
+    REDIS_ASSERT(vals == res_vals, "failed to test mget");
+
+    REDIS_ASSERT(!_redis.msetnx(kvs), "failed to test msetnx");
+}
+
+}
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_TEST_STRING_CMDS_TEST_HPP

+ 303 - 0
ext/redis-plus-plus-1.1.1/test/src/sw/redis++/test_main.cpp

@@ -0,0 +1,303 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#include <unistd.h>
+#include <chrono>
+#include <tuple>
+#include <iostream>
+#include <sw/redis++/redis++.h>
+#include "sanity_test.h"
+#include "connection_cmds_test.h"
+#include "keys_cmds_test.h"
+#include "string_cmds_test.h"
+#include "list_cmds_test.h"
+#include "hash_cmds_test.h"
+#include "set_cmds_test.h"
+#include "zset_cmds_test.h"
+#include "hyperloglog_cmds_test.h"
+#include "geo_cmds_test.h"
+#include "script_cmds_test.h"
+#include "pubsub_test.h"
+#include "pipeline_transaction_test.h"
+#include "threads_test.h"
+#include "stream_cmds_test.h"
+#include "benchmark_test.h"
+
+namespace {
+
+void print_help();
+
+auto parse_options(int argc, char **argv)
+    -> std::tuple<sw::redis::Optional<sw::redis::ConnectionOptions>,
+                    sw::redis::Optional<sw::redis::ConnectionOptions>,
+                    sw::redis::Optional<sw::redis::test::BenchmarkOptions>>;
+
+template <typename RedisInstance>
+void run_test(const sw::redis::ConnectionOptions &opts);
+
+template <typename RedisInstance>
+void run_benchmark(const sw::redis::ConnectionOptions &opts,
+        const sw::redis::test::BenchmarkOptions &benchmark_opts);
+
+}
+
+int main(int argc, char **argv) {
+    try {
+        sw::redis::Optional<sw::redis::ConnectionOptions> opts;
+        sw::redis::Optional<sw::redis::ConnectionOptions> cluster_node_opts;
+        sw::redis::Optional<sw::redis::test::BenchmarkOptions> benchmark_opts;
+        std::tie(opts, cluster_node_opts, benchmark_opts) = parse_options(argc, argv);
+
+        if (opts) {
+            std::cout << "Testing Redis..." << std::endl;
+
+            if (benchmark_opts) {
+                run_benchmark<sw::redis::Redis>(*opts, *benchmark_opts);
+            } else {
+                run_test<sw::redis::Redis>(*opts);
+            }
+        }
+
+        if (cluster_node_opts) {
+            std::cout << "Testing RedisCluster..." << std::endl;
+
+            if (benchmark_opts) {
+                run_benchmark<sw::redis::RedisCluster>(*cluster_node_opts, *benchmark_opts);
+            } else {
+                run_test<sw::redis::RedisCluster>(*cluster_node_opts);
+            }
+        }
+
+        std::cout << "Pass all tests" << std::endl;
+    } catch (const sw::redis::Error &e) {
+        std::cerr << "Test failed: " << e.what() << std::endl;
+        return -1;
+    }
+
+    return 0;
+}
+
+namespace {
+
+void print_help() {
+    std::cerr << "Usage: test_redis++ -h host -p port"
+        << " -n cluster_node -c cluster_port [-a auth] [-b]\n\n";
+    std::cerr << "See https://github.com/sewenew/redis-plus-plus#run-tests-optional"
+        << " for details on how to run test" << std::endl;
+}
+
+auto parse_options(int argc, char **argv)
+    -> std::tuple<sw::redis::Optional<sw::redis::ConnectionOptions>,
+                    sw::redis::Optional<sw::redis::ConnectionOptions>,
+                    sw::redis::Optional<sw::redis::test::BenchmarkOptions>> {
+    std::string host;
+    int port = 0;
+    std::string auth;
+    std::string cluster_node;
+    int cluster_port = 0;
+    bool benchmark = false;
+    sw::redis::test::BenchmarkOptions tmp_benchmark_opts;
+
+    int opt = 0;
+    while ((opt = getopt(argc, argv, "h:p:a:n:c:k:v:r:t:bs:")) != -1) {
+        try {
+            switch (opt) {
+            case 'h':
+                host = optarg;
+                break;
+
+            case 'p':
+                port = std::stoi(optarg);
+                break;
+
+            case 'a':
+                auth = optarg;
+                break;
+
+            case 'n':
+                cluster_node = optarg;
+                break;
+
+            case 'c':
+                cluster_port = std::stoi(optarg);
+                break;
+
+            case 'b':
+                benchmark = true;
+                break;
+
+            case 'k':
+                tmp_benchmark_opts.key_len = std::stoi(optarg);
+                break;
+
+            case 'v':
+                tmp_benchmark_opts.val_len = std::stoi(optarg);
+                break;
+
+            case 'r':
+                tmp_benchmark_opts.total_request_num = std::stoi(optarg);
+                break;
+
+            case 't':
+                tmp_benchmark_opts.thread_num = std::stoi(optarg);
+                break;
+
+            case 's':
+                tmp_benchmark_opts.pool_size = std::stoi(optarg);
+                break;
+
+            default:
+                throw sw::redis::Error("Unknow command line option");
+                break;
+            }
+        } catch (const sw::redis::Error &e) {
+            print_help();
+            throw;
+        } catch (const std::exception &e) {
+            print_help();
+            throw sw::redis::Error("Invalid command line option");
+        }
+    }
+
+    sw::redis::Optional<sw::redis::ConnectionOptions> opts;
+    if (!host.empty() && port > 0) {
+        sw::redis::ConnectionOptions tmp;
+        tmp.host = host;
+        tmp.port = port;
+        tmp.password = auth;
+
+        opts = sw::redis::Optional<sw::redis::ConnectionOptions>(tmp);
+    }
+
+    sw::redis::Optional<sw::redis::ConnectionOptions> cluster_opts;
+    if (!cluster_node.empty() && cluster_port > 0) {
+        sw::redis::ConnectionOptions tmp;
+        tmp.host = cluster_node;
+        tmp.port = cluster_port;
+        tmp.password = auth;
+
+        cluster_opts = sw::redis::Optional<sw::redis::ConnectionOptions>(tmp);
+    }
+
+    if (!opts && !cluster_opts) {
+        print_help();
+        throw sw::redis::Error("Invalid connection options");
+    }
+
+    sw::redis::Optional<sw::redis::test::BenchmarkOptions> benchmark_opts;
+    if (benchmark) {
+        benchmark_opts = sw::redis::Optional<sw::redis::test::BenchmarkOptions>(tmp_benchmark_opts);
+    }
+
+    return std::make_tuple(std::move(opts), std::move(cluster_opts), std::move(benchmark_opts));
+}
+
+template <typename RedisInstance>
+void run_test(const sw::redis::ConnectionOptions &opts) {
+    auto instance = RedisInstance(opts);
+
+    sw::redis::test::SanityTest<RedisInstance> sanity_test(opts, instance);
+    sanity_test.run();
+
+    std::cout << "Pass sanity tests" << std::endl;
+
+    sw::redis::test::ConnectionCmdTest<RedisInstance> connection_test(instance);
+    connection_test.run();
+
+    std::cout << "Pass connection commands tests" << std::endl;
+
+    sw::redis::test::KeysCmdTest<RedisInstance> keys_test(instance);
+    keys_test.run();
+
+    std::cout << "Pass keys commands tests" << std::endl;
+
+    sw::redis::test::StringCmdTest<RedisInstance> string_test(instance);
+    string_test.run();
+
+    std::cout << "Pass string commands tests" << std::endl;
+
+    sw::redis::test::ListCmdTest<RedisInstance> list_test(instance);
+    list_test.run();
+
+    std::cout << "Pass list commands tests" << std::endl;
+
+    sw::redis::test::HashCmdTest<RedisInstance> hash_test(instance);
+    hash_test.run();
+
+    std::cout << "Pass hash commands tests" << std::endl;
+
+    sw::redis::test::SetCmdTest<RedisInstance> set_test(instance);
+    set_test.run();
+
+    std::cout << "Pass set commands tests" << std::endl;
+
+    sw::redis::test::ZSetCmdTest<RedisInstance> zset_test(instance);
+    zset_test.run();
+
+    std::cout << "Pass zset commands tests" << std::endl;
+
+    sw::redis::test::HyperloglogCmdTest<RedisInstance> hll_test(instance);
+    hll_test.run();
+
+    std::cout << "Pass hyperloglog commands tests" << std::endl;
+
+    sw::redis::test::GeoCmdTest<RedisInstance> geo_test(instance);
+    geo_test.run();
+
+    std::cout << "Pass geo commands tests" << std::endl;
+
+    sw::redis::test::ScriptCmdTest<RedisInstance> script_test(instance);
+    script_test.run();
+
+    std::cout << "Pass script commands tests" << std::endl;
+
+    sw::redis::test::PubSubTest<RedisInstance> pubsub_test(instance);
+    pubsub_test.run();
+
+    std::cout << "Pass pubsub tests" << std::endl;
+
+    sw::redis::test::PipelineTransactionTest<RedisInstance> pipe_tx_test(instance);
+    pipe_tx_test.run();
+
+    std::cout << "Pass pipeline and transaction tests" << std::endl;
+
+    sw::redis::test::ThreadsTest<RedisInstance> threads_test(opts);
+    threads_test.run();
+
+    std::cout << "Pass threads tests" << std::endl;
+
+    sw::redis::test::StreamCmdsTest<RedisInstance> stream_test(instance);
+    stream_test.run();
+
+    std::cout << "Pass stream commands tests" << std::endl;
+}
+
+template <typename RedisInstance>
+void run_benchmark(const sw::redis::ConnectionOptions &opts,
+        const sw::redis::test::BenchmarkOptions &benchmark_opts) {
+    std::cout << "Benchmark test options:" << std::endl;
+    std::cout << "  Thread number: " << benchmark_opts.thread_num << std::endl;
+    std::cout << "  Connection pool size: " << benchmark_opts.pool_size << std::endl;
+    std::cout << "  Length of key: " << benchmark_opts.key_len << std::endl;
+    std::cout << "  Length of value: " << benchmark_opts.val_len << std::endl;
+
+    auto instance = RedisInstance(opts);
+
+    sw::redis::test::BenchmarkTest<RedisInstance> benchmark_test(benchmark_opts, instance);
+    benchmark_test.run();
+}
+
+}

+ 51 - 0
ext/redis-plus-plus-1.1.1/test/src/sw/redis++/threads_test.h

@@ -0,0 +1,51 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_TEST_THREADS_TEST_H
+#define SEWENEW_REDISPLUSPLUS_TEST_THREADS_TEST_H
+
+#include <sw/redis++/redis++.h>
+
+namespace sw {
+
+namespace redis {
+
+namespace test {
+
+template <typename RedisInstance>
+class ThreadsTest {
+public:
+    explicit ThreadsTest(const ConnectionOptions &opts) : _opts(opts) {}
+
+    void run();
+
+private:
+    void _test_multithreads(RedisInstance redis, int threads_num, int times);
+
+    void _test_timeout();
+
+    ConnectionOptions _opts;
+};
+
+}
+
+}
+
+}
+
+#include "threads_test.hpp"
+
+#endif // end SEWENEW_REDISPLUSPLUS_TEST_THREADS_TEST_H

+ 147 - 0
ext/redis-plus-plus-1.1.1/test/src/sw/redis++/threads_test.hpp

@@ -0,0 +1,147 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_TEST_THREADS_TEST_HPP
+#define SEWENEW_REDISPLUSPLUS_TEST_THREADS_TEST_HPP
+
+#include <thread>
+#include <chrono>
+#include <atomic>
+#include <memory>
+#include "utils.h"
+
+namespace sw {
+
+namespace redis {
+
+namespace test {
+
+template <typename RedisInstance>
+void ThreadsTest<RedisInstance>::run() {
+    // 100 * 10000 = 1 million writes
+    auto thread_num = 100;
+    auto times = 10000;
+
+    // Default pool options: single connection and wait forever.
+    _test_multithreads(RedisInstance(_opts), thread_num, times);
+
+    // Pool with 10 connections.
+    ConnectionPoolOptions pool_opts;
+    pool_opts.size = 10;
+    _test_multithreads(RedisInstance(_opts, pool_opts), thread_num, times);
+
+    _test_timeout();
+}
+
+template <typename RedisInstance>
+void ThreadsTest<RedisInstance>::_test_multithreads(RedisInstance redis,
+        int thread_num,
+        int times) {
+    std::vector<std::string> keys;
+    keys.reserve(thread_num);
+    for (auto idx = 0; idx != thread_num; ++idx) {
+        auto key = test_key("multi-threads::" + std::to_string(idx));
+        keys.push_back(key);
+    }
+
+    using DeleterUPtr = std::unique_ptr<KeyDeleter<RedisInstance>>;
+    std::vector<DeleterUPtr> deleters;
+    for (const auto &key : keys) {
+        deleters.emplace_back(new KeyDeleter<RedisInstance>(redis, key));
+    }
+
+    std::vector<std::thread> workers;
+    workers.reserve(thread_num);
+    for (const auto &key : keys) {
+        workers.emplace_back([&redis, key, times]() {
+                                try {
+                                    for (auto i = 0; i != times; ++i) {
+                                        redis.incr(key);
+                                    }
+                                } catch (...) {
+                                    // Something bad happens.
+                                    return;
+                                }
+                            });
+    }
+
+    for (auto &worker : workers) {
+        worker.join();
+    }
+
+    for (const auto &key : keys) {
+        auto val = redis.get(key);
+        REDIS_ASSERT(bool(val), "failed to test multithreads, cannot get value of " + key);
+
+        auto num = std::stoi(*val);
+        REDIS_ASSERT(num == times, "failed to test multithreads, num: "
+                + *val + ", times: " + std::to_string(times));
+    }
+}
+
+template <typename RedisInstance>
+void ThreadsTest<RedisInstance>::_test_timeout() {
+    using namespace std::chrono;
+
+    ConnectionPoolOptions pool_opts;
+    pool_opts.size = 1;
+    pool_opts.wait_timeout = milliseconds(100);
+
+    auto redis = RedisInstance(_opts, pool_opts);
+
+    auto key = test_key("key");
+
+    std::atomic<bool> slow_get_is_running{false};
+    auto slow_get = [&slow_get_is_running](Connection &connection, const StringView &key) {
+                        slow_get_is_running = true;
+
+                        // Sleep a while to simulate a slow get.
+                        std::this_thread::sleep_for(seconds(5));
+
+                        connection.send("GET %b", key.data(), key.size());
+                    };
+    auto slow_get_thread = std::thread([&redis, slow_get, &key]() {
+                                            redis.command(slow_get, key);
+                                        });
+
+    auto get_thread = std::thread([&redis, &slow_get_is_running, &key]() {
+                                        try {
+                                            while (!slow_get_is_running) {
+                                                std::this_thread::sleep_for(milliseconds(10));
+                                            }
+
+                                            redis.get(key);
+
+                                            // Slow get is running, this thread should
+                                            // timeout before obtaining the connection.
+                                            // So it never reaches here.
+                                            REDIS_ASSERT(false, "failed to test pool timeout");
+                                        } catch (const Error &err) {
+                                            // This thread timeout.
+                                        }
+                                    });
+
+    slow_get_thread.join();
+    get_thread.join();
+}
+
+}
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_TEST_THREADS_TEST_HPP

+ 96 - 0
ext/redis-plus-plus-1.1.1/test/src/sw/redis++/utils.h

@@ -0,0 +1,96 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_TEST_UTILS_H
+#define SEWENEW_REDISPLUSPLUS_TEST_UTILS_H
+
+#include <string>
+#include <vector>
+#include <sw/redis++/redis++.h>
+
+#define REDIS_ASSERT(condition, msg) \
+    sw::redis::test::redis_assert((condition), (msg), __FILE__, __LINE__)
+
+namespace sw {
+
+namespace redis {
+
+namespace test {
+
+inline void redis_assert(bool condition,
+                            const std::string &msg,
+                            const std::string &file,
+                            int line) {
+    if (!condition) {
+        auto err_msg = "ASSERT: " + msg + ". " + file + ":" + std::to_string(line);
+        throw Error(err_msg);
+    }
+}
+
+inline std::string test_key(const std::string &k) {
+    // Key prefix with hash tag,
+    // so that we can call multiple-key commands on RedisCluster.
+    return "{sw::redis::test}::" + k;
+}
+
+template <typename Test>
+void cluster_specializing_test(Test &test, void (Test::*func)(Redis &instance), Redis &instance) {
+    (test.*func)(instance);
+}
+
+template <typename Test>
+void cluster_specializing_test(Test &test,
+        void (Test::*func)(Redis &instance),
+        RedisCluster &cluster) {
+    auto instance = cluster.redis("hash-tag");
+    (test.*func)(instance);
+}
+
+template <typename RedisInstance>
+class KeyDeleter {
+public:
+    template <typename Input>
+    KeyDeleter(RedisInstance &redis, Input first, Input last) : _redis(redis), _keys(first, last) {
+        _delete();
+    }
+
+    KeyDeleter(RedisInstance &redis, std::initializer_list<std::string> il) :
+                KeyDeleter(redis, il.begin(), il.end()) {}
+
+    KeyDeleter(RedisInstance &redis, const std::string &key) : KeyDeleter(redis, {key}) {}
+
+    ~KeyDeleter() {
+        _delete();
+    }
+
+private:
+    void _delete() {
+        if (!_keys.empty()) {
+            _redis.del(_keys.begin(), _keys.end());
+        }
+    }
+
+    RedisInstance &_redis;
+    std::vector<std::string> _keys;
+};
+
+}
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_TEST_UTILS_H

+ 61 - 0
ext/redis-plus-plus-1.1.1/test/src/sw/redis++/zset_cmds_test.h

@@ -0,0 +1,61 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_TEST_ZSET_CMDS_TEST_H
+#define SEWENEW_REDISPLUSPLUS_TEST_ZSET_CMDS_TEST_H
+
+#include <sw/redis++/redis++.h>
+
+namespace sw {
+
+namespace redis {
+
+namespace test {
+
+template <typename RedisInstance>
+class ZSetCmdTest {
+public:
+    explicit ZSetCmdTest(RedisInstance &instance) : _redis(instance) {}
+
+    void run();
+
+private:
+    void _test_zset();
+
+    void _test_zscan();
+
+    void _test_range();
+
+    void _test_lex();
+
+    void _test_multi_zset();
+
+    void _test_zpop();
+
+    void _test_bzpop();
+
+    RedisInstance &_redis;
+};
+
+}
+
+}
+
+}
+
+#include "zset_cmds_test.hpp"
+
+#endif // end SEWENEW_REDISPLUSPLUS_TEST_ZSET_CMDS_TEST_H

+ 350 - 0
ext/redis-plus-plus-1.1.1/test/src/sw/redis++/zset_cmds_test.hpp

@@ -0,0 +1,350 @@
+/**************************************************************************
+   Copyright (c) 2017 sewenew
+
+   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.
+ *************************************************************************/
+
+#ifndef SEWENEW_REDISPLUSPLUS_TEST_ZSET_CMDS_TEST_HPP
+#define SEWENEW_REDISPLUSPLUS_TEST_ZSET_CMDS_TEST_HPP
+
+#include <map>
+#include <unordered_map>
+#include <vector>
+#include <algorithm>
+#include "utils.h"
+
+namespace sw {
+
+namespace redis {
+
+namespace test {
+
+template <typename RedisInstance>
+void ZSetCmdTest<RedisInstance>::run() {
+    _test_zset();
+
+    _test_zscan();
+
+    _test_range();
+
+    _test_lex();
+
+    _test_multi_zset();
+
+    _test_zpop();
+
+    _test_bzpop();
+}
+
+template <typename RedisInstance>
+void ZSetCmdTest<RedisInstance>::_test_zset() {
+    auto key = test_key("zset");
+
+    KeyDeleter<RedisInstance> deleter(_redis, key);
+
+    std::map<std::string, double> s = {
+        std::make_pair("m1", 1.2),
+        std::make_pair("m2", 2),
+        std::make_pair("m3", 3),
+    };
+
+    const auto &ele = *(s.begin());
+    REDIS_ASSERT(_redis.zadd(key, ele.first, ele.second, UpdateType::EXIST) == 0,
+            "failed to test zadd with noexistent member");
+
+    REDIS_ASSERT(_redis.zadd(key, s.begin(), s.end()) == 3, "failed to test zadd");
+
+    REDIS_ASSERT(_redis.zadd(key, ele.first, ele.second, UpdateType::NOT_EXIST) == 0,
+            "failed to test zadd with exist member");
+
+    REDIS_ASSERT(_redis.zadd(key, s.begin(), s.end(), UpdateType::ALWAYS, true) == 0,
+            "failed to test zadd");
+
+    REDIS_ASSERT(_redis.zcard(key) == 3, "failed to test zcard");
+
+    auto rank = _redis.zrank(key, "m2");
+    REDIS_ASSERT(bool(rank) && *rank == 1, "failed to test zrank");
+    rank = _redis.zrevrank(key, "m4");
+    REDIS_ASSERT(!rank, "failed to test zrevrank with nonexistent member");
+
+    auto score = _redis.zscore(key, "m4");
+    REDIS_ASSERT(!score, "failed to test zscore with nonexistent member");
+
+    REDIS_ASSERT(_redis.zincrby(key, 1, "m3") == 4, "failed to test zincrby");
+
+    score = _redis.zscore(key, "m3");
+    REDIS_ASSERT(score && *score == 4, "failed to test zscore");
+
+    REDIS_ASSERT(_redis.zrem(key, "m1") == 1, "failed to test zrem");
+    REDIS_ASSERT(_redis.zrem(key, {"m1", "m2", "m3", "m4"}) == 2, "failed to test zrem");
+}
+
+template <typename RedisInstance>
+void ZSetCmdTest<RedisInstance>::_test_zscan() {
+    auto key = test_key("zscan");
+
+    KeyDeleter<RedisInstance> deleter(_redis, key);
+
+    std::map<std::string, double> s = {
+        std::make_pair("m1", 1.2),
+        std::make_pair("m2", 2),
+        std::make_pair("m3", 3),
+    };
+    _redis.zadd(key, s.begin(), s.end());
+
+    std::map<std::string, double> res;
+    auto cursor = 0;
+    while (true) {
+        cursor = _redis.zscan(key, cursor, "m*", 2, std::inserter(res, res.end()));
+        if (cursor == 0) {
+            break;
+        }
+    }
+    REDIS_ASSERT(res == s, "failed to test zscan");
+}
+
+template <typename RedisInstance>
+void ZSetCmdTest<RedisInstance>::_test_range() {
+    auto key = test_key("range");
+
+    KeyDeleter<RedisInstance> deleter(_redis, key);
+
+    std::map<std::string, double> s = {
+        std::make_pair("m1", 1),
+        std::make_pair("m2", 2),
+        std::make_pair("m3", 3),
+    };
+    _redis.zadd(key, s.begin(), s.end());
+
+    REDIS_ASSERT(_redis.zcount(key, UnboundedInterval<double>{}) == 3, "failed to test zcount");
+
+    std::vector<std::string> members;
+    _redis.zrange(key, 0, -1, std::back_inserter(members));
+    REDIS_ASSERT(members.size() == s.size(), "failed to test zrange");
+    for (const auto &mem : {"m1", "m2", "m3"}) {
+        REDIS_ASSERT(std::find(members.begin(), members.end(), mem) != members.end(),
+                "failed to test zrange");
+    }
+
+    std::map<std::string, double> res;
+    _redis.zrange(key, 0, -1, std::inserter(res, res.end()));
+    REDIS_ASSERT(s == res, "failed to test zrange with score");
+
+    members.clear();
+    _redis.zrevrange(key, 0, 0, std::back_inserter(members));
+    REDIS_ASSERT(members.size() == 1 && members[0] == "m3", "failed to test zrevrange");
+
+    res.clear();
+    _redis.zrevrange(key, 0, 0, std::inserter(res, res.end()));
+    REDIS_ASSERT(res.size() == 1 && res.find("m3") != res.end() && res["m3"] == 3,
+            "failed to test zrevrange with score");
+
+    members.clear();
+    _redis.zrangebyscore(key, UnboundedInterval<double>{}, std::back_inserter(members));
+    REDIS_ASSERT(members.size() == s.size(), "failed to test zrangebyscore");
+    for (const auto &mem : {"m1", "m2", "m3"}) {
+        REDIS_ASSERT(std::find(members.begin(), members.end(), mem) != members.end(),
+                "failed to test zrangebyscore");
+    }
+
+    members.clear();
+    _redis.zrangebyscore(key,
+            BoundedInterval<double>(1, 2, BoundType::RIGHT_OPEN),
+            std::back_inserter(members));
+    REDIS_ASSERT(members.size() == 1 && members[0] == "m1", "failed to test zrangebyscore");
+
+    res.clear();
+    _redis.zrangebyscore(key,
+            LeftBoundedInterval<double>(2, BoundType::OPEN),
+            std::inserter(res, res.end()));
+    REDIS_ASSERT(res.size() == 1 && res.find("m3") != res.end() && res["m3"] == 3,
+            "failed to test zrangebyscore");
+
+    members.clear();
+    _redis.zrevrangebyscore(key,
+            BoundedInterval<double>(1, 3, BoundType::CLOSED),
+            std::back_inserter(members));
+    REDIS_ASSERT(members == std::vector<std::string>({"m3", "m2", "m1"}),
+            "failed to test zrevrangebyscore");
+
+    res.clear();
+    _redis.zrevrangebyscore(key,
+            RightBoundedInterval<double>(1, BoundType::LEFT_OPEN),
+            std::inserter(res, res.end()));
+    REDIS_ASSERT(res.size() == 1 && res.find("m1") != res.end() && res["m1"] == 1,
+            "failed to test zrevrangebyscore");
+
+    REDIS_ASSERT(_redis.zremrangebyrank(key, 0, 0) == 1, "failed to test zremrangebyrank");
+
+    REDIS_ASSERT(_redis.zremrangebyscore(key,
+                BoundedInterval<double>(2, 3, BoundType::LEFT_OPEN)) == 1,
+            "failed to test zremrangebyscore");
+}
+
+template <typename RedisInstance>
+void ZSetCmdTest<RedisInstance>::_test_lex() {
+    auto key = test_key("lex");
+
+    KeyDeleter<RedisInstance> deleter(_redis, key);
+
+    auto s = {
+        std::make_pair("m1", 0),
+        std::make_pair("m2", 0),
+        std::make_pair("m3", 0),
+    };
+    _redis.zadd(key, s.begin(), s.end());
+
+    REDIS_ASSERT(_redis.zlexcount(key, UnboundedInterval<std::string>{}) == 3,
+            "failed to test zlexcount");
+
+    std::vector<std::string> members;
+    _redis.zrangebylex(key,
+            LeftBoundedInterval<std::string>("m2", BoundType::OPEN),
+            std::back_inserter(members));
+    REDIS_ASSERT(members.size() == 1 && members[0] == "m3",
+            "failed to test zrangebylex");
+
+    members.clear();
+    _redis.zrevrangebylex(key,
+            RightBoundedInterval<std::string>("m1", BoundType::LEFT_OPEN),
+            std::back_inserter(members));
+    REDIS_ASSERT(members.size() == 1 && members[0] == "m1",
+            "failed to test zrevrangebylex");
+
+    REDIS_ASSERT(_redis.zremrangebylex(key,
+                BoundedInterval<std::string>("m1", "m3", BoundType::OPEN)) == 1,
+            "failed to test zremrangebylex");
+}
+
+template <typename RedisInstance>
+void ZSetCmdTest<RedisInstance>::_test_multi_zset() {
+    auto k1 = test_key("k1");
+    auto k2 = test_key("k2");
+    auto k3 = test_key("k3");
+
+    KeyDeleter<RedisInstance> deleter(_redis, {k1, k2, k3});
+
+    _redis.zadd(k1, {std::make_pair("a", 1), std::make_pair("b", 2)});
+    _redis.zadd(k2, {std::make_pair("a", 2), std::make_pair("c", 3)});
+
+    REDIS_ASSERT(_redis.zinterstore(k3, {k1, k2}) == 1, "failed to test zinterstore");
+    auto score = _redis.zscore(k3, "a");
+    REDIS_ASSERT(bool(score) && *score == 3, "failed to test zinterstore");
+
+    REDIS_ASSERT(_redis.zinterstore(k3, k1, 2) == 2, "failed to test zinterstore");
+
+    _redis.del(k3);
+
+    REDIS_ASSERT(_redis.zinterstore(k3, {k1, k2}, Aggregation::MAX) == 1,
+            "failed to test zinterstore");
+    score = _redis.zscore(k3, "a");
+    REDIS_ASSERT(bool(score) && *score == 2, "failed to test zinterstore");
+
+    _redis.del(k3);
+
+    REDIS_ASSERT(_redis.zunionstore(k3,
+                {std::make_pair(k1, 1), std::make_pair(k2, 2)},
+                Aggregation::MIN) == 3,
+            "failed to test zunionstore");
+    std::vector<std::pair<std::string, double>> res;
+    _redis.zrange(k3, 0, -1, std::back_inserter(res));
+    for (const auto &ele : res) {
+        if (ele.first == "a") {
+            REDIS_ASSERT(ele.second == 1, "failed to test zunionstore");
+        } else if (ele.first == "b") {
+            REDIS_ASSERT(ele.second == 2, "failed to test zunionstore");
+        } else if (ele.first == "c") {
+            REDIS_ASSERT(ele.second == 6, "failed to test zunionstore");
+        } else {
+            REDIS_ASSERT(false, "failed to test zuionstore");
+        }
+    }
+
+    REDIS_ASSERT(_redis.zunionstore(k3, k1, 2) == 2, "failed to test zunionstore");
+}
+
+template <typename RedisInstance>
+void ZSetCmdTest<RedisInstance>::_test_zpop() {
+    auto key = test_key("zpop");
+
+    KeyDeleter<RedisInstance> deleter(_redis, key);
+
+    _redis.zadd(key, {std::make_pair("m1", 1.1),
+                        std::make_pair("m2", 2.2),
+                        std::make_pair("m3", 3.3),
+                        std::make_pair("m4", 4.4),
+                        std::make_pair("m5", 5.5),
+                        std::make_pair("m6", 6.6)});
+
+    auto item = _redis.zpopmax(key);
+    REDIS_ASSERT(item && item->first == "m6", "failed to test zpopmax");
+
+    item = _redis.zpopmin(key);
+    REDIS_ASSERT(item && item->first == "m1", "failed to test zpopmin");
+
+    std::vector<std::pair<std::string, double>> vec;
+    _redis.zpopmax(key, 2, std::back_inserter(vec));
+    REDIS_ASSERT(vec.size() == 2 && vec[0].first == "m5" && vec[1].first == "m4",
+            "failed to test zpopmax");
+
+    std::unordered_map<std::string, double> m;
+    _redis.zpopmin(key, 2, std::inserter(m, m.end()));
+    REDIS_ASSERT(m.size() == 2 && m.find("m3") != m.end() && m.find("m2") != m.end(),
+            "failed to test zpopmin");
+}
+
+template <typename RedisInstance>
+void ZSetCmdTest<RedisInstance>::_test_bzpop() {
+    auto key1 = test_key("bzpop1");
+    auto key2 = test_key("bzpop2");
+
+    KeyDeleter<RedisInstance> deleter(_redis, {key1, key2});
+
+    _redis.zadd(key1, {std::make_pair("m1", 1.1),
+                        std::make_pair("m2", 2.2),
+                        std::make_pair("m3", 3.3),
+                        std::make_pair("m4", 4.4),
+                        std::make_pair("m5", 5.5),
+                        std::make_pair("m6", 6.6)});
+
+    _redis.zadd(key2, {std::make_pair("m1", 1.1),
+                        std::make_pair("m2", 2.2),
+                        std::make_pair("m3", 3.3),
+                        std::make_pair("m4", 4.4),
+                        std::make_pair("m5", 5.5),
+                        std::make_pair("m6", 6.6)});
+
+    auto item = _redis.bzpopmax(key1);
+    REDIS_ASSERT(item && std::get<0>(*item) == key1 && std::get<1>(*item) == "m6",
+            "failed to test bzpopmax");
+
+    item = _redis.bzpopmin(key1, std::chrono::seconds(1));
+    REDIS_ASSERT(item && std::get<0>(*item) == key1 && std::get<1>(*item) == "m1",
+            "failed to test zpopmin");
+
+    item = _redis.bzpopmax({key1, key2}, std::chrono::seconds(1));
+    REDIS_ASSERT(item && std::get<0>(*item) == key1 && std::get<1>(*item) == "m5",
+            "failed to test zpopmax");
+
+    item = _redis.bzpopmin({key2, key1});
+    REDIS_ASSERT(item && std::get<0>(*item) == key2 && std::get<1>(*item) == "m1",
+            "failed to test zpopmin");
+}
+
+}
+
+}
+
+}
+
+#endif // end SEWENEW_REDISPLUSPLUS_TEST_ZSET_CMDS_TEST_HPP