Initial commit.

This commit is contained in:
Christoph Heiss 2018-01-19 18:31:32 +01:00
commit b3daaceb3a
15 changed files with 3335 additions and 0 deletions

12
.editorconfig Normal file
View file

@ -0,0 +1,12 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 8
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
build/
.DS_Store

127
CMakeLists.txt Normal file
View file

@ -0,0 +1,127 @@
cmake_minimum_required(VERSION 3.6)
project(resply VERSION 0.1.0 DESCRIPTION "Modern redis client for C++")
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
if (POLICY CMP0025)
cmake_policy(SET CMP0025 NEW)
endif ()
option(BUILD_DOC "Build documentation using doxygen" ON)
add_definitions(-DASIO_STANDALONE)
if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR
"${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang" OR
"${CMAKE_CXX_COMPILER_ID}" STREQUAL "AppleClang")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pedantic-errors -Werror -Wall -Wextra")
# Exceptions are not used, asio is used with explicit error checking.
# Apparently, protobuf uses __builtin_offsetof in a way which clang declares as
# a extension to the language - gcc not. Disable that warning to get it to compile.
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions -Wno-extended-offsetof")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g")
else () # currently not maintained any more:
add_definitions(-DASIO_HAS_STD_ADDRESSOF)
add_definitions(-DASIO_HAS_STD_ARRAY)
add_definitions(-DASIO_HAS_CSTDINT)
add_definitions(-DASIO_HAS_STD_SHARED_PTR)
add_definitions(-DASIO_HAS_STD_TYPE_TRAITS)
add_definitions(-DASIO_HAS_STD_ATOMIC)
add_definitions(-D_WIN32_WINNT=0x0501)
add_definitions(/Wall /EHsc)
endif ()
include_directories(include)
# https://github.com/chriskohlhoff/asio
include_directories(ASIO_INCLUDE_PATH)
# https://github.com/nlohmann/json
# should refer to the directory 'src' of json
include_directories(JSON_INCLUDE_PATH)
# https://github.com/muellan/clipp
include_directories(CLIPP_INCLUDE_PATH)
# https://github.com/gabime/spdlog
include_directories(SPDLOG_INCLUDE_PATH)
# protobuf
find_package(Protobuf REQUIRED)
include_directories(${Protobuf_INCLUDE_DIRS} ${CMAKE_CURRENT_BINARY_DIR})
file(GLOB protos protos/*.proto)
protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS ${protos})
# libresply
file(GLOB libresply_source src/libresply.cc)
add_library(libresply OBJECT ${libresply_source})
target_compile_definitions(libresply PRIVATE RESPLY_VERSION="${PROJECT_VERSION}")
add_library(resply-shared SHARED $<TARGET_OBJECTS:libresply>)
add_library(resply-static STATIC $<TARGET_OBJECTS:libresply>)
set_target_properties(resply-static PROPERTIES OUTPUT_NAME resply)
set_target_properties(resply-shared PROPERTIES OUTPUT_NAME resply)
install(TARGETS resply-shared DESTINATION lib)
install(TARGETS resply-static DESTINATION lib)
install(FILES include/resply.h DESTINATION include)
# cli
add_executable(resply-cli src/resply-cli.cc)
target_link_libraries(resply-cli resply-static)
add_executable(proto-cli src/proto-cli.cc ${PROTO_SRCS})
target_link_libraries(proto-cli ${PROTOBUF_LIBRARIES})
add_executable(grpc-cli src/grpc-cli.cc)
# proxy
add_executable(proxy src/proxy.cc ${PROTO_SRCS})
target_link_libraries(proxy resply-static ${PROTOBUF_LIBRARIES})
# tests
file(GLOB tests_source tests/*.cc)
foreach (test_source ${tests_source})
get_filename_component(name ${test_source} NAME_WE)
set(tests "${tests} ${name}")
add_executable(${name} ${test_source})
target_link_libraries(${name} resply-static)
set_target_properties(${name} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/tests)
endforeach ()
string(STRIP ${tests} tests)
add_custom_target(
tests
COMMAND for t in \"${tests}\"\; do ./tests/$$t\; done
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
DEPENDS ${tests}
COMMENT "Running tests")
# documentation
find_package(Doxygen)
if (BUILD_DOC AND DOXYGEN_FOUND)
set(DOXYGEN_IN ${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile.in)
set(DOXYGEN_OUT ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile)
configure_file(${DOXYGEN_IN} ${DOXYGEN_OUT} @ONLY)
add_custom_target(
doc
COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYGEN_OUT}
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMENT "Generating API documentation with Doxygen"
VERBATIM)
else ()
message("Doxygen need to be installed to generate the doxygen documentation")
endif ()

2482
Doxyfile.in Normal file

File diff suppressed because it is too large Load diff

26
LICENSE Normal file
View file

@ -0,0 +1,26 @@
Boost Software License - Version 1.0 - August 17th, 2003
Copyright (c) 2018 Christoph Heiss <me@christoph-heiss.me>
Permission is hereby granted, free of charge, to any person or organization
obtaining a copy of the software and accompanying documentation covered by
this license (the "Software") to use, reproduce, display, distribute,
execute, and transmit the Software, and to prepare derivative works of the
Software, and to permit third-parties to whom the Software is furnished to
do so, all subject to the following:
The copyright notices in the Software and this entire statement, including
the above license grant, this restriction and the following disclaimer,
must be included in all copies of the Software, in whole or in part, and
all derivative works of the Software, unless such copies or derivative
works are solely in the form of machine-executable object code generated by
a source language processor.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

6
README.md Normal file
View file

@ -0,0 +1,6 @@
# resply - a modern C++ redis client :honeybee:
# License
resply is licensed under the terms of the
[Boost Software License, Version 1.0](http://www.boost.org/LICENSE_1_0.txt)

51
include/cli-common.h Normal file
View file

@ -0,0 +1,51 @@
//
// Copyright 2018 Christoph Heiss <me@christoph-heiss.me>
// Distributed under the Boost Software License, Version 1.0.
//
// See accompanying file LICENSE in the project root directory
// or copy at http://www.boost.org/LICENSE_1_0.txt
//
#include <iostream>
#include <utility>
#include <clipp.h>
#include "resply.h"
namespace common {
struct Options {
Options() : host{"localhost"}, port{"6379"}, show_version{} { }
std::string host;
std::string port;
bool show_version;
};
Options parse_commandline(int argc, char** argv)
{
Options options;
bool show_help{};
auto cli = (
clipp::option("-h", "--host").set(options.host)
.doc("Set the host to connect to [default: localhost]"),
clipp::option("-p", "--port").set(options.port)
.doc("Set the port to connect to [default: 6379]"),
clipp::option("--help").set(show_help).doc("Show help and exit."),
clipp::option("--version").set(options.show_version).doc("Show version and exit.")
);
if (!clipp::parse(argc, argv, cli) || show_help) {
std::cout << clipp::make_man_page(cli, argv[0]) << std::endl;
std::exit(0);
}
return options;
}
}

148
include/resply.h Normal file
View file

@ -0,0 +1,148 @@
//
// Copyright 2018 Christoph Heiss <me@christoph-heiss.me>
// Distributed under the Boost Software License, Version 1.0.
//
// See accompanying file LICENSE in the project root directory
// or copy at http://www.boost.org/LICENSE_1_0.txt
//
#pragma once
#include <memory>
#include <string>
#include <vector>
#include <cstddef>
#include <sstream>
#include <type_traits>
#include <iostream>
namespace resply {
/*! \return The version of the resply library. */
const std::string& version();
/*! \brief Holds the response of a redis command. */
struct Result {
Result() : type{Type::Nil} { }
/*! \brief Indicates the type of the response. */
enum class Type {
String,
Integer,
Array,
ProtError,
IOError,
Nil
};
/*! \brief Holds the type of the response. */
Type type;
/*! \brief Use when type is String, ProtError or IOError */
std::string string;
/*! \brief Use when type is Integer */
long long integer;
/*! \brief Use when type is Array */
std::vector<Result> array;
/*! \brief This outputs the stringify'd version of the response into the supplied stream.
*
* It acts according to the #type field.
* If #type is either Type::ProtError or Type::IOError, "(error) " is
* prepended to the error message. If #type is Type::Nil, the output is "(nil)".
*/
friend std::ostream& operator<<(std::ostream& ostream, const Result& result);
};
class ClientImpl;
/*! \brief Redis client interface
*
* This class implements the protocol RESP to communicate with a redis server.
*/
class Client {
public:
/*! \brief Constructs a new redis client which connects to localhost:6379. */
Client();
/*! \brief Constructs a new redis client.
* \param address Redis server address in the format "<host>[:<port>]".
* The ":port" component may be omitted, in which case it defaults to "6379".
* \param timeout Timeout in milliseconds when connecting to server. Default are 500ms.
*/
Client(const std::string& address, size_t timeout=500);
/*! \brief Constructs a new redis client.
* \param host Redis server address.
* \param port Redis server port.
* \param timeout Timeout in milliseconds when connecting to server. Default are 500ms.
*/
Client(const std::string& host, const std::string& port, size_t timeout=500);
/*! \brief Closes the connection to the redis server. */
~Client();
/*! \brief Establishes a connection to the server. */
void connect();
/*! \brief Closes the connection to the server.
*
* Optional, is also called in the destructor.
*/
void close();
/*! \brief Send a command to the server.
* \param command List of command name and its parameters.
*
* The command and parameters are automatically serialized into the RESP protocol
* as specificed at <https://redis.io/topics/protocol>.
*/
Result command(const std::vector<std::string>& str);
/*! \brief Send a command to the server.
* \param str The name of the command.
* \param args A series of command arguments.
*
* The command and parameters are automatically serialized into the RESP protocol
* as specificed at <https://redis.io/topics/protocol>.
*/
template <typename... ArgTypes>
Result command(const std::string& str, ArgTypes... args)
{
if (str.empty()) {
return {};
}
std::stringstream builder;
builder << '*' << sizeof...(ArgTypes) + 1 << "\r\n";
return command(builder, str, args...);
}
private:
template <typename... ArgTypes>
Result command(std::stringstream& builder, const std::string& str, ArgTypes... args)
{
builder << '$' << str.length() << "\r\n" << str << "\r\n";
return command(builder, args...);
}
template <typename T,
typename = typename std::enable_if<std::is_integral<T>::value, T>::type,
typename... ArgTypes>
Result command(std::stringstream& builder, T num, ArgTypes... args)
{
builder << ':' << num << "\r\n";
return command(builder, args...);
}
Result command(const std::stringstream& builder);
std::unique_ptr<ClientImpl> impl_;
};
}

35
protos/resp.proto Normal file
View file

@ -0,0 +1,35 @@
//
// Copyright 2018 Christoph Heiss <me@christoph-heiss.me>
// Distributed under the Boost Software License, Version 1.0.
//
// See accompanying file LICENSE in the project root directory
// or copy at http://www.boost.org/LICENSE_1_0.txt
//
syntax = "proto3";
package rslp;
message Command {
enum Type {
SIMPLE_STRING = 0;
ERROR = 1;
INTEGER = 2;
BULK_STRING = 3;
ARRAY = 4;
}
message Data {
oneof data {
string str = 1;
sint64 int = 2;
Command subdata = 3;
}
}
Type type = 1;
repeated Data data = 2;
}

17
src/grpc-cli.cc Normal file
View file

@ -0,0 +1,17 @@
//
// Copyright 2018 Christoph Heiss <me@christoph-heiss.me>
// Distributed under the Boost Software License, Version 1.0.
//
// See accompanying file LICENSE in the project root directory
// or copy at http://www.boost.org/LICENSE_1_0.txt
//
#include "cli-common.h"
int main(int argc, char* argv[])
{
auto options{common::parse_commandline(argc, argv)};
return 0;
}

299
src/libresply.cc Normal file
View file

@ -0,0 +1,299 @@
//
// Copyright 2018 Christoph Heiss <me@christoph-heiss.me>
// Distributed under the Boost Software License, Version 1.0.
//
// See accompanying file LICENSE in the project root directory
// or copy at http://www.boost.org/LICENSE_1_0.txt
//
#include <cstdlib>
#include <sstream>
#include <cctype>
#include <algorithm>
#include <iostream>
#include <asio.hpp>
#include "resply.h"
namespace {
bool is_number(const std::string& str) {
return !str.empty() && std::all_of(str.begin(), str.end(), ::isdigit);
}
}
namespace resply {
const std::string& version()
{
const static std::string version{RESPLY_VERSION};
return version;
}
std::ostream& operator<<(std::ostream& ostream, const Result& result)
{
switch (result.type) {
case Result::Type::ProtError:
case Result::Type::IOError:
ostream << "(error) ";
/* fallthrough */
case Result::Type::String:
ostream << result.string;
break;
case Result::Type::Integer:
ostream << result.integer;
break;
case Result::Type::Nil:
ostream << "(nil)";
break;
case Result::Type::Array:
for (const auto& res: result.array) {
ostream << res;
}
break;
};
return ostream;
}
class ClientImpl {
public:
ClientImpl(const std::string& host, const std::string& port, size_t timeout)
: host_{host}, port_{port}, timeout_{timeout}, socket_{io_context_}
{
(void)timeout_;
}
~ClientImpl()
{
close();
}
void connect()
{
asio::error_code error_code;
asio::ip::tcp::resolver resolver{io_context_};
auto results = resolver.resolve(host_, port_, error_code);
check_asio_error(error_code);
asio::connect(socket_, results, error_code);
check_asio_error(error_code);
}
void close()
{
socket_.close();
}
Result command(const std::string& command)
{
asio::error_code error_code;
asio::write(socket_, asio::buffer(command), error_code);
check_asio_error(error_code);
Result result;
asio::streambuf buffer;
size_t remaining{};
do {
asio::read_until(socket_, buffer, "\r\n" , error_code);
check_asio_error(error_code);
parse_response(result, buffer, remaining);
} while (remaining);
return result;
}
private:
static bool check_asio_error(asio::error_code& error_code)
{
if (error_code) {
std::cerr << error_code.message() << std::endl;
}
return !!error_code;
}
static void parse_response(Result& result, asio::streambuf& streambuf, size_t& remaining)
{
std::string buffer{asio::buffers_begin(streambuf.data()),
asio::buffers_end(streambuf.data())};
if (!remaining) {
// First pass
parse_type(result, buffer, remaining);
} else {
// All other
continue_parse_type(result, buffer, remaining);
}
streambuf.consume(streambuf.size());
}
static void parse_type(Result& result, const std::string& buffer, size_t& remaining)
{
switch (buffer.front()) {
case '+':
result.type = Result::Type::String;
// Exclude the final \r\n bytes
result.string += buffer.substr(1, buffer.length() - 3);
break;
case '-':
result.type = Result::Type::ProtError;
// Exclude the final \r\n bytes
result.string += buffer.substr(1, buffer.length() - 3);
break;
case ':':
result.type = Result::Type::Integer;
result.integer = std::stoll(buffer.substr(1));
break;
case '$': {
size_t size;
long length{std::stol(buffer.substr(1), &size)};
if (length == -1) {
result.type = Result::Type::Nil;
break;
}
result.type = Result::Type::String;
if (static_cast<size_t>(length) < buffer.length()) {
result.string += buffer.substr(size + 3, buffer.length() - size - 3);
} else {
result.string += buffer.substr(size + 3);
remaining = length - result.string.length();
}
break;
}
case '*': {
size_t size;
long length{std::stol(buffer.substr(1), &size)};
if (length == -1) {
result.type = Result::Type::Nil;
break;
}
result.type = Result::Type::Array;
remaining = length;
while (remaining) {
}
break;
}
default:
// Error
break;
}
}
static void continue_parse_type(Result& result, const std::string& buffer, size_t& remaining)
{
switch (result.type) {
case Result::Type::String:
case Result::Type::ProtError:
case Result::Type::IOError:
if (remaining == buffer.length() - 2) {
result.string += buffer.substr(0, remaining);
remaining = 0;
} else {
result.string += buffer;
remaining -= buffer.length();
}
default:
break;
}
}
const std::string host_;
const std::string port_;
const size_t timeout_;
asio::io_context io_context_;
asio::ip::tcp::socket socket_;
};
Client::Client() : Client{"localhost", "6379"} { }
Client::Client(const std::string& address, size_t timeout)
{
std::string host, port;
std::stringstream sstream{address};
std::getline(sstream, host, ':');
std::getline(sstream, port);
impl_ = std::make_unique<ClientImpl>(host, port, timeout);
}
Client::Client(const std::string& host, const std::string& port, size_t timeout)
: impl_{std::make_unique<ClientImpl>(host, port, timeout)}
{
}
Client::~Client() { }
void Client::connect() { impl_->connect(); }
void Client::close() { impl_->close(); }
Result Client::command(const std::vector<std::string>& str)
{
if (str.empty()) {
return {};
}
std::stringstream builder;
builder << '*' << str.size() << "\r\n";
for (const std::string& part: str) {
if (is_number(part)) {
builder << ':' << part << "\r\n";
} else {
builder << '$' << part.length() << "\r\n" << part << "\r\n";
}
}
return impl_->command(builder.str());
}
Result Client::command(const std::stringstream& builder)
{
return impl_->command(builder.str());
}
}

22
src/proto-cli.cc Normal file
View file

@ -0,0 +1,22 @@
//
// Copyright 2018 Christoph Heiss <me@christoph-heiss.me>
// Distributed under the Boost Software License, Version 1.0.
//
// See accompanying file LICENSE in the project root directory
// or copy at http://www.boost.org/LICENSE_1_0.txt
//
#include "cli-common.h"
#include "resp.pb.h"
int main(int argc, char* argv[])
{
GOOGLE_PROTOBUF_VERIFY_VERSION;
auto options{common::parse_commandline(argc, argv)};
google::protobuf::ShutdownProtobufLibrary();
return 0;
}

31
src/proxy.cc Normal file
View file

@ -0,0 +1,31 @@
//
// Copyright 2018 Christoph Heiss <me@christoph-heiss.me>
// Distributed under the Boost Software License, Version 1.0.
//
// See accompanying file LICENSE in the project root directory
// or copy at http://www.boost.org/LICENSE_1_0.txt
//
#include "cli-common.h"
#include "resply.h"
#include "resp.pb.h"
int main(int argc, char* argv[])
{
GOOGLE_PROTOBUF_VERIFY_VERSION;
auto options{common::parse_commandline(argc, argv)};
if (options.show_version) {
std::cout
<< argv[0] << '\n'
<< "Using resply version " << resply::version() << std::endl;
return 0;
}
google::protobuf::ShutdownProtobufLibrary();
return 0;
}

56
src/resply-cli.cc Normal file
View file

@ -0,0 +1,56 @@
//
// Copyright 2018 Christoph Heiss <me@christoph-heiss.me>
// Distributed under the Boost Software License, Version 1.0.
//
// See accompanying file LICENSE in the project root directory
// or copy at http://www.boost.org/LICENSE_1_0.txt
//
#include <iostream>
#include <string>
#include <vector>
#include "cli-common.h"
#include "resply.h"
int main(int argc, char* argv[])
{
auto options{common::parse_commandline(argc, argv)};
if (options.show_version) {
std::cout
<< argv[0] << '\n'
<< "Using resply version " << resply::version() << std::endl;
return 0;
}
resply::Client client{options.host, options.port};
client.connect();
while (std::cin) {
std::cout << options.host << ':' << options.port << "> ";
std::string line;
std::getline(std::cin, line);
std::stringstream linestream{line};
std::vector<std::string> command;
while (linestream >> line) {
command.push_back(line);
}
std::stringstream sstream;
sstream << client.command(command);
std::cout << sstream.str();
if (sstream.str().back() != '\n') {
std::cout << std::endl;
}
}
client.close();
std::cout << std::endl;
return 0;
}

21
tests/ping-pong.cc Normal file
View file

@ -0,0 +1,21 @@
//
// Copyright 2018 Christoph Heiss <me@christoph-heiss.me>
// Distributed under the Boost Software License, Version 1.0.
//
// See accompanying file LICENSE in the project root directory
// or copy at http://www.boost.org/LICENSE_1_0.txt
//
#include "resply.h"
int main()
{
resply::Client client;
client.connect();
std::stringstream stream;
stream << client.command("ping");
return stream.str() != "PONG";
}