diff options
Diffstat (limited to 'client')
-rw-r--r-- | client/CMakeLists.txt | 5 | ||||
-rw-r--r-- | client/client.hpp | 52 | ||||
-rw-r--r-- | client/error.hpp | 15 | ||||
-rw-r--r-- | client/input.hpp | 139 | ||||
-rw-r--r-- | client/main.cpp | 34 | ||||
-rw-r--r-- | client/settings.hpp | 111 | ||||
-rw-r--r-- | client/transport.hpp | 104 |
7 files changed, 460 insertions, 0 deletions
diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt new file mode 100644 index 0000000..cd23de8 --- /dev/null +++ b/client/CMakeLists.txt @@ -0,0 +1,5 @@ +find_package(Boost REQUIRED COMPONENTS filesystem program_options) + +add_executable(client main.cpp) +target_include_directories(client SYSTEM PRIVATE ${Boost_INCLUDE_DIRS}) +target_link_libraries(client PRIVATE ${Boost_LIBRARIES}) diff --git a/client/client.hpp b/client/client.hpp new file mode 100644 index 0000000..139ebb2 --- /dev/null +++ b/client/client.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include "input.hpp" +#include "settings.hpp" +#include "transport.hpp" + +#include <iostream> +#include <string> +#include <utility> + +namespace math::client { + +class Client { +public: + explicit Client(const Settings& settings) + : Client{make_input_reader(settings), make_transport(settings)} + { } + + Client(input::ReaderPtr&& input_reader, TransportPtr&& transport) + : m_input_reader{std::move(input_reader)} + , m_transport{std::move(transport)} + { } + + void run() { + m_input_reader->for_each_input([this] (const std::string& input) { + m_transport->send_query(input, [] (const std::string& reply) { + std::cout << reply << '\n'; + }); + return true; + }); + } + +private: + static input::ReaderPtr make_input_reader(const Settings& settings) { + if (settings.input_from_string()) { + return input::make_string_reader(settings.m_input); + } + if (settings.input_from_files()) { + return input::make_file_reader(settings.m_files); + } + return input::make_console_reader(); + } + + static TransportPtr make_transport(const Settings& settings) { + return make_blocking_network_transport(settings.m_host, settings.m_port); + } + + const input::ReaderPtr m_input_reader; + TransportPtr m_transport; +}; + +} diff --git a/client/error.hpp b/client/error.hpp new file mode 100644 index 0000000..98e1305 --- /dev/null +++ b/client/error.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include <stdexcept> +#include <string> + +namespace math::client { + +class Error : public std::runtime_error { +public: + explicit Error(const std::string& what) + : std::runtime_error{"client error: " + what} + { } +}; + +} diff --git a/client/input.hpp b/client/input.hpp new file mode 100644 index 0000000..077d5c7 --- /dev/null +++ b/client/input.hpp @@ -0,0 +1,139 @@ +#pragma once + +#include "error.hpp" + +#include <exception> +#include <fstream> +#include <functional> +#include <iostream> +#include <memory> +#include <string> +#include <vector> + +namespace math::client::input { + +class Error : public client::Error { +public: + explicit Error(const std::string& what) + : client::Error{"input error: " + what} { + } +}; + +class Reader { +public: + using InputHandler = std::function<bool (const std::string&)>; + + virtual ~Reader() = default; + + virtual bool for_each_input(const InputHandler& process) const = 0; +}; + +using ReaderPtr = std::unique_ptr<input::Reader>; + +class FileReader : public Reader { +public: + explicit FileReader(const std::string& path) + : m_path{path} + { } + + bool for_each_input(const InputHandler& process) const override { + return enum_lines(process); + } + +private: + bool enum_lines(const InputHandler& process) const { + std::ifstream ifs; + ifs.exceptions(std::ifstream::badbit); + + try { + ifs.open(m_path); + if (!ifs.is_open()) { + throw Error{"couldn't open file: " + m_path}; + } + + for (std::string line; std::getline(ifs, line);) { + if (!process(line)) { + return false; + } + } + } catch (const std::exception& e) { + throw Error{e.what()}; + } + + return true; + } + + const std::string m_path; +}; + +class MultiFileReader : public Reader { +public: + explicit MultiFileReader(const std::vector<std::string>& paths) + : m_paths{paths} + { } + + bool for_each_input(const InputHandler& process) const override { + for (const auto& path : m_paths) { + const FileReader reader{path}; + if (!reader.for_each_input(process)) { + return false; + } + } + return true; + } + +private: + const std::vector<std::string> m_paths; +}; + +inline input::ReaderPtr make_file_reader(const std::string& path) { + return std::make_unique<input::FileReader>(path); +} + +inline input::ReaderPtr make_file_reader(const std::vector<std::string>& paths) { + return std::make_unique<input::MultiFileReader>(paths); +} + +class StringReader : public Reader { +public: + explicit StringReader(const std::string& input) + : m_input{input} + { } + + bool for_each_input(const InputHandler& process) const override { + return process(m_input); + } + +private: + const std::string m_input; +}; + +inline input::ReaderPtr make_string_reader(const std::string& input) { + return std::make_unique<input::StringReader>(input); +} + +class ConsoleReader : public Reader { +public: + ConsoleReader() = default; + + bool for_each_input(const InputHandler& process) const override { + std::string line; + while (read_line(line)) { + if (!process(line)) { + return false; + } + } + return true; + } + +private: + static bool read_line(std::string& dest) { + return static_cast<bool>(std::getline(std::cin, dest)); + } +}; + +inline input::ReaderPtr make_console_reader() { + return std::make_unique<input::ConsoleReader>(); +} + +} diff --git a/client/main.cpp b/client/main.cpp new file mode 100644 index 0000000..cfe5026 --- /dev/null +++ b/client/main.cpp @@ -0,0 +1,34 @@ +#include "client.hpp" +#include "settings.hpp" + +#include <boost/program_options.hpp> + +#include <exception> +#include <iostream> + +int main(int argc, char* argv[]) { + try { + math::client::SettingsParser parser{argv[0]}; + + try { + const auto settings = parser.parse(argc, argv); + if (settings.exit_with_usage()) { + parser.usage(); + return 0; + } + + math::client::Client client{settings}; + client.run(); + } catch (const boost::program_options::error& e) { + parser.usage_error(e); + return 1; + } + } catch (const std::exception& e) { + std::cerr << "An error occured: " << e.what() << "\n"; + return 1; + } catch (...) { + std::cerr << "An unknown error occured\n"; + return 1; + } + return 0; +} diff --git a/client/settings.hpp b/client/settings.hpp new file mode 100644 index 0000000..5c2522e --- /dev/null +++ b/client/settings.hpp @@ -0,0 +1,111 @@ +#pragma once + +#include "transport.hpp" + +#include <boost/filesystem.hpp> +#include <boost/program_options.hpp> + +#include <exception> +#include <iostream> +#include <string> +#include <vector> + +namespace math::client { + +struct Settings { + std::string m_input; + std::string m_host; + std::string m_port; + std::vector<std::string> m_files; + + bool exit_with_usage() const { return m_vm.count("help"); } + + bool input_from_string() const { + return m_vm.count("command"); + } + + bool input_from_files() const { + return !input_from_string() && !m_files.empty(); + } + + bool input_from_console() const { + return !input_from_string() && !input_from_files(); + } + + boost::program_options::variables_map m_vm; +}; + +class SettingsParser { +public: + explicit SettingsParser(const std::string& argv0) + : m_prog_name{extract_filename(argv0)} + { + m_visible.add_options() + ("help,h", + "show this message and exit") + ("command,c", + boost::program_options::value(&m_settings.m_input), + "evaluate the argument expression and exit") + ("host,H", + boost::program_options::value(&m_settings.m_host)->default_value("localhost"), + "server host address") + ("port,p", + boost::program_options::value(&m_settings.m_port)->default_value(NetworkTransport::DEFAULT_PORT), + "server port number"); + m_hidden.add_options() + ("files", + boost::program_options::value<std::vector<std::string>>(&m_settings.m_files), + "shouldn't be visible"); + m_positional.add("files", -1); + } + + static const char* get_short_description() { + return "[-h|--help] [-c|--command arg] [-H|--host] [-p|--port] [file...]"; + } + + Settings parse(int argc, char* argv[]) { + boost::program_options::options_description all; + all.add(m_hidden).add(m_visible); + boost::program_options::store( + boost::program_options::command_line_parser{argc, argv} + .options(all) + .positional(m_positional) + .run(), + m_settings.m_vm); + if (m_settings.exit_with_usage()) { + return m_settings; + } + boost::program_options::notify(m_settings.m_vm); + return m_settings; + } + + void usage() const { + std::cout << *this; + } + + void usage_error(const std::exception& e) const { + std::cerr << "usage error: " << e.what() << '\n'; + std::cerr << *this; + } + +private: + static std::string extract_filename(const std::string& path) { + return boost::filesystem::path{path}.filename().string(); + } + + const std::string m_prog_name; + + boost::program_options::options_description m_hidden; + boost::program_options::options_description m_visible; + boost::program_options::positional_options_description m_positional; + + Settings m_settings; + + friend std::ostream& operator<<(std::ostream& os, const SettingsParser& parser) { + os << "usage: " << parser.m_prog_name << ' ' << get_short_description() << '\n'; + os << parser.m_visible; + return os; + } +}; + +} diff --git a/client/transport.hpp b/client/transport.hpp new file mode 100644 index 0000000..ae56f73 --- /dev/null +++ b/client/transport.hpp @@ -0,0 +1,104 @@ +#pragma once + +#include "error.hpp" + +#include <boost/asio.hpp> +#include <boost/system/system_error.hpp> + +#include <functional> +#include <memory> +#include <string> + +namespace math::client { +namespace transport { + +class Error : public client::Error { +public: + explicit Error(const std::string& msg) + : client::Error{"transport error: " + msg} + { } +}; + +} + +class Transport { +public: + virtual ~Transport() = default; + + using ProcessResult = std::function<void (const std::string&)>; + + virtual void send_query(const std::string&, const ProcessResult&) = 0; +}; + +using TransportPtr = std::unique_ptr<Transport>; + +class NetworkTransport : public Transport { +public: + static constexpr auto DEFAULT_PORT = "18000"; + + NetworkTransport(const std::string& host, const std::string& port) + : m_host{host}, m_port{port} + { } + +protected: + const std::string m_host; + const std::string m_port; +}; + +class BlockingNetworkTransport : public NetworkTransport { +public: + BlockingNetworkTransport(const std::string &host, const std::string& port) + : NetworkTransport{host, port}, m_socket{m_io_context} { + try { + connect(); + } catch (const boost::system::system_error& e) { + throw transport::Error{e.what()}; + } + } + + void send_query(const std::string& query, const ProcessResult& on_reply) override { + std::string reply; + try { + reply = send_query(query); + } catch (const boost::system::system_error& e) { + throw transport::Error{e.what()}; + } + on_reply(reply); + } + +private: + void connect() { + boost::asio::ip::tcp::resolver resolver{m_io_context}; + boost::asio::connect(m_socket, resolver.resolve(m_host, m_port)); + } + + std::string send_query(const std::string& query) { + write(query); + return read_line(); + } + + void write(std::string input) { + input += '\n'; + boost::asio::write(m_socket, boost::asio::const_buffer{input.c_str(), input.size()}); + } + + std::string read_line() { + const auto bytes = boost::asio::read_until(m_socket, m_buffer, '\n'); + const auto data = boost::asio::buffer_cast<const char*>(m_buffer.data()); + const std::string result{data, bytes - 1}; + m_buffer.consume(bytes); + return result; + } + + boost::asio::io_context m_io_context; + boost::asio::ip::tcp::socket m_socket; + boost::asio::streambuf m_buffer; +}; + +inline TransportPtr make_blocking_network_transport( + const std::string& host, const std::string& port) { + + return std::make_unique<BlockingNetworkTransport>(host, port); +} + +} |