aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/client
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--client/CMakeLists.txt5
-rw-r--r--client/client.hpp52
-rw-r--r--client/error.hpp15
-rw-r--r--client/input.hpp139
-rw-r--r--client/main.cpp34
-rw-r--r--client/settings.hpp111
-rw-r--r--client/transport.hpp104
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);
+}
+
+}