diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..bc1b84f737049855fb65124945387c97717c0156 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +build/ +* +!*/ +build/* +!.gitignore +!.gitmodules +!/spdlog/* +!./CMakeLists.txt +!./README.md +!/img/** +!/server/** +!/qt/** +!/test_client/** +!/plugins/**/*.py \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000000000000000000000000000000000..5a1c5018e7d973e3daedfac9e2119592c8f0e709 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "spdlog"] + path = spdlog + url = https://github.com/gabime/spdlog +[submodule "argparse"] + path = argparse + url = https://github.com/p-ranav/argparse diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..2bd623abe80eef379c99f75d840336fc36d129be --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,36 @@ +cmake_minimum_required(VERSION 3.22) +project(Dict2FlashcardsQT) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +set(CMAKE_CXX_STANDARD_REQUIRED On) + +find_package(Boost COMPONENTS program_options REQUIRED) + + +enable_testing() +include(FetchContent) +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip +) + +# For Windows: Prevent overriding the parent project's compiler/linker settings +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +FetchContent_MakeAvailable(googletest) + +enable_testing() +include(GoogleTest) + +# Logger +add_subdirectory(spdlog) +link_libraries(spdlog::spdlog_header_only) + +add_subdirectory(client) +add_subdirectory(server) +target_include_directories(server_main + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/argparse/include/argparse) +add_subdirectory(qt) diff --git a/README.md b/README.md index 672fae760c20e92b8bbfa519033a3678c379bae9..d332372b4a14b669497a60b0c69b0068c093f119 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ ## Микрообзор функционала Dict2Flashcards Лучше посмотреть [страничку на гитхабе](https://github.com/Blackdeer1524/Dict2Flashcards). -Да, демка отличается от текущей версии, но основной функционал один и тот же. -## Навигация по кололе +## Навигация по колоде ![](img/2023-04-20-23-31-47.png) Приложение является "визуализацией" коллекиции Deck. Deck - это по сути вектор @@ -30,11 +29,10 @@ * Локальные (директория с аудио) * Внешние (парсеры HTML страниц, API, и т.д. ) -Каждый плагин должен реализовывать соответствующий интерфейс. - -* Замечание: плагины предоставления аудио, изображений и предложений являются -генераторами т.е. они предоставляют свои данные по пакетам. Как это реализовать -через сервер - пока не знаю. +Каждый плагин должен реализовать 3 функции: +def get(word: str) - возвращает список карт, в которых заполнены соответствующие типу плагина поля +def load() - выполняет необходимые действия при первом использовании плагина +def unload() - выполняет необходимые действия, когда плагин больше не нужен С плагинами мы будет общаться через запросы к серверу плагинов: @@ -49,23 +47,6 @@ так как нужно управлять графом зависимостей цепей друг от друга (по этому цепи цепей мы оставим до самого конца, если на это будет время): -### Сложности с графом зависимостей -![](img/2023-04-20-23-33-02.png) -* при изменении одной цепи нужно изменить все зависимые цепи - * при изменении D должны измениться B, C, A и E. (A меняется так как поменялась - цепь B) -* нельзя допускать циклов - -Существует задать условия прекращения поиска по цепи. Под условием прекращения -подразумевается следующее: -* Первый непустой результат поиска; -* Все непустые результаты поиска в цепи. - -Пример окна настройки вложенной цепи: -Цепь "cambset" зависит от цепи "bi-mono cambridge" - -![](img/2023-04-20-23-34-25.png) - ## Интерфейс добавления изображений Существует возможность добавление к карточкам изображений ![](img/2023-04-20-23-34-42.png) @@ -79,5 +60,3 @@ ## Язык запросов Реализован язык запросов который позволяет искать карточки по колоде. [см. стандарт](https://github.com/Blackdeer1524/Dict2Flashcards#query-language-documentation) - -Нужно будет переделать т.к. он написан ОЧЕНЬ плохо. diff --git a/argparse b/argparse new file mode 160000 index 0000000000000000000000000000000000000000..557948f1236db9e27089959de837cc23de6c6bbd --- /dev/null +++ b/argparse @@ -0,0 +1 @@ +Subproject commit 557948f1236db9e27089959de837cc23de6c6bbd diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..8570eeb13f05951fa8322dc0a3576b4a0ca10b76 --- /dev/null +++ b/client/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(lib) \ No newline at end of file diff --git a/client/lib/CMakeLists.txt b/client/lib/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..4ae7d6c823b333bdf9a064de71624d18b1e3ce87 --- /dev/null +++ b/client/lib/CMakeLists.txt @@ -0,0 +1,3 @@ +add_subdirectory(connection) +add_subdirectory(plugin_wrappers) +add_subdirectory(card) diff --git a/client/lib/card/CMakeLists.txt b/client/lib/card/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..4ecf579e34021a3bb69852ce1bcc1a894458d063 --- /dev/null +++ b/client/lib/card/CMakeLists.txt @@ -0,0 +1,10 @@ +message(${CMAKE_CURRENT_SOURCE_DIR}) + +add_library(card Card.cpp) +target_include_directories(card PUBLIC ./) + +add_executable(card_tests Card_tests.cpp) +target_link_libraries(card_tests PRIVATE + GTest::gtest_main + card) +add_test(card_tests card_tests) diff --git a/client/lib/card/Card.cpp b/client/lib/card/Card.cpp new file mode 100644 index 0000000000000000000000000000000000000000..73dc1793eba30308d3fbd7146be0df478c8c4484 --- /dev/null +++ b/client/lib/card/Card.cpp @@ -0,0 +1,74 @@ +#include "Card.h" + +#include +#include +#include +#include +#include + +#include + +using namespace nlohmann; + +void traverse_tags(const json &tags, + const std::string &prefix, + std::string &result) { + if (tags.is_object()) { + for (auto &tag : tags.items()) { + std::string new_prefix = + prefix.empty() ? tag.key() : prefix + "::" + tag.key(); + traverse_tags(tag.value(), new_prefix, result); + } + return; + } + if (tags.is_array()) { + for (auto &value : tags) { + traverse_tags(value, prefix, result); + } + return; + } + result += (prefix.empty() ? "" : prefix + "::") + tags.get() + ' '; +} + +std::string parse_tags(const json &tags) { + std::string result; + traverse_tags(tags, "", result); + if (result.back() == ' ') { + result.pop_back(); + } + return result; +} + +std::ostream &operator<<(std::ostream &os, const Card &card) { + os << json(card).dump(2); + return os; +} + +std::pair, std::string> load_cards(const std::string &path) { + std::ifstream file(path); + if (!file.is_open()) { + return {{}, "Can't open the file"}; + } + std::stringstream buffer; + if (buffer << file.rdbuf()) { + try { + return {json::parse(buffer.str()).get>(), ""}; + } catch (...) { + return {{}, "Wrong data format"}; + } + } + return {{}, "Can't read from file"}; +} + +std::string save_cards(const std::vector &cards, + const std::string &path) { + std::ofstream file(path); + if (!file.is_open()) { + return "Can't open the file"; + } + std::string cards_dump = json(cards).dump(); + if (file << cards_dump) { + return ""; + } + return "Can't write into file"; +} \ No newline at end of file diff --git a/client/lib/card/Card.h b/client/lib/card/Card.h new file mode 100644 index 0000000000000000000000000000000000000000..8a1ee111ce8c651572682b6b34dc2b779d5b972b --- /dev/null +++ b/client/lib/card/Card.h @@ -0,0 +1,52 @@ +#ifndef CARD_H +#define CARD_H + +#include +#include +#include +#include +#include +#include + +#include "Media.h" + +struct Card { + Card() = default; + Card(const Card &) = default; + Card(Card &&) = default; + Card &operator=(const Card &) = default; + Card &operator=(Card &&) = default; + + friend bool operator==(const Card &lhs, const Card &rhs) = default; + + std::string word; + std::vector special; + std::string definition; + std::vector examples; + Media audios; + Media images; + nlohmann::json tags; + nlohmann::json other; +}; + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Card, + word, + special, + definition, + examples, + images, + audios, + tags, + other); + +std::ostream &operator<<(std::ostream &os, const Card &card); + +void traverse_tags(const nlohmann::json &tags, + const std::string &prefix, + std::string &result); +std::string parse_tags(const nlohmann::json &tags); + +std::pair, std::string> load_cards(const std::string &path); +std::string save_cards(const std::vector &cards, const std::string &path); + +#endif // CARD_H diff --git a/client/lib/card/Card_tests.cpp b/client/lib/card/Card_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..19c6369553e819e115f2773860a3095e07ae12c8 --- /dev/null +++ b/client/lib/card/Card_tests.cpp @@ -0,0 +1,70 @@ +#include "Card.h" + +#include +#include + +#include + +#include + +using namespace nlohmann; + +TEST(ParseTags, JSON_to_STR) { + json tags = json::parse(R"({ "pos" : "noun", + "languages" : ["Russian", "English"], + "very_deep_tag" : { "not_the_end": "value" } + })"); + + std::string actual = parse_tags(tags); + std::string expected = "languages::Russian languages::English pos::noun " + "very_deep_tag::not_the_end::value"; + EXPECT_EQ(expected, actual); +} + +TEST(ParseTags, STR_to_STR) { + json tags = json::parse( + R"({ "tags" : "languages::Russian languages::English pos::noun very_deep_tag::not_the_end::value" })"); + + std::string actual = parse_tags(tags.at("tags")); + std::string expected = "languages::Russian languages::English pos::noun " + "very_deep_tag::not_the_end::value"; + EXPECT_EQ(expected, actual); +} + +//TEST(LOAD_SAVE, SuccessfulSaveLoad) { +// std::vector cards = { +// {"go", +// {"special_1", "special_2"}, +// "move", {"go there", "go here"}, +// {{"image_link_1", "image_link_2"}, {"image_link_3", "image_link_4"}}, +// {{"audio_link_1", "audio_link_2"}, {"audio_link_1", "audio_link_2"}}, +// "pos::verb", +// "other"}, +// {"go2", +// {"special_3", "special_4"}, +// "move", {"go there", "go here"}, +// {"image_link_3", "image_link_4"}, +// {"audio_link_3", "audio_link_4"}, +// "pos::verb", +// "other"} +// }; +// std::string path = "test.json"; +// +// std::string save_result = save_cards(cards, path); +// EXPECT_EQ("", save_result); +// std::pair, std::string> load_result = load_cards(path); +// EXPECT_EQ("", load_result.second); +// +// EXPECT_EQ(cards, load_result.first); +//} + +TEST(LOAD_SAVE, WrognFormat) { + std::string data = R"({"word": "go"})"; + std::string path = "test.json"; + + std::ofstream file(path); + file << data; + file.close(); + std::pair, std::string> load_result = load_cards(path); + EXPECT_EQ("Wrong data format", load_result.second); +} diff --git a/client/lib/card/Media.h b/client/lib/card/Media.h new file mode 100644 index 0000000000000000000000000000000000000000..ec019c02c9c5b46f07de69902da72d5a7f32423c --- /dev/null +++ b/client/lib/card/Media.h @@ -0,0 +1,35 @@ +#ifndef MEDIA_H +#define MEDIA_H + +#include +#include +#include + +struct SourceWithAdditionalInfo { + std::string src; + std::string info; + + friend bool operator==(const SourceWithAdditionalInfo &lhs, const SourceWithAdditionalInfo &rhs) = default; +}; + +inline void to_json(nlohmann ::json &nlohmann_json_j, + const SourceWithAdditionalInfo &nlohmann_json_t) { + nlohmann_json_j = {nlohmann_json_t.src, nlohmann_json_t.info}; +} + +inline void from_json(const nlohmann ::json &nlohmann_json_j, + SourceWithAdditionalInfo &nlohmann_json_t) { + nlohmann_json_j[0].get_to(nlohmann_json_t.src); + nlohmann_json_j[1].get_to(nlohmann_json_t.info); +} + +struct Media { + std::vector local; + std::vector web; + + friend bool operator==(const Media &lhs, const Media &rhs) = default; +}; + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Media, local, web); + +#endif // !MEDIA_H \ No newline at end of file diff --git a/client/lib/connection/CMakeLists.txt b/client/lib/connection/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..5032c81077477a90992f8bdcccc14bc437f9e2dd --- /dev/null +++ b/client/lib/connection/CMakeLists.txt @@ -0,0 +1,5 @@ +message(${CMAKE_CURRENT_SOURCE_DIR}) + +add_library(connection src/ServerConnection.cpp) + +target_include_directories(connection PUBLIC include) \ No newline at end of file diff --git a/client/lib/connection/include/IRequestable.h b/client/lib/connection/include/IRequestable.h new file mode 100644 index 0000000000000000000000000000000000000000..633508d725df565340acbdc74140e2d68f6fd6ea --- /dev/null +++ b/client/lib/connection/include/IRequestable.h @@ -0,0 +1,13 @@ +#ifndef IREQUESTABLE_H +#define IREQUESTABLE_H + +#include + +class IRequestable { + public: + virtual ~IRequestable() = default; + virtual std::pair + request(const std::string &message) = 0; +}; + +#endif // IREQUESTABLE_H diff --git a/client/lib/connection/include/ServerConnection.h b/client/lib/connection/include/ServerConnection.h new file mode 100644 index 0000000000000000000000000000000000000000..17a960e09c49270bd2fb2ed63a2a2a36c6d4d0ea --- /dev/null +++ b/client/lib/connection/include/ServerConnection.h @@ -0,0 +1,25 @@ +#ifndef SERVERCONNECTION_H +#define SERVERCONNECTION_H + +#include + +#include + +#include "IRequestable.h" + +class ServerConnection : public IRequestable { + public: + ServerConnection(unsigned short port, + const std::string &host = "127.0.0.1"); + + bool is_connected(); + std::pair request(const std::string &message) override; + + private: + boost::asio::io_context io_context_; + boost::asio::ip::tcp::socket socket_; + boost::asio::streambuf buffer_; + bool is_connected_; +}; + +#endif // SERVERCONNECTION_H diff --git a/client/lib/connection/include/mock_classes.h b/client/lib/connection/include/mock_classes.h new file mode 100644 index 0000000000000000000000000000000000000000..012b7bc5005d172bc005af54200dfc0a365e48e4 --- /dev/null +++ b/client/lib/connection/include/mock_classes.h @@ -0,0 +1,29 @@ +#ifndef MOCK_CLASSES_H +#define MOCK_CLASSES_H + +#include "IRequestable.h" + +#include + +struct Memorizer : public IRequestable { + std::pair request(const std::string &message) override { + received_message = message; + return std::make_pair(true, ""); + } + + std::string received_message; +}; + +struct FixedAnswer : public IRequestable { + explicit FixedAnswer(std::string answer, bool connected = true) + : answer(std::move(answer)), connected(connected){}; + + std::pair request(const std::string &message) override { + return std::make_pair(connected, answer); + } + + bool connected; + std::string answer; +}; + +#endif // MOCK_CLASSES_H diff --git a/client/lib/connection/src/ServerConnection.cpp b/client/lib/connection/src/ServerConnection.cpp new file mode 100644 index 0000000000000000000000000000000000000000..278419dc25cb5a470ba393fabb765be9ade29e95 --- /dev/null +++ b/client/lib/connection/src/ServerConnection.cpp @@ -0,0 +1,44 @@ +#include "ServerConnection.h" + +#include +#include + +#include + +#include + +ServerConnection::ServerConnection(unsigned short port, const std::string &host) + : io_context_(), socket_(io_context_), is_connected_(true) { + boost::asio::ip::tcp::endpoint endpoint( + boost::asio::ip::address::from_string(host), port); + boost::system::error_code error; + socket_.connect(endpoint, error); + is_connected_ = !error; +} + +bool ServerConnection::is_connected() { + return is_connected_; +} + +std::pair +ServerConnection::request(const std::string &message) { + std::string request_message = message + "\r\n"; + boost::system::error_code error; + + boost::asio::write(socket_, boost::asio::buffer(request_message), error); + if (error) { + is_connected_ = false; + return std::make_pair(false, error.message()); + } + + boost::asio::read_until(socket_, buffer_, "\r\n", error); + if (error) { + is_connected_ = false; + return std::make_pair(false, error.message()); + } + + std::string response; + std::istream is(&buffer_); + std::getline(is, response); + return std::make_pair(true, response); +} diff --git a/client/lib/plugin_wrappers/CMakeLists.txt b/client/lib/plugin_wrappers/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..ac8c58f1347a9ced507c708568595bb6cd213f50 --- /dev/null +++ b/client/lib/plugin_wrappers/CMakeLists.txt @@ -0,0 +1,6 @@ +add_subdirectory(basic) +add_subdirectory(audio) +add_subdirectory(image) +add_subdirectory(sentence) +add_subdirectory(format_processor) +add_subdirectory(word) diff --git a/client/lib/plugin_wrappers/audio/AudioPluginWrapper.cpp b/client/lib/plugin_wrappers/audio/AudioPluginWrapper.cpp new file mode 100644 index 0000000000000000000000000000000000000000..56eb50ec80609d4c5e57c098be891308d1ffd950 --- /dev/null +++ b/client/lib/plugin_wrappers/audio/AudioPluginWrapper.cpp @@ -0,0 +1,40 @@ +#include "AudioPluginWrapper.h" + +#include +#include +#include + +#include + +using namespace nlohmann; + +AudioPluginWrapper::AudioPluginWrapper(std::shared_ptr connection) + : BasicPluginWrapper(std::move(connection), "audios") { +} + +std::pair AudioPluginWrapper::get(const std::string &word, + size_t batch_size, + bool restart) { + json request_message = { + {"query_type", "get" }, + {"plugin_type", plugin_type_}, + {"word", word }, + {"batch_size", batch_size }, + {"restart", restart } + }; + std::pair response( + connection_->request(request_message.dump())); + if (!response.first) { + return {{}, "Server disconnected"}; + } + try { + json response_message = json::parse(response.second); + if (response_message.at("status").get() != 0) { + return {{}, response_message.at("message").get()}; + } + return {response_message.at("result")[0].get(), + response_message.at("result")[1].get()}; + } catch (...) { + return {{}, "Wrong response format: " + response.second}; + } +} diff --git a/client/lib/plugin_wrappers/audio/AudioPluginWrapper.h b/client/lib/plugin_wrappers/audio/AudioPluginWrapper.h new file mode 100644 index 0000000000000000000000000000000000000000..3b7fe90d483375f38b951910ce634edf62bdc025 --- /dev/null +++ b/client/lib/plugin_wrappers/audio/AudioPluginWrapper.h @@ -0,0 +1,19 @@ +#ifndef AUDIOPLUGINWRAPPER_H +#define AUDIOPLUGINWRAPPER_H + +#include +#include +#include + +#include "BasicPluginWrapper.h" +#include "IAudioPluginWrapper.h" + +class AudioPluginWrapper : public BasicPluginWrapper, + virtual public IAudioPluginWrapper { + public: + explicit AudioPluginWrapper(std::shared_ptr connection); + std::pair + get(const std::string &word, size_t batch_size, bool restart) override; +}; + +#endif // AUDIOPLUGINWRAPPER_H diff --git a/client/lib/plugin_wrappers/audio/AudioPluginWrapper_tests.cpp b/client/lib/plugin_wrappers/audio/AudioPluginWrapper_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..79124608d1d36e679ab6d44116116b9135770f36 --- /dev/null +++ b/client/lib/plugin_wrappers/audio/AudioPluginWrapper_tests.cpp @@ -0,0 +1,74 @@ +#include "AudioPluginWrapper.h" +#include "mock_classes.h" + +#include +#include +#include + +#include +#include + +using namespace nlohmann; + +TEST(AudioPWGet, Output) { + auto memorizer = std::make_shared(); + AudioPluginWrapper wrapper(memorizer); + wrapper.get("test_word", 5, true); + + json actual = json::parse(memorizer->received_message); + json expected = { + {"query_type", "get" }, + {"plugin_type", "audios" }, + {"word", "test_word"}, + {"batch_size", 5 }, + {"restart", true } + }; + EXPECT_EQ(expected, actual); +} + +// TEST(AudioPWGet, PartialSuccess) { +// json answer = { +// {"status", 0 }, +// {"result", +// json::array({{"link_1", "info_1"}, +// {"link_2", "info_2"}, +// {"link_3", "info_3"}})}, +// {"message", "something" } +// }; +// // std::string answer = R"({ "status":0, +// // +// "result":[{"link_1":"info_1"},{"link_2":"info_2"},{"link_3":"info_3"}], +// // "message":"null"})"; +// auto fixed_answer = std::make_shared(answer.dump()); +// AudioPluginWrapper wrapper(fixed_answer); +// +// std::pair actual = +// wrapper.get("test_word", 3, true); +// std::pair expected = { +// audio_vector{ +// {"link_1", "info_1"}, {"link_2", "info_2"}, {"link_3", +// "info_3"}}, +// "something" +// }; +// EXPECT_EQ(expected, actual); +// } + +TEST(AudioPWGet, Error) { + json answer = { + {"status", 1 }, + {"result", "null" }, + {"message", "something"} + }; + auto fixed_answer = std::make_shared(answer.dump()); + AudioPluginWrapper wrapper(fixed_answer); + + std::pair actual = wrapper.get("test_word", 3, true); + std::pair expected = {{}, "something"}; + EXPECT_EQ(expected, actual); +} + +// TEST(AudioPWGet, WrongResponseFormat) { +// } +// +// TEST(AudioPWGet, Disconnect) { +// } diff --git a/client/lib/plugin_wrappers/audio/CMakeLists.txt b/client/lib/plugin_wrappers/audio/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..ead70df11e17a4e8b561ae83710405a2f32fff93 --- /dev/null +++ b/client/lib/plugin_wrappers/audio/CMakeLists.txt @@ -0,0 +1,15 @@ +message(${CMAKE_CURRENT_SOURCE_DIR}) + +add_library(audio_plugin_wrapper AudioPluginWrapper.cpp) +target_link_libraries(audio_plugin_wrapper PUBLIC + card + connection + basic_plugin_wrapper) +target_include_directories(audio_plugin_wrapper PUBLIC ./) + +add_executable(audio_plugin_wrapper_test AudioPluginWrapper_tests.cpp) +target_link_libraries(audio_plugin_wrapper_test PRIVATE + GTest::gtest_main + basic_plugin_wrapper + audio_plugin_wrapper) +add_test(audio_plugin_wrapper_test audio_plugin_wrapper_test) diff --git a/client/lib/plugin_wrappers/audio/IAudioPluginWrapper.h b/client/lib/plugin_wrappers/audio/IAudioPluginWrapper.h new file mode 100644 index 0000000000000000000000000000000000000000..7b4a7d7c820111804606c7f0a7984a55257e1ca9 --- /dev/null +++ b/client/lib/plugin_wrappers/audio/IAudioPluginWrapper.h @@ -0,0 +1,15 @@ +#ifndef IAUDIOPLUGINWRAPPER_H +#define IAUDIOPLUGINWRAPPER_H + +#include +#include + +#include "IBasicPluginWrapper.h" +#include "Media.h" + +struct IAudioPluginWrapper : virtual public IBasicPluginWrapper { + virtual std::pair + get(const std::string &word, size_t batch_size, bool restart) = 0; +}; + +#endif // IAUDIOPLUGINWRAPPER_H diff --git a/client/lib/plugin_wrappers/basic/BasicPluginWrapper.cpp b/client/lib/plugin_wrappers/basic/BasicPluginWrapper.cpp new file mode 100644 index 0000000000000000000000000000000000000000..3a687cca8f532191e7106a915223ba86c707cbf5 --- /dev/null +++ b/client/lib/plugin_wrappers/basic/BasicPluginWrapper.cpp @@ -0,0 +1,158 @@ +#include "BasicPluginWrapper.h" + +#include +#include +#include +#include + +#include + +using namespace nlohmann; + +BasicPluginWrapper::BasicPluginWrapper(std::shared_ptr connection, + std::string plugin_type) + : connection_(std::move(connection)), plugin_type_(std::move(plugin_type)) { +} + +std::string BasicPluginWrapper::init(const std::string &plugin_name) { + json request_message = { + {"query_type", "init" }, + {"plugin_type", plugin_type_}, + {"plugin_name", plugin_name } + }; + std::pair response( + connection_->request(request_message.dump())); + if (!response.first) + return "Server disconnected"; + try { + json response_message = json::parse(response.second); + if (response_message.at("status").get() != 0) + return response_message.at("message").get(); + return {}; + } catch (...) { + return "Wrong response format: " + response.second; + } +} + +std::pair BasicPluginWrapper::get_default_config() { + json request_message = { + {"query_type", "get_default_config"}, + {"plugin_type", plugin_type_ } + }; + std::pair response( + connection_->request(request_message.dump())); + if (!response.first) + return {"", "Server disconnected"}; + try { + json response_message = json::parse(response.second); + if (response_message.at("status").get() != 0) + return {"", response_message.at("message").get()}; + return {response_message.at("result")[0].dump(2), + response_message.at("result")[1].get()}; + } catch (...) { + return {"", "Wrong response format: " + response.second}; + } +} + +std::pair BasicPluginWrapper::get_default_scheme() { + json request_message = { + {"query_type", "get_default_scheme"}, + {"plugin_type", plugin_type_ } + }; + std::pair response( + connection_->request(request_message.dump())); + if (!response.first) + return {"", "Server disconnected"}; + try { + json response_message = json::parse(response.second); + if (response_message.at("status").get() != 0) + return {"", response_message.at("message").get()}; + return {response_message.at("result")[0].dump(2), + response_message.at("result")[1].get()}; + } catch (...) { + return {"", "Wrong response format: " + response.second}; + } +} + +std::pair, std::string> +BasicPluginWrapper::set_config(const std::string &new_config) { + json config; + try { + config = json::parse(new_config); + } catch (...) { + return {{}, "Wrong input format"}; + } + json request_message = { + {"query_type", "set_config"}, + {"plugin_type", plugin_type_}, + {"query", config } + }; + std::pair response( + connection_->request(request_message.dump())); + if (!response.first) + return {{}, "Server disconnected"}; + try { + json response_message = json::parse(response.second); + if (response_message.at("status").get() != 0) + return {response_message.at("result") + .get>(), + ""}; + return {{}, ""}; + } catch (...) { + return {{}, "Wrong response format: " + response.second}; + } +} + +std::pair BasicPluginWrapper::list_plugins() { + json request_message = { + {"query_type", "list_plugins"}, + {"plugin_type", plugin_type_ } + }; + std::pair response( + connection_->request(request_message.dump())); + if (!response.first) + return {{}, "Server disconnected"}; + try { + json response_message = json::parse(response.second); + if (response_message.at("status").get() != 0) + return {{}, response_message.at("message").get()}; + return { + LoadResult{response_message.at("result") + .at("success") + .get>(), + response_message.at("result") + .at("failed") + .get>()}, + "" + }; + } catch (...) { + return {{}, "Wrong response format: " + response.second}; + } +} + +std::pair BasicPluginWrapper::load_new_plugins() { + json request_message = { + {"query_type", "load_new_plugins"}, + {"plugin_type", plugin_type_ } + }; + std::pair response( + connection_->request(request_message.dump())); + if (!response.first) + return {{}, "Server disconnected"}; + try { + json response_message = json::parse(response.second); + if (response_message.at("status").get() != 0) + return {{}, response_message.at("message").get()}; + return { + LoadResult{response_message.at("result") + .at("success") + .get>(), + response_message.at("result") + .at("failed") + .get>()}, + "" + }; + } catch (...) { + return {{}, "Wrong response format: " + response.second}; + } +} diff --git a/client/lib/plugin_wrappers/basic/BasicPluginWrapper.h b/client/lib/plugin_wrappers/basic/BasicPluginWrapper.h new file mode 100644 index 0000000000000000000000000000000000000000..09a919f311b43d8a0cde657b2a7334f774bc736b --- /dev/null +++ b/client/lib/plugin_wrappers/basic/BasicPluginWrapper.h @@ -0,0 +1,29 @@ +#ifndef BASICPLUGINWRAPPER_H +#define BASICPLUGINWRAPPER_H + +#include +#include +#include +#include + +#include "IBasicPluginWrapper.h" +#include "IRequestable.h" + +class BasicPluginWrapper : virtual public IBasicPluginWrapper { + public: + BasicPluginWrapper(std::shared_ptr connection, + std::string plugin_type); + std::string init(const std::string &plugin_name) override; + std::pair get_default_config() override; + std::pair get_default_scheme() override; + std::pair, std::string> + set_config(const std::string &new_config) override; + std::pair list_plugins() override; + std::pair load_new_plugins() override; + + protected: + std::shared_ptr connection_; + std::string plugin_type_; +}; + +#endif // BASICPLUGINWRAPPER_H diff --git a/client/lib/plugin_wrappers/basic/CMakeLists.txt b/client/lib/plugin_wrappers/basic/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..70d35b15b1607ab18ff1001d8c4a13e8a7699926 --- /dev/null +++ b/client/lib/plugin_wrappers/basic/CMakeLists.txt @@ -0,0 +1,7 @@ +message(${CMAKE_CURRENT_SOURCE_DIR}) + +add_library(basic_plugin_wrapper BasicPluginWrapper.cpp) +target_link_libraries(basic_plugin_wrapper PUBLIC card connection) +target_include_directories(basic_plugin_wrapper PUBLIC ./) + +add_subdirectory(tests) diff --git a/client/lib/plugin_wrappers/basic/IBasicPluginWrapper.h b/client/lib/plugin_wrappers/basic/IBasicPluginWrapper.h new file mode 100644 index 0000000000000000000000000000000000000000..c7626db7049f30f6ccf588c083dbc7279e91294e --- /dev/null +++ b/client/lib/plugin_wrappers/basic/IBasicPluginWrapper.h @@ -0,0 +1,28 @@ +#ifndef IBASICPLUGINWRAPPER_H +#define IBASICPLUGINWRAPPER_H + +#include +#include +#include + +struct LoadResult { + std::vector success; + std::vector fail; +}; + +inline bool operator==(const LoadResult &lhs, const LoadResult &rhs) { + return lhs.success == rhs.success && lhs.fail == rhs.fail; +} + +struct IBasicPluginWrapper { + virtual ~IBasicPluginWrapper() = default; + virtual std::string init(const std::string &plugin_name) = 0; + virtual std::pair get_default_config() = 0; + virtual std::pair get_default_scheme() = 0; + virtual std::pair, std::string> + set_config(const std::string &new_config) = 0; + virtual std::pair list_plugins() = 0; + virtual std::pair load_new_plugins() = 0; +}; + +#endif // IBASICPLUGINWRAPPER_H diff --git a/client/lib/plugin_wrappers/basic/tests/CMakeLists.txt b/client/lib/plugin_wrappers/basic/tests/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..b8fa7540cef638cfed5b69a1e6d765ef5ccde69c --- /dev/null +++ b/client/lib/plugin_wrappers/basic/tests/CMakeLists.txt @@ -0,0 +1,10 @@ +add_executable(basic_plugin_wrapper_test + init_tests.cpp + get_default_config_tests.cpp + set_config_tests.cpp) + +target_link_libraries(basic_plugin_wrapper_test PRIVATE + GTest::gtest_main + basic_plugin_wrapper) + +add_test(basic_plugin_wrapper_test basic_plugin_wrapper_test) diff --git a/client/lib/plugin_wrappers/basic/tests/get_default_config_tests.cpp b/client/lib/plugin_wrappers/basic/tests/get_default_config_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..00b5d2dd6b3642f506c2c8a1d33753a8cf8ade23 --- /dev/null +++ b/client/lib/plugin_wrappers/basic/tests/get_default_config_tests.cpp @@ -0,0 +1,65 @@ +#include "BasicPluginWrapper.h" +#include "mock_classes.h" + +#include +#include + +#include +#include + +using namespace nlohmann; + +TEST(BasicPWGetDefaultConfig, Output) { + auto memorizer = std::make_shared(); + BasicPluginWrapper wrapper(memorizer, "tests"); + wrapper.get_default_config(); + + json actual = json::parse(memorizer->received_message); + json expected = { + {"query_type", "get_default_config"}, + {"plugin_type", "tests" } + }; + EXPECT_EQ(expected, actual); +} + +TEST(BasicPWGetDefaultConfig, PartialSuccess) { + json answer = { + {"status", 0 }, + {"result", + json::array({json::object({{"language", "english"}, {"level", "C2"}}), + "something"})} + }; + auto fixed_answer = std::make_shared(answer.dump()); + BasicPluginWrapper wrapper(fixed_answer, "tests"); + + std::pair actual = { + json::parse(wrapper.get_default_config().first), + wrapper.get_default_config().second}; + std::pair expected = { + {{"language", "english"}, {"level", "C2"}}, + "something" + }; + + EXPECT_EQ(expected, actual); +} + +TEST(BasicPWGetDefaultConfig, Error) { + json answer = { + {"status", 1 }, + {"response", "null" }, + {"message", "something"} + }; + auto fixed_answer = std::make_shared(answer.dump()); + BasicPluginWrapper wrapper(fixed_answer, "tests"); + + std::pair actual = wrapper.get_default_config(); + std::pair expected = {"", "something"}; + + EXPECT_EQ(actual, expected); +} + +// TEST(BasicPWGetDefaultConfig, WrongResponseFormat) { +// } +// +// TEST(BasicPWGetDefaultConfig, Disconnect) { +// } diff --git a/client/lib/plugin_wrappers/basic/tests/init_tests.cpp b/client/lib/plugin_wrappers/basic/tests/init_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..f837a82c10f248f02bdee9482f9aa104e64ab4da --- /dev/null +++ b/client/lib/plugin_wrappers/basic/tests/init_tests.cpp @@ -0,0 +1,65 @@ +#include "BasicPluginWrapper.h" +#include "mock_classes.h" + +#include +#include + +#include +#include + +using namespace nlohmann; + +TEST(BasicPWInitTest, Output) { + auto memorizer = std::make_shared(); + BasicPluginWrapper wrapper(memorizer, "tests"); + wrapper.init("test_name"); + + json actual = json::parse(memorizer->received_message); + json expected = { + {"query_type", "init" }, + {"plugin_type", "tests" }, + {"plugin_name", "test_name"} + }; + EXPECT_EQ(expected, actual); +} + +TEST(BasicPWInit, Success) { + std::string answer = R"({ "status" : 0, "message" : "null"})"; + auto fixed_answer = std::make_shared(answer); + BasicPluginWrapper wrapper(fixed_answer, "tests"); + + EXPECT_TRUE(wrapper.init("exiting_plugin").empty()); +} + +TEST(BasicPWInit, Error) { + std::string answer = R"({ "status" : 1, "message" : "something" })"; + auto fixed_answer = std::make_shared(answer); + BasicPluginWrapper wrapper(fixed_answer, "tests"); + + std::string actual = wrapper.init("nonexiten_plugin"); + std::string expected = "something"; + + EXPECT_EQ(actual, expected); +} + +TEST(BasicPWInit, WrongResponseFormat) { + std::string answer = R"({ "status" : "ok", "message" : "something" })"; + auto fixed_answer = std::make_shared(answer); + BasicPluginWrapper wrapper(fixed_answer, "tests"); + + std::string actual = wrapper.init("nonexiten_plugin"); + std::string expected = "Wrong response format: " + answer; + + EXPECT_EQ(actual, expected); +} + +TEST(BasicPWInit, Disconnect) { + std::string answer = R"({ "status" : "0", "message" : "something" })"; + auto fixed_answer = std::make_shared(answer, false); + BasicPluginWrapper wrapper(fixed_answer, "tests"); + + std::string actual = wrapper.init("nonexiten_plugin"); + std::string expected = "Server disconnected"; + + EXPECT_EQ(actual, expected); +} diff --git a/client/lib/plugin_wrappers/basic/tests/set_config_tests.cpp b/client/lib/plugin_wrappers/basic/tests/set_config_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..2a45622c59d30f2b0ae80cee48bee7d95969a57a --- /dev/null +++ b/client/lib/plugin_wrappers/basic/tests/set_config_tests.cpp @@ -0,0 +1,76 @@ +#include "BasicPluginWrapper.h" +#include "mock_classes.h" + +#include +#include + +#include +#include + +using namespace nlohmann; + +TEST(BasicPWSetConfig, Output) { + auto memorizer = std::make_shared(); + BasicPluginWrapper wrapper(memorizer, "tests"); + wrapper.set_config(R"({ "language" : "english", "level" : "C2" })"); + + json actual = json::parse(memorizer->received_message); + json expected = { + {"query_type", "set_config" }, + {"plugin_type", "tests" }, + {"query", {{"language", "english"}, {"level", "C2"}}} + }; + EXPECT_EQ(expected, actual); +} + +TEST(BasicPWSetConfig, WrongInput) { + auto memorizer = std::make_shared(); + BasicPluginWrapper wrapper(memorizer, "tests"); + + std::pair, std::string> actual = + wrapper.set_config("not a json"); + std::pair, std::string> expected = { + {}, "Wrong input format"}; + + EXPECT_EQ(expected, actual); +} + +TEST(BasicPWSetConfig, Success) { + json answer = { + {"status", 0 }, + {"result", "null"} + }; + auto fixed_answer = std::make_shared(answer.dump()); + BasicPluginWrapper wrapper(fixed_answer, "tests"); + + std::pair, std::string> actual = + wrapper.set_config(R"({ "language" : "english", "level" : "C2" })"); + std::pair, std::string> expected = {{}, + ""}; + + EXPECT_EQ(expected, actual); +} + +TEST(BasicPWSetConfig, Error) { + json answer = { + {"status", 1}, + {"result",{{"field_name", "error_type"}, {"field_name2", "error_type2"}}} + }; + auto fixed_answer = std::make_shared(answer.dump()); + BasicPluginWrapper wrapper(fixed_answer, "tests"); + + std::pair, std::string> actual = + wrapper.set_config(R"({ "language" : "english"})"); + std::pair, std::string> expected = { + {{"field_name", "error_type"}, {"field_name2", "error_type2"}}, + "" + }; + + EXPECT_EQ(actual, expected); +} + +// TEST(BasicPWSetConfig, WrongResponseFormat) { +// } +// +// TEST(BasicPWSetConfig, Disconnect) { +// } \ No newline at end of file diff --git a/client/lib/plugin_wrappers/common/CMakeLists.txt b/client/lib/plugin_wrappers/common/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..bf9b84902baf728ef549091f6dab220a6827526b --- /dev/null +++ b/client/lib/plugin_wrappers/common/CMakeLists.txt @@ -0,0 +1,4 @@ +message(${CMAKE_CURRENT_SOURCE_DIR}) + +add_library(common INTERFACE) +target_include_directories(common INTERFACE ./) \ No newline at end of file diff --git a/client/lib/plugin_wrappers/common/Card.h b/client/lib/plugin_wrappers/common/Card.h new file mode 100644 index 0000000000000000000000000000000000000000..e68fbb87bf7dd07cc4faf5cbbc791e0618a4660b --- /dev/null +++ b/client/lib/plugin_wrappers/common/Card.h @@ -0,0 +1,39 @@ +#ifndef CARD_H +#define CARD_H + +#include +#include + +#include + +struct Card { + std::string word; + std::vector special; + std::string definition; + std::vector examples; + std::vector image_links; + std::vector audio_links; + nlohmann::json tags; + nlohmann::json other; +}; + +inline void from_json(const nlohmann::json &j, Card &card) { + j.at("word").get_to(card.word); + j.at("special").get_to(card.special); + j.at("definition").get_to(card.definition); + j.at("examples").get_to(card.examples); + j.at("image_links").get_to(card.image_links); + j.at("audio_links").get_to(card.audio_links); + j.at("tags").get_to(card.tags); + j.at("other").get_to(card.other); +} + +inline bool operator==(const Card &lhs, const Card &rhs) { + return lhs.word == rhs.word && lhs.special == rhs.special && + lhs.definition == rhs.definition && lhs.examples == rhs.examples && + lhs.image_links == rhs.image_links && + lhs.audio_links == rhs.audio_links && lhs.tags == rhs.tags && + lhs.other == rhs.other; +} + +#endif // CARD_H diff --git a/client/lib/plugin_wrappers/common/LoadResult.h b/client/lib/plugin_wrappers/common/LoadResult.h new file mode 100644 index 0000000000000000000000000000000000000000..31b6a8999f32ef215e357315eedb375b5fdb8e4d --- /dev/null +++ b/client/lib/plugin_wrappers/common/LoadResult.h @@ -0,0 +1,16 @@ +#ifndef LOADRESULT_H +#define LOADRESULT_H + +#include +#include + +struct LoadResult { + std::vector success; + std::vector fail; +}; + +inline bool operator==(const LoadResult &lhs, const LoadResult &rhs) { + return lhs.success == rhs.success && lhs.fail == rhs.fail; +} + +#endif // LOADRESULT_H diff --git a/client/lib/plugin_wrappers/format_processor/CMakeLists.txt b/client/lib/plugin_wrappers/format_processor/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..34a46b90bc46ad073ffc88522c217e5a08d51dcf --- /dev/null +++ b/client/lib/plugin_wrappers/format_processor/CMakeLists.txt @@ -0,0 +1,14 @@ +message(${CMAKE_CURRENT_SOURCE_DIR}) + +add_library(format_processor_plugin_wrapper FormatProcessorPluginWrapper.cpp) +target_link_libraries(format_processor_plugin_wrapper PUBLIC + card + connection + basic_plugin_wrapper) +target_include_directories(format_processor_plugin_wrapper PUBLIC ./) + +add_executable(format_processor_plugin_wrapper_test FormatProcessorPluginWrapper_tests.cpp) +target_link_libraries(format_processor_plugin_wrapper_test PRIVATE + GTest::gtest_main + format_processor_plugin_wrapper) +add_test(format_processor_plugin_wrapper_test format_processor_plugin_wrapper_test) diff --git a/client/lib/plugin_wrappers/format_processor/FormatProcessorPluginWrapper.cpp b/client/lib/plugin_wrappers/format_processor/FormatProcessorPluginWrapper.cpp new file mode 100644 index 0000000000000000000000000000000000000000..028bee8ca266fbbfff102e1e2ea7d3c3158fbeb8 --- /dev/null +++ b/client/lib/plugin_wrappers/format_processor/FormatProcessorPluginWrapper.cpp @@ -0,0 +1,35 @@ +#include "FormatProcessorPluginWrapper.h" + +#include +#include + +#include "IRequestable.h" + +#include + +using namespace nlohmann; + +FormatProcessorPluginWrapper::FormatProcessorPluginWrapper( + std::shared_ptr connection) + : BasicPluginWrapper(std::move(connection), "format") { +} + +std::string FormatProcessorPluginWrapper::save(const std::string &cards_path) { + json request_message = { + {"query_type", "save" }, + {"cards_path", cards_path} + }; + std::pair response( + connection_->request(request_message.dump())); + if (!response.first) + return "Server disconnected"; + try { + json response_message = json::parse(response.second); + if (response_message.at("status").get() != 0) { + return response_message.at("message").get(); + } + return {}; + } catch (...) { + return "Wrong response format: " + response.second; + } +} diff --git a/client/lib/plugin_wrappers/format_processor/FormatProcessorPluginWrapper.h b/client/lib/plugin_wrappers/format_processor/FormatProcessorPluginWrapper.h new file mode 100644 index 0000000000000000000000000000000000000000..4d6037f4d134e4ede1a2ebe949b0bf760c3ed1f3 --- /dev/null +++ b/client/lib/plugin_wrappers/format_processor/FormatProcessorPluginWrapper.h @@ -0,0 +1,20 @@ +#ifndef FORMATPROCESSORPLUGINWRAPPER_H +#define FORMATPROCESSORPLUGINWRAPPER_H + +#include +#include + +#include "BasicPluginWrapper.h" +#include "IFormatProcessorPluginWrapper.h" +#include "IRequestable.h" + +class FormatProcessorPluginWrapper + : public BasicPluginWrapper, + virtual public IFormatProcessorPluginWrapper { + public: + explicit FormatProcessorPluginWrapper( + std::shared_ptr connection); + std::string save(const std::string &cards_path) override; +}; + +#endif // FORMATPROCESSORPLUGINWRAPPER_H diff --git a/client/lib/plugin_wrappers/format_processor/FormatProcessorPluginWrapper_tests.cpp b/client/lib/plugin_wrappers/format_processor/FormatProcessorPluginWrapper_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..2b4a56f67f79ed8909b6eafbff11aebbf272d37e --- /dev/null +++ b/client/lib/plugin_wrappers/format_processor/FormatProcessorPluginWrapper_tests.cpp @@ -0,0 +1,50 @@ +#include "FormatProcessorPluginWrapper.h" +#include "mock_classes.h" + +#include +#include +#include + +#include + +#include + +using namespace nlohmann; + +TEST(FormatPeocessorPWSave, Output) { + auto memorizer = std::make_shared(); + FormatProcessorPluginWrapper wrapper(memorizer); + wrapper.save("path_1"); + + json actual = json::parse(memorizer->received_message); + json expected = { + {"query_type", "save" }, + {"cards_path", "path_1"} + }; + EXPECT_EQ(expected, actual); +} + +TEST(FormatPeocessorPWSave, Success) { + std::string answer = R"({ "status" : 0, "message" : "null"})"; + auto fixed_answer = std::make_shared(answer); + FormatProcessorPluginWrapper wrapper(fixed_answer); + + EXPECT_TRUE(wrapper.save("path_1").empty()); +} + +TEST(FormatPeocessorPWSave, Error) { + std::string answer = R"({ "status" : 1, "message" : "something" })"; + auto fixed_answer = std::make_shared(answer); + FormatProcessorPluginWrapper wrapper(fixed_answer); + + std::string actual = wrapper.save("path_1"); + std::string expected = "something"; + + EXPECT_EQ(actual, expected); +} + +// TEST(FormatPeocessorPWSave, WrongResponseFormat) { +// } +// +// TEST(FormatPeocessorPWSave, Disconnect) { +// } diff --git a/client/lib/plugin_wrappers/format_processor/IFormatProcessorPluginWrapper.h b/client/lib/plugin_wrappers/format_processor/IFormatProcessorPluginWrapper.h new file mode 100644 index 0000000000000000000000000000000000000000..5e5c40dcc88c6606f0cffbb425ab9771383ae148 --- /dev/null +++ b/client/lib/plugin_wrappers/format_processor/IFormatProcessorPluginWrapper.h @@ -0,0 +1,12 @@ +#ifndef IFORMATPROCESSORPLUGINWRAPPER_H +#define IFORMATPROCESSORPLUGINWRAPPER_H + +#include + +#include "IBasicPluginWrapper.h" + +struct IFormatProcessorPluginWrapper : virtual public IBasicPluginWrapper { + virtual std::string save(const std::string &cards_path) = 0; +}; + +#endif // IFORMATPROCESSORPLUGINWRAPPER_H diff --git a/client/lib/plugin_wrappers/image/CMakeLists.txt b/client/lib/plugin_wrappers/image/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..892ac94caf2a1c1467e3c093d8a7ba64fe7e4165 --- /dev/null +++ b/client/lib/plugin_wrappers/image/CMakeLists.txt @@ -0,0 +1,15 @@ +message(${CMAKE_CURRENT_SOURCE_DIR}) + +add_library(image_plugin_wrapper ImagePluginWrapper.cpp) +target_link_libraries(image_plugin_wrapper PUBLIC + card + connection + basic_plugin_wrapper) +target_include_directories(image_plugin_wrapper PUBLIC ./) + +add_executable(image_plugin_wrapper_test ImagePluginWrapper_tests.cpp) +target_link_libraries(image_plugin_wrapper_test PRIVATE + GTest::gtest_main + basic_plugin_wrapper + image_plugin_wrapper) +add_test(image_plugin_wrapper_test image_plugin_wrapper_test) diff --git a/client/lib/plugin_wrappers/image/IImagePluginWrapper.h b/client/lib/plugin_wrappers/image/IImagePluginWrapper.h new file mode 100644 index 0000000000000000000000000000000000000000..5b16f396f9e49ed4bb6b6e8e7fd55516b145fe08 --- /dev/null +++ b/client/lib/plugin_wrappers/image/IImagePluginWrapper.h @@ -0,0 +1,15 @@ +#ifndef IIMAGEPLUGINWRAPPER_H +#define IIMAGEPLUGINWRAPPER_H + +#include +#include + +#include "IBasicPluginWrapper.h" +#include "Media.h" + +struct IImagePluginWrapper : virtual public IBasicPluginWrapper { + virtual std::pair + get(const std::string &word, size_t batch_size, bool restart) = 0; +}; + +#endif // IIMAGEPLUGINWRAPPER_H diff --git a/client/lib/plugin_wrappers/image/ImagePluginWrapper.cpp b/client/lib/plugin_wrappers/image/ImagePluginWrapper.cpp new file mode 100644 index 0000000000000000000000000000000000000000..75d1641606f93ceb45e4e9f30ec849aff5361cda --- /dev/null +++ b/client/lib/plugin_wrappers/image/ImagePluginWrapper.cpp @@ -0,0 +1,44 @@ +#include "ImagePluginWrapper.h" + +#include +#include +#include +#include + +#include + +using namespace nlohmann; + +ImagePluginWrapper::ImagePluginWrapper(std::shared_ptr connection) + : BasicPluginWrapper(std::move(connection), "images") { +} + +std::pair ImagePluginWrapper::get(const std::string &word, + size_t batch_size, + bool restart) { + json request_message = { + {"query_type", "get" }, + {"plugin_type", plugin_type_}, + {"word", word }, + {"batch_size", batch_size }, + {"restart", restart } + }; + std::pair response( + connection_->request(request_message.dump())); + if (!response.first) { + return {{}, "Server disconnected"}; + } + try { + std::cout << response.second << std::endl; + json response_message = json::parse(response.second); + if (response_message.at("status").get() != 0) { + return {{}, response_message.at("message").get()}; + } + + json links_with_error = response_message.at("result"); + Media links = links_with_error[0]; + return {links, links_with_error[1]}; + } catch (...) { + return {{}, "Wrong response format: " + response.second}; + } +} diff --git a/client/lib/plugin_wrappers/image/ImagePluginWrapper.h b/client/lib/plugin_wrappers/image/ImagePluginWrapper.h new file mode 100644 index 0000000000000000000000000000000000000000..3114bd367b2c486b9b3d492598b5da5eb76e4d3e --- /dev/null +++ b/client/lib/plugin_wrappers/image/ImagePluginWrapper.h @@ -0,0 +1,20 @@ +#ifndef IMAGEPLUGINWRAPPER_H +#define IMAGEPLUGINWRAPPER_H + +#include +#include +#include + +#include "IImagePluginWrapper.h" +#include "BasicPluginWrapper.h" +#include "Media.h" + +class ImagePluginWrapper : public BasicPluginWrapper, + virtual public IImagePluginWrapper { + public: + explicit ImagePluginWrapper(std::shared_ptr connection); + std::pair + get(const std::string &word, size_t batch_size, bool restart) override; +}; + +#endif // IMAGEPLUGINWRAPPER_H diff --git a/client/lib/plugin_wrappers/image/ImagePluginWrapper_tests.cpp b/client/lib/plugin_wrappers/image/ImagePluginWrapper_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..3e81d742e47152ddd23543f12f05c5f69abc57d2 --- /dev/null +++ b/client/lib/plugin_wrappers/image/ImagePluginWrapper_tests.cpp @@ -0,0 +1,111 @@ +#include +#include +#include + +#include +#include + +#include "ImagePluginWrapper.h" +#include "Media.h" +#include "mock_classes.h" + +using namespace nlohmann; + +TEST(ImagePWGet, Output) { + auto memorizer = std::make_shared(); + ImagePluginWrapper wrapper(memorizer); + wrapper.get("test_word", 5, true); + + json actual = json::parse(memorizer->received_message); + json expected = { + {"query_type", "get" }, + {"plugin_type", "images" }, + {"word", "test_word"}, + {"batch_size", 5 }, + {"restart", true } + }; + EXPECT_EQ(expected, actual); +} + +//TEST(ImagePWGet, FullSuccess) { +// json answer = { +// {"status", 0 }, +// {"result", json::array({"link_1", "link_2", "link_3"})}, +// {"message", "" } +// }; +// auto fixed_answer = std::make_shared(answer.dump()); +// ImagePluginWrapper wrapper(fixed_answer); +// +// std::pair actual = wrapper.get("test_word", 3, true); +// std::pair expected = { +// {std::vector{ +// {"link_1", "info_1"}, {"link_2", "info_2"}, {"link_3", "info_3"}}, +// std::vector{ +// {"link_4", "info_4"}, {"link_5", "info_5"}, {"link_6", "info_6"}}}, +// "" +// }; +// EXPECT_EQ(expected, actual); +//} +// +//TEST(ImagePWGet, PartialSuccess) { +// // json answer = { +// // {"status", 0}, +// // {"result", +// // json::object({"web", +// // json::array({{"link_1", "info_1"}, +// // {"link_2", "info_2"}, +// // {"link_3", "info_3"}})}), +// // {"local", +// // json::array({{"link_4", "info_4"}, +// // {"link_5", "info_5"}, +// // {"link_6", "info_6"}})}}, +// // {"message", "something"} +// // }; +// std::string answer = R"({ +// "status": 0, +// "result": { +// "web": [ +// ["link_1", "info_1"], +// ["link_2", "info_2"], +// ["link_3", "info_3"] +// ], +// "local": [ +// ["link_4", "info_4"], +// ["link_5", "info_5"], +// ["link_6", "info_6"]] +// }, +// "message": "something" +// })"; +// auto fixed_answer = std::make_shared(answer); +// ImagePluginWrapper wrapper(fixed_answer); +// +// std::pair actual = wrapper.get("test_word", 3, true); +// std::pair expected = { +// {std::vector{ +// {"link_1", "info_1"}, {"link_2", "info_2"}, {"link_3", "info_3"}}, +// std::vector{ +// {"link_4", "info_4"}, {"link_5", "info_5"}, {"link_6", "info_6"}}}, +// "" +// }; +// EXPECT_EQ(expected, actual); +//} + +TEST(ImagePWGet, Error) { + json answer = { + {"status", 1 }, + {"result", "null" }, + {"message", "something"} + }; + auto fixed_answer = std::make_shared(answer.dump()); + ImagePluginWrapper wrapper(fixed_answer); + + std::pair actual = wrapper.get("test_word", 3, true); + std::pair expected = {{}, "something"}; + EXPECT_EQ(expected, actual); +} + +// TEST(ImagePWGet, WrongResponseFormat) { +// } +// +// TEST(ImagePWGet, Disconnect) { +// } diff --git a/client/lib/plugin_wrappers/sentence/CMakeLists.txt b/client/lib/plugin_wrappers/sentence/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..5e3b68139f21ad401ba39c8bcc58b239ad771118 --- /dev/null +++ b/client/lib/plugin_wrappers/sentence/CMakeLists.txt @@ -0,0 +1,14 @@ +message(${CMAKE_CURRENT_SOURCE_DIR}) + +add_library(sentence_plugin_wrapper SentencePluginWrapper.cpp) +target_link_libraries(sentence_plugin_wrapper PUBLIC + card + connection + basic_plugin_wrapper) +target_include_directories(sentence_plugin_wrapper PUBLIC ./) + +add_executable(sentence_plugin_wrapper_test SentencePluginWrapper_tests.cpp) +target_link_libraries(sentence_plugin_wrapper_test PRIVATE + GTest::gtest_main + sentence_plugin_wrapper) +add_test(sentence_plugin_wrapper_test sentence_plugin_wrapper_test) diff --git a/client/lib/plugin_wrappers/sentence/ISentencePluginWrapper.h b/client/lib/plugin_wrappers/sentence/ISentencePluginWrapper.h new file mode 100644 index 0000000000000000000000000000000000000000..68e5f5fb84aebe60cecd5176c2a585fc15b24c39 --- /dev/null +++ b/client/lib/plugin_wrappers/sentence/ISentencePluginWrapper.h @@ -0,0 +1,14 @@ +#ifndef ISENTENCEPLUGINWRAPPER_H +#define ISENTENCEPLUGINWRAPPER_H + +#include +#include + +#include "IBasicPluginWrapper.h" + +struct ISentencePluginWrapper : virtual public IBasicPluginWrapper { + virtual std::pair, std::string> + get(const std::string &word, size_t batch_size, bool restart) = 0; +}; + +#endif // ISENTENCEPLUGINWRAPPER_H diff --git a/client/lib/plugin_wrappers/sentence/SentencePluginWrapper.cpp b/client/lib/plugin_wrappers/sentence/SentencePluginWrapper.cpp new file mode 100644 index 0000000000000000000000000000000000000000..f48515276523df84ad3fefd92244ade7cd092321 --- /dev/null +++ b/client/lib/plugin_wrappers/sentence/SentencePluginWrapper.cpp @@ -0,0 +1,46 @@ +#include "SentencePluginWrapper.h" + +#include +#include +#include + +#include + +#include "IRequestable.h" + +using namespace nlohmann; + +SentencePluginWrapper::SentencePluginWrapper( + std::shared_ptr connection) + : BasicPluginWrapper(std::move(connection), "sentences") { +} + +std::pair, std::string> +SentencePluginWrapper::get(const std::string &word, + size_t batch_size, + bool restart) { + json request_message = { + {"query_type", "get" }, + {"plugin_type", plugin_type_}, + {"word", word }, + {"batch_size", batch_size }, + {"restart", restart } + }; + std::pair response( + connection_->request(request_message.dump())); + if (!response.first) { + return {std::vector(), "Server disconnected"}; + } + try { + json response_message = json::parse(response.second); + if (response_message.at("status").get() != 0) { + return {std::vector(), + response_message.at("message").get()}; + } + json sentences_with_error = response_message["result"]; + std::vector sentences = sentences_with_error[0]; + return {sentences, sentences_with_error[1]}; + } catch (...) { + return {std::vector(), "Wrong response format: " + response.second}; + } +} diff --git a/client/lib/plugin_wrappers/sentence/SentencePluginWrapper.h b/client/lib/plugin_wrappers/sentence/SentencePluginWrapper.h new file mode 100644 index 0000000000000000000000000000000000000000..a467c2ddc14c91b40e578eb4d5efbe1efe6dd39c --- /dev/null +++ b/client/lib/plugin_wrappers/sentence/SentencePluginWrapper.h @@ -0,0 +1,19 @@ +#ifndef SENTENCEPLUGINWRAPPER_H +#define SENTENCEPLUGINWRAPPER_H + +#include +#include +#include + +#include "BasicPluginWrapper.h" +#include "ISentencePluginWrapper.h" + +class SentencePluginWrapper : public BasicPluginWrapper, + virtual public ISentencePluginWrapper { + public: + explicit SentencePluginWrapper(std::shared_ptr connection); + std::pair, std::string> + get(const std::string &word, size_t batch_size, bool restart) override; +}; + +#endif // SENTENCEPLUGINWRAPPER_H diff --git a/client/lib/plugin_wrappers/sentence/SentencePluginWrapper_tests.cpp b/client/lib/plugin_wrappers/sentence/SentencePluginWrapper_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..67009e3f5c12876642baf15a9446adf69a863a89 --- /dev/null +++ b/client/lib/plugin_wrappers/sentence/SentencePluginWrapper_tests.cpp @@ -0,0 +1,56 @@ +#include "SentencePluginWrapper.h" +#include "mock_classes.h" + +#include +#include +#include + +#include + +#include + +using namespace nlohmann; + +TEST(SentensePWGet, Output) { + auto memorizer = std::make_shared(); + SentencePluginWrapper wrapper(memorizer); + wrapper.get("test_word", 5, true); + + json actual = json::parse(memorizer->received_message); + json expected = { + {"query_type", "get" }, + {"plugin_type", "sentences"}, + {"word", "test_word"}, + {"batch_size", 5 }, + {"restart", true } + }; + EXPECT_EQ(expected, actual); +} + +TEST(SentensePWGet, PartialSuccess) { + json answer = { + {"status", 0 }, + {"result", + json::array({json::array({"sentence_1", "sentence_2", "sentence_3"}), + "something"})} + }; + auto fixed_answer = std::make_shared(answer.dump()); + SentencePluginWrapper wrapper(fixed_answer); + + std::pair, std::string> actual = + wrapper.get("test_word", 3, true); + std::pair, std::string> expected = { + std::vector{"sentence_1", "sentence_2", "sentence_3"}, + "something" + }; + EXPECT_EQ(expected, actual); +} + +// TEST(SentencePWGet, FullSuccess) { +// } +// +// TEST(ImagePWGet, WrongResponseFormat) { +// } +// +// TEST(ImagePWGet, Disconnect) { +// } diff --git a/client/lib/plugin_wrappers/word/CMakeLists.txt b/client/lib/plugin_wrappers/word/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..77d4ef7f9a4432d16cbb74b92175da4fda4671d3 --- /dev/null +++ b/client/lib/plugin_wrappers/word/CMakeLists.txt @@ -0,0 +1,15 @@ +message(${CMAKE_CURRENT_SOURCE_DIR}) + +add_library(word_plugin_wrapper WordPluginWrapper.cpp) +target_link_libraries(word_plugin_wrapper PUBLIC + card + connection + basic_plugin_wrapper) +target_include_directories(word_plugin_wrapper PUBLIC ./) + +add_executable(word_plugin_wrapper_test WordPluginWrapper_tests.cpp) +target_link_libraries(word_plugin_wrapper_test PRIVATE + GTest::gtest_main + card + word_plugin_wrapper) +add_test(word_plugin_wrapper_test word_plugin_wrapper_test) diff --git a/client/lib/plugin_wrappers/word/IWordPluginWrapper.h b/client/lib/plugin_wrappers/word/IWordPluginWrapper.h new file mode 100644 index 0000000000000000000000000000000000000000..9341f72a9132a6654c1be08ab972c20428dfdc72 --- /dev/null +++ b/client/lib/plugin_wrappers/word/IWordPluginWrapper.h @@ -0,0 +1,19 @@ +#ifndef IWORDPLUGINWRAPPER_H +#define IWORDPLUGINWRAPPER_H + +#include + +#include "Card.h" +#include "IBasicPluginWrapper.h" + +class IWordPluginWrapper : virtual public IBasicPluginWrapper { + public: + virtual std::pair, std::string> + get(const std::string &word, + const std::string &query_language, + size_t batch_size, + bool restart) = 0; + virtual std::pair get_dict_scheme() = 0; +}; + +#endif // IWORDPLUGINWRAPPER_H diff --git a/client/lib/plugin_wrappers/word/WordPluginWrapper.cpp b/client/lib/plugin_wrappers/word/WordPluginWrapper.cpp new file mode 100644 index 0000000000000000000000000000000000000000..2f99d375f346c2df53d1ff1d8cf28545a58c159c --- /dev/null +++ b/client/lib/plugin_wrappers/word/WordPluginWrapper.cpp @@ -0,0 +1,69 @@ +#include "WordPluginWrapper.h" + +#include +#include + +#include + +#include "Card.h" +#include "IRequestable.h" + +using namespace nlohmann; + +WordPluginWrapper::WordPluginWrapper(std::shared_ptr connection) + : BasicPluginWrapper(std::move(connection), "word") { +} + +std::pair, std::string> +WordPluginWrapper::get(const std::string &word, + const std::string &query_language, + size_t batch_size, + bool restart) { + json request_message = { + {"query_type", "get" }, + {"plugin_type", plugin_type_ }, + {"word", word }, + {"filter", query_language}, + {"batch_size", batch_size }, + {"restart", restart } + }; + std::pair response( + connection_->request(request_message.dump())); + if (!response.first) + return {{}, "Server disconnected"}; + try { + json response_message = json::parse(response.second); + if (response_message.at("status").get() != 0) { + return {std::vector(), + response_message.at("message").get()}; + } + json cards_with_error = response_message["result"]; + std::vector cards = cards_with_error[0]; + + return {cards, cards_with_error[1]}; + } catch (...) { + return {{}, "Wrong response format: " + response.second}; + } +} + +std::pair WordPluginWrapper::get_dict_scheme() { + json request_message = { + {"query_type", "get_dict_scheme"}, + {"plugin_type", plugin_type_ } + }; + std::pair response( + connection_->request(request_message.dump())); + if (!response.first) { + return {"", "Server disconnected"}; + } + try { + json response_message = json::parse(response.second); + if (response_message.at("status").get() != 0) { + return {"", response_message.at("message").get()}; + } + return {response_message.at("result")[0].dump(2), + response_message.at("result")[1].get()}; + } catch (...) { + return {"", "Wrong response format: " + response.second}; + } +} diff --git a/client/lib/plugin_wrappers/word/WordPluginWrapper.h b/client/lib/plugin_wrappers/word/WordPluginWrapper.h new file mode 100644 index 0000000000000000000000000000000000000000..67226af3c66f7075a7366c70687e40e287829dc0 --- /dev/null +++ b/client/lib/plugin_wrappers/word/WordPluginWrapper.h @@ -0,0 +1,25 @@ +#ifndef WORDPLUGINWRAPPER_H +#define WORDPLUGINWRAPPER_H + +#include +#include +#include + +#include "BasicPluginWrapper.h" +#include "Card.h" +#include "IRequestable.h" +#include "IWordPluginWrapper.h" + +class WordPluginWrapper : public BasicPluginWrapper, + virtual public IWordPluginWrapper { + public: + explicit WordPluginWrapper(std::shared_ptr connection); + std::pair, std::string> + get(const std::string &word, + const std::string &query_language, + size_t batch_size, + bool restart) override; + std::pair get_dict_scheme() override; +}; + +#endif // WORDPLUGINWRAPPER_H diff --git a/client/lib/plugin_wrappers/word/WordPluginWrapper_tests.cpp b/client/lib/plugin_wrappers/word/WordPluginWrapper_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..27f87600bfdabfd3712324b68347b71cdf487ebb --- /dev/null +++ b/client/lib/plugin_wrappers/word/WordPluginWrapper_tests.cpp @@ -0,0 +1,116 @@ +#include +#include +#include + +#include "Card.h" +#include "WordPluginWrapper.h" +#include "mock_classes.h" + +#include +#include + +using namespace nlohmann; + +TEST(WordPWGet, Output) { + auto memorizer = std::make_shared(); + WordPluginWrapper wrapper(memorizer); + wrapper.get("test_word", "pos::noun", 3, true); + + json actual = json::parse(memorizer->received_message); + json expected = { + {"query_type", "get" }, + {"plugin_type", "word" }, + {"word", "test_word"}, + {"filter", "pos::noun"}, + {"batch_size", 3 }, + {"restart", true } + }; + EXPECT_EQ(expected, actual); +} + +// TEST(WordPWGet, PartialSuccess) { +// std::string answer = +// R"({"status":0, +// "result":[{ +// "word":"go", +// "special":["something special"], +// "definition":"move", +// "examples":["go somewhere"], +// "image_links":[], +// "audio_links":[], +// "tags":{"tag":"tag"} +// "other":"other" +// }], +// "message":"something"})"; +// auto fixed_answer = std::make_shared(answer); +// WordPluginWrapper wrapper(fixed_answer); +// +// std::pair, std::string> actual = +// wrapper.get("go", "pos::noun", 1, true); +// std::pair, std::string> expected = { +// {Card{"go", +// {"something special"}, +// "move", +// {"go somewhere"}, +// Media(), +// Media(), +// "tag::tag", +// "other"}}, +// "something"}; +// EXPECT_EQ(expected, actual); +// } + +TEST(WordPWGet, Error) { + std::string answer = + R"({"status":1, + "result":"null", + "message":"something"})"; + auto fixed_answer = std::make_shared(answer); + WordPluginWrapper wrapper(fixed_answer); + + std::pair, std::string> actual = + wrapper.get("go", "pos::noun", 1, true); + std::pair, std::string> expected = {{}, "something"}; + EXPECT_EQ(expected, actual); +} + +// TEST(WordPWGet, FullSuccess) { +// } +// +// TEST(WordPWGet, WrongResponseFormat) { +// } +// +// TEST(WordPWGet, Disconnect) { +// } + +// TEST(WordPWOutputTest, GetDictScheme) { +// auto memorizer = std::make_shared(); +// WordPluginWrapper wrapper(memorizer); +// EXPECT_THROW(wrapper.get_dict_scheme(), std::runtime_error); +// +// json actual = json::parse(memorizer->received_message); +// json expected = json::parse( +// R"({ "query_type" : "get_dict_scheme" , "plugin_type" : "word" })"); +// EXPECT_EQ(expected, actual); +// } +// TEST(WordPWInputTest, GetDefaultConfigSuccess) { +// std::string answer = +// R"({ "status" : 0, "result" : { "field_1" : "value_1", "field_2" : +// "value_2" }})"; +// auto fixed_answer = std::make_shared(answer); +// WordPluginWrapper wrapper(fixed_answer); +// +// json actual = json::parse(wrapper.get_dict_scheme()); +// json expected = +// json::parse(R"({ "field_1" : "value_1", "field_2" : "value_2" })"); +// EXPECT_EQ(expected, actual); +// } +// +// TEST(WordPWInputTest, GetDefaultConfigFailure) { +// std::string answer = R"({ "status" : 1, "result" : "null" +// })"; auto fixed_answer = +// std::make_shared(answer); WordPluginWrapper +// wrapper(fixed_answer); +// +// EXPECT_THROW(wrapper.get_dict_scheme(), std::runtime_error); +// } diff --git a/plugins/audios/audios/__init__.py b/plugins/audios/audios/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..15b6a64ba0b5ae38cb7e3b6e9c0f6f9f49af0c24 --- /dev/null +++ b/plugins/audios/audios/__init__.py @@ -0,0 +1 @@ +from .main import * diff --git a/plugins/audios/audios/consts.py b/plugins/audios/audios/consts.py new file mode 100644 index 0000000000000000000000000000000000000000..eb4500a09e10da2e0c30df01cf112fec8c9114ae --- /dev/null +++ b/plugins/audios/audios/consts.py @@ -0,0 +1,5 @@ +import os + +_PLUGIN_LOCATION = os.path.dirname(__file__) +_PLUGIN_NAME = os.path.split(_PLUGIN_LOCATION)[-1] +_HEADERS = {'User-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:77.0) Gecko/20100101 Firefox/77.0'} diff --git a/plugins/audios/audios/language_list.py b/plugins/audios/audios/language_list.py new file mode 100644 index 0000000000000000000000000000000000000000..5d0c7136bf8ac21dda0ba2ff284c767d34b93795 --- /dev/null +++ b/plugins/audios/audios/language_list.py @@ -0,0 +1,102 @@ +from .page_processing import get_forvo_page + +#all the languages forvo supports with their language code used in forvo word pages +languages = [ + "Abaza_abq", "Abkhazian_ab", "Adygean_ady", "Afar_aa", "Afrikaans_af", + "Aghul_agx", "Akan_ak", "Albanian_sq", "Algerian Arabic_arq", "Algonquin_alq", + "Amharic_am", "Ancient Greek_grc", "Arabic_ar", "Aragonese_an", "Arapaho_arp", + "Arbëresh_aae", "Armenian_hy", "Aromanian_rup", "Assamese_as", "Assyrian Neo-Aramaic_aii", + "Asturian_ast", "Avaric_av", "Aymara_ay", "Azerbaijani_az", "Bakhtiari_bqi", + "Balochi_bal", "Bambara_bm", "Bardi_bcj", "Bashkir_ba", "Basque_eu", + "Bavarian_bar", "Belarusian_be", "Bemba_bem", "Bench_bcq", "Bengali_bn", + "Biblical Hebrew_hbo", "Bihari_bh", "Bislama_bi", "Bosnian_bs", "Bouyei_pcc", + "Breton_br", "Bulgarian_bg", "Burmese_my", "Burushaski_bsk", "Buryat_bxr", + "Campidanese_sro", "Cantonese_yue", "Cape Verdean Creole_kea", "Catalan_ca", "Cebuano_ceb", + "Central Atlas Tamazight_tzm", "Central Bikol_bcl", "Chamorro_ch", "Changzhou_plig", "Chechen_ce", + "Cherokee_chr", "Chichewa_ny", "Chuvash_cv", "Coptic_cop", "Cornish_kw", + "Corsican_co", "Cree_cr", "Crimean Tatar_crh", "Croatian_hr", "Czech_cs", + "Dagbani_dag", "Danish_da", "Dari_prs", "Divehi_dv", "Dusun_dtp", + "Dutch_nl", "Dzongkha_dz", "Edo_bin", "Egyptian Arabic_arz", "Emilian_egl", + "English_en", "Erzya_myv", "Esperanto_eo", "Estonian_et", "Etruscan_ett", + "Ewe_ee", "Ewondo_ewo", "Faroese_fo", "Fiji Hindi_hif", "Fijian_fj", + "Finnish_fi", "Flemish_vls", "Franco-Provençal_frp", "French_fr", "Frisian_fy", + "Friulan_fur", "Fulah_ff", "Fuzhou_fzho", "Ga_gaa", "Galician_gl", + "Gan Chinese_gan", "Georgian_ka", "German_de", "Gilaki_glk", "Greek_el", + "Guarani_gn", "Gujarati_gu", "Gulf Arabic_afb", "Gusii_guz", "Haitian Creole_ht", + "Hakka_hak", "Hassaniyya_mey", "Hausa_ha", "Hawaiian_haw", "Hebrew_he", + "Herero_hz", "Hiligaynon_hil", "Hindi_hi", "Hmong_hmn", "Hungarian_hu", + "Icelandic_is", "Igbo_ig", "Iloko_ilo", "Indonesian_ind", "Ingush_inh", + "Interlingua_ia", "Inuktitut_iu", "Irish_ga", "Italian_it", "Iwaidja_ibd", + "Jamaican Patois_jam", "Japanese_ja", "Javanese_jv", "Jeju_jje", "Jiaoliao Mandarin_jliu", + "Jin Chinese_cjy", "Judeo-Spanish_lad", "Kabardian_kbd", "Kabyle_kab", "Kalaallisut_kl", + "Kalenjin_kln", "Kalmyk_xal", "Kannada_kn", "Karachay-Balkar_krc", "Karakalpak_kaa", + "Kashmiri_ks", "Kashubian_csb", "Kazakh_kk", "Khasi_kha", "Khmer_km", + "Kikuyu_ki", "Kimbundu_kmb", "Kinyarwanda_rw", "Kirundi_rn", "Klingon_tlh", + "Komi_kv", "Konkani_gom", "Korean_ko", "Kotava_avk", "Krio_kri", + "Kurdish_ku", "Kurmanji_kmr", "Kutchi_kfr", "Kyrgyz_ky", "Ladin_lld", + "Lakota_lkt", "Lao_lo", "Latgalian_ltg", "Latin_la", "Latvian_lv", + "Laz_lzz", "Lezgian_lez", "Ligurian_lij", "Limburgish_li", "Lingala_ln", + "Lithuanian_lt", "Lombard_lmo", "Louisiana Creole_lou", "Low German_nds", "Lower Yangtze Mandarin_juai", + "Lozi_loz", "Luganda_lg", "Luo_luo", "Lushootseed_lut", "Luxembourgish_lb", + "Macedonian_mk", "Mainfränkisch_vmf", "Malagasy_mg", "Malay_ms", "Malayalam_ml", + "Maltese_mt", "Manchu_mnc", "Mandarin Chinese_zh", "Mansi_mns", "Manx_gv", + "Māori_mi", "Mapudungun_arn", "Marathi_mr", "Mari_chm", "Marshallese_mh", + "Masbateño_msb", "Mauritian Creole_mfe", "Mazandarani_mzn", "Mbe_mfo", "Mennonite Low German_pdt", + "Micmac_mic", "Middle Chinese_ltc", "Middle English_enm", "Min Dong_cdo", "Min Nan_nan", + "Minangkabau_min", "Mingrelian_xmf", "Minjaee Luri_lrc", "Mohawk_moh", "Moksha_mdf", + "Moldovan_mo", "Mongolian_mn", "Moroccan Arabic_ary", "Nahuatl_nah", "Naskapi_nsk", + "Navajo_nv", "Naxi_nxq", "Ndonga_ng", "Neapolitan_nap", "Nepal Bhasa_new", + "Nepali_ne", "Nogai_nog", "North Levantine Arabic_apc", "Northern Sami_sme", "Norwegian_no", + "Norwegian Nynorsk_nn", "Nuosu_ii", "Nǀuu_ngh", "Occitan_oc", "Ojibwa_oj", + "Okinawan_ryu", "Old English_ang", "Old Norse_non", "Old Turkic_otk", "Oriya_or", + "Oromo_om", "Ossetian_os", "Ottoman Turkish_ota", "Palauan_pau", "Palenquero_pln", + "Pali_pi", "Pangasinan_pag", "Papiamento_pap", "Pashto_ps", "Pennsylvania Dutch_pdc", + "Persian_fa", "Picard_pcd", "Piedmontese_pms", "Pitjantjatjara_pjt", "Polish_pl", + "Portuguese_pt", "Pu-Xian Min_cpx", "Pulaar_fuc", "Punjabi_pa", "Quechua_qu", + "Quenya_qya", "Quiatoni Zapotec_zpf", "Rapa Nui_rap", "Reunionese Creole_rcf", "Romagnol_rgn", + "Romani_rom", "Romanian_ro", "Romansh_rm", "Rukiga_cgg", "Russian_ru", + "Rusyn_rue", "Samoan_sm", "Sango_sg", "Sanskrit_sa", "Saraiki_skr", + "Sardinian_sc", "Scots_sco", "Scottish Gaelic_gd", "Seediq_trv", "Serbian_sr", + "Shanghainese_jusi", "Shilha_shi", "Shona_sn", "Siberian Tatar_sty", "Sicilian_scn", + "Silesian_szl", "Silesian German_sli", "Sindhi_sd", "Sinhalese_si", "Slovak_sk", + "Slovenian_sl", "Somali_so", "Soninke_snk", "Sotho_st", "Southwestern Mandarin_xghu", + "Spanish_es", "Sranan Tongo_srn", "Sundanese_su", "Swabian German_swg", "Swahili_sw", + "Swati_ss", "Swedish_sv", "Swiss German_gsw", "Sylheti_syl", "Tagalog_tl", + "Tahitian_ty", "Tajik_tg", "Talossan_tzl", "Talysh_tly", "Tamil_ta", + "Tatar_tt", "Telugu_te", "Thai_th", "Tibetan_bo", "Tigrinya_ti", + "Toisanese Cantonese_tisa", "Tok Pisin_tpi", "Toki Pona_x-tp", "Tondano_tdn", "Tongan_to", + "Tswana_tn", "Tunisian Arabic_aeb", "Turkish_tr", "Turkmen_tk", "Tuvan_tyv", + "Twi_tw", "Ubykh_uby", "Udmurt_udm", "Ukrainian_uk", "Upper Saxon_sxu", + "Upper Sorbian_hsb", "Urdu_ur", "Uyghur_ug", "Uzbek_uz", "Venda_ve", + "Venetian_vec", "Vietnamese_vi", "Volapük_vo", "Võro_vro", "Walloon_wa", + "Welsh_cy", "Wenzhounese_qjio", "Wolof_wo", "Wu Chinese_wuu", "Xhosa_xh", + "Xiang Chinese_hsn", "Yakut_sah", "Yeyi_yey", "Yiddish_yi", "Yoruba_yo", + "Yucatec Maya_yua", "Yupik_esu", "Zazaki_zza", "Zhuang_za", "Zulu_zu", + ] + +def getLanguages(listPage): + #TODO: see line 99 + # takes a forvo language list page and returns all the languages with their code (I think forvo uses ISO 639-2) + langList = [] + languagesUl = listPage.select_one("ul.alphabetically") + for languageLi in languagesUl.findChildren("li", recursive=False): #LOL recursive lookup is true by default. Guess it makes sense, but got me confused (lxml habits die hard) + languageName = languageLi.select_one("a").getText() + languageCode = languageLi.select_one("abbr").getText() + langList.append(languageName + "_" + languageCode) + return langList + + +def updateForvoLanguages(): + # probably never needed, but useful if forvo adds extra languages + languageList = [] + # You can't get all pagination numbers displayed on 1 page, I check page 1 and go up untill the page returns None (404) + pageNumber = 1 + while(True): #TODO: Forvo has a single page with all languages and lang-codes. Use that instead of this + page, error_message = get_forvo_page("https://forvo.com/languages/alphabetically/" + "page-" + str(pageNumber), 1) + if page is None: + break + print("fetching languages from page: " + str(pageNumber)) + languageList.extend(getLanguages(page)) + pageNumber += 1 + print(languageList) + return languageList diff --git a/plugins/audios/audios/main.py b/plugins/audios/audios/main.py new file mode 100644 index 0000000000000000000000000000000000000000..17ccf55c5ad53a58181ddb9e5ac7911ba94bc83e --- /dev/null +++ b/plugins/audios/audios/main.py @@ -0,0 +1,182 @@ +""" +Credits: + https://github.com/Rascalov/Anki-Simple-Forvo-Audio +""" + + +import re +from typing import Any, Literal, TypedDict, Union + +import requests.utils + +from .consts import _PLUGIN_LOCATION, _PLUGIN_NAME +from .page_processing import get_audio_link, get_forvo_page + +CACHED_RESULT = {} + +REMOVE_SPACES_PATTERN = re.compile(r"\s+", re.MULTILINE) + + +def remove_spaces(string: str) -> str: + return re.sub(REMOVE_SPACES_PATTERN, " ", string.strip()) + + +def get(word: str): + global CACHED_RESULT + + word_with_lang_code = "{} {}".format(word, "en") + + if (audioListLis := CACHED_RESULT.get(word_with_lang_code)) is None: + wordEncoded = requests.utils.requote_uri(word) + forvoPage, error_message = get_forvo_page( + "https://forvo.com/word/" + wordEncoded + ) + if error_message: + return [], error_message + speachSections = forvoPage.select("div#language-container-" + "en") + if not len(speachSections): + return ( + [], + f"[{_PLUGIN_NAME}] Word not found (Language Container does not exist!)", + ) + speachSections = forvoPage.select_one("div#language-container-" + "en") + audioListUl = speachSections.select_one("ul") + if audioListUl is None or not len( + audioListUl.findChildren(recursive=False) + ): + return ( + [], + f"[{_PLUGIN_NAME}] Word not found (Language Container exists, but audio not found)", + ) + # if config["language_code"] == "en": + audioListLis = forvoPage.select("li[class*=en_]") + # else: + # audioListLis = audioListUl.find_all("li") + + if audioListLis: + CACHED_RESULT[word_with_lang_code] = audioListLis + + audio_batch: list[tuple[str, str]] = [] + batch_size = yield + for li in audioListLis: + if (r := li.find("div")) is not None and ( + onclick := r.get("onclick") + ) is not None: + audio_link = get_audio_link(onclick) + by_whom_data = li.find("span", {"class": "info"}) + by_whom_data = ( + remove_spaces(by_whom_data.text) + if by_whom_data is not None + else "" + ) + from_data = li.find("span", {"class": "from"}) + from_data = ( + remove_spaces(from_data.text) if from_data is not None else "" + ) + additional_info = ( + (f"{by_whom_data}\n{from_data}") + if from_data is not None + else "" + ) + audio_batch.append((audio_link, additional_info)) + if len(audio_batch) == batch_size: + batch_size = yield {"web": audio_batch, "local": []}, "" + audio_batch = [] + return audio_batch, "" + + +def load(): + return + + +def unload(): + return + + +ConfigKeys = Literal["audio region", "timeout"] +AUDIO_REGION = "audio region" +TIMEOUT = "timeout" + + +class ConfigFieldInfo(TypedDict): + docs: str + type: str + + +ConfigDescription = dict[ + ConfigKeys, Union[ConfigFieldInfo, "ConfigDescription"] +] + + +def get_config_description() -> ConfigDescription: + return { + AUDIO_REGION: { + "docs": "Current audio region", + "type": "string", + }, + TIMEOUT: {"docs": "Request timeout", "type": "number"}, + } + + +def get_default_config() -> dict[ConfigKeys, Any]: + return {AUDIO_REGION: "us", TIMEOUT: 3} + + +class ErrorSummary(TypedDict): + error_type: Literal[ + "invalid_type", "invalid_value", "empty", "unknown_field" + ] + description: str + + +SetConfigError = dict[str, Union[ErrorSummary, "SetConfigError"]] + + +def validate_config(new_config: dict) -> SetConfigError: + res: SetConfigError = {} + + if (new_audio_region := new_config.get(AUDIO_REGION)) is not None: + if type(new_audio_region) != str: + res[AUDIO_REGION] = { + "error_type": "invalid_type", + "description": f"`{AUDIO_REGION}` is expected to be a string", + } + elif new_audio_region not in ["uk", "us"]: + res[AUDIO_REGION] = { + "error_type": "invalid_value", + "description": f'`{AUDIO_REGION}` is expected to be one of: ["uk", "us"]', + } + else: + res[AUDIO_REGION] = { + "error_type": "empty", + "description": f"`{AUDIO_REGION}` was left empty", + } + + if (new_timeout := new_config.get(TIMEOUT)) is not None: + if type(new_timeout) not in [float, int]: + res[TIMEOUT] = { + "error_type": "invalid_type", + "description": f"`{TIMEOUT}` is expected to be a number. Got `{new_timeout}`", + } + elif new_timeout < 0: + res[TIMEOUT] = { + "error_type": "invalid_value", + "description": f"`{TIMEOUT}` is expected to be non negative", + } + + else: + res[TIMEOUT] = { + "error_type": "empty", + "description": "`timeout` was left empty", + } + + conf_keys = [AUDIO_REGION, TIMEOUT] + for key in new_config: + if key in conf_keys: + continue + res[key] = { + "error_type": "unknown_field", + "description": f"Unknown field: `{key}`", + } + + return res diff --git a/plugins/audios/audios/page_processing.py b/plugins/audios/audios/page_processing.py new file mode 100644 index 0000000000000000000000000000000000000000..602b55cc3145216b3d189878d6fcc7743e13b6f8 --- /dev/null +++ b/plugins/audios/audios/page_processing.py @@ -0,0 +1,42 @@ +import base64 +from typing import Optional + +import requests +from bs4 import BeautifulSoup + +from .consts import _HEADERS, _PLUGIN_NAME + + +def get_forvo_page(url: str, timeout: int = 1) -> tuple[Optional[BeautifulSoup], str]: + try: + r = requests.get(url, headers=_HEADERS) + r.raise_for_status() + decoded_page_content = r.content.decode('UTF-8') + except requests.RequestException as e: + return None, f"[{_PLUGIN_NAME}] Couldn't get web page! Error: {str(e)}" + except UnicodeDecodeError as e: + return None, f"[{_PLUGIN_NAME}] Couldn't decode page to UTF-8 format! Error: {str(e)}" + + soup = BeautifulSoup(decoded_page_content, "html.parser") + return soup, "" + + +def get_audio_link(onclickFunction) -> str: + #example js play functions from forvo: + #Play(6166435,'OTg4MTIyMC8xMzgvOTg4MTIyMF8xMzhfMzM5MDIxLm1wMw==','OTg4MTIyMC8xMzgvOTg4MTIyMF8xMzhfMzM5MDIxLm9nZw==',false,'by80L280Xzk4ODEyMjBfMTM4XzMzOTAyMS5tcDM=','by80L280Xzk4ODEyMjBfMTM4XzMzOTAyMS5vZ2c=','h');return false; + #Play(6687207,'OTU5NzcxMy8xMzgvOTU5NzcxM18xMzhfNjk2MDEyMi5tcDM=','OTU5NzcxMy8xMzgvOTU5NzcxM18xMzhfNjk2MDEyMi5vZ2c=',false,'','','l');return false; + # All audios have an ogg version as a fallback on the mp3. Ogg is open source and compresses audio to a smaller size than mp3. + # So I grab the ogg base64 string and decode it. + # + # Ogg doesn't work properly with playsound on Windows :( + # Ogg index - 2; MP3 index - 1 + base64audio = onclickFunction.split(',')[1].replace('\'', "") + decodedLink = base64.b64decode(base64audio.encode('ascii')).decode('ascii') + return "https://audio00.forvo.com/mp3/" + decodedLink + + +def get_forvo_audio_link(audioLi) -> str: + #selector = CSSSelector("span") + audioTag = audioLi.select_one("span") + audioLink = get_audio_link(audioTag["onclick"]) + return audioLink \ No newline at end of file diff --git a/plugins/definitions/definitions/__init__.py b/plugins/definitions/definitions/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..2201cffa7e14be4ddd2485114abd88f5c6ad6281 --- /dev/null +++ b/plugins/definitions/definitions/__init__.py @@ -0,0 +1 @@ +from .definitions_provider import * diff --git a/plugins/definitions/definitions/definitions_provider.py b/plugins/definitions/definitions/definitions_provider.py new file mode 100644 index 0000000000000000000000000000000000000000..167242ce6285edf8b08373c798c6d16f421931b4 --- /dev/null +++ b/plugins/definitions/definitions/definitions_provider.py @@ -0,0 +1,217 @@ +import os +from typing import Any, Union + +from .utils import RESULT_FORMAT, Literal, TypedDict +from .utils import define as _define + + +def translate(definitons_data: RESULT_FORMAT): + audio_region_field = "UK_audio_links" + word_list = [] + + for word, pos_lists in definitons_data.items(): + for pos_data in pos_lists: + pos = pos_data["POS"] + pos_fields = pos_data["data"] + + for ( + definition, + definition_translation, + examples, + domain, + level, + region, + usage, + image, + alt_terms, + irreg_forms, + region_audio_links, + ) in zip( + pos_fields["definitions"], + pos_fields["definitions_translations"], + pos_fields["examples"], + pos_fields["domains"], + pos_fields["levels"], + pos_fields["regions"], + pos_fields["usages"], + pos_fields["image_links"], + pos_fields["alt_terms"], + pos_fields["irregular_forms"], + pos_fields[audio_region_field], + ): # type: ignore + current_word_dict = { + "word": word.strip(), + "special": irreg_forms + alt_terms, + "definition": f"{definition_translation}\n{definition}" + if definition_translation + else definition, + "examples": examples, + "audios": { + "web": [(link, "") for link in region_audio_links], + "local": [], + }, + "images": { + "web": [(image, "")] if image else [], + "local": [], + }, + "other": [], + "tags": { + "domain": domain, + "region": region, + "usage": usage, + "pos": pos, + }, + } + if level: + current_word_dict["tags"]["level"] = level + + word_list.append(current_word_dict) + return word_list + + +def get_dict_scheme(): + return { + "special": { + "docs": "additional informatio provided", + "type": "list[string]", + }, + "tags": { + "docs": "Dictionary tags", + "children": { + "pos": {"docs": "[P]art [O]f [S]peach", "type": "string"}, + "level": { + "docs": "English proficiency level. One of [A1, A2, B1, B2, C1, C2]", + "type": "string", + }, + "usage": { + "docs": "How current word is used", + "type": "list[string]", + }, + "domain": { + "docs": "Domain in which word is used", + "type": "list[string]", + }, + "region": { + "docs": "Region where word is used", + "type": "list[string]", + }, + }, + }, + } + + +def get(word: str): + batch_size = yield + definitions, error = _define( + word, + ) + cards = translate(definitions) + i = 0 + while i < len(cards): + res_batch = cards[i : i + batch_size] + i += batch_size + batch_size = yield res_batch, error + return + + +def load(): + return + + +def unload(): + return + + +ConfigKeys = Literal["audio region", "timeout"] +AUDIO_REGION = "audio region" +TIMEOUT = "timeout" + + +class ConfigFieldInfo(TypedDict): + docs: str + type: str + + +ConfigDescription = dict[ + ConfigKeys, Union[ConfigFieldInfo, "ConfigDescription"] +] + + +def get_config_description() -> ConfigDescription: + return { + AUDIO_REGION: { + "docs": "Current audio region", + "type": "string", + }, + TIMEOUT: {"docs": "Request timeout", "type": "number"}, + } + + +def get_default_config() -> dict[ConfigKeys, Any]: + return {AUDIO_REGION: "us", TIMEOUT: 3} + + +class ErrorSummary(TypedDict): + error_type: Literal[ + "invalid_type", "invalid_value", "empty", "unknown_field" + ] + description: str + + +SetConfigError = dict[str, Union[ErrorSummary, "SetConfigError"]] + + +def validate_config(new_config: dict) -> SetConfigError: + res: SetConfigError = {} + + if (new_audio_region := new_config.get(AUDIO_REGION)) is not None: + if type(new_audio_region) != str: + res[AUDIO_REGION] = { + "error_type": "invalid_type", + "description": f"`{AUDIO_REGION}` is expected to be a string", + } + elif new_audio_region not in ["uk", "us"]: + res[AUDIO_REGION] = { + "error_type": "invalid_value", + "description": f'`{AUDIO_REGION}` is expected to be one of: ["uk", "us"]', + } + else: + res[AUDIO_REGION] = { + "error_type": "empty", + "description": f"`{AUDIO_REGION}` was left empty", + } + + if (new_timeout := new_config.get(TIMEOUT)) is not None: + if type(new_timeout) not in [float, int]: + res[TIMEOUT] = { + "error_type": "invalid_type", + "description": f"`{TIMEOUT}` is expected to be a number. Got `{new_timeout}`", + } + elif new_timeout < 0: + res[TIMEOUT] = { + "error_type": "invalid_value", + "description": f"`{TIMEOUT}` is expected to be non negative", + } + + else: + res[TIMEOUT] = { + "error_type": "empty", + "description": "`timeout` was left empty", + } + + conf_keys = [AUDIO_REGION, TIMEOUT] + for key in new_config: + if key in conf_keys: + continue + res[key] = { + "error_type": "unknown_field", + "description": f"Unknown field: `{key}`", + } + + return res + + +if __name__ == "__main__": + a = get("test") + next(a) + qwerwq = a.send(5) diff --git a/plugins/definitions/definitions/utils.py b/plugins/definitions/definitions/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..f98854421a63400b16edd364f4d1b402fa2e0691 --- /dev/null +++ b/plugins/definitions/definitions/utils.py @@ -0,0 +1,674 @@ +import re +from enum import IntEnum, auto +from typing import Literal, Optional, TypedDict + +import bs4 +import requests + +DEFAULT_REQUESTS_HEADERS = { + "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64)" +} +LINK_PREFIX = "https://dictionary.cambridge.org" + +DEFINITION_T = str +DEFINITION_TRANSLATION_T = str +IMAGE_LINK_T = str +LEVEL_T = str +UK_IPA_T = list[str] +UK_AUDIO_LINKS_T = list[str] +US_IPA_T = list[str] +US_AUDIO_LINKS_T = list[str] +ALT_TERMS_T = list[str] +DOMAINS_T = list[str] +EXAMPLES_T = list[str] +EXAMPLES_TRANSLATIONS_T = list[str] +IRREGULAR_FORMS_T = list[str] +LABELS_AND_CODES_T = list[str] +REGIONS_T = list[str] +USAGES_T = list[str] + +WORD_T = str +POS_T = list[str] + + +class POSFields(TypedDict): + UK_IPA: list[UK_IPA_T] + UK_audio_links: list[UK_AUDIO_LINKS_T] + US_IPA: list[US_IPA_T] + US_audio_links: list[US_AUDIO_LINKS_T] + alt_terms: list[ALT_TERMS_T] + definitions: list[DEFINITION_T] + definitions_translations: list[DEFINITION_TRANSLATION_T] + domains: list[DOMAINS_T] + examples: list[EXAMPLES_T] + examples_translations: list[EXAMPLES_TRANSLATIONS_T] + image_links: list[IMAGE_LINK_T] + irregular_forms: list[IRREGULAR_FORMS_T] + labels_and_codes: list[LABELS_AND_CODES_T] + levels: list[LEVEL_T] + regions: list[REGIONS_T] + usages: list[USAGES_T] + + +class POSData(TypedDict): + POS: POS_T + data: POSFields + + +RESULT_FORMAT = dict[WORD_T, list[POSData]] + + +BilingualVariations = Literal[ + "", + "dutch", + "french", + "german", + "indonesian", + "italian", + "japanese", + "norwegian", + "polish", + "portuguese", + "spanish", + "arabic", + "catalan", + "chinese-simplified", + "chinese-traditional", + "czech", + "danish", + "korean", + "malay", + "russian", + "thai", + "turkish", + "ukrainian", + "vietnamese", +] + + +class DictionaryVariation(IntEnum): + English = 0 + American = auto() + Business = auto() + + +def get_tags( + tags_section: Optional[bs4.Tag], +) -> tuple[LEVEL_T, LABELS_AND_CODES_T, REGIONS_T, USAGES_T, DOMAINS_T]: + def find_tag(html_tag: str, params: dict) -> str: + nonlocal tags_section + + if tags_section is None: + return "" + + found_tag = tags_section.find(html_tag, params) + if found_tag is None: + return "" + + tag_grandparent = found_tag.parent.parent.get("class") + # var - var dvar; group - inf-group dinfg + if not any("var" in x or "group" in x for x in tag_grandparent): + tag_text = found_tag.text + return tag_text + return "" + + def find_all_tags(html_tag: str, params: dict) -> list[str]: + nonlocal tags_section + + tags: list[str] = [] + if tags_section is None: + return tags + + found_tags = tags_section.find_all(html_tag, params) + for tag in found_tags: + tag_grandparent = tag.parent.parent.get("class") + # var - var dvar; group - inf-group dinfg + if not any("var" in x or "group" in x for x in tag_grandparent): + tag_text = tag.text.strip() + if tag_text: + tags.append(tag_text) + return tags + + level = find_tag("span", {"class": "epp-xref"}) + labels_and_codes = find_all_tags("span", {"class": "gram dgram"}) + region = find_all_tags("span", {"class": "region dregion"}) + usage = find_all_tags("span", {"class": "usage dusage"}) + domain = find_all_tags("span", {"class": "domain ddomain"}) + return level, labels_and_codes, region, usage, domain + + +def get_phonetics( + header_block: Optional[bs4.Tag], dictionary_index: DictionaryVariation +) -> tuple[UK_IPA_T, US_IPA_T, UK_AUDIO_LINKS_T, US_AUDIO_LINKS_T]: + uk_ipa: UK_IPA_T = [] + us_ipa: US_IPA_T = [] + uk_audio_links: UK_AUDIO_LINKS_T = [] + us_audio_links: US_AUDIO_LINKS_T = [] + if header_block is None: + return uk_ipa, us_ipa, uk_audio_links, us_audio_links + + audio_block = header_block.find_all("span", {"class": "daud"}) + for daud in audio_block: + parent_class = [item.lower() for item in daud.parent.get("class")] + audio_source = daud.find("source") + if audio_source is None: + continue + audio_source_link = audio_source.get("src") + if not audio_source_link: # None or empty + continue + + result_audio_link = f"{LINK_PREFIX}/{audio_source_link}" + if "uk" in parent_class: + uk_audio_links.append(result_audio_link) + elif "us" in parent_class: + us_audio_links.append(result_audio_link) + + if dictionary_index == DictionaryVariation.English: + ipa = header_block.find_all("span", {"class": "pron dpron"}) + + prev_ipa_parrent: str = "" + for child in ipa: + ipa_parent = child.parent.get("class") + + if ipa_parent is None: + ipa_parent = prev_ipa_parrent + else: + prev_ipa_parrent = ipa_parent + + if "uk" in ipa_parent: + uk_ipa.append(child.text) + elif "us" in ipa_parent: + us_ipa.append(child.text) + else: + # Cambridge has different ways of adding IPA to american and english dictionaries + uk_ipa = [] + us_ipa_block = header_block.find("span", {"class": "pron dpron"}) + us_ipa = [us_ipa_block.text] if us_ipa_block is not None else [] + return uk_ipa, us_ipa, uk_audio_links, us_audio_links + + +def concatenate_tags( + tag_section: Optional[bs4.Tag], + global_level: LEVEL_T, + global_labels_and_codes: LABELS_AND_CODES_T, + global_region: REGIONS_T, + global_usage: USAGES_T, + global_domain: DOMAINS_T, +) -> tuple[LEVEL_T, LABELS_AND_CODES_T, REGIONS_T, USAGES_T, DOMAINS_T]: + level, labels_and_codes, region, usage, domain = get_tags(tag_section) + + result_level = level if level else global_level + result_labels_and_codes = global_labels_and_codes + labels_and_codes + result_word_region = global_region + region + result_word_usage = global_usage + usage + result_word_domain = global_domain + domain + return ( + result_level, + result_labels_and_codes, + result_word_region, + result_word_usage, + result_word_domain, + ) + + +BLANKS_REMOVING_PATTERN = re.compile(r"(\s{2,})|(\r\n|\r|\n)+") + + +def update_word_dict( + word_dict: RESULT_FORMAT, + word: Optional[WORD_T] = None, + pos: Optional[POS_T] = None, + definition: Optional[DEFINITION_T] = None, + definition_translation: Optional[DEFINITION_TRANSLATION_T] = None, + alt_terms: Optional[ALT_TERMS_T] = None, + irregular_forms: Optional[IRREGULAR_FORMS_T] = None, + examples: Optional[EXAMPLES_T] = None, + examples_translations: Optional[EXAMPLES_TRANSLATIONS_T] = None, + level: Optional[LEVEL_T] = None, + labels_and_codes: Optional[LABELS_AND_CODES_T] = None, + regions: Optional[REGIONS_T] = None, + usages: Optional[USAGES_T] = None, + domains: Optional[DOMAINS_T] = None, + image_link: Optional[IMAGE_LINK_T] = None, + uk_ipa: Optional[UK_IPA_T] = None, + us_ipa: Optional[US_IPA_T] = None, + uk_audio_links: Optional[UK_AUDIO_LINKS_T] = None, + us_audio_links: Optional[US_AUDIO_LINKS_T] = None, +): + def remove_blanks_from_str(src: str) -> str: + return re.sub(BLANKS_REMOVING_PATTERN, " ", src.strip()) + + def remove_blanks_from_list(src: list[str]) -> list[str]: + return [remove_blanks_from_str(item) for item in src] + + word = remove_blanks_from_str(word) if word is not None else "" + pos = remove_blanks_from_list(pos) if pos is not None else [] + + if word_dict.get(word) is None: + word_dict[word] = [] + + if not word_dict[word] or word_dict[word][-1]["POS"] != pos: + word_dict[word].append( + { + "POS": pos, + "data": { + "definitions": [], + "definitions_translations": [], + "examples": [], + "examples_translations": [], + "UK_IPA": [], + "US_IPA": [], + "UK_audio_links": [], + "US_audio_links": [], + "image_links": [], + "alt_terms": [], + "irregular_forms": [], + "levels": [], + "labels_and_codes": [], + "regions": [], + "usages": [], + "domains": [], + }, + } + ) + + last_appended_data = word_dict[word][-1]["data"] + last_appended_data["definitions"].append( + remove_blanks_from_str(definition.strip(": ")) + if definition is not None + else "" + ) + last_appended_data["definitions_translations"].append( + remove_blanks_from_str(definition_translation) + if definition_translation is not None + else "" + ) + last_appended_data["levels"].append( + remove_blanks_from_str(level) if level is not None else "" + ) + last_appended_data["image_links"].append( + remove_blanks_from_str(image_link) if image_link is not None else "" + ) + last_appended_data["UK_IPA"].append( + remove_blanks_from_list(uk_ipa) if uk_ipa is not None else [] + ) + last_appended_data["US_IPA"].append( + remove_blanks_from_list(us_ipa) if us_ipa is not None else [] + ) + last_appended_data["UK_audio_links"].append( + remove_blanks_from_list(uk_audio_links) + if uk_audio_links is not None + else [] + ) + last_appended_data["US_audio_links"].append( + remove_blanks_from_list(us_audio_links) + if us_audio_links is not None + else [] + ) + last_appended_data["examples"].append( + remove_blanks_from_list(examples) if examples is not None else [] + ) + last_appended_data["examples_translations"].append( + remove_blanks_from_list(examples_translations) + if examples_translations is not None + else [] + ) + last_appended_data["alt_terms"].append( + remove_blanks_from_list(alt_terms) if alt_terms is not None else [] + ) + last_appended_data["irregular_forms"].append( + remove_blanks_from_list(irregular_forms) + if irregular_forms is not None + else [] + ) + last_appended_data["labels_and_codes"].append( + remove_blanks_from_list(labels_and_codes) + if labels_and_codes is not None + else [] + ) + last_appended_data["regions"].append( + remove_blanks_from_list(regions) if regions is not None else [] + ) + last_appended_data["usages"].append( + remove_blanks_from_list(usages) if usages is not None else [] + ) + last_appended_data["domains"].append( + remove_blanks_from_list(domains) if domains is not None else [] + ) + + +def get_irregular_forms( + word_header_block: Optional[bs4.Tag], +) -> IRREGULAR_FORMS_T: + forms: IRREGULAR_FORMS_T = [] + if word_header_block is None: + return forms + + all_irreg_forms_block = word_header_block.find( + "span", {"class": "irreg-infls dinfls"} + ) + if all_irreg_forms_block is None: + return forms + + for irreg_form_block in all_irreg_forms_block: + text = [] + for containing_tag in ( + x for x in irreg_form_block if isinstance(x, bs4.Tag) + ): + tag_class = containing_tag.get("class") + if tag_class is not None and not any( + "dpron" in x for x in tag_class + ): + text.append(containing_tag.text) + if joined_text := " ".join(text): + forms.append(joined_text) + return forms + + +def get_alt_terms(word_header_block: Optional[bs4.Tag]) -> ALT_TERMS_T: + alt_terms: ALT_TERMS_T = [] + if word_header_block is None: + return alt_terms + + var_block = word_header_block.find_all("span", {"class": "var dvar"}) + var_block.extend( + word_header_block.find_all("span", {"class": "spellvar dspellvar"}) + ) + for alt_term in var_block: + alt_terms.append(alt_term.text) + return alt_terms + + +def define( + word: str, + dictionary_index: DictionaryVariation = DictionaryVariation.English, + bilingual_vairation: BilingualVariations = "", + request_headers: Optional[dict] = None, + timeout: float = 5.0, +) -> tuple[RESULT_FORMAT, str]: + """ + dictionary_index: DictionaryVariation + | Ignored if bilingual_vairation != "" + | One of three available english dictionaries: + | * English + | * American + | * Business + | + bilingual_vairation: str + | Type of bilingual dictionary. Empty string specifies monolingual dictionary. + | Note: + | List of available bilingual dictionaries ("BilingualVariations" type) can be easily modified if needed by adding a lowercase "-"-separated name of adding dictionary to it. + | Example: English-Russian bilingual -> russian; English-Chinese (Traditional) -> chinese-traditional + | Available types: + | "dutch" + | "french" + | "german" + | "indonesian" + | "italian" + | "japanese" + | "norwegian" + | "polish" + | "portuguese" + | "spanish" + | "arabic" + | "catalan" + | "chinese-simplified" + | "chinese-traditional" + | "czech" + | "danish" + | "korean" + | "malay" + | "russian" + | "thai" + | "turkish" + | "ukrainian" + | "vietnamese" + """ + if request_headers is None: + request_headers = DEFAULT_REQUESTS_HEADERS + + if bilingual_vairation: + link = f"{LINK_PREFIX}/dictionary/english-{bilingual_vairation}/{word}" + dictionary_index = DictionaryVariation.English + else: + link = f"{LINK_PREFIX}/dictionary/english/{word}" + # will raise error if request_headers are None + try: + page = requests.get(link, headers=request_headers, timeout=timeout) + except: + return {}, "Timeout" + + word_info: RESULT_FORMAT = {} + + soup = bs4.BeautifulSoup(page.content, "html.parser") + # Only english dictionary + # word block which contains definitions for every POS_T. + primal_block = soup.find_all("div", {"class": "pr di superentry"}) + if len(primal_block) <= dictionary_index: + return {}, "" + + main_block = primal_block[dictionary_index].find_all( + "div", {"class": "pr entry-body__el"} + ) + main_block.extend( + primal_block[dictionary_index].find_all("div", {"class": "pv-block"}) + ) + main_block.extend( + primal_block[dictionary_index].find_all( + "div", {"class": "pr idiom-block"} + ) + ) + + for entity in main_block: + header_block = entity.find("span", {"class": "di-info"}) + if header_block is None: + header_block = entity.find("div", {"class": "pos-header dpos-h"}) + pos_alt_terms_list = get_alt_terms(header_block) + pos_irregular_forms_list = get_irregular_forms(header_block) + + parsed_word_block = entity.find("h2", {"class": "headword"}) + if parsed_word_block is None: + parsed_word_block = ( + header_block.find("span", {"class": "hw dhw"}) + if header_block is not None + else None + ) + header_word = ( + parsed_word_block.text if parsed_word_block is not None else "" + ) + + pos_block = ( + header_block.find_all("span", {"class": "pos dpos"}) + if header_block is not None + else [] + ) + pos = [] + i = 0 + while i < len(pos_block): + i_pos = pos_block[i].text + pos.append(i_pos) + if ( + i_pos == "phrasal verb" + ): # after "phrasal verb" goes verb that was + i += 1 # used in a construction of this phrasal verb. We skip it. + i += 1 + uk_ipa, us_ipa, uk_audio_links, us_audio_links = get_phonetics( + header_block, dictionary_index + ) + + # data gathered from the word header + ( + pos_level, + pos_labels_and_codes, + pos_regions, + pos_usages, + pos_domains, + ) = get_tags(header_block) + + for def_and_sent_block in entity.find_all( + "div", {"class": "def-block ddef_block"} + ): + definition: DEFINITION_T = "" + alt_terms: ALT_TERMS_T = [] + irregular_forms: IRREGULAR_FORMS_T = [] + current_word_level: LEVEL_T = "" + current_word_labels_and_codes: LABELS_AND_CODES_T = [] + current_word_regions: REGIONS_T = [] + current_word_usages: USAGES_T = [] + current_word_domains: DOMAINS_T = [] + + current_def_block_word = header_word + + image_section = def_and_sent_block.find("div", {"class": "dimg"}) + image_link = "" + if image_section is not None: + image_link_block = image_section.find("amp-img") + if image_link_block is not None: + image_link = LINK_PREFIX + image_link_block.get("src", "") + + # sentence examples + sentences_and_translation_block = def_and_sent_block.find( + "div", {"class": "def-body ddef_b"} + ) + definition_translation = "" + sentence_blocks = [] + if sentences_and_translation_block is not None: + definition_translation_block = ( + sentences_and_translation_block.find( + lambda tag: tag.name == "span" + and any( + class_attr == "trans" + for class_attr in tag.attrs.get("class", [""]) + ) + ) + ) + definition_translation = ( + definition_translation_block.text + if definition_translation_block is not None + else "" + ) + sentence_blocks = sentences_and_translation_block.find_all( + "div", {"class": "examp dexamp"} + ) + + examples = [] + examples_translations = [] + for item in sentence_blocks: + sent_ex = item.find("span", {"class": "eg deg"}) + sent_translation = item.find( + "span", {"class": "trans dtrans dtrans-se hdb break-cj"} + ) + examples.append(sent_ex.text if sent_ex is not None else "") + examples_translations.append( + sent_translation.text + if sent_translation is not None + else "" + ) + + found_definition_block = def_and_sent_block.find( + "div", {"class": "ddef_h"} + ) + + if found_definition_block is not None: + found_definition_string = found_definition_block.find( + "div", {"class": "def ddef_d db"} + ) + definition = ( + "" + if found_definition_string is None + else found_definition_string.text + ) + + # Gathering specific tags for every word usage + tag_section = found_definition_block.find( + "span", {"class": "def-info ddef-info"} + ) + ( + current_word_level, + current_word_labels_and_codes, + current_word_regions, + current_word_usages, + current_word_domains, + ) = concatenate_tags( + tag_section, + pos_level, + pos_labels_and_codes, + pos_regions, + pos_usages, + pos_domains, + ) + + alt_terms = get_alt_terms(found_definition_block) + irregular_forms = get_irregular_forms(found_definition_block) + + # Phrase-block check + # The reason for this is that on website there are two different tags for phrase-blocks + phrase_block = found_definition_block.find_parent( + "div", {"class": "pr phrase-block dphrase-block"} + ) + if phrase_block is None: + phrase_block = found_definition_block.find_parent( + "div", {"class": "pr phrase-block dphrase-block lmb-25"} + ) + if phrase_block is not None: + phrase_tags_section = phrase_block.find( + "span", {"class": "phrase-info dphrase-info"} + ) + if phrase_tags_section is not None: + alt_terms += get_alt_terms(phrase_tags_section) + irregular_forms += get_irregular_forms( + phrase_tags_section + ) + + ( + current_word_level, + current_word_labels_and_codes, + current_word_regions, + current_word_usages, + current_word_domains, + ) = concatenate_tags( + phrase_tags_section, + current_word_level, + current_word_labels_and_codes, + current_word_regions, + current_word_usages, + current_word_domains, + ) + current_def_block_word = phrase_block.find( + "span", {"class": "phrase-title dphrase-title"} + ).text + + update_word_dict( + word_info, + word=current_def_block_word, + pos=pos, + definition_translation=definition_translation, + definition=definition, + alt_terms=pos_alt_terms_list + alt_terms, + irregular_forms=irregular_forms + pos_irregular_forms_list, + examples=examples, + examples_translations=examples_translations, + level=current_word_level, + labels_and_codes=current_word_labels_and_codes, + regions=current_word_regions, + usages=current_word_usages, + domains=current_word_domains, + image_link=image_link, + uk_ipa=uk_ipa, + us_ipa=us_ipa, + uk_audio_links=uk_audio_links, + us_audio_links=us_audio_links, + ) + return word_info, "" + + +if __name__ == "__main__": + from pprint import pprint + + pprint( + define( + word="endowing", + dictionary_index=DictionaryVariation.English, + bilingual_vairation="", + ) + ) diff --git a/plugins/format_processors/processor/__init__.py b/plugins/format_processors/processor/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..fc04d33498df17ae6a31ea6481c67d5ce647a7b3 --- /dev/null +++ b/plugins/format_processors/processor/__init__.py @@ -0,0 +1 @@ +from .format_processor import * diff --git a/plugins/format_processors/processor/anki_deck_params.py b/plugins/format_processors/processor/anki_deck_params.py new file mode 100644 index 0000000000000000000000000000000000000000..8df878956966078ede575d9e25991319aa3f6b4c --- /dev/null +++ b/plugins/format_processors/processor/anki_deck_params.py @@ -0,0 +1,79 @@ +import genanki + +MODEL_FIELDS = [ + {"name": "Sentence"}, + {"name": "Word"}, + {"name": "Definition"}, + {"name": "Image"}, + {"name": "Word Audio"}, +] + + +MODEL_CSS = """\ +.card { + font-size: 23px; + text-align: left; + color: black; + background-color: #FFFAF0; + margin: 20px auto 20px auto; + padding: 0 20px 0 20px; + max-width: 600px; +} + +.accent { + font-size: 40px; +} +""" + + +# ONE_SENTENCE_RESULTING_MODEL = genanki.Model( +# 1869993568, # just a random number +# "Mined Sentence Vocab", +# fields=MODEL_FIELDS, +# templates=[ +# { +# "name": "Recognition", +# "qfmt": "{{Sentence}}", +# "afmt": """\ +# {{FrontSide}} +#
+#
+# {{Word}} +#
+# {{Definition}}
+# {{Image}}
+# {{Word Audio}}
+# +# Tags{{#Tags}}|{{/Tags}}{{Tags}} +# """, +# }, +# ], +# css=MODEL_CSS, +# ) + + +# https://ankiweb.net/shared/info/1639213385 +MERGING_RESULTING_MODEL = genanki.Model( + 1607392319, # just a random number + "[Random] Mined Sentence Vocab", + fields=MODEL_FIELDS, + templates=[ + { + "name": "Recognition", + "qfmt": "{{rand-alg:Sentence}}", + "afmt": """\ +{{Sentence}} +
+
+ {{Word}} +
+{{Definition}}
+{{Image}}
+{{Word Audio}}
+ +Tags{{#Tags}}|{{/Tags}}{{Tags}} +""", + }, + ], + css=MODEL_CSS, +) diff --git a/plugins/format_processors/processor/format_processor.py b/plugins/format_processors/processor/format_processor.py new file mode 100644 index 0000000000000000000000000000000000000000..12d09a068f9e6ba7a8290b077974f5849f8f3d33 --- /dev/null +++ b/plugins/format_processors/processor/format_processor.py @@ -0,0 +1,101 @@ +import json +import os +from typing import Literal, TypedDict, Union + +import genanki + +from .anki_deck_params import * +from .utils import * + + +def save(deck_path: str): + if not os.path.exists(deck_path): + return f"given deck path {deck_path} does not exitst" + + with open(deck_path, "r", encoding="utf-8") as f: + res = json.load(f) + + anki_deck_name = os.path.basename(deck_path).split(".", 1)[0] + anki_deck_id = int(str(abs(hash(anki_deck_name)))[:10]) + anki_deck = genanki.Deck(anki_deck_id, anki_deck_name) + + for item in res: + saving_word = item["word"] + special = item["special"] + definition = item["definition"] + examples = item["examples"] + + audios = item["audios"] + local_audios = audios["local"] + web_audios = audios["web"] + + images = item["images"] + local_images = images["local"] + web_images = images["web"] + + tags = item["tags"] + other = item["other"] + + images = " ".join( + [get_card_image_name(name) for name, info in web_images] + ) + audios = " ".join( + [get_card_audio_name(name) for name, info in web_audios] + ) + + sentence_example = " |

".join(examples) + note = genanki.Note( + model=MERGING_RESULTING_MODEL, + # I have no idea why, but if any of the fields is empty, then card won't be added, + fields=[ + sentence_example if sentence_example else " ", + saving_word if saving_word else " ", + definition if definition else " ", + images if images else " ", + audios if audios else " ", + ], + tags=tags, + ) + anki_deck.add_note(note) + + my_package = genanki.Package(anki_deck) + saving_path = deck_path[: deck_path.find(".")] + my_package.write_to_file(f"{saving_path}.apkg") + return "" + + +def load(): + return + + +def unload(): + return + + +def get_config_description(): + return {} + + +def get_default_config(): + return {} + + +class ErrorSummary(TypedDict): + error_type: Literal[ + "invalid_type", "invalid_value", "empty", "unknown_field" + ] + description: str + + +SetConfigError = dict[str, Union[ErrorSummary, "SetConfigError"]] + + +def validate_config(new_config: dict) -> SetConfigError: + res: SetConfigError = {} + for key in new_config: + res[key] = { + "error_type": "unknown_field", + "description": f"Unknown field: `{key}`", + } + + return res diff --git a/plugins/format_processors/processor/utils.py b/plugins/format_processors/processor/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..12706df443edd871c7c6fd67b14460a99cd510e7 --- /dev/null +++ b/plugins/format_processors/processor/utils.py @@ -0,0 +1,54 @@ +import os + + +def remove_special_chars( + text, sep=" ", special_chars="№!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~ " +): + """ + :param text: to to clean + :param sep: replacement for special chars + :param special_chars: special characters to remove + :return: + """ + new_text = "" + start_index = 0 + while start_index < len(text) and text[start_index] in special_chars: + start_index += 1 + + while start_index < len(text): + if text[start_index] in special_chars: + while text[start_index] in special_chars: + start_index += 1 + if start_index >= len(text): + return new_text + new_text += sep + new_text += text[start_index] + start_index += 1 + return new_text.strip(sep) + + +def get_card_image_name(target: str) -> str: + return f"" + # return f"" + + +def get_card_audio_name(target: str) -> str: + return f"[sound:{target}]" + + +def get_save_image_name(target) -> str: + return f"mined-Anki-{remove_special_chars(target, sep='-')}.png" + + +def get_save_audio_name(target, tags) -> str: + word = target.strip().lower() + pos = tags.get("pos") + + raw_audio_name = ( + f"{remove_special_chars(pos, sep='-')}-{remove_special_chars(word, sep='-')}" + if pos is not None + else remove_special_chars(word, sep="-") + ) + + audio_name = f"mined-Anki--{raw_audio_name}.mp3" + return audio_name diff --git a/plugins/images/images/__init__.py b/plugins/images/images/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..82b9edbb6af72bf84645f01490cdb8c5fe7326cd --- /dev/null +++ b/plugins/images/images/__init__.py @@ -0,0 +1 @@ +from .images_provider import * diff --git a/plugins/images/images/images_provider.py b/plugins/images/images/images_provider.py new file mode 100644 index 0000000000000000000000000000000000000000..1237f315496d9959adce74c567ee9c9331dbb0dd --- /dev/null +++ b/plugins/images/images/images_provider.py @@ -0,0 +1,154 @@ +import json +import os +import re +from typing import Any, Literal, TypedDict, Union + +import bs4 +import requests + +PLUGIN_NAME = os.path.split(os.path.dirname(__file__))[-1] + + +def get(word: str): + link = f"https://www.google.com/search?tbm=isch&q={word}" + user_agent = ( + "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/112.0" + ) + headers = {"User-Agent": user_agent} + try: + r = requests.get(link, headers=headers) + r.raise_for_status() + except requests.RequestException: + return [], f"[{PLUGIN_NAME}]: Couldn't get a web page!" + + html = r.text + soup = bs4.BeautifulSoup(r.text, "html.parser") + rg_meta = soup.find_all("div", {"class": "rg_meta"}) + metadata = [json.loads(e.text) for e in rg_meta] + results = [d["ou"] for d in metadata] + + batch_size = yield + if not results: + regex = re.escape("AF_initDataCallback({") + regex += r"[^<]*?data:[^<]*?" + r"(\[[^<]+\])" + + for txt in re.findall(regex, html): + data = json.loads(txt) + + try: + for d in data[31][0][12][2]: + try: + results.append(d[1][3][0]) + if not len(results) % batch_size: + batch_size = ( + yield { + "web": [(link, "") for link in results], + "local": [], + }, + "", + ) + results = [] + except Exception as exception: + pass + except Exception as exception: + try: + for d in data[56][1][0][0][1][0]: + try: + results.append(d[0][0]["444383007"][1][3][0]) + if not len(results) % batch_size: + batch_size = ( + yield { + "web": [(link, "") for link in results], + "local": [], + }, + "", + ) + results = [] + except Exception as exception: + pass + except Exception as exception: + pass + return results, "" + + +def load(): + return + + +def unload(): + return + + +ConfigKeys = Literal["timeout"] +TIMEOUT = "timeout" + + +class ConfigFieldInfo(TypedDict): + docs: str + type: str + + +ConfigDescription = dict[ + ConfigKeys, Union[ConfigFieldInfo, "ConfigDescription"] +] + + +def get_config_description() -> ConfigDescription: + return { + TIMEOUT: {"docs": "Request timeout", "type": "number"}, + } + + +def get_default_config() -> dict[ConfigKeys, Any]: + return {TIMEOUT: 3} + + +class ErrorSummary(TypedDict): + error_type: Literal[ + "invalid_type", "invalid_value", "empty", "unknown_field" + ] + description: str + + +SetConfigError = dict[str, Union[ErrorSummary, "SetConfigError"]] + + +def validate_config(new_config: dict) -> SetConfigError: + res: SetConfigError = {} + + if (new_timeout := new_config.get(TIMEOUT)) is not None: + if type(new_timeout) not in [float, int]: + res[TIMEOUT] = { + "error_type": "invalid_type", + "description": f"`{TIMEOUT}` is expected to be a number. Got `{new_timeout}`", + } + elif new_timeout < 0: + res[TIMEOUT] = { + "error_type": "invalid_value", + "description": f"`{TIMEOUT}` is expected to be non negative", + } + + else: + res[TIMEOUT] = { + "error_type": "empty", + "description": "`timeout` was left empty", + } + + conf_keys = [TIMEOUT] + for key in new_config: + if key in conf_keys: + continue + res[key] = { + "error_type": "unknown_field", + "description": f"Unknown field: `{key}`", + } + + return res + + +if __name__ == "__main__": + a = get("word") + next(a) + while True: + res = a.send(1) + print(res) diff --git a/plugins/sentences/sentences/__init__.py b/plugins/sentences/sentences/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f13fae8d489281699931980440a24d34ddf93d52 --- /dev/null +++ b/plugins/sentences/sentences/__init__.py @@ -0,0 +1 @@ +from .sentences_provider import * diff --git a/plugins/sentences/sentences/sentences_provider.py b/plugins/sentences/sentences/sentences_provider.py new file mode 100644 index 0000000000000000000000000000000000000000..e2ba49976118c334e7f6bac71216fbc492f4dda4 --- /dev/null +++ b/plugins/sentences/sentences/sentences_provider.py @@ -0,0 +1,111 @@ +import os +from typing import Any, Literal, TypedDict, Union + +import bs4 +import requests + +FILE_PATH = os.path.split(os.path.dirname(__file__))[-1] + + +def get(word: str): + try: + page = requests.get( + f"https://searchsentences.com/words/{word}-in-a-sentence", + ) + page.raise_for_status() + except requests.RequestException as e: + return [], f"{FILE_PATH} couldn't get a web page: {e}" + + soup = bs4.BeautifulSoup(page.content, "html.parser") + src = soup.find_all("li", {"class": "sentence-row"}) + sentences = [] + for sentence_block in src: + if (sentence := sentence_block.find("span")) is None: + continue + text = sentence.get_text() + if text: + sentences.append(text) + + sentences.sort(key=len) + i = 0 + size = yield + while i < len(sentences): + size = yield sentences[i : i + size], "" + i += size + + return [], "" + + +def load(): + return + + +def unload(): + return + + +ConfigKeys = Literal["timeout"] +TIMEOUT = "timeout" + + +class ConfigFieldInfo(TypedDict): + docs: str + type: str + + +ConfigDescription = dict[ + ConfigKeys, Union[ConfigFieldInfo, "ConfigDescription"] +] + + +def get_config_description() -> ConfigDescription: + return { + TIMEOUT: {"docs": "Request timeout", "type": "number"}, + } + + +def get_default_config() -> dict[ConfigKeys, Any]: + return {TIMEOUT: 3} + + +class ErrorSummary(TypedDict): + error_type: Literal[ + "invalid_type", "invalid_value", "empty", "unknown_field" + ] + description: str + + +SetConfigError = dict[str, Union[ErrorSummary, "SetConfigError"]] + + +def validate_config(new_config: dict) -> SetConfigError: + res: SetConfigError = {} + + if (new_timeout := new_config.get(TIMEOUT)) is not None: + if type(new_timeout) not in [float, int]: + res[TIMEOUT] = { + "error_type": "invalid_type", + "description": f"`{TIMEOUT}` is expected to be a number. Got `{new_timeout}`", + } + elif new_timeout < 0: + res[TIMEOUT] = { + "error_type": "invalid_value", + "description": f"`{TIMEOUT}` is expected to be non negative", + } + + else: + res[TIMEOUT] = { + "error_type": "empty", + "description": "`timeout` was left empty", + } + + conf_keys = [TIMEOUT] + for key in new_config: + if key in conf_keys: + continue + res[key] = { + "error_type": "unknown_field", + "description": f"Unknown field: `{key}`", + } + + return res diff --git a/qt/.gitignore b/qt/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..ee2fffbdb6de8856b24f9295d284cabb8a046ab8 --- /dev/null +++ b/qt/.gitignore @@ -0,0 +1,72 @@ +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.autosave +*.a +*.core +*.moc +*.o +*.obj +*.orig +*.rej +*.so +*.so.* +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +*.rc +/.qmake.cache +/.qmake.stash + +# qtcreator generated files +*.pro.user* + +# xemacs temporary files +*.flc + +# Vim temporary files +.*.swp + +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* + +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe \ No newline at end of file diff --git a/qt/CMakeLists.txt b/qt/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..92385c06ba59254d82923859dd26dec27771af2e --- /dev/null +++ b/qt/CMakeLists.txt @@ -0,0 +1,36 @@ +message(${CMAKE_CURRENT_SOURCE_DIR}) + +cmake_minimum_required(VERSION 3.5) +set(CMAKE_CXX_STANDARD 20) + +message(${CMAKE_CURRENT_SOURCE_DIR}) + +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +find_package(Qt5 COMPONENTS Core REQUIRED) +find_package(Qt5 COMPONENTS Widgets REQUIRED) + +add_subdirectory(Models) +add_subdirectory(Widgets) +add_subdirectory(Downloader) + +add_executable(app +main.cpp +) + +target_link_libraries(app +Qt5::Core +Qt5::Widgets +main_window +sentences_widget +audios_widget +images_widget +connection +sentence_plugin_wrapper +image_plugin_wrapper +audio_plugin_wrapper +player +card +) diff --git a/qt/Downloader/CMakeLists.txt b/qt/Downloader/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..6a7a6adebad81dc53a923ba7eebf2cba3f89cad1 --- /dev/null +++ b/qt/Downloader/CMakeLists.txt @@ -0,0 +1,25 @@ +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +find_package(Qt5 COMPONENTS Widgets REQUIRED) +find_package(Qt5 COMPONENTS Network REQUIRED) + + +set(PROJECT_SOURCES + Downloader.cpp + Downloader.hpp +) + +add_library(downloader ${PROJECT_SOURCES}) + +target_include_directories(downloader PUBLIC ./) + +target_link_libraries(downloader +Qt5::Widgets +Qt5::Network +image_plugin_wrapper +card +) diff --git a/qt/Downloader/Downloader.cpp b/qt/Downloader/Downloader.cpp new file mode 100644 index 0000000000000000000000000000000000000000..d1901da89f67c76fa414754193ad4365e7ae7aec --- /dev/null +++ b/qt/Downloader/Downloader.cpp @@ -0,0 +1,30 @@ +#include "Downloader.hpp" +#include +#include +#include +#include +#include +#include + +Downloader::Downloader(QObject *parent) + : QObject{parent} +{ + manager = new QNetworkAccessManager; + connect(manager, SIGNAL(finished(QNetworkReply*)), + this, SLOT(slotFinished(QNetworkReply*))); +} + +void Downloader::download(const QUrl &url) { + QNetworkRequest request(url); + manager->get(request); +} + +void Downloader::slotFinished(QNetworkReply *reply) { + if (reply->error() != QNetworkReply::NoError) { + emit error(); + } + else { + emit done(reply->url(), reply->readAll()); + } + reply->deleteLater(); +} diff --git a/qt/Downloader/Downloader.hpp b/qt/Downloader/Downloader.hpp new file mode 100644 index 0000000000000000000000000000000000000000..d72b79376895f29f9b16f6977d07ab0c13f9e1ab --- /dev/null +++ b/qt/Downloader/Downloader.hpp @@ -0,0 +1,31 @@ +#ifndef DOWNLOADER_HPP +#define DOWNLOADER_HPP + +#include +#include +#include +#include +#include +#include + +class Downloader : public QObject +{ + Q_OBJECT +public: + explicit Downloader(QObject *parent = nullptr); + +public slots: + void download(const QUrl &url); + +signals: + void done(const QUrl &url, const QByteArray&); + void error(); + +private slots: + void slotFinished(QNetworkReply *reply); + +private: + QNetworkAccessManager *manager; +}; + +#endif // DOWNLOADER_HPP diff --git a/qt/Models/CMakeLists.txt b/qt/Models/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..86dc56073c173a0b977f20519ca6e9d1b817ac94 --- /dev/null +++ b/qt/Models/CMakeLists.txt @@ -0,0 +1,3 @@ +add_subdirectory(WordCards) +add_subdirectory(DeckModel) +add_subdirectory(SavedDeckModel) \ No newline at end of file diff --git a/qt/Models/DeckModel/CMakeLists.txt b/qt/Models/DeckModel/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..d997dfdb8280117dc14f6fe31e66f4a5774694b2 --- /dev/null +++ b/qt/Models/DeckModel/CMakeLists.txt @@ -0,0 +1,28 @@ +message(${CMAKE_CURRENT_SOURCE_DIR}) + +# set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +find_package(Qt5 COMPONENTS Core REQUIRED) +find_package(Qt5 COMPONENTS Widgets REQUIRED) + +add_library(deck_model +Deck.cpp +deck_model.cpp +# Deck.hpp +# deck_model.h +# IDeck.h +) + +target_include_directories(deck_model PUBLIC ./) + +target_link_libraries(deck_model PUBLIC +Qt5::Core +Qt5::Widgets +word_cards +card +word_plugin_wrapper +) diff --git a/qt/Models/DeckModel/Deck.cpp b/qt/Models/DeckModel/Deck.cpp new file mode 100644 index 0000000000000000000000000000000000000000..990b623f8df44fdae732458ab66e35f138be8372 --- /dev/null +++ b/qt/Models/DeckModel/Deck.cpp @@ -0,0 +1,50 @@ +#include "Deck.hpp" +#include "word_cards.h" +#include + +Deck::Deck(std::unique_ptr wordPlugin) { + wordPlugin_ = std::move(wordPlugin); + wordPlugin_->init("definitions"); +} + +size_t Deck::size() const { + return cards_.size(); +} +std::string Deck::getWord(size_t index) const { + return cards_.at(index).get_word(); +} +const Card* Deck::getCard(size_t index) const { + return cards_.at(index).get_card(); +} + +void Deck::next(size_t index) { + cards_.at(index).next(); +} + +void Deck::prev(size_t index) { + cards_.at(index).prev(); +} + +void Deck::load(std::string word, std::string query) { + std::vector wordCards; + std::string error; + tie(wordCards, error) = wordPlugin_->get(word, query, batch_size, false); + int i = indexOfWord(word); + if (i == -1) { + if (!wordCards.empty()) { + cards_.push_back(WordCards(word, wordCards)); + } + } + else { + cards_.at(i).addCards(wordCards); + } +} + +int Deck::indexOfWord(const std::string &word) const { + for (int i = 0; i < cards_.size(); ++i) { + if (cards_.at(i).get_word() == word) { + return i; + } + } + return -1; +} diff --git a/qt/Models/DeckModel/Deck.hpp b/qt/Models/DeckModel/Deck.hpp new file mode 100644 index 0000000000000000000000000000000000000000..0818ae22a4d08687f6745da9e5f4f3baa0dac645 --- /dev/null +++ b/qt/Models/DeckModel/Deck.hpp @@ -0,0 +1,30 @@ +#ifndef DECK_H +#define DECK_H + +#include "word_cards.h" +#include "IDeck.h" +#include "IWordPluginWrapper.h" +#include +#include +#include + +class Deck: public IDeck { +public: + Deck(std::unique_ptr wordPlugin); + ~Deck() {std::cout << "Destructor of deck" << std::endl;} + size_t size() const; + std::string getWord(size_t index) const; + const Card* getCard(size_t index) const; + void next(size_t index); + void prev(size_t index); + void load(std::string word, std::string query); + + virtual int indexOfWord(const std::string &word) const; + +private: + const size_t batch_size = 20; + std::vector cards_; + std::unique_ptr wordPlugin_; +}; + +#endif // DECK_H \ No newline at end of file diff --git a/qt/Models/DeckModel/IDeck.h b/qt/Models/DeckModel/IDeck.h new file mode 100644 index 0000000000000000000000000000000000000000..aeb2a196c2698171d59f2453bb236d855f2fd7f1 --- /dev/null +++ b/qt/Models/DeckModel/IDeck.h @@ -0,0 +1,19 @@ +#ifndef IDECK_H +#define IDECK_H + +#include +#include "Card.h" + +class IDeck { +public: + virtual ~IDeck() = default; + virtual size_t size() const = 0; + virtual std::string getWord(size_t index) const = 0; + virtual const Card* getCard(size_t index) const = 0; + virtual void next(size_t index) = 0; + virtual void prev(size_t index) = 0; + virtual void load(std::string word, std::string query) = 0; + virtual int indexOfWord(const std::string &word) const = 0; +}; + +#endif // IDECK_H diff --git a/qt/Models/DeckModel/deck_model.cpp b/qt/Models/DeckModel/deck_model.cpp new file mode 100644 index 0000000000000000000000000000000000000000..44df0af6bc3f9432bca8086937b00f201f9186e9 --- /dev/null +++ b/qt/Models/DeckModel/deck_model.cpp @@ -0,0 +1,68 @@ +#include "deck_model.h" +#include +#include + +DeckModel::DeckModel(std::unique_ptr deck, QObject *parent) + : QAbstractListModel(parent) +{ + deck_ = std::move(deck); +} + +int DeckModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + return deck_->size(); +} + +QVariant DeckModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + switch (role) + { + case Qt::DisplayRole: + return QString::fromStdString(deck_->getWord(index.row())); + case CardRole: + { + Card* card = const_cast(deck_->getCard(index.row())); + void* card_void = static_cast(card); + return QVariant::fromValue(card_void); + } + default: + return QVariant(); + } + return QVariant(); +} + +int DeckModel::indexOfWord(const QString &word) +{ + return deck_->indexOfWord(word.toStdString()); +} + +QModelIndex DeckModel::index(int row, int column, const QModelIndex &parent) const +{ + if (row < 0 || row >= rowCount() || column != 0) + { + return QModelIndex(); + } + return createIndex(row, 0); +} + +void DeckModel::load(const QString &word, QString query) +{ + beginResetModel(); + deck_->load(word.toStdString(), query.toStdString()); + endResetModel(); +} + +void DeckModel::next(const QModelIndex &index) +{ + deck_->next(index.row()); +} + +void DeckModel::prev(const QModelIndex &index) +{ + deck_->prev(index.row()); +} diff --git a/qt/Models/DeckModel/deck_model.h b/qt/Models/DeckModel/deck_model.h new file mode 100644 index 0000000000000000000000000000000000000000..8019b55d94c86ccd49b763652f54a55fb223007f --- /dev/null +++ b/qt/Models/DeckModel/deck_model.h @@ -0,0 +1,38 @@ +#ifndef DECKMODEL_H +#define DECKMODEL_H + +#include +#include +#include +#include "IDeck.h" +#include "word_cards.h" + +class DeckModel : public QAbstractListModel +{ + Q_OBJECT + +public: + enum DeckRoles { + CardRole = Qt::UserRole + 1 + }; + + explicit DeckModel(std::unique_ptr deck, QObject *parent = nullptr); + DeckModel(const DeckModel& other) = delete; + + // Basic functionality: + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + int indexOfWord(const QString& word); + QModelIndex index(int row, int column = 0, const QModelIndex &parent = QModelIndex()) const override; + +public slots: + void load(const QString& word, QString query); + void next(const QModelIndex& index); + void prev(const QModelIndex& index); + +public: + std::unique_ptr deck_; +}; + +#endif // DECKMODEL_H diff --git a/qt/Models/SavedDeckModel/CMakeLists.txt b/qt/Models/SavedDeckModel/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..5dedffe48fb24c35dff4c8a0f1345bf0df38cf22 --- /dev/null +++ b/qt/Models/SavedDeckModel/CMakeLists.txt @@ -0,0 +1,22 @@ +message(${CMAKE_CURRENT_SOURCE_DIR}) + +# set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +find_package(Qt5 COMPONENTS Core REQUIRED) +find_package(Qt5 COMPONENTS Widgets REQUIRED) + +add_library(saved_deck_model +SavedDeckModel.cpp +) + +target_include_directories(saved_deck_model PUBLIC ./) + +target_link_libraries(saved_deck_model PUBLIC +Qt5::Core +Qt5::Widgets +card +) diff --git a/qt/Models/SavedDeckModel/SavedDeckModel.cpp b/qt/Models/SavedDeckModel/SavedDeckModel.cpp new file mode 100644 index 0000000000000000000000000000000000000000..c2b296e9cbbc16d336a881ad10889b6e5856985b --- /dev/null +++ b/qt/Models/SavedDeckModel/SavedDeckModel.cpp @@ -0,0 +1,100 @@ +#include "SavedDeckModel.hpp" +#include +#include +#include + +SavedDeckModel::SavedDeckModel(std::vector cards, QObject *parent) + : QAbstractTableModel(parent) +{ + for (const Card& card: cards) { + SavedCard savedCard; + savedCard.card = card; + savedCard.sentencesMask = std::vector(card.examples.size(), true); + + std::vector localAudiosMask = std::vector(card.audios.local.size(), true); + std::vector webAudiosMask = std::vector(card.audios.web.size(), true); + savedCard.audiosMask = std::make_pair(localAudiosMask, webAudiosMask); + + std::vector localImagesMask = std::vector(card.images.local.size(), true); + std::vector webImagesMask = std::vector(card.images.web.size(), true); + savedCard.imagesMask= std::make_pair(localImagesMask, webImagesMask); + + cards_.push_back(savedCard); + } +} + +QVariant SavedDeckModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role == Qt::DisplayRole && orientation == Qt::Horizontal) { + switch (section) { + case 0: + return "word"; + case 1: + return "definition"; + default: + return QVariant(); + } + } + return QVariant(); +} + +int SavedDeckModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + + return cards_.size(); +} + +int SavedDeckModel::columnCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + + return 2; +} + +QVariant SavedDeckModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (!hasIndex(index.row(), index.column())) { + return QVariant(); + } + switch (role) { + case Qt::DisplayRole: + if (index.column() == 0) { + return QString::fromStdString(cards_.at(index.row()).card.word); + } + if (index.column() == 1) { + return QString::fromStdString(cards_.at(index.row()).card.definition); + } + return QVariant(); + default: + return QVariant(); + } + return QVariant(); +} + +bool SavedDeckModel::removeRows(int row, int count, const QModelIndex &parent) +{ + beginRemoveRows(parent, row, row + count - 1); + cards_.erase(cards_.begin() + row, cards_.begin() + row + count); + endRemoveRows(); + return true; +} + +QModelIndex SavedDeckModel::next(const QModelIndex& index) { + if (!hasIndex(index.row() + 1, index.column())) { + return index; + } + return createIndex(index.row() + 1, index.column()); +} + +QModelIndex SavedDeckModel::prev(const QModelIndex& index) { + if (!hasIndex(index.row() - 1, index.column())) { + return index; + } + return createIndex(index.row() - 1, index.column()); +} diff --git a/qt/Models/SavedDeckModel/SavedDeckModel.hpp b/qt/Models/SavedDeckModel/SavedDeckModel.hpp new file mode 100644 index 0000000000000000000000000000000000000000..3cf6dbda0d8e5c3e2b149cd615f7e0e093e6787c --- /dev/null +++ b/qt/Models/SavedDeckModel/SavedDeckModel.hpp @@ -0,0 +1,40 @@ +#ifndef SAVEDDECKMODEL_H +#define SAVEDDECKMODEL_H + +#include +#include +#include "Card.h" + +class SavedDeckModel : public QAbstractTableModel +{ + Q_OBJECT + +public: + struct SavedCard { + Card card; + std::vector sentencesMask; + std::pair, std::vector> audiosMask; + std::pair, std::vector> imagesMask; + }; + + explicit SavedDeckModel(std::vector cards, QObject *parent = nullptr); + + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + + // Basic functionality: + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + // Remove data: + bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override; + +public slots: + QModelIndex next(const QModelIndex& index); + QModelIndex prev(const QModelIndex& index); + +private: + std::vector cards_; +}; + +#endif // SAVEDDECKMODEL_H diff --git a/qt/Models/WordCards/CMakeLists.txt b/qt/Models/WordCards/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..0353096b681b7054c2b9796fefb72f8802b75068 --- /dev/null +++ b/qt/Models/WordCards/CMakeLists.txt @@ -0,0 +1,22 @@ +message(${CMAKE_CURRENT_SOURCE_DIR}) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +find_package(Qt5 COMPONENTS Core REQUIRED) +find_package(Qt5 COMPONENTS Widgets REQUIRED) + +add_library(word_cards +word_cards.cpp +# word_cards.h +) +target_include_directories(word_cards PUBLIC ./) + +target_link_libraries(word_cards PUBLIC +Qt5::Core +Qt5::Widgets +card +) diff --git a/qt/Models/WordCards/word_cards.cpp b/qt/Models/WordCards/word_cards.cpp new file mode 100644 index 0000000000000000000000000000000000000000..eaf22972d5614973c39857185b7e07c4b3067ed5 --- /dev/null +++ b/qt/Models/WordCards/word_cards.cpp @@ -0,0 +1,62 @@ +#include "word_cards.h" + +WordCards::WordCards(const std::string& word) + : word_(word) + , pos_(0) +{ +} + +WordCards::WordCards(const std::string& word, std::vector cards) + : word_(word) + , pos_(0) + , cards_(cards) +{ +} + +size_t WordCards::size() const +{ + return cards_.size(); +} + +std::string WordCards::get_word() const +{ + return word_; +} + +const Card* WordCards::get_card() const +{ + if (pos_ < 0 || pos_ >= size()) + { + return nullptr; + } + return &cards_[pos_]; +} + +void WordCards::next() +{ + if (pos_ == size() - 1) + { + return; + } + ++pos_; +} + +void WordCards::prev() +{ + if (pos_ == 0) + { + return; + } + --pos_; +} + +void WordCards::addCard(Card card) +{ + cards_.push_back(card); +} + +void WordCards::addCards(std::vector cards) { + for (const Card& card: cards) { + addCard(card); + } +} \ No newline at end of file diff --git a/qt/Models/WordCards/word_cards.h b/qt/Models/WordCards/word_cards.h new file mode 100644 index 0000000000000000000000000000000000000000..fb9576211be1d7d1877aee519141296b78df7cbf --- /dev/null +++ b/qt/Models/WordCards/word_cards.h @@ -0,0 +1,26 @@ +#ifndef WORD_CARDS_H +#define WORD_CARDS_H + +#include +#include +#include "Card.h" + +class WordCards { +public: + explicit WordCards(const std::string& word); + WordCards(const std::string& word, std::vector cards); + size_t size() const; + std::string get_word() const; + const Card* get_card() const; // pointer invalidation is impossible + void next(); + void prev(); + void addCards(std::vector cards); + void addCard(Card card); + +private: + std::string word_; + std::vector cards_; + size_t pos_; +}; + +#endif // WORD_CARDS_H diff --git a/qt/Widgets/AudiosWidget/AudiosWidget.cpp b/qt/Widgets/AudiosWidget/AudiosWidget.cpp new file mode 100644 index 0000000000000000000000000000000000000000..fe9c32bf743c11e5a029c8a991485e96b99d46ab --- /dev/null +++ b/qt/Widgets/AudiosWidget/AudiosWidget.cpp @@ -0,0 +1,187 @@ +#include "AudiosWidget.hpp" +#include "Media.h" +#include "ui_AudiosWidget.h" +#include +#include +#include "Player.hpp" + + +AudiosWidget::AudiosWidget(std::unique_ptr audioPlugin, + QWidget *parent) : + QWidget(parent), + ui(new Ui::AudiosWidget) +{ + ui->setupUi(this); + audioPlugin_ = std::move(audioPlugin); + audioPlugin_->init("audios"); + gridLayout = new QGridLayout; + gridLayout->setAlignment(Qt::AlignTop); + QWidget *audiosListWidget = new QWidget; + audiosListWidget->setLayout(gridLayout); + ui->scrollArea->setWidget(audiosListWidget); + mediaPlayer = new QMediaPlayer(this); +} + +AudiosWidget::~AudiosWidget() +{ + delete ui; +} + +void AudiosWidget::addAudio(SourceWithAdditionalInfo audio, bool isLocal, bool isChosen) { + QPushButton *pushButton = new QPushButton; + pushButton->setCheckable(true); + pushButton->setStyleSheet("QPushButton:checked { background-color: green; }"); + pushButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Expanding); + + Player *audioPlayer = new Player(mediaPlayer); + audioPlayer->set(audio, isLocal); + + if (isChosen) { + pushButton->setChecked(true); + } + + audioPlayer->setMaximumHeight(100); + pushButton->setMaximumHeight(40); + pushButton->setMaximumWidth(40); + + int new_row = gridLayout->count() / gridLayout->columnCount(); + + gridLayout->addWidget(pushButton, new_row, 0); + gridLayout->addWidget(audioPlayer, new_row, 1); +} + +void AudiosWidget::load(int batch_size) { + currentWord = ui->loadAudioLine->text().toStdString(); + if (currentWord.empty()) { + return; + } + auto [audios, error] = audioPlugin_->get(currentWord, batch_size, false); + + for (SourceWithAdditionalInfo localAudio: audios.local) { + addAudio(localAudio, true); + } + for (SourceWithAdditionalInfo webAudio: audios.web) { + addAudio(webAudio, false); + } +} + +void AudiosWidget::clear() { + QLayoutItem *item; + while ((item = gridLayout->takeAt(0)) != nullptr) { + QWidget *widget = item->widget(); + gridLayout->removeItem(item); + widget->deleteLater(); + delete item; + } +} + +void AudiosWidget::set(std::string word, Media audios, + std::pair, std::vector> chosen_mask) { + clear(); + currentWord = word; + ui->loadAudioLine->setText(QString::fromStdString(currentWord)); + for (int i = 0; i < audios.local.size(); ++i) { + bool chosen = i < chosen_mask.first.size() ? chosen_mask.first.at(i) : false; + addAudio(audios.local.at(i), true, chosen); + } + for (int i = 0; i < audios.web.size(); ++i) { + bool chosen = i < chosen_mask.second.size() ? chosen_mask.second.at(i) : false; + addAudio(audios.web.at(i), false, chosen); + } +} + +Media AudiosWidget::getAudios() { + Media audios; + for (int row = 0; row < gridLayout->count() / gridLayout->columnCount(); ++row) { + QLayoutItem* audioPlayerItem = gridLayout->itemAtPosition(row, 1); + if (!audioPlayerItem) { + continue; + } + Player *audioPlayer = qobject_cast(audioPlayerItem->widget()); + if (!audioPlayer) { + continue; + } + SourceWithAdditionalInfo audio; + audio.info = audioPlayer->getInfo().toStdString(); + audio.src = audioPlayer->getSrc().toStdString(); + if (audioPlayer->isLocal()) { + audios.local.push_back(audio); + } + else { + audios.web.push_back(audio); + } + } + return audios; +} + +std::pair, std::vector> AudiosWidget::getMask() { + std::pair, std::vector> audiosMask; + for (int row = 0; row < gridLayout->count() / gridLayout->columnCount(); ++row) { + QLayoutItem* pushButtonItem = gridLayout->itemAtPosition(row, 0); + if (!pushButtonItem) { + continue; + } + + QPushButton* pushButton = qobject_cast(pushButtonItem->widget()); + if (!pushButton) { + continue; + } + + QLayoutItem* audioPlayerItem = gridLayout->itemAtPosition(row, 1); + if (!audioPlayerItem) { + continue; + } + Player *audioPlayer = qobject_cast(audioPlayerItem->widget()); + if (!audioPlayer) { + continue; + } + + if (audioPlayer->isLocal()) { + audiosMask.first.push_back(pushButton->isChecked()); + } + else { + audiosMask.second.push_back(pushButton->isChecked()); + } + } + return audiosMask; +} + +Media AudiosWidget::getChosenAudio() { + Media chosenAudios; + for (int row = 0; row < gridLayout->count() / gridLayout->columnCount(); ++row) { + QLayoutItem* pushButtonItem = gridLayout->itemAtPosition(row, 0); + if (!pushButtonItem) { + continue; + } + + QPushButton* pushButton = qobject_cast(pushButtonItem->widget()); + if (!pushButton) { + continue; + } + + if (!pushButton->isChecked()) { + continue; + } + + QLayoutItem* audioPlayerItem = gridLayout->itemAtPosition(row, 1); + if (!audioPlayerItem) { + continue; + } + + Player *audioPlayer = qobject_cast(audioPlayerItem->widget()); + if (!audioPlayer) { + continue; + } + + SourceWithAdditionalInfo audio; + audio.info = audioPlayer->getInfo().toStdString(); + audio.src = audioPlayer->getSrc().toStdString(); + if (audioPlayer->isLocal()) { + chosenAudios.local.push_back(audio); + } + else { + chosenAudios.web.push_back(audio); + } + } + return chosenAudios; +} diff --git a/qt/Widgets/AudiosWidget/AudiosWidget.hpp b/qt/Widgets/AudiosWidget/AudiosWidget.hpp new file mode 100644 index 0000000000000000000000000000000000000000..feaba24581c0f53e4323b2ab07ef51ec09ca0d85 --- /dev/null +++ b/qt/Widgets/AudiosWidget/AudiosWidget.hpp @@ -0,0 +1,46 @@ +#ifndef AUDIOSWIDGET_HPP +#define AUDIOSWIDGET_HPP + +#include +#include +#include +#include +#include +#include +#include "Card.h" +#include "IAudioPluginWrapper.h" +#include "Media.h" + +namespace Ui { +class AudiosWidget; +} + +class AudiosWidget : public QWidget +{ + Q_OBJECT + +public: + explicit AudiosWidget(std::unique_ptr audioPlugin, + QWidget *parent = nullptr); + ~AudiosWidget(); + +public slots: + void addAudio(SourceWithAdditionalInfo audio, bool isLocal, bool isChosen = false); + void load(int batch_size = 5); + void clear(); + void set(std::string word, Media audios, + std::pair, std::vector> chosen_mask = + std::pair, std::vector>()); + Media getAudios(); + std::pair, std::vector> getMask(); + Media getChosenAudio(); + +private: + Ui::AudiosWidget *ui; + QGridLayout *gridLayout; + QMediaPlayer *mediaPlayer; + std::string currentWord; + std::unique_ptr audioPlugin_; +}; + +#endif // AUDIOSWIDGET_HPP diff --git a/qt/Widgets/AudiosWidget/AudiosWidget.ui b/qt/Widgets/AudiosWidget/AudiosWidget.ui new file mode 100644 index 0000000000000000000000000000000000000000..7114f09aa4fc5a7f0f583e2dc8f197403fa7e0d2 --- /dev/null +++ b/qt/Widgets/AudiosWidget/AudiosWidget.ui @@ -0,0 +1,85 @@ + + + AudiosWidget + + + + 0 + 0 + 400 + 300 + + + + Form + + + + + + + + Load + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + true + + + + + 0 + 0 + 374 + 231 + + + + + + + + + + + loadButton + clicked() + AudiosWidget + load() + + + 53 + 28 + + + 4 + 60 + + + + + + load() + + diff --git a/qt/Widgets/AudiosWidget/CMakeLists.txt b/qt/Widgets/AudiosWidget/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..07261fe1269b11826daa3876cd429ee1eaf68aa7 --- /dev/null +++ b/qt/Widgets/AudiosWidget/CMakeLists.txt @@ -0,0 +1,25 @@ +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +find_package(Qt5 COMPONENTS Widgets REQUIRED) + + +set(PROJECT_SOURCES + AudiosWidget.hpp + AudiosWidget.cpp + AudiosWidget.ui +) + +add_library(audios_widget ${PROJECT_SOURCES}) + +target_include_directories(audios_widget PUBLIC ./) + +target_link_libraries(audios_widget +Qt5::Widgets +player +card +audio_plugin_wrapper +) diff --git a/qt/Widgets/CMakeLists.txt b/qt/Widgets/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..5e6abe9d7b9fbc0308cb23b71500b0d9067a5d76 --- /dev/null +++ b/qt/Widgets/CMakeLists.txt @@ -0,0 +1,5 @@ +add_subdirectory(Player) +add_subdirectory(AudiosWidget) +add_subdirectory(SentencesWidget) +add_subdirectory(ImagesWidget) +add_subdirectory(MainWindow) \ No newline at end of file diff --git a/qt/Widgets/ImagesWidget/CMakeLists.txt b/qt/Widgets/ImagesWidget/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..b7a2b559f25b2866dc3e3fdb8cd9a32818968986 --- /dev/null +++ b/qt/Widgets/ImagesWidget/CMakeLists.txt @@ -0,0 +1,29 @@ +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +find_package(Qt5 COMPONENTS Widgets REQUIRED) +find_package(Qt5 COMPONENTS Network REQUIRED) +find_package(Qt5Concurrent REQUIRED) + + +set(PROJECT_SOURCES + ImagesWidget.hpp + ImagesWidget.cpp + ImagesWidget.ui +) + +add_library(images_widget ${PROJECT_SOURCES}) + +target_include_directories(images_widget PUBLIC ./) + +target_link_libraries(images_widget +Qt5::Widgets +Qt5::Network +Qt5::Concurrent +image_plugin_wrapper +card +downloader +) diff --git a/qt/Widgets/ImagesWidget/ImagesWidget.cpp b/qt/Widgets/ImagesWidget/ImagesWidget.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e0ac9656c66439884ef82d05a4f9d2fdf0beca68 --- /dev/null +++ b/qt/Widgets/ImagesWidget/ImagesWidget.cpp @@ -0,0 +1,188 @@ +#include "ImagesWidget.hpp" +#include "Downloader.hpp" +#include "ui_ImagesWidget.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +ImagesWidget::ImagesWidget(std::unique_ptr imagePlugin, + QWidget *parent) : + QWidget(parent), + ui(new Ui::ImagesWidget) +{ + ui->setupUi(this); + imagePlugin_ = std::move(imagePlugin); + imagePlugin_->init("images"); + gridLayout = new QGridLayout; + QWidget *imagesListWidget = new QWidget; + imagesListWidget->setLayout(gridLayout); + ui->scrollArea->setWidget(imagesListWidget); +} + +ImagesWidget::~ImagesWidget() +{ + delete ui; +} + +void ImagesWidget::addImage(SourceWithAdditionalInfo image, bool isLocal, bool isChosen) { + QPushButton *pushButton = new QPushButton; + + pushButton->setCheckable(true); + pushButton->setStyleSheet("QPushButton:checked { background-color: green; }"); + pushButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + pushButton->setProperty("isLocal", isLocal); + pushButton->setProperty("src", QString::fromStdString(image.src)); + pushButton->setProperty("info", QString::fromStdString(image.info)); + + if (isChosen) { + pushButton->setChecked(true); + } + + int new_row = gridLayout->count() / picsInRow; + int new_col = gridLayout->count() % picsInRow; + + gridLayout->addWidget(pushButton, new_row, new_col); + + if (isLocal) { + auto pixmap = QPixmap(QString::fromStdString(image.src)); + pushButton->setIcon(pixmap); + } + else if (QUrl(QString::fromStdString(image.src)).isValid()) { + downloader = new Downloader(this); + connect(downloader, &Downloader::done, [=](const QUrl &url, const QByteArray &bytes) { + QPixmap pixmap; + pixmap.loadFromData(bytes); + QIcon icon(pixmap); + pushButton->setIcon(icon); + pushButton->setIconSize(QSize(150, 150)); + }); + downloader->download(QUrl(QString::fromStdString(image.src))); + } +} + +void ImagesWidget::load(int batch_size) { + currentWord = ui->loadImageLine->text().toStdString(); + if (currentWord.empty()) { + return; + } + auto [images, error] = imagePlugin_->get(currentWord, batch_size, false); + + for (SourceWithAdditionalInfo localImage: images.local) { + addImage(localImage, true); + } + for (SourceWithAdditionalInfo webImage: images.web) { + addImage(webImage, false); + } +} + +void ImagesWidget::clear() { + QLayoutItem *item; + while ((item = gridLayout->takeAt(0)) != nullptr) { + QWidget *widget = item->widget(); + gridLayout->removeItem(item); + widget->deleteLater(); + delete item; + } +} + +void ImagesWidget::set(std::string word, Media images, + std::pair, std::vector> chosen_mask) { + clear(); + currentWord = word; + ui->loadImageLine->setText(QString::fromStdString(currentWord)); + for (int i = 0; i < images.local.size(); ++i) { + bool chosen = i < chosen_mask.first.size() ? chosen_mask.first.at(i) : false; + addImage(images.local.at(i), true, chosen); + } + for (int i = 0; i < images.web.size(); ++i) { + bool chosen = i < chosen_mask.second.size() ? chosen_mask.second.at(i) : false; + addImage(images.web.at(i), false, chosen); + } +} + +Media ImagesWidget::getImages() { + Media images; + for (int i = 0; i < gridLayout->count(); ++i) { + QLayoutItem *pushButtonItem = gridLayout->itemAt(i); + if (!pushButtonItem) { + continue; + } + QPushButton *pushButton = qobject_cast(pushButtonItem->widget()); + if (!pushButton) { + continue; + } + SourceWithAdditionalInfo image; + bool isLocal = pushButton->property("isLocal").toBool(); + image.src = pushButton->property("src").toString().toStdString(); + image.info = pushButton->property("info").toString().toStdString(); + + if (isLocal) { + images.local.push_back(image); + } + else { + images.web.push_back(image); + } + } + return images; +} + +std::pair, std::vector> ImagesWidget::getMask() { + std::pair, std::vector> imagesMask; + for (int i = 0; i < gridLayout->count(); ++i) { + QLayoutItem *pushButtonItem = gridLayout->itemAt(i); + if (!pushButtonItem) { + continue; + } + QPushButton *pushButton = qobject_cast(pushButtonItem->widget()); + if (!pushButton) { + continue; + } + if (pushButton->property("isLocal").toBool()) { + imagesMask.first.push_back(pushButton->isChecked()); + } + else { + imagesMask.second.push_back(pushButton->isChecked()); + } + } + return imagesMask; +} + +Media ImagesWidget::getChosenImages() { + Media chosenImages; + for (int i = 0; i < gridLayout->count(); ++i) { + QLayoutItem *pushButtonItem = gridLayout->itemAt(i); + if (!pushButtonItem) { + continue; + } + QPushButton *pushButton = qobject_cast(pushButtonItem->widget()); + if (!pushButton) { + continue; + } + if (!pushButton->isChecked()) { + continue; + } + SourceWithAdditionalInfo image; + bool isLocal = pushButton->property("isLocal").toBool(); + image.src = pushButton->property("src").toString().toStdString(); + image.info = pushButton->property("info").toString().toStdString(); + + if (isLocal) { + chosenImages.local.push_back(image); + } + else { + chosenImages.web.push_back(image); + } + } + return chosenImages; +} diff --git a/qt/Widgets/ImagesWidget/ImagesWidget.hpp b/qt/Widgets/ImagesWidget/ImagesWidget.hpp new file mode 100644 index 0000000000000000000000000000000000000000..e43df9a910fde77a9aee0d823f1bd9f0ce1ef9d9 --- /dev/null +++ b/qt/Widgets/ImagesWidget/ImagesWidget.hpp @@ -0,0 +1,45 @@ +#ifndef IMAGESWIDGET_HPP +#define IMAGESWIDGET_HPP + +#include +#include +#include +#include "Card.h" +#include "IImagePluginWrapper.h" +#include "ImagePluginWrapper.h" +#include "Downloader.hpp" + +namespace Ui { +class ImagesWidget; +} + +class ImagesWidget : public QWidget +{ + Q_OBJECT + +public: + explicit ImagesWidget(std::unique_ptr imagePlugin, + QWidget *parent = nullptr); + ~ImagesWidget(); + +public slots: + void addImage(SourceWithAdditionalInfo audio, bool isLocal, bool isChosen = false); + void load(int batch_size = 10); + void clear(); + void set(std::string word, Media audios, + std::pair, std::vector> chosen_mask = + std::pair, std::vector>()); + Media getImages(); + std::pair, std::vector> getMask(); + Media getChosenImages(); + +private: + Ui::ImagesWidget *ui; + QGridLayout *gridLayout; + Downloader *downloader; + const int picsInRow = 3; + std::string currentWord; + std::unique_ptr imagePlugin_; +}; + +#endif // IMAGESWIDGET_HPP diff --git a/qt/Widgets/ImagesWidget/ImagesWidget.ui b/qt/Widgets/ImagesWidget/ImagesWidget.ui new file mode 100644 index 0000000000000000000000000000000000000000..a59354e593d77cf318a8c7d46008027cc0de17bb --- /dev/null +++ b/qt/Widgets/ImagesWidget/ImagesWidget.ui @@ -0,0 +1,85 @@ + + + ImagesWidget + + + + 0 + 0 + 400 + 300 + + + + Form + + + + + + + + Load + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + true + + + + + 0 + 0 + 374 + 231 + + + + + + + + + + + loadButton + clicked() + ImagesWidget + load() + + + 68 + 34 + + + 4 + 81 + + + + + + load() + + diff --git a/qt/Widgets/MainWindow/CMakeLists.txt b/qt/Widgets/MainWindow/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..83af8194fd5f2f6f7829db7dca8ed4b7618249dc --- /dev/null +++ b/qt/Widgets/MainWindow/CMakeLists.txt @@ -0,0 +1,30 @@ +cmake_minimum_required(VERSION 3.5) +set(CMAKE_CXX_STANDARD 20) +# set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +find_package(Qt5 COMPONENTS Core REQUIRED) +find_package(Qt5 COMPONENTS Widgets REQUIRED) + +add_library(main_window +mainwindow.cpp +# mainwindow.h +mainwindow.ui +) + +target_include_directories(main_window PUBLIC ./) + +target_link_libraries(main_window PUBLIC +Qt5::Widgets +connection +deck_model +card +sentences_widget +audios_widget +images_widget +word_plugin_wrapper +format_processor_plugin_wrapper +) \ No newline at end of file diff --git a/qt/Widgets/MainWindow/mainwindow.cpp b/qt/Widgets/MainWindow/mainwindow.cpp new file mode 100644 index 0000000000000000000000000000000000000000..867915d3565f949b4487c10ed5e07206e88fcdd4 --- /dev/null +++ b/qt/Widgets/MainWindow/mainwindow.cpp @@ -0,0 +1,192 @@ +#include "mainwindow.h" +#include "AudioPluginWrapper.h" +#include "AudiosWidget.hpp" +#include "Card.h" +#include "IAudioPluginWrapper.h" +#include "IImagePluginWrapper.h" +#include "IRequestable.h" +#include "ISentencePluginWrapper.h" +#include "IWordPluginWrapper.h" +#include "ImagePluginWrapper.h" +#include "ImagesWidget.hpp" +#include "ISentencePluginWrapper.h" +#include "SentencePluginWrapper.h" +#include "SentencesWidget.hpp" +#include "ui_mainwindow.h" +#include "deck_model.h" +#include "Deck.hpp" +#include "WordPluginWrapper.h" +#include "FormatProcessorPluginWrapper.h" +#include "ServerConnection.h" +#include +#include +#include +#include +#include +#include +#include + +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) + , ui(new Ui::MainWindow) +{ + ui->setupUi(this); + connection = std::make_shared(8888); + std::unique_ptr wordPlugin = std::make_unique(connection); + std::unique_ptr deck = std::make_unique(std::move(wordPlugin)); + deckModel = new DeckModel(std::move(deck), this); + ui->deckView->setModel(deckModel); + + std::unique_ptr sentencePlugin = std::make_unique(connection); + sentencesWidget = new SentencesWidget(std::move(sentencePlugin)); + ui->examplesTab->layout()->addWidget(sentencesWidget); + + std::unique_ptr audioPlugin = std::make_unique(connection); + audiosWidget = new AudiosWidget(std::move(audioPlugin)); + ui->audioTab->layout()->addWidget(audiosWidget); + + std::unique_ptr imagePlugin = std::make_unique(connection); + imagesWidget = new ImagesWidget(std::move(imagePlugin)); + ui->imagesTab->layout()->addWidget(imagesWidget); + + connect(ui->searchLine, SIGNAL(returnPressed()), this, SLOT(onSearchReturned())); + connect(ui->deckView, SIGNAL(clicked(QModelIndex)), this, SLOT(setCurrentIndex(QModelIndex))); +} + +MainWindow::~MainWindow() +{ + delete ui; +} + +void MainWindow::onSearchReturned() +{ + QString word = ui->searchLine->text(); + if (word == "") + { + return; + } + int int_idx = deckModel->indexOfWord(word); + if (int_idx == -1) + { + deckModel->load(ui->searchLine->text(), ui->filterEdit->toPlainText()); + int rows = deckModel->rowCount() - 1; + QModelIndex qm_idx = deckModel->index(deckModel->rowCount() - 1); + ui->deckView->setCurrentIndex(qm_idx); + ui->deckView->clicked(qm_idx); + return; + } + QModelIndex qm_idx = deckModel->index(int_idx); + ui->deckView->setCurrentIndex(qm_idx); + ui->deckView->clicked(qm_idx); +} + +void MainWindow::updateCardFields() +{ + ui->tabWidget->setCurrentIndex(0); + QVariant qvar = deckModel->data(current_index, DeckModel::CardRole); + void* void_card = qvar.value(); + const Card* card = static_cast(void_card); + updateWord(card); + updateDefinition(card); + updateExamples(card); + updateAudio(card); + updateImages(card); + updateTags(card); +} + +void MainWindow::updateWord(const Card *card) +{ + if (!card) + { + return; + } + ui->wordLine->setText(QString::fromStdString(card->word)); +} + +void MainWindow::updateDefinition(const Card *card) +{ + if (!card) + { + return; + } + ui->definitionEdit->setText(QString::fromStdString(card->definition)); +} + +void MainWindow::updateTags(const Card *card) +{ + if (!card) { + return; + } + currentTags = card->tags; + ui->tagsLine->setText(QString::fromStdString(parse_tags(card->tags))); +} + +void MainWindow::updateExamples(const Card *card) +{ + if (!card) { + return; + } + sentencesWidget->set(card->word, card->examples); +} + +void MainWindow::updateAudio(const Card *card) +{ + if (!card) { + return; + } + audiosWidget->set(card->word, card->audios); +} + +void MainWindow::updateImages(const Card *card) +{ + if (!card) { + return; + } + imagesWidget->set(card->word, card->images); +} + +void MainWindow::setCurrentIndex(QModelIndex index) +{ + current_index = index; + updateCardFields(); +} + +void MainWindow::onNextClicked() +{ + if (!current_index.isValid()) { + return; + } + deckModel->next(current_index); + updateCardFields(); +} + +void MainWindow::onPrevClicked() +{ + if (!current_index.isValid()) { + return; + } + deckModel->prev(current_index); + updateCardFields(); +} + +void MainWindow::onAddClicked() { + Card card; + card.word = ui->wordLine->text().toStdString(); + card.tags = currentTags; + card.definition = ui->definitionEdit->toPlainText().toStdString(); + card.examples = sentencesWidget->getChosenSentences(); + card.audios = audiosWidget->getChosenAudio(); + card.images = imagesWidget->getChosenImages(); + savedDeck.push_back(card); + onNextClicked(); +} + +void MainWindow::save() { + std::string relative_path = "./savedDeck.json"; + auto absolute_path = std::filesystem::absolute(relative_path).string(); + save_cards(savedDeck, absolute_path); + FormatProcessorPluginWrapper savingPlugin(connection); + savingPlugin.init("processor"); + savingPlugin.save(absolute_path); + exit(0); +} diff --git a/qt/Widgets/MainWindow/mainwindow.h b/qt/Widgets/MainWindow/mainwindow.h new file mode 100644 index 0000000000000000000000000000000000000000..bce9b5ef305326c81445b1f392c153e431ae4f24 --- /dev/null +++ b/qt/Widgets/MainWindow/mainwindow.h @@ -0,0 +1,52 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include +#include "deck_model.h" +#include "Card.h" +#include "AudiosWidget.hpp" +#include "ImagesWidget.hpp" +#include "SentencesWidget.hpp" +#include "IRequestable.h" + + +QT_BEGIN_NAMESPACE +namespace Ui { class MainWindow; } +QT_END_NAMESPACE + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + MainWindow(QWidget *parent = nullptr); + ~MainWindow(); + +private slots: + void onSearchReturned(); + void updateCardFields(); + void updateWord(const Card*); + void updateDefinition(const Card*); + void updateTags(const Card*); + void updateExamples(const Card*); + void updateAudio(const Card*); + void updateImages(const Card*); + void setCurrentIndex(QModelIndex); + void onNextClicked(); + void onPrevClicked(); + void onAddClicked(); + void save(); + +private: + Ui::MainWindow *ui; + DeckModel* deckModel; + std::vector savedDeck; + std::shared_ptr connection; + SentencesWidget* sentencesWidget; + AudiosWidget* audiosWidget; + ImagesWidget* imagesWidget; + QModelIndex current_index; + nlohmann::json currentTags; +}; +#endif // MAINWINDOW_H diff --git a/qt/Widgets/MainWindow/mainwindow.ui b/qt/Widgets/MainWindow/mainwindow.ui new file mode 100644 index 0000000000000000000000000000000000000000..8c6b28721af9ea92f775973256bcc82092306777 --- /dev/null +++ b/qt/Widgets/MainWindow/mainwindow.ui @@ -0,0 +1,728 @@ + + + MainWindow + + + + 0 + 0 + 509 + 567 + + + + CardGen + + + + + + + + 0 + 0 + + + + + 100 + 16777215 + + + + + + + + + + Search + + + + + + + + 16777215 + 60 + + + + filter + + + + + + + Save and quit + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + + + + + + 255 + 255 + 255 + + + + + + + 108 + 108 + 108 + + + + + + + 162 + 162 + 162 + + + + + + + 135 + 135 + 135 + + + + + + + 54 + 54 + 54 + + + + + + + 72 + 72 + 72 + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 0 + 0 + 0 + + + + + + + 108 + 108 + 108 + + + + + + + 0 + 0 + 0 + + + + + + + 54 + 54 + 54 + + + + + + + 255 + 255 + 220 + + + + + + + 0 + 0 + 0 + + + + + + + 255 + 255 + 255 + + + + + + + + + 255 + 255 + 255 + + + + + + + 108 + 108 + 108 + + + + + + + 162 + 162 + 162 + + + + + + + 135 + 135 + 135 + + + + + + + 54 + 54 + 54 + + + + + + + 72 + 72 + 72 + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 0 + 0 + 0 + + + + + + + 108 + 108 + 108 + + + + + + + 0 + 0 + 0 + + + + + + + 54 + 54 + 54 + + + + + + + 255 + 255 + 220 + + + + + + + 0 + 0 + 0 + + + + + + + 255 + 255 + 255 + + + + + + + + + 54 + 54 + 54 + + + + + + + 108 + 108 + 108 + + + + + + + 162 + 162 + 162 + + + + + + + 135 + 135 + 135 + + + + + + + 54 + 54 + 54 + + + + + + + 72 + 72 + 72 + + + + + + + 54 + 54 + 54 + + + + + + + 255 + 255 + 255 + + + + + + + 54 + 54 + 54 + + + + + + + 108 + 108 + 108 + + + + + + + 108 + 108 + 108 + + + + + + + 0 + 0 + 0 + + + + + + + 108 + 108 + 108 + + + + + + + 255 + 255 + 220 + + + + + + + 0 + 0 + 0 + + + + + + + 255 + 255 + 255 + + + + + + + + Qt::Horizontal + + + + + + + + 0 + 0 + + + + + + + word + + + + + + + tags + + + + + + + + 16777215 + 60 + + + + definition + + + + + + + + + < + + + + + + + > + + + + + + + add + + + + + + + + + QLayout::SetDefaultConstraint + + + + + + 16777215 + 500 + + + + 0 + + + + Examples + + + + + + + + + + Audio + + + + + + + + + + Images + + + + + + + + + + + + + + + + + + + 0 + 0 + 509 + 28 + + + + + + + + + prevButton + clicked() + MainWindow + onPrevClicked() + + + 192 + 341 + + + 141 + 471 + + + + + nextButton + clicked() + MainWindow + onNextClicked() + + + 287 + 347 + + + 277 + 462 + + + + + pushButton_4 + clicked() + MainWindow + onAddClicked() + + + 411 + 344 + + + 440 + 533 + + + + + saveButton + clicked() + MainWindow + save() + + + 388 + 158 + + + 502 + 179 + + + + + + onPrevClicked() + onNextClicked() + onAddClicked() + save() + + diff --git a/qt/Widgets/Player/CMakeLists.txt b/qt/Widgets/Player/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..6803b32fb7a0dede78071400d995b7cba00e4048 --- /dev/null +++ b/qt/Widgets/Player/CMakeLists.txt @@ -0,0 +1,31 @@ +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +find_package(Qt5 COMPONENTS Widgets REQUIRED) +find_package(Qt5Multimedia REQUIRED) +FIND_PACKAGE( Qt5MultimediaWidgets REQUIRED ) + +SET(QT_USE_QTMULTIMEDIA TRUE) +SET(QT_USE_QTMULTIMEDIAWIDGETS TRUE) + + +set(PROJECT_SOURCES + Player.hpp + Player.cpp + Player.ui +) + +add_library(player ${PROJECT_SOURCES}) + +target_include_directories(player PUBLIC ./) + +target_link_libraries(player +Qt5::Widgets +Qt5::Multimedia +card +) + +QT5_USE_MODULES (player Multimedia MultimediaWidgets) diff --git a/qt/Widgets/Player/Player.cpp b/qt/Widgets/Player/Player.cpp new file mode 100644 index 0000000000000000000000000000000000000000..a78c4b50855207f8e71b44ebd5fca33fd9331054 --- /dev/null +++ b/qt/Widgets/Player/Player.cpp @@ -0,0 +1,83 @@ +#include "Player.hpp" +#include "ui_Player.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Player::Player(QMediaPlayer *audioPlayer, QWidget *parent) : + QWidget(parent), + audioPlayer(audioPlayer), + ui(new Ui::Player) +{ + ui->setupUi(this); + if (!audioPlayer) { + qDebug() << "Svoi"; + audioPlayer = new QMediaPlayer(this); + } + ui->playButton->setEnabled(false); + ui->playButton->setIcon(style()->standardIcon(QStyle::SP_MediaPlay)); + connect(audioPlayer, &QMediaPlayer::stateChanged, + this, &Player::onStateChanged); + connect(audioPlayer, &QMediaPlayer::mediaStatusChanged, + this, &Player::onStatusChanged); + ui->infoEdit->setEnabled(false); + ui->infoEdit->setStyleSheet("color: black; background-color: #F0F0F0; border: 1px solid gray;"); +} + +Player::~Player() +{ + delete ui; +} + +void Player::set(SourceWithAdditionalInfo audio, bool local) { + QString info = QString::fromStdString(audio.info); + local_ = local; + if (local_) { + url = QUrl::fromLocalFile(QString::fromStdString(audio.src)); + return; + } + url = QUrl(QString::fromStdString(audio.src)); + ui->infoEdit->setText(QString::fromStdString(audio.info)); + ui->playButton->setEnabled(true); + ui->infoEdit->setEnabled(false); + ui->infoEdit->setStyleSheet("color: black; background-color: #F0F0F0; border: 1px solid gray;"); +} + +void Player::onStateChanged(QMediaPlayer::State state) { + if (state == QMediaPlayer::StoppedState) { + ui->playButton->setEnabled(true); + } +} + +void Player::onStatusChanged(QMediaPlayer::MediaStatus status) { + if (status == QMediaPlayer::LoadedMedia) { + ui->playButton->setEnabled(true); + } +} + +void Player::onPlayClicked() { + audioPlayer->setMedia(url); + audioPlayer->play(); + ui->playButton->setEnabled(false); +} + +bool Player::isLocal() const { + return local_; +} + +QString Player::getSrc() const { + return url.toString(); +} + +QString Player::getInfo() const { + return info; +} diff --git a/qt/Widgets/Player/Player.hpp b/qt/Widgets/Player/Player.hpp new file mode 100644 index 0000000000000000000000000000000000000000..87cd3ff6186ed15446a27672c6286a1236b4557d --- /dev/null +++ b/qt/Widgets/Player/Player.hpp @@ -0,0 +1,42 @@ +#ifndef PLAYER_HPP +#define PLAYER_HPP + +#include +#include +#include +#include +#include +#include "Card.h" +#include "Media.h" + +namespace Ui { +class Player; +} + +class Player : public QWidget +{ + Q_OBJECT + +public: + explicit Player(QMediaPlayer *audioPlayer, QWidget *parent = nullptr); + ~Player(); + void download(QUrl url); + bool isLocal() const; + QString getSrc() const; + QString getInfo() const; + +public slots: + void set(SourceWithAdditionalInfo audio, bool local); + void onStateChanged(QMediaPlayer::State state); + void onStatusChanged(QMediaPlayer::MediaStatus status); + void onPlayClicked(); + +private: + Ui::Player *ui; + QMediaPlayer *audioPlayer; + QUrl url; + bool local_; + QString info; +}; + +#endif // PLAYER_HPP diff --git a/qt/Widgets/Player/Player.ui b/qt/Widgets/Player/Player.ui new file mode 100644 index 0000000000000000000000000000000000000000..f414cc96bceb39b8a59a1ba9a5d8cde5aba3a919 --- /dev/null +++ b/qt/Widgets/Player/Player.ui @@ -0,0 +1,64 @@ + + + Player + + + + 0 + 0 + 400 + 115 + + + + Form + + + + + + + 40 + 40 + + + + + + + + + + + + 16777215 + 100 + + + + + + + + + + playButton + clicked() + Player + onPlayClicked() + + + 36 + 59 + + + 9 + 7 + + + + + + onPlayClicked() + + diff --git a/qt/Widgets/SentencesWidget/CMakeLists.txt b/qt/Widgets/SentencesWidget/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..ac52d39d5e96a4e66f48e69a2ecf174a2c533ed7 --- /dev/null +++ b/qt/Widgets/SentencesWidget/CMakeLists.txt @@ -0,0 +1,23 @@ +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +find_package(Qt5 COMPONENTS Widgets REQUIRED) + + +set(PROJECT_SOURCES + SentencesWidget.hpp + SentencesWidget.cpp + SentencesWidget.ui +) + +add_library(sentences_widget ${PROJECT_SOURCES}) + +target_include_directories(sentences_widget PUBLIC ./) + +target_link_libraries(sentences_widget +Qt5::Widgets +sentence_plugin_wrapper +) diff --git a/qt/Widgets/SentencesWidget/SentencesWidget.cpp b/qt/Widgets/SentencesWidget/SentencesWidget.cpp new file mode 100644 index 0000000000000000000000000000000000000000..40cdb8ef07f88adeecae6caddc50358abce17b5a --- /dev/null +++ b/qt/Widgets/SentencesWidget/SentencesWidget.cpp @@ -0,0 +1,190 @@ +#include "SentencesWidget.hpp" +#include "spdlog/fmt/bundled/format.h" +#include "spdlog/spdlog.h" +#include "ui_SentencesWidget.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +SentencesWidget::SentencesWidget(std::unique_ptr sentencePlugin, + QWidget *parent) : + QWidget(parent), + ui(new Ui::SentencesWidget) +{ + ui->setupUi(this); + sentencePlugin_ = std::move(sentencePlugin); + sentencePlugin_->init("sentences"); + gridLayout = new QGridLayout; + gridLayout->setAlignment(Qt::AlignTop); + QWidget *sentencesListWidget = new QWidget; + sentencesListWidget->setObjectName("ListWidget"); + sentencesListWidget->setLayout(gridLayout); + ui->scrollArea->setWidget(sentencesListWidget); +} + +SentencesWidget::~SentencesWidget() +{ + delete ui; +} + +void SentencesWidget::addSentence(QString sentence, bool is_chosen) +{ + SPDLOG_INFO("Adding sentence with text: {}", sentence.toStdString()); + + QTextEdit *textEdit = new QTextEdit; + textEdit->setPlaceholderText("Sentence"); + textEdit->setText(sentence); + + QPushButton *pushButton = new QPushButton; + pushButton->setCheckable(true); + pushButton->setStyleSheet("QPushButton:checked { background-color: green; }"); + pushButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Expanding); + if (is_chosen) { + pushButton->setChecked(true); + } + + textEdit->setMaximumHeight(100); + pushButton->setMaximumHeight(100); + + int new_row = gridLayout->count() / gridLayout->columnCount(); + + gridLayout->addWidget(textEdit, new_row, 0); + gridLayout->addWidget(pushButton, new_row, 1); + + SPDLOG_INFO("Sentence added. Row count = {}", gridLayout->count() / gridLayout->columnCount()); +} + +void SentencesWidget::load(int batch_size) { + SPDLOG_INFO("Start loading sentences"); + currentWord = ui->loadSentenceLine->text().toStdString(); + if (currentWord.empty()) { + return; + } + auto [sentences, error] = sentencePlugin_->get(currentWord, batch_size, false); + + for (const std::string& sentence: sentences) { + addSentence(QString::fromStdString(sentence)); + } + + SPDLOG_INFO("Loaded {} sentences", sentences.size()); + SPDLOG_INFO("Message from sentencePlugin: {}", error); +} + +void SentencesWidget::clear() { + SPDLOG_INFO("Clear start. Row count = {}", gridLayout->count() / gridLayout->columnCount()); + QLayoutItem *item; + while ((item = gridLayout->takeAt(0)) != nullptr) { + QWidget *widget = item->widget(); + gridLayout->removeItem(item); + widget->deleteLater(); + delete item; + } + SPDLOG_INFO("Clear end. Row count = {}", gridLayout->count() / gridLayout->columnCount()); +} + +void SentencesWidget::set(std::string word, std::vector sentences, + std::vector chosen_mask) { + clear(); + currentWord = word; + ui->loadSentenceLine->setText(QString::fromStdString(currentWord)); + for (int i = 0; i < sentences.size(); ++i) { + bool chosen = i < chosen_mask.size() ? chosen_mask.at(i) : false; + addSentence(QString::fromStdString(sentences.at(i)), chosen); + } +} + +std::vector SentencesWidget::getSentences() { + std::vector sentences; + for (int row = 0; row < gridLayout->count() / gridLayout->columnCount(); ++row) { + QLayoutItem* textEditItem = gridLayout->itemAtPosition(row, 0); + if (!textEditItem) { + continue; + } + QTextEdit* textEdit = qobject_cast(textEditItem->widget()); + if (!textEdit) { + continue; + } + QString text = textEdit->toPlainText(); + if (!text.isEmpty()) { + sentences.push_back(text.toStdString()); + } + } + return sentences; +} + +std::vector SentencesWidget::getMask() { + std::vector sentencesMask; + for (int row = 0; row < gridLayout->count() / gridLayout->columnCount(); ++row) { + + QLayoutItem* textEditItem = gridLayout->itemAtPosition(row, 0); + if (!textEditItem) { + continue; + } + QTextEdit* textEdit = qobject_cast(textEditItem->widget()); + if (!textEdit) { + continue; + } + + QString text = textEdit->toPlainText(); + if (text.isEmpty()) { + continue; + } + + QLayoutItem* pushButtonItem = gridLayout->itemAtPosition(row, 1); + if (!pushButtonItem) { + continue; + } + + QPushButton* pushButton = qobject_cast(pushButtonItem->widget()); + if (!pushButton) { + continue; + } + sentencesMask.push_back(pushButton->isChecked()); + } + return sentencesMask; +} + +std::vector SentencesWidget::getChosenSentences() { + std::vector chosenSentences; + for (int row = 0; row < gridLayout->count() / gridLayout->columnCount(); ++row) { + + QLayoutItem* textEditItem = gridLayout->itemAtPosition(row, 0); + if (!textEditItem) { + continue; + } + QTextEdit* textEdit = qobject_cast(textEditItem->widget()); + if (!textEdit) { + continue; + } + + QString text = textEdit->toPlainText(); + if (text.isEmpty()) { + continue; + } + + QLayoutItem* pushButtonItem = gridLayout->itemAtPosition(row, 1); + if (!pushButtonItem) { + continue; + } + + QPushButton* pushButton = qobject_cast(pushButtonItem->widget()); + if (!pushButton) { + continue; + } + if (pushButton->isChecked()) { + chosenSentences.push_back(text.toStdString()); + } + } + return chosenSentences; +} diff --git a/qt/Widgets/SentencesWidget/SentencesWidget.hpp b/qt/Widgets/SentencesWidget/SentencesWidget.hpp new file mode 100644 index 0000000000000000000000000000000000000000..b43886ce3dce5a73e944ef5ffb3b893f29de04d8 --- /dev/null +++ b/qt/Widgets/SentencesWidget/SentencesWidget.hpp @@ -0,0 +1,40 @@ +#ifndef SENTENCESWIDGET_HPP +#define SENTENCESWIDGET_HPP + +#include +#include +#include +#include "ISentencePluginWrapper.h" + +namespace Ui { +class SentencesWidget; +} + +class SentencesWidget : public QWidget +{ + Q_OBJECT + +public: + explicit SentencesWidget(std::unique_ptr sentencePlugin, + QWidget *parent = nullptr); + ~SentencesWidget(); + +public slots: + void addSentence(QString sentence = QString(), bool is_chosen = false); + void load(int batch_size = 5); + void clear(); + void set(std::string word, std::vector sentences, + std::vector chosen_mask = std::vector() + ); + std::vector getSentences(); + std::vector getMask(); + std::vector getChosenSentences(); + +private: + Ui::SentencesWidget *ui; + QGridLayout *gridLayout; + std::string currentWord; + std::unique_ptr sentencePlugin_; +}; + +#endif // SENTENCESWIDGET_HPP diff --git a/qt/Widgets/SentencesWidget/SentencesWidget.ui b/qt/Widgets/SentencesWidget/SentencesWidget.ui new file mode 100644 index 0000000000000000000000000000000000000000..62b88fb81b299299e201d5adb8767919526d190a --- /dev/null +++ b/qt/Widgets/SentencesWidget/SentencesWidget.ui @@ -0,0 +1,96 @@ + + + SentencesWidget + + + + 0 + 0 + 400 + 300 + + + + Form + + + + + + + + Create + + + + + + + Load + + + + + + + + + + + + true + + + + + 0 + 0 + 374 + 231 + + + + + + + + + + + createButton + clicked() + SentencesWidget + addSentence() + + + 38 + 30 + + + 5 + 88 + + + + + LoadButton + clicked() + SentencesWidget + load() + + + 141 + 39 + + + 6 + 172 + + + + + + addSentence() + load() + + diff --git a/qt/main.cpp b/qt/main.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e7b5e2939ae085d945cf554d40ec18da43a15fa8 --- /dev/null +++ b/qt/main.cpp @@ -0,0 +1,35 @@ +#include "IImagePluginWrapper.h" +#include "ISentencePluginWrapper.h" +#include "Media.h" +#include "SentencePluginWrapper.h" +#include "AudioPluginWrapper.h" +#include "mainwindow.h" +#include "ImagePluginWrapper.h" +#include +#include +#include +#include "SentencesWidget.hpp" +#include "AudiosWidget.hpp" +#include "ImagesWidget.hpp" +#include "ServerConnection.h" +#include "Card.h" +#include "spdlog/spdlog.h" +#include "Player.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +int main(int argc, char *argv[]) { + QApplication app(argc, argv); + MainWindow wgt; + wgt.show(); + app.exec(); + return 0; +} diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..5dcce1fa5be6c07f0ddd9118c85798c494b853ee --- /dev/null +++ b/server/CMakeLists.txt @@ -0,0 +1,19 @@ +find_package(nlohmann_json 3.7.0 REQUIRED) + +find_package(Boost COMPONENTS filesystem system date_time python REQUIRED) +message("Include dirs of boost: " ${Boost_INCLUDE_DIRS}) +message("Libs of boost: " ${Boost_LIBRARIES}) + +find_package(PythonLibs REQUIRED) +message("Include dirs of Python: " ${PYTHON_INCLUDE_DIRS}) +message("Libs of Python: " ${PYTHON_LIBRARIES}) + +add_subdirectory(lib) + +add_executable(server_main main.cpp) +target_link_libraries(server_main + plugin_server + plugins_provider +) +target_include_directories(server_main PRIVATE {}) +add_subdirectory(test_client) diff --git a/server/lib/CMakeLists.txt b/server/lib/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..004b0ccf5e538e8e0ed9d5db29aa406d4a739afc --- /dev/null +++ b/server/lib/CMakeLists.txt @@ -0,0 +1,3 @@ +add_subdirectory(query_language) +add_subdirectory(plugins) +add_subdirectory(network) diff --git a/server/lib/network/CMakeLists.txt b/server/lib/network/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..59fd31112bde34a3534ecf6d46d380f1d4c3c237 --- /dev/null +++ b/server/lib/network/CMakeLists.txt @@ -0,0 +1,3 @@ +add_subdirectory(ResponseGenerators) +add_subdirectory(Session) +add_subdirectory(Server) diff --git a/server/lib/network/ResponseGenerators/CMakeLists.txt b/server/lib/network/ResponseGenerators/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..e43584caf7e3c7bfc8a9c9f83d9807fb132e602e --- /dev/null +++ b/server/lib/network/ResponseGenerators/CMakeLists.txt @@ -0,0 +1,13 @@ +message(${CMAKE_CURRENT_SOURCE_DIR}) + +add_library(response_generators ResponseGenerators.cpp) +target_include_directories(response_generators + PUBLIC + ./ +) +target_link_libraries(response_generators + querying + # query_lang + plugins_bundle + plugins_provider +) diff --git a/server/lib/network/ResponseGenerators/ResponseGenerators.cpp b/server/lib/network/ResponseGenerators/ResponseGenerators.cpp new file mode 100644 index 0000000000000000000000000000000000000000..1690a4b54299c02c24b58e53a8e00b39de615698 --- /dev/null +++ b/server/lib/network/ResponseGenerators/ResponseGenerators.cpp @@ -0,0 +1,757 @@ +#include "ResponseGenerators.hpp" +#include "AudiosProviderWrapper.hpp" +#include "DefinitionsProviderWrapper.hpp" +#include "FormatProcessorWrapper.hpp" +#include "IAudiosProviderWrapper.hpp" +#include "IFormatProcessorWrapper.hpp" +#include "IImagesProviderWrapper.hpp" +#include "IPluginWrapper.hpp" +#include "ISentencesProviderWrapper.hpp" +#include "ImagesProviderWrapper.hpp" +#include "PyExceptionInfo.hpp" +#include "SentencesProviderWrapper.hpp" +#include "exception.hpp" +#include "querying.hpp" +#include "spdlog/common.h" +#include "spdlog/spdlog.h" + +#include +#include +#include +#include +#include +#include +#include + +using nlohmann::json; +using std::string_literals::operator""s; + +ResponseGenerator::ResponseGenerator( + std::shared_ptr plugins_provider) + : plugins_provider_(std::move(plugins_provider)) { +} + +static auto return_error(const std::string &message) -> json { + json dst; + dst["status"] = 1; + dst["message"] = message; + SPDLOG_ERROR(message); + return dst; +} + +auto ResponseGenerator::handle(const std::string &request) -> json { + SPDLOG_INFO("Got new request"); + json parsed_request; + try { + parsed_request = json::parse(request); + } catch (json::exception &e) { + return return_error(e.what()); + } + if (!parsed_request.is_object()) { + return return_error("Got non-json object as a request"); + } + if (!parsed_request.contains("query_type")) { + return return_error( + "Every server request has to have \"query_type\" field"); + } + json json_query_type = parsed_request[QUERY_TYPE_FIELD]; + if (!json_query_type.is_string()) { + return return_error("\""s + QUERY_TYPE_FIELD + + "\" field is has to be a string"); + } + std::string query_type = json_query_type; + if (query_type == INIT_QUERY_TYPE) { + SPDLOG_INFO("Started handling `{}` request", INIT_QUERY_TYPE); + return handle_init(parsed_request); + } + if (query_type == GET_DEFAULT_CONFIG_QUERY_TYPE) { + SPDLOG_INFO("Started handling `{}` request", + GET_DEFAULT_CONFIG_QUERY_TYPE); + return handle_get_default_config(parsed_request); + } + if (query_type == GET_CONFIG_SCHEME_QUERY_TYPE) { + SPDLOG_INFO("Started handling `{}` request", + GET_CONFIG_SCHEME_QUERY_TYPE); + return handle_get_config_scheme(parsed_request); + } + if (query_type == VALIDATE_CONFIG_QUERY_TYPE) { + SPDLOG_INFO("Started handling `{}` request", + VALIDATE_CONFIG_QUERY_TYPE); + return handle_validate_config(parsed_request); + } + if (query_type == LIST_PLUGINS_QUERY_TYPE) { + SPDLOG_INFO("Started handling `{}` request", LIST_PLUGINS_QUERY_TYPE); + return handle_list_plugins(parsed_request); + } + if (query_type == LOAD_NEW_PLUGINS_QUERY_TYPE) { + SPDLOG_INFO("Started handling `{}` request", + LOAD_NEW_PLUGINS_QUERY_TYPE); + return handle_load_new_plugins(parsed_request); + } + if (query_type == GET_QUERY_TYPE) { + SPDLOG_INFO("Started handling `{}` request", GET_QUERY_TYPE); + return handle_get(parsed_request); + } + if (query_type == GET_DICT_SCHEME_QUERY_TYPE) { + SPDLOG_INFO("Started handling `{}` request", + GET_DICT_SCHEME_QUERY_TYPE); + return handle_get_dict_scheme(parsed_request); + } + if (query_type == SAVE_QUERY_TYPE) { + SPDLOG_INFO("Started handling `{}` request", SAVE_QUERY_TYPE); + return handle_save(parsed_request); + } + return return_error("Unknown query type: "s + query_type); +} + +auto ResponseGenerator::handle_init(const nlohmann::json &request) + -> nlohmann::json { + if (!request.contains(PLUGIN_TYPE_FIELD)) { + return return_error("\""s + PLUGIN_TYPE_FIELD + + "\" filed was not found in request"); + } + if (!request[PLUGIN_TYPE_FIELD].is_string()) { + return return_error("\""s + PLUGIN_TYPE_FIELD + + "\" field is expected to be a string"); + } + auto plugin_type = request[PLUGIN_TYPE_FIELD].get(); + + if (!request.contains(PLUGIN_NAME_FIELD)) { + return return_error("\""s + PLUGIN_NAME_FIELD + + "\" filed was not found in request"); + } + if (!request[PLUGIN_NAME_FIELD].is_string()) { + return return_error("\""s + PLUGIN_NAME_FIELD + + "\" field is expected to be a string"); + } + auto plugin_name = request[PLUGIN_NAME_FIELD].get(); + + if (plugin_type == DEFINITION_PROVIDER_PLUGIN_TYPE) { + SPDLOG_INFO("Handling {} definitions provider's initialization request", + plugin_name); + + auto requested_wrapper_option = + plugins_provider_->get_definitions_provider(plugin_name); + if (!requested_wrapper_option.has_value()) { + return return_error("\"" + plugin_name + + "\" definition provider not found"); + } + auto wrapper_variant = std::move(requested_wrapper_option.value()); + if (std::holds_alternative(wrapper_variant)) { + auto exception_info = std::get(wrapper_variant); + + return return_error("Exception was thrown during \"" + plugin_name + + "\" definitions provider's construction:\n" + + exception_info.stack_trace()); + } + + auto wrapper = + std::move(std::get>>( + wrapper_variant)); + + plugins_bundle_.set_definitions_provider(std::move(wrapper)); + + SPDLOG_INFO("Successfully handled {} definition provider's " + "initialization request", + plugin_name); + return R"({"status": 0, "message": ""})"_json; + } + if (plugin_type == SENTENCES_PROVIDER_PLUGIN_TYPE) { + SPDLOG_INFO("Handling {} sentences provider's initialization request", + plugin_name); + + auto requested_wrapper_option = + plugins_provider_->get_sentences_provider(plugin_name); + if (!requested_wrapper_option.has_value()) { + return return_error("\"" + plugin_name + + "\" sentences provider not found"); + } + auto &wrapper_variant = requested_wrapper_option.value(); + if (std::holds_alternative(wrapper_variant)) { + auto exception_info = std::get(wrapper_variant); + + return return_error("Exception was thrown during \"" + plugin_name + + "\" sentence provider's construction:\n" + + exception_info.stack_trace()); + } + auto wrapper = + std::move(std::get>>( + wrapper_variant)); + plugins_bundle_.set_sentences_provider(std::move(wrapper)); + + SPDLOG_INFO("Successfully handled {} sentences provider's " + "initialization request", + plugin_name); + return R"({"status": 0, "message": ""})"_json; + } + if (plugin_type == IMAGES_PROVIDER_PLUGIN_TYPE) { + SPDLOG_INFO("Handling {} images provider's initialization request", + plugin_name); + + auto requested_wrapper_option = + plugins_provider_->get_images_provider(plugin_name); + if (!requested_wrapper_option.has_value()) { + return return_error("\"" + plugin_name + + "\" images provider not found"); + } + auto &wrapper_variant = requested_wrapper_option.value(); + if (std::holds_alternative(wrapper_variant)) { + auto exception_info = std::get(wrapper_variant); + + return return_error("Exception was thrown during \"" + plugin_name + + "\" images provider's construction:\n" + + exception_info.stack_trace()); + } + auto wrapper = std::move( + std::get< + std::unique_ptr>>( + wrapper_variant)); + plugins_bundle_.set_images_provider(std::move(wrapper)); + + SPDLOG_INFO("Successfully handled {} images provider's " + "initialization request", + plugin_name); + return R"({"status": 0, "message": ""})"_json; + } + if (plugin_type == AUDIOS_PROVIDER_PLUGIN_TYPE) { + SPDLOG_INFO("Handling {} audios provider's initialization request", + plugin_name); + + auto requested_wrapper_option = + plugins_provider_->get_audios_provider(plugin_name); + if (!requested_wrapper_option.has_value()) { + return return_error("\"" + plugin_name + + "\" audios provider not found"); + } + auto &wrapper_variant = requested_wrapper_option.value(); + if (std::holds_alternative(wrapper_variant)) { + auto exception_info = std::get(wrapper_variant); + + return return_error("Exception was thrown during \"" + plugin_name + + "\" audios provider's construction:\n" + + exception_info.stack_trace()); + } + auto wrapper = std::move( + std::get< + std::unique_ptr>>( + wrapper_variant)); + plugins_bundle_.set_audios_provider(std::move(wrapper)); + + SPDLOG_INFO("Successfully handled {} audios provider's " + "initialization request", + plugin_name); + return R"({"status": 0, "message": ""})"_json; + } + if (plugin_type == FORMAT_PROCESSOR_PLUGIN_TYPE) { + SPDLOG_INFO("Handling {} format processor's initialization request", + plugin_name); + + auto requested_wrapper_option = + plugins_provider_->get_format_processor(plugin_name); + if (!requested_wrapper_option.has_value()) { + return return_error("\"" + plugin_name + + "\" format processor not found"); + } + auto &wrapper_variant = requested_wrapper_option.value(); + if (std::holds_alternative(wrapper_variant)) { + auto exception_info = std::get(wrapper_variant); + + return return_error("Exception was thrown during \"" + plugin_name + + "\" format processor's construction:\n" + + exception_info.stack_trace()); + } + auto wrapper = + std::move(std::get>>( + wrapper_variant)); + plugins_bundle_.set_format_processor(std::move(wrapper)); + + SPDLOG_INFO("Successfully handled {} format processor's " + "initialization request", + plugin_name); + return R"({"status": 0, "message": ""})"_json; + } + return return_error("Unknown plugin_type: " + plugin_type); +} + +auto ResponseGenerator::handle_get_default_config(const nlohmann::json &request) + -> nlohmann::json { + return return_error("handle_get_default_config() is not implemented"); +} + +auto ResponseGenerator::handle_get_config_scheme(const nlohmann::json &request) + -> nlohmann::json { + return return_error("handle_get_config_scheme() is not implemented"); +} + +auto ResponseGenerator::handle_validate_config(const nlohmann::json &request) + -> nlohmann::json { + using std::string_literals::operator""s; + + if (!request.contains(PLUGIN_TYPE_FIELD)) { + return return_error("\""s + PLUGIN_TYPE_FIELD + + "\" filed was not found in request"); + } + if (!request[PLUGIN_TYPE_FIELD].is_string()) { + return return_error("\""s + PLUGIN_TYPE_FIELD + + "\" field is expected to be a string"); + } + auto plugin_type = request[PLUGIN_TYPE_FIELD].get(); + + if (!request.contains(CONFIG_FIELD)) { + return return_error("\""s + CONFIG_FIELD + + "\" filed was not found in request"); + } + auto config = request[CONFIG_FIELD]; + + IPluginWrapper *provider; + if (plugin_type == DEFINITION_PROVIDER_PLUGIN_TYPE) { + provider = plugins_bundle_.definitions_provider(); + if (provider == nullptr) { + return return_error("Definitions provider is not initialized"); + } + } else if (plugin_type == SENTENCES_PROVIDER_PLUGIN_TYPE) { + provider = plugins_bundle_.sentences_provider(); + if (provider == nullptr) { + return return_error("Sentences provider is not initialized"); + } + } else if (plugin_type == IMAGES_PROVIDER_PLUGIN_TYPE) { + provider = plugins_bundle_.images_provider(); + if (provider == nullptr) { + return return_error("Images provider is not initialized"); + } + } else if (plugin_type == AUDIOS_PROVIDER_PLUGIN_TYPE) { + provider = plugins_bundle_.audios_provider(); + if (provider == nullptr) { + return return_error("Audios provider is not initialized"); + } + } else if (plugin_type == FORMAT_PROCESSOR_PLUGIN_TYPE) { + provider = plugins_bundle_.format_processor(); + if (provider == nullptr) { + return return_error("Format processor is not initialized"); + } + } else { + return return_error("Unknown plugin type for `validate_config`: " + + plugin_type); + } + + auto res_or_error = provider->validate_config(std::move(config)); + + if (std::holds_alternative(res_or_error)) { + auto error = std::get(res_or_error); + return return_error(error.stack_trace()); + } + auto res = std::get(res_or_error); + auto response = nlohmann::json(); + response["status"] = 0; + response["result"] = res; + return response; +} + +auto ResponseGenerator::handle_list_plugins(const nlohmann::json &request) + -> nlohmann::json { + if (!request.contains(PLUGIN_TYPE_FIELD)) { + return return_error("\""s + PLUGIN_TYPE_FIELD + + "\" filed was not found in request"); + } + if (!request[PLUGIN_TYPE_FIELD].is_string()) { + return return_error("\""s + PLUGIN_TYPE_FIELD + + "\" field is expected to be a string"); + } + auto plugin_type = request[PLUGIN_TYPE_FIELD].get(); + + PluginsInfo listings; + if (plugin_type == DEFINITION_PROVIDER_PLUGIN_TYPE) { + listings = plugins_provider_->list_definitions_providers(); + } else if (plugin_type == SENTENCES_PROVIDER_PLUGIN_TYPE) { + listings = plugins_provider_->list_sentences_providers(); + } else if (plugin_type == IMAGES_PROVIDER_PLUGIN_TYPE) { + listings = plugins_provider_->list_images_providers(); + } else if (plugin_type == AUDIOS_PROVIDER_PLUGIN_TYPE) { + listings = plugins_provider_->list_audios_providers(); + } else if (plugin_type == FORMAT_PROCESSOR_PLUGIN_TYPE) { + listings = plugins_provider_->list_format_processors(); + } else { + return return_error( + "Unknown plugin type for `list_plugins` request: "s + plugin_type); + } + json res; + res["status"] = 0; + res["result"] = listings; + return res; +} + +auto ResponseGenerator::handle_load_new_plugins(const nlohmann::json &request) + -> nlohmann::json { + return return_error("handle_load_new_plugins() is not implemented"); +} + +auto ResponseGenerator::handle_get(const nlohmann::json &request) + -> nlohmann::json { + if (!request.contains(PLUGIN_TYPE_FIELD)) { + return return_error("\""s + PLUGIN_TYPE_FIELD + + "\" filed was not found in request"); + } + if (!request[PLUGIN_TYPE_FIELD].is_string()) { + return return_error("\""s + PLUGIN_TYPE_FIELD + + "\" field is expected to be a string"); + } + auto plugin_type = request[PLUGIN_TYPE_FIELD].get(); + + if (plugin_type == DEFINITION_PROVIDER_PLUGIN_TYPE) { + return handle_get_definitions(request); + } + if (plugin_type == SENTENCES_PROVIDER_PLUGIN_TYPE) { + return handle_get_sentences(request); + } + if (plugin_type == IMAGES_PROVIDER_PLUGIN_TYPE) { + return handle_get_images(request); + } + if (plugin_type == AUDIOS_PROVIDER_PLUGIN_TYPE) { + return handle_get_audios(request); + } + return return_error("Unknown plugin type for `get` request: "s + + plugin_type); +} + +auto ResponseGenerator::handle_get_definitions(const nlohmann::json &request) + -> nlohmann::json { + SPDLOG_INFO("Starting handling `get` request for definitions"); + + auto *provider = plugins_bundle_.definitions_provider(); + if (provider == nullptr) { + return return_error("Definitions provider is not initialized"); + } + + if (!request.contains(WORD_FIELD)) { + return return_error("\""s + WORD_FIELD + + "\" filed was not found in request"); + } + if (!request[WORD_FIELD].is_string()) { + return return_error("\""s + WORD_FIELD + + "\" field is expected to be a string"); + } + auto word = request[WORD_FIELD].get(); + + if (!request.contains(FILTER_QUERY_FIELD)) { + return return_error("\""s + FILTER_QUERY_FIELD + + "\" fieled was not found in request"); + } + if (!request[FILTER_QUERY_FIELD].is_string()) { + return return_error("\""s + FILTER_QUERY_FIELD + + "\" field is expected to be a string"); + } + std::string filter_query = request[FILTER_QUERY_FIELD].get(); + + if (!request.contains(BATCH_SIZE_FIELD)) { + return return_error("\""s + BATCH_SIZE_FIELD + + "\" filed was not found in request"); + } + if (!request[BATCH_SIZE_FIELD].is_number()) { + return return_error("\""s + BATCH_SIZE_FIELD + + "\" field is expected to be a number"); + } + auto batch_size = request[BATCH_SIZE_FIELD].get(); + + if (!request.contains(RESTART_FIELD)) { + return return_error("\""s + RESTART_FIELD + + "\" filed was not found in request"); + } + if (!request[RESTART_FIELD].is_boolean()) { + return return_error("\""s + RESTART_FIELD + + "\" field is expected to be a boolean"); + } + auto restart = request[RESTART_FIELD].get(); + + std::function(const nlohmann::json &)> + filter_function; + try { + filter_function = prepare_filter(filter_query); + } catch (const ComponentException &error) { + return return_error("Could'n parse query. Reason: "s + error.what()); + } + DefinitionsProviderWrapper::type batch; + while (batch_size && batch.second.empty()) { + auto result_or_error = provider->get(word, batch_size, restart); + if (std::holds_alternative(result_or_error)) { + auto exception_info = std::get(result_or_error); + return return_error("Exception was thrown during \"" + + provider->name() + "\" definitions request:\n" + + exception_info.stack_trace()); + } + if (std::holds_alternative( + result_or_error)) { + auto result = + std::get(result_or_error); + + if (result.first.empty()) { + batch.second = std::move(result.second); + break; + } + + auto insertion_res = + result.first | + std::ranges::views::filter( + [&filter_function](const Card &item) -> bool { + auto filtration_res = filter_function(item); + if (std::holds_alternative(filtration_res)) { + auto res = std::get(filtration_res); + return res; + } + auto res = std::get(filtration_res); + SPDLOG_WARN("Couldn't filter card, thus evaluating " + "filter as `false`. Reason: {}", + res); + return false; + }); + + batch.first.insert( + batch.first.end(), insertion_res.begin(), insertion_res.end()); + batch.second = std::move(result.second); + batch_size = (batch.first.size() >= batch_size) + ? 0 + : batch_size - batch.first.size(); + } else { + auto non_python_error_message = + std::get(result_or_error); + + auto json_message = R"({"status": 1, "message": ")"s + + non_python_error_message + R"("})"; + + return json::parse(json_message); + } + } + + json res; + res["status"] = static_cast(!batch.second.empty()); + res["result"] = batch; + SPDLOG_INFO("Successfully handled `get` request for definitions"); + return res; +} + +auto ResponseGenerator::handle_get_sentences(const nlohmann::json &request) + -> nlohmann::json { + SPDLOG_INFO("Starting handling `get` request for sentences"); + + auto *provider = plugins_bundle_.sentences_provider(); + if (provider == nullptr) { + return return_error("Sentences provider is not initialized"); + } + + if (!request.contains(WORD_FIELD)) { + return return_error("\""s + WORD_FIELD + + "\" filed was not found in request"); + } + if (!request[WORD_FIELD].is_string()) { + return return_error("\""s + WORD_FIELD + + "\" field is expected to be a string"); + } + auto word = request[WORD_FIELD].get(); + + if (!request.contains(BATCH_SIZE_FIELD)) { + return return_error("\""s + BATCH_SIZE_FIELD + + "\" filed was not found in request"); + } + if (!request[BATCH_SIZE_FIELD].is_number()) { + return return_error("\""s + BATCH_SIZE_FIELD + + "\" field is expected to be a number"); + } + auto batch_size = request[BATCH_SIZE_FIELD].get(); + + if (!request.contains(RESTART_FIELD)) { + return return_error("\""s + RESTART_FIELD + + "\" filed was not found in request"); + } + if (!request[RESTART_FIELD].is_boolean()) { + return return_error("\""s + RESTART_FIELD + + "\" field is expected to be a boolean"); + } + auto restart = request[RESTART_FIELD].get(); + + auto result_or_error = provider->get(word, batch_size, restart); + if (std::holds_alternative(result_or_error)) { + auto exception_info = std::get(result_or_error); + return return_error("Exception was thrown during \"" + + provider->name() + "\" sentences request:\n" + + exception_info.stack_trace()); + } + if (std::holds_alternative( + result_or_error)) { + auto result = std::get(result_or_error); + json res; + res["status"] = 0; + res["result"] = result; + SPDLOG_INFO("Successfully handled `get` request for sentences"); + return res; + } + auto non_python_error_message = std::get(result_or_error); + + auto json_message = + R"({"status": 1, "message": ")"s + non_python_error_message + R"("})"; + + return json::parse(json_message); +} + +auto ResponseGenerator::handle_get_images(const nlohmann::json &request) + -> nlohmann::json { + SPDLOG_INFO("Starting handling `get` request for images"); + + auto *provider = plugins_bundle_.images_provider(); + if (provider == nullptr) { + return return_error("Images provider is not initialized"); + } + + if (!request.contains(WORD_FIELD)) { + return return_error("\""s + WORD_FIELD + + "\" filed was not found in request"); + } + if (!request[WORD_FIELD].is_string()) { + return return_error("\""s + WORD_FIELD + + "\" field is expected to be a string"); + } + auto word = request[WORD_FIELD].get(); + + if (!request.contains(BATCH_SIZE_FIELD)) { + return return_error("\""s + BATCH_SIZE_FIELD + + "\" filed was not found in request"); + } + if (!request[BATCH_SIZE_FIELD].is_number()) { + return return_error("\""s + BATCH_SIZE_FIELD + + "\" field is expected to be a number"); + } + auto batch_size = request[BATCH_SIZE_FIELD].get(); + + if (!request.contains(RESTART_FIELD)) { + return return_error("\""s + RESTART_FIELD + + "\" filed was not found in request"); + } + if (!request[RESTART_FIELD].is_boolean()) { + return return_error("\""s + RESTART_FIELD + + "\" field is expected to be a boolean"); + } + auto restart = request[RESTART_FIELD].get(); + + auto result_or_error = provider->get(word, batch_size, restart); + if (std::holds_alternative(result_or_error)) { + auto exception_info = std::get(result_or_error); + return return_error("Exception was thrown during \"" + + provider->name() + "\" images request:\n" + + exception_info.stack_trace()); + } + if (std::holds_alternative(result_or_error)) { + auto result = std::get(result_or_error); + json res; + res["status"] = 0; + res["result"] = result; + SPDLOG_INFO("Successfully handled `get` request for images"); + return res; + } + auto non_python_error_message = std::get(result_or_error); + + auto json_message = + R"({"status": 1, "message": ")"s + non_python_error_message + R"("})"; + + return json::parse(json_message); +} + +auto ResponseGenerator::handle_get_audios(const nlohmann::json &request) + -> nlohmann::json { + SPDLOG_INFO("Starting handling `get` request for audios"); + + auto *provider = plugins_bundle_.audios_provider(); + if (provider == nullptr) { + return return_error("Audios provider is not initialized"); + } + + if (!request.contains(WORD_FIELD)) { + return return_error("\""s + WORD_FIELD + + "\" filed was not found in request"); + } + if (!request[WORD_FIELD].is_string()) { + return return_error("\""s + WORD_FIELD + + "\" field is expected to be a string"); + } + auto word = request[WORD_FIELD].get(); + + if (!request.contains(BATCH_SIZE_FIELD)) { + return return_error("\""s + BATCH_SIZE_FIELD + + "\" filed was not found in request"); + } + if (!request[BATCH_SIZE_FIELD].is_number()) { + return return_error("\""s + BATCH_SIZE_FIELD + + "\" field is expected to be a number"); + } + auto batch_size = request[BATCH_SIZE_FIELD].get(); + + if (!request.contains(RESTART_FIELD)) { + return return_error("\""s + RESTART_FIELD + + "\" filed was not found in request"); + } + if (!request[RESTART_FIELD].is_boolean()) { + return return_error("\""s + RESTART_FIELD + + "\" field is expected to be a boolean"); + } + auto restart = request[RESTART_FIELD].get(); + + auto result_or_error = provider->get(word, batch_size, restart); + if (std::holds_alternative(result_or_error)) { + auto exception_info = std::get(result_or_error); + return return_error("Exception was thrown during \"" + + provider->name() + "\" definitions request:\n" + + exception_info.stack_trace()); + } + if (std::holds_alternative(result_or_error)) { + auto result = std::get(result_or_error); + json res; + res["status"] = 0; + res["result"] = result; + SPDLOG_INFO("Successfully handled `get` request for audios"); + return res; + } + auto non_python_error_message = std::get(result_or_error); + + return return_error(non_python_error_message); +} + +auto ResponseGenerator::handle_save(const nlohmann::json &request) + -> nlohmann::json { + auto *provider = plugins_bundle_.format_processor(); + if (provider == nullptr) { + return return_error("Format Processor is not initialized"); + } + + if (!request.contains(CARDS_PATH_FIELD)) { + return return_error("\""s + CARDS_PATH_FIELD + + "\" filed was not found in request"); + } + if (!request[CARDS_PATH_FIELD].is_string()) { + return return_error("\""s + CARDS_PATH_FIELD + + "\" field is expected to be a string"); + } + auto cards_path_field = request[CARDS_PATH_FIELD].get(); + auto string_error_or_py_exception = provider->save({cards_path_field}); + + if (std::holds_alternative(string_error_or_py_exception)) { + auto py_exception_info = + std::get(string_error_or_py_exception); + return return_error(py_exception_info.stack_trace()); + } + auto string_error = std::get(string_error_or_py_exception); + if (string_error.empty()) { + SPDLOG_INFO("Successfully handled `save` request"); + return R"({"status": 0, "message": ""})"_json; + } + return return_error(string_error); +} + +auto ResponseGenerator::handle_get_dict_scheme(const nlohmann::json &request) + -> nlohmann::json { + return return_error("handle_get_dict_scheme() is not implemented"); +} diff --git a/server/lib/network/ResponseGenerators/ResponseGenerators.hpp b/server/lib/network/ResponseGenerators/ResponseGenerators.hpp new file mode 100644 index 0000000000000000000000000000000000000000..0afb18000d1b85ccf9b951d57dc43447a19ea870 --- /dev/null +++ b/server/lib/network/ResponseGenerators/ResponseGenerators.hpp @@ -0,0 +1,94 @@ +#ifndef RESPONSE_GENERATORS_H +#define RESPONSE_GENERATORS_H + +#include "PluginsBundle.hpp" +#include "PluginsProvider.hpp" +#include +#include +#include +#include + +class IResponceGenerator { + public: + virtual ~IResponceGenerator() = default; + virtual auto handle(const std::string &request) -> nlohmann::json = 0; + + private: + virtual auto handle_init(const nlohmann::json &) -> nlohmann::json = 0; + virtual auto handle_get_default_config(const nlohmann::json &) + -> nlohmann::json = 0; + virtual auto handle_get_config_scheme(const nlohmann::json &) + -> nlohmann::json = 0; + virtual auto handle_validate_config(const nlohmann::json &) + -> nlohmann::json = 0; + virtual auto handle_list_plugins(const nlohmann::json &) + -> nlohmann::json = 0; + virtual auto handle_load_new_plugins(const nlohmann::json &) + -> nlohmann::json = 0; + virtual auto handle_get(const nlohmann::json &) -> nlohmann::json = 0; + virtual auto handle_get_dict_scheme(const nlohmann::json &) + -> nlohmann::json = 0; +}; + +class ResponseGenerator : public IResponceGenerator { + public: + explicit ResponseGenerator( + std::shared_ptr plugins_provider); + + auto handle(const std::string &request) -> nlohmann::json override; + + private: + static constexpr auto QUERY_TYPE_FIELD = "query_type"; + static constexpr auto PLUGIN_TYPE_FIELD = "plugin_type"; + static constexpr auto CONFIG_FIELD = "config"; + static constexpr auto PLUGIN_NAME_FIELD = "plugin_name"; + static constexpr auto FILTER_QUERY_FIELD = "filter"; + static constexpr auto WORD_FIELD = "word"; + static constexpr auto BATCH_SIZE_FIELD = "batch_size"; + static constexpr auto RESTART_FIELD = "restart"; + static constexpr auto CARDS_PATH_FIELD = "cards_path"; + + static constexpr auto DEFINITION_PROVIDER_PLUGIN_TYPE = "word"; + static constexpr auto SENTENCES_PROVIDER_PLUGIN_TYPE = "sentences"; + static constexpr auto AUDIOS_PROVIDER_PLUGIN_TYPE = "audios"; + static constexpr auto IMAGES_PROVIDER_PLUGIN_TYPE = "images"; + static constexpr auto FORMAT_PROCESSOR_PLUGIN_TYPE = "format"; + + static constexpr auto INIT_QUERY_TYPE = "init"; + static constexpr auto LIST_PLUGINS_QUERY_TYPE = "list_plugins"; + static constexpr auto GET_DEFAULT_CONFIG_QUERY_TYPE = "get_default_config"; + static constexpr auto GET_CONFIG_SCHEME_QUERY_TYPE = "get_confit_scheme"; + static constexpr auto VALIDATE_CONFIG_QUERY_TYPE = "validate_config"; + static constexpr auto LOAD_NEW_PLUGINS_QUERY_TYPE = "load_new_plugins"; + static constexpr auto GET_QUERY_TYPE = "get"; + static constexpr auto SAVE_QUERY_TYPE = "save"; + static constexpr auto GET_DICT_SCHEME_QUERY_TYPE = "get_dict_scheme"; + + auto handle_init(const nlohmann::json &request) -> nlohmann::json override; + auto handle_get_default_config(const nlohmann::json &request) + -> nlohmann::json override; + auto handle_get_config_scheme(const nlohmann::json &request) + -> nlohmann::json override; + auto handle_validate_config(const nlohmann::json &request) + -> nlohmann::json override; + auto handle_list_plugins(const nlohmann::json &request) + -> nlohmann::json override; + auto handle_load_new_plugins(const nlohmann::json &request) + -> nlohmann::json override; + auto handle_get(const nlohmann::json &request) -> nlohmann::json override; + auto handle_get_dict_scheme(const nlohmann::json &request) + -> nlohmann::json override; + + auto handle_get_definitions(const nlohmann::json &request) + -> nlohmann::json; + auto handle_get_sentences(const nlohmann::json &request) -> nlohmann::json; + auto handle_get_images(const nlohmann::json &request) -> nlohmann::json; + auto handle_get_audios(const nlohmann::json &request) -> nlohmann::json; + auto handle_save(const nlohmann::json &request) -> nlohmann::json; + + private: + PluginsBundle plugins_bundle_; + const std::shared_ptr plugins_provider_; +}; + +#endif // !RESPONSE_GENERATORS_H diff --git a/server/lib/network/Server/CMakeLists.txt b/server/lib/network/Server/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..9ca53d26f190e9595178e744b6afac1045b6b2c0 --- /dev/null +++ b/server/lib/network/Server/CMakeLists.txt @@ -0,0 +1,15 @@ +message(${CMAKE_CURRENT_SOURCE_DIR}) + +add_library(plugin_server Server.cpp) +target_link_libraries(plugin_server + session + ${Boost_LIBRARIES} + ${PYTHON_LIBRARIES} +) +target_include_directories(plugin_server + PUBLIC + ./ + PRIVATE + ${Boost_INCLUDE_DIRS} + ${PYTHON_INCLUDE_DIRS} +) diff --git a/server/lib/network/Server/Server.cpp b/server/lib/network/Server/Server.cpp new file mode 100644 index 0000000000000000000000000000000000000000..93b86211db76db6914e1c85e6133f5ccbb7046f5 --- /dev/null +++ b/server/lib/network/Server/Server.cpp @@ -0,0 +1,47 @@ +#include "Server.hpp" +#include "ResponseGenerators.hpp" +#include "Session.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using boost::asio::ip::tcp; +using nlohmann::json; + +PluginServer::PluginServer(std::shared_ptr &&provider, + boost::asio::io_context &context, + uint16_t port) + : io_context_(context), + acceptor_(io_context_, tcp::endpoint(tcp::v4(), port)), + plugins_provider_(std::move(provider)) { + SPDLOG_INFO("Initializing Python interpreter"); + Py_Initialize(); + SPDLOG_INFO("Successfully initialized Python interpreter"); + + SPDLOG_INFO("Starting accepting requests"); + start_accept(); +} + +void PluginServer::start_accept() { + acceptor_.async_accept([this](boost::system::error_code ec, + tcp::socket socket) { + if (!ec) { + auto test = std::make_unique(plugins_provider_); + std::make_shared(std::move(socket), std::move(test)) + ->start(); + } + start_accept(); + }); +} diff --git a/server/lib/network/Server/Server.hpp b/server/lib/network/Server/Server.hpp new file mode 100644 index 0000000000000000000000000000000000000000..7d7efda7efd26dffc432d8236b6fda991bafa200 --- /dev/null +++ b/server/lib/network/Server/Server.hpp @@ -0,0 +1,42 @@ +#ifndef SERVER_H +#define SERVER_H + +#include "PluginsProvider.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class PluginServer { + public: + explicit PluginServer(std::shared_ptr &&provider, + boost::asio::io_context &context, + uint16_t port); + + ~PluginServer() = default; + PluginServer(const PluginServer &) = delete; + PluginServer(PluginServer &&) = delete; + auto operator=(const PluginServer &) -> PluginServer & = delete; + auto operator=(PluginServer &&) -> PluginServer & = delete; + + private: + boost::asio::io_context &io_context_; + boost::asio::ip::tcp::acceptor acceptor_; + std::shared_ptr plugins_provider_; + + void start_accept(); + + void handle_accept(const boost::system::error_code &error); +}; + +#endif // SERVER_H! diff --git a/server/lib/network/Session/CMakeLists.txt b/server/lib/network/Session/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..ab5aaa5b444b103d4fe1927089ac8f0d0c80d733 --- /dev/null +++ b/server/lib/network/Session/CMakeLists.txt @@ -0,0 +1,10 @@ +message(${CMAKE_CURRENT_SOURCE_DIR}) + +add_library(session Session.cpp) +target_include_directories(session PUBLIC + ./ +) + +target_link_libraries(session + response_generators +) diff --git a/server/lib/network/Session/Session.cpp b/server/lib/network/Session/Session.cpp new file mode 100644 index 0000000000000000000000000000000000000000..2361119573a17e6830ed7ea2c0e2597596f510e6 --- /dev/null +++ b/server/lib/network/Session/Session.cpp @@ -0,0 +1,59 @@ +#include "Session.hpp" +#include "spdlog/common.h" +#include "spdlog/spdlog.h" +#include +#include + +using nlohmann::json; + +Session::Session(boost::asio::ip::tcp::socket socket, + std::unique_ptr response_generator) + : socket_(std::move(socket)), + response_generator_(std::move(response_generator)) { +} + +void Session::start() { + SPDLOG_INFO("Started new session"); + do_read(); +} + +void Session::do_read() { + // https://stackoverflow.com/questions/3058589/boostasioasync-read-until-reads-all-data-instead-of-just-some + auto self = shared_from_this(); + boost::asio::async_read_until( + socket_, + request_buffer, + "\r\n", + [this, self](boost::system::error_code error_code, std::size_t length) { + if (error_code) { + SPDLOG_ERROR("Couldn't read from user"); + return; + } + + std::stringstream ss_out; + std::copy(boost::asio::buffers_begin(request_buffer.data()), + boost::asio::buffers_begin(request_buffer.data()) + + length - 2, + std::ostream_iterator(ss_out)); + + std::string result = ss_out.str(); + request_buffer.consume(length); + + json response = response_generator_->handle(result); + auto str_response = response.dump() + "\r\n"; + do_write(str_response); + }); +} + +void Session::do_write(std::string response) { + auto self(shared_from_this()); + boost::asio::async_write( + socket_, + boost::asio::buffer(response, response.length()), + [this, self](boost::system::error_code ec, std::size_t length) { + if (ec) { + SPDLOG_ERROR("Couldn't respond to user"); + } + do_read(); + }); +} diff --git a/server/lib/network/Session/Session.hpp b/server/lib/network/Session/Session.hpp new file mode 100644 index 0000000000000000000000000000000000000000..5c797c7b2b6422fdd159e9111d1772645c0c895e --- /dev/null +++ b/server/lib/network/Session/Session.hpp @@ -0,0 +1,29 @@ +#ifndef SESSION_H +#define SESSION_H + +#include "ResponseGenerators.hpp" +#include +#include +#include +#include +#include +#include +#include + +class Session : public std::enable_shared_from_this { + public: + explicit Session(boost::asio::ip::tcp::socket socket, + std::unique_ptr response_generator); + + void start(); + + private: + boost::asio::ip::tcp::socket socket_; + boost::asio::streambuf request_buffer; + std::unique_ptr response_generator_; + + void do_read(); + void do_write(std::string response); +}; + +#endif // SESSION_H diff --git a/server/lib/plugins/CMakeLists.txt b/server/lib/plugins/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..808f6c5f65cba640945569e191ad6c1815cca6cf --- /dev/null +++ b/server/lib/plugins/CMakeLists.txt @@ -0,0 +1,6 @@ +add_subdirectory(PyExceptionInfo) +add_subdirectory(Media) +add_subdirectory(wrappers) +add_subdirectory(PluginsBundle) +add_subdirectory(PluginsLoader) +add_subdirectory(PluginsProvider) diff --git a/server/lib/plugins/Media/CMakeLists.txt b/server/lib/plugins/Media/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..6229fb0fe064931bc833b3bc42f5e1f0b39992ba --- /dev/null +++ b/server/lib/plugins/Media/CMakeLists.txt @@ -0,0 +1,2 @@ +add_library(media INTERFACE) +target_include_directories(media INTERFACE ./) diff --git a/server/lib/plugins/Media/Media.hpp b/server/lib/plugins/Media/Media.hpp new file mode 100644 index 0000000000000000000000000000000000000000..c834f938b894984daf300709c17c181a94e6e618 --- /dev/null +++ b/server/lib/plugins/Media/Media.hpp @@ -0,0 +1,31 @@ +#ifndef MEDIA_H +#define MEDIA_H + +#include +#include +#include + +struct SourceWithAdditionalInfo { + std::string src; + std::string info; +}; + +inline void to_json(nlohmann ::json &nlohmann_json_j, + const SourceWithAdditionalInfo &nlohmann_json_t) { + nlohmann_json_j = {nlohmann_json_t.src, nlohmann_json_t.info}; +} + +inline void from_json(const nlohmann ::json &nlohmann_json_j, + SourceWithAdditionalInfo &nlohmann_json_t) { + nlohmann_json_j[0].get_to(nlohmann_json_t.src); + nlohmann_json_j[1].get_to(nlohmann_json_t.info); +} + +struct Media { + std::vector local; + std::vector web; +}; + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Media, local, web); + +#endif // !MEDIA_H diff --git a/server/lib/plugins/PluginsBundle/CMakeLists.txt b/server/lib/plugins/PluginsBundle/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..6f0be7b6d52717e3db4cfbad3b90e0adcb1ab88e --- /dev/null +++ b/server/lib/plugins/PluginsBundle/CMakeLists.txt @@ -0,0 +1,11 @@ +message(${CMAKE_CURRENT_SOURCE_DIR}) + +add_library(plugins_bundle PluginsBundle.cpp) +target_link_libraries(plugins_bundle + definitions_provider_interface + images_provider_interface + sentences_provider_interface + audios_provider_interface + format_processor_interface +) +target_include_directories(plugins_bundle PUBLIC ./) diff --git a/server/lib/plugins/PluginsBundle/PluginsBundle.cpp b/server/lib/plugins/PluginsBundle/PluginsBundle.cpp new file mode 100644 index 0000000000000000000000000000000000000000..58d49028727bae332c6bd5fdb6c30565ddd76a71 --- /dev/null +++ b/server/lib/plugins/PluginsBundle/PluginsBundle.cpp @@ -0,0 +1,62 @@ +#include "PluginsBundle.hpp" +#include "IDefinitionsProviderWrapper.hpp" +#include "PyExceptionInfo.hpp" +#include +#include + +PluginsBundle::PluginsBundle() = default; + +auto PluginsBundle::set_definitions_provider( + std::unique_ptr> + new_provider) -> void { + definitions_provider_ = std::move(new_provider); +} + +auto PluginsBundle::set_sentences_provider( + std::unique_ptr> + new_provider) -> void { + sentences_provider_ = std::move(new_provider); +} + +auto PluginsBundle::set_images_provider( + std::unique_ptr> new_provider) + -> void { + images_provider_ = std::move(new_provider); +} + +auto PluginsBundle::set_audios_provider( + std::unique_ptr> new_provider) + -> void { + audios_provider_ = std::move(new_provider); +} + +auto PluginsBundle::set_format_processor( + std::unique_ptr> + new_provider) -> void { + format_processor_ = std::move(new_provider); +} + +auto PluginsBundle::definitions_provider() -> IDefinitionsProviderWrapper * { + return definitions_provider_.get(); +} + +auto PluginsBundle::sentences_provider() -> ISentencesProviderWrapper * { + return sentences_provider_.get(); +} + +auto PluginsBundle::images_provider() -> IImagesProviderWrapper * { + return images_provider_.get(); +} + +auto PluginsBundle::audios_provider() -> IAudiosProviderWrapper * { + return audios_provider_.get(); +} + +auto PluginsBundle::format_processor() -> IFormatProcessorWrapper * { + return format_processor_.get(); +} diff --git a/server/lib/plugins/PluginsBundle/PluginsBundle.hpp b/server/lib/plugins/PluginsBundle/PluginsBundle.hpp new file mode 100644 index 0000000000000000000000000000000000000000..3a0e4fe86e97faa5b5134265c868e2ff4b406eaf --- /dev/null +++ b/server/lib/plugins/PluginsBundle/PluginsBundle.hpp @@ -0,0 +1,75 @@ +#ifndef PLUGINS_BUNDLE_H +#define PLUGINS_BUNDLE_H + +#include "IAudiosProviderWrapper.hpp" +#include "IDefinitionsProviderWrapper.hpp" +#include "IFormatProcessorWrapper.hpp" +#include "IImagesProviderWrapper.hpp" +#include "ISentencesProviderWrapper.hpp" + +#include +#include +#include +#include +#include +#include +#include + +class PluginsBundle { + public: + PluginsBundle(); + + auto set_definitions_provider( + std::unique_ptr> + new_provider) -> void; + + auto set_sentences_provider( + std::unique_ptr> + new_provider) -> void; + + auto set_images_provider( + std::unique_ptr> + new_provider) -> void; + + auto set_audios_provider( + std::unique_ptr> + new_provider) -> void; + + auto set_format_processor( + std::unique_ptr> + new_provider) -> void; + + auto definitions_provider() -> IDefinitionsProviderWrapper *; + + auto sentences_provider() -> ISentencesProviderWrapper *; + + auto images_provider() -> IImagesProviderWrapper *; + + auto audios_provider() -> IAudiosProviderWrapper *; + + auto format_processor() -> IFormatProcessorWrapper *; + + private: + std::unique_ptr> + definitions_provider_ = nullptr; + std::unique_ptr> + sentences_provider_ = nullptr; + std::unique_ptr> + images_provider_ = nullptr; + std::unique_ptr> + audios_provider_ = nullptr; + std::unique_ptr> + format_processor_ = nullptr; +}; + +#endif // !PLUGINS_BUNDLE_H diff --git a/server/lib/plugins/PluginsLoader/CMakeLists.txt b/server/lib/plugins/PluginsLoader/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..dab2d14466edef6bf755b907fc0104b462feb180 --- /dev/null +++ b/server/lib/plugins/PluginsLoader/CMakeLists.txt @@ -0,0 +1,17 @@ +message(${CMAKE_CURRENT_SOURCE_DIR}) + +add_library(plugins_loader INTERFACE) +target_include_directories(plugins_loader INTERFACE + ./ + ${Boost_INCLUDE_DIRS} + ${PYTHON_INCLUDE_DIRS} +) + +target_link_libraries(plugins_loader + INTERFACE + base_plugin_wrapper + py_exception_info + definitions_provider_wrapper + ${Boost_LIBRARIES} + ${PYTHON_LIBRARIES} +) diff --git a/server/lib/plugins/PluginsLoader/PluginsLoader.hpp b/server/lib/plugins/PluginsLoader/PluginsLoader.hpp new file mode 100644 index 0000000000000000000000000000000000000000..8f7f444d27880a29e530b3201073eed74f0ea7ac --- /dev/null +++ b/server/lib/plugins/PluginsLoader/PluginsLoader.hpp @@ -0,0 +1,185 @@ +#ifndef PLUGINS_LOADER_H +#define PLUGINS_LOADER_H + +#include "IDefinitionsProviderWrapper.hpp" +#include "spdlog/common.h" +#include "spdlog/spdlog.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "IPluginWrapper.hpp" +#include "PyExceptionInfo.hpp" + +struct PluginsInfo { + std::vector success; + std::vector failed; +}; + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(PluginsInfo, success, failed); + +template + requires is_plugin_wrapper +class IPluginsLoader { + public: + virtual ~IPluginsLoader() = default; + + virtual auto get(const std::string &plugin_name) -> std::optional< + std::variant>, + PyExceptionInfo>> = 0; + + virtual auto load_new_plugins() -> void = 0; + + [[nodiscard]] virtual auto list_plugins() const -> PluginsInfo = 0; +}; + +// #include "DefinitionsProviderWrapper.hpp" +// using Wrapper = DefinitionsProviderWrapper; +// using IWrapper = IDefinitionsProviderWrapper; + +// Почему через темплейты? Не знаю, надо будет переделать +// TODO(blackdeer): REWORK PluginsLoader +template + requires std::derived_from && is_plugin_wrapper +class PluginsLoader : public IPluginsLoader { + public: + explicit PluginsLoader(std::filesystem::path &&plugins_dir) noexcept( + false) { + try { + boost::python::object sys = boost::python::import("sys"); + sys.attr("path").attr("append")(plugins_dir.c_str()); + } catch (const boost::python::error_already_set &) { + SPDLOG_THROW("Couldn't import sys module"); + } + + std::ranges::for_each( + std::filesystem::directory_iterator(plugins_dir), + [this](const std::filesystem::path &dir_entry) { + using std::string_literals::operator""s; + SPDLOG_INFO("Registering plugin from "s + dir_entry.string()); + + if (!std::filesystem::is_directory(dir_entry)) { + return; + } + + auto module_name = dir_entry.filename(); + boost::python::object loaded_module; + try { + loaded_module = boost::python::import(module_name.c_str()); + } catch (const boost::python::error_already_set &) { + SPDLOG_INFO("Failed to import module: "s + + module_name.string()); + auto error_info = PyExceptionInfo::build(); + failed_containers_.emplace(std::move(module_name), + std::move(error_info)); + return; + } + + auto wrapper_or_error = + Wrapper::build(module_name.string(), loaded_module); + + SPDLOG_INFO("Successfully imported Python module: "s + + module_name.string()); + + if (std::holds_alternative(wrapper_or_error)) { + auto info = std::get(wrapper_or_error); + SPDLOG_INFO("Failed to Register plugin from "s + + dir_entry.string()); + failed_containers_.emplace(std::move(module_name), + std::move(info)); + } else if (std::holds_alternative(wrapper_or_error)) { + auto wrapper = + std::move(std::get(wrapper_or_error)); + SPDLOG_INFO("Successfully registered plugin from "s + + dir_entry.string()); + loaded_containers_.emplace(std::move(module_name), + std::move(wrapper)); + } else { + SPDLOG_THROW("Unknown return from a container build"); + } + }); + } + + auto get(const std::string &plugin_name) -> std::optional< + std::variant>, + PyExceptionInfo>> override { + using std::string_literals::operator""s; + SPDLOG_INFO(plugin_name + " was requested"); + + auto res = loaded_containers_.find(plugin_name); + if (res == loaded_containers_.end()) { + SPDLOG_INFO(plugin_name + " not found"); + return std::nullopt; + } + SPDLOG_INFO(plugin_name + " was found"); + auto &found_wrapper_usage_pair = res->second; + if (++found_wrapper_usage_pair.usage_count == 1) { + auto load_result = found_wrapper_usage_pair.wrapper.load(); + if (load_result.has_value()) { + return load_result.value(); + } + } + + auto wrapper_copy = found_wrapper_usage_pair.wrapper; + auto wrapper_with_usage_counter = + std::unique_ptr>( + new Wrapper(wrapper_copy), [&](IWrapper *contained_wrapper) { + if (--found_wrapper_usage_pair.usage_count == 0) { + auto unload_result = contained_wrapper->unload(); + if (unload_result.has_value()) { + SPDLOG_ERROR("Wrapper {} unloaded with error: {}", + contained_wrapper->name(), + unload_result->stack_trace()); + } + } + delete contained_wrapper; + }); + return wrapper_with_usage_counter; + } + + auto load_new_plugins() -> void override { + throw std::runtime_error("load_new_plugins() is not implemented"); + } + + [[nodiscard]] auto list_plugins() const -> PluginsInfo override { + std::vector loaded_plugins_names{}; + loaded_plugins_names.reserve(loaded_plugins_names.size()); + for (const auto &item : loaded_containers_) { + loaded_plugins_names.push_back(item.first); + } + + std::vector failed_plugins_names{}; + failed_plugins_names.reserve(failed_containers_.size()); + for (const auto &item : failed_containers_) { + failed_plugins_names.push_back(item.first); + } + + return {.success = std::move(loaded_plugins_names), + .failed = std::move(failed_plugins_names)}; + } + + private: + struct WrapperUsageTracker { + Wrapper wrapper; + uint64_t usage_count; + }; + + std::unordered_map loaded_containers_; + std::unordered_map> + failed_containers_; +}; + +#endif diff --git a/server/lib/plugins/PluginsProvider/CMakeLists.txt b/server/lib/plugins/PluginsProvider/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..3942ab61f3acc3ca78c65f7baf46823153ad20a0 --- /dev/null +++ b/server/lib/plugins/PluginsProvider/CMakeLists.txt @@ -0,0 +1,15 @@ +message(${CMAKE_CURRENT_SOURCE_DIR}) + +add_library(plugins_provider PluginsProvider.cpp) +target_include_directories(plugins_provider PUBLIC + ./ +) + +target_link_libraries(plugins_provider + plugins_loader + definitions_provider_wrapper + images_provider_wrapper + sentences_provider_wrapper + audios_provider_wrapper + format_processor_wrapper +) diff --git a/server/lib/plugins/PluginsProvider/PluginsProvider.cpp b/server/lib/plugins/PluginsProvider/PluginsProvider.cpp new file mode 100644 index 0000000000000000000000000000000000000000..4fec89ee2696952a0dd0e0ba0bb4d19f64ee0499 --- /dev/null +++ b/server/lib/plugins/PluginsProvider/PluginsProvider.cpp @@ -0,0 +1,109 @@ +#include "PluginsProvider.hpp" +#include "AudiosProviderWrapper.hpp" +#include "DefinitionsProviderWrapper.hpp" +#include "FormatProcessorWrapper.hpp" +#include "IAudiosProviderWrapper.hpp" +#include "IDefinitionsProviderWrapper.hpp" +#include "IFormatProcessorWrapper.hpp" +#include "IImagesProviderWrapper.hpp" +#include "ISentencesProviderWrapper.hpp" +#include "ImagesProviderWrapper.hpp" +#include "pylifecycle.h" +#include "spdlog/common.h" + +PluginsProvider::PluginsProvider(PluginTypesLocationsConfig &&confg) + : definitions_providers_( + std::make_unique>( + std::move(confg.definitions_providers_dir))), + sentences_providers_( + std::make_unique>( + std::move(confg.sentences_providers_dir))), + images_providers_( + std::make_unique< + PluginsLoader>( + std::move(confg.images_providers_dir))), + audios_providers_( + std::make_unique< + PluginsLoader>( + std::move(confg.audios_providers_dir))), + format_processors_( + std::make_unique< + PluginsLoader>( + std::move(confg.format_processors_dir))) { +} + +[[nodiscard]] auto +PluginsProvider::get_definitions_provider(const std::string &name) const + -> std::optional>, + PyExceptionInfo>> { + return definitions_providers_->get(name); +} + +[[nodiscard]] auto +PluginsProvider::get_sentences_provider(const std::string &name) const + -> std::optional>, + PyExceptionInfo>> { + return sentences_providers_->get(name); +} + +[[nodiscard]] auto +PluginsProvider::get_audios_provider(const std::string &name) const + -> std::optional>, + PyExceptionInfo>> { + return audios_providers_->get(name); +} + +[[nodiscard]] auto +PluginsProvider::get_images_provider(const std::string &name) const + -> std::optional>, + PyExceptionInfo>> { + return images_providers_->get(name); +} + +[[nodiscard]] auto +PluginsProvider::get_format_processor(const std::string &name) const + -> std::optional>, + PyExceptionInfo>> { + return format_processors_->get(name); +} + +auto PluginsProvider::load_new_plugins() -> void { + SPDLOG_THROW("load_new_plugins() is not implemented"); +} + +[[nodiscard]] auto PluginsProvider::list_definitions_providers() const + -> PluginsInfo { + return definitions_providers_->list_plugins(); +} + +[[nodiscard]] auto PluginsProvider::list_sentences_providers() const + -> PluginsInfo { + return sentences_providers_->list_plugins(); +} + +[[nodiscard]] auto PluginsProvider::list_images_providers() const + -> PluginsInfo { + return images_providers_->list_plugins(); +} + +[[nodiscard]] auto PluginsProvider::list_audios_providers() const + -> PluginsInfo { + return audios_providers_->list_plugins(); +} + +[[nodiscard]] auto PluginsProvider::list_format_processors() const + -> PluginsInfo { + return format_processors_->list_plugins(); +} diff --git a/server/lib/plugins/PluginsProvider/PluginsProvider.hpp b/server/lib/plugins/PluginsProvider/PluginsProvider.hpp new file mode 100644 index 0000000000000000000000000000000000000000..8dcbefe6b0b533d5b7dc5830b29c4cb1ee9ffbc2 --- /dev/null +++ b/server/lib/plugins/PluginsProvider/PluginsProvider.hpp @@ -0,0 +1,149 @@ +#ifndef PLUGINS_PROVIDER_H +#define PLUGINS_PROVIDER_H + +#include "AudiosProviderWrapper.hpp" +#include "DefinitionsProviderWrapper.hpp" +#include "FormatProcessorWrapper.hpp" +#include "IAudiosProviderWrapper.hpp" +#include "IDefinitionsProviderWrapper.hpp" +#include "IFormatProcessorWrapper.hpp" +#include "IImagesProviderWrapper.hpp" +#include "ISentencesProviderWrapper.hpp" +#include "ImagesProviderWrapper.hpp" +#include "PluginsLoader.hpp" +#include "SentencesProviderWrapper.hpp" +#include +#include +#include +#include +#include + +class IPluginsProvider { + public: + virtual ~IPluginsProvider() = default; + + [[nodiscard]] virtual auto + get_definitions_provider(const std::string &name) const + -> std::optional>, + PyExceptionInfo>> = 0; + + [[nodiscard]] virtual auto + get_sentences_provider(const std::string &name) const + -> std::optional>, + PyExceptionInfo>> = 0; + + [[nodiscard]] virtual auto + get_images_provider(const std::string &name) const + -> std::optional>, + PyExceptionInfo>> = 0; + + [[nodiscard]] virtual auto + get_audios_provider(const std::string &name) const + -> std::optional>, + PyExceptionInfo>> = 0; + + [[nodiscard]] virtual auto + get_format_processor(const std::string &name) const + -> std::optional>, + PyExceptionInfo>> = 0; + + virtual auto load_new_plugins() -> void = 0; + + [[nodiscard]] virtual auto list_definitions_providers() const + -> PluginsInfo = 0; + + [[nodiscard]] virtual auto list_sentences_providers() const + -> PluginsInfo = 0; + + [[nodiscard]] virtual auto list_images_providers() const -> PluginsInfo = 0; + + [[nodiscard]] virtual auto list_audios_providers() const -> PluginsInfo = 0; + + [[nodiscard]] virtual auto list_format_processors() const + -> PluginsInfo = 0; +}; + +struct PluginTypesLocationsConfig { + std::filesystem::path definitions_providers_dir; + std::filesystem::path sentences_providers_dir; + std::filesystem::path images_providers_dir; + std::filesystem::path audios_providers_dir; + std::filesystem::path format_processors_dir; +}; + +class PluginsProvider : public IPluginsProvider { + public: + explicit PluginsProvider(PluginTypesLocationsConfig &&config); + + [[nodiscard]] auto get_definitions_provider(const std::string &name) const + -> std::optional>, + PyExceptionInfo>> override; + + [[nodiscard]] auto get_sentences_provider(const std::string &name) const + -> std::optional>, + PyExceptionInfo>> override; + + [[nodiscard]] auto get_images_provider(const std::string &name) const + -> std::optional>, + PyExceptionInfo>> override; + + [[nodiscard]] auto get_audios_provider(const std::string &name) const + -> std::optional>, + PyExceptionInfo>> override; + + [[nodiscard]] auto get_format_processor(const std::string &name) const + -> std::optional>, + PyExceptionInfo>> override; + + auto load_new_plugins() -> void override; + + [[nodiscard]] auto list_definitions_providers() const + -> PluginsInfo override; + + [[nodiscard]] auto list_sentences_providers() const -> PluginsInfo override; + + [[nodiscard]] auto list_images_providers() const -> PluginsInfo override; + + [[nodiscard]] auto list_audios_providers() const -> PluginsInfo override; + + [[nodiscard]] auto list_format_processors() const -> PluginsInfo override; + + private: + std::unique_ptr< + IPluginsLoader> + definitions_providers_; + std::unique_ptr< + IPluginsLoader> + sentences_providers_; + std::unique_ptr< + IPluginsLoader> + images_providers_; + std::unique_ptr< + IPluginsLoader> + audios_providers_; + std::unique_ptr< + IPluginsLoader> + format_processors_; +}; + +#endif // !PLUGINS_PROVIDER_H diff --git a/server/lib/plugins/PyExceptionInfo/CMakeLists.txt b/server/lib/plugins/PyExceptionInfo/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..d59941128c4c5ab79f7b86bfb36f576794036e2d --- /dev/null +++ b/server/lib/plugins/PyExceptionInfo/CMakeLists.txt @@ -0,0 +1,14 @@ +message(${CMAKE_CURRENT_SOURCE_DIR}) + +add_library(py_exception_info PyExceptionInfo.cpp) +target_include_directories(py_exception_info + PUBLIC + ./ + ${Boost_INCLUDE_DIRS} + ${PYTHON_INCLUDE_DIRS} +) + +target_link_libraries(py_exception_info + ${Boost_LIBRARIES} + ${PYTHON_LIBRARIES} +) diff --git a/server/lib/plugins/PyExceptionInfo/PyExceptionInfo.cpp b/server/lib/plugins/PyExceptionInfo/PyExceptionInfo.cpp new file mode 100644 index 0000000000000000000000000000000000000000..1b94555a8e0aed0b810c18dc9c0dda13c4505cb3 --- /dev/null +++ b/server/lib/plugins/PyExceptionInfo/PyExceptionInfo.cpp @@ -0,0 +1,43 @@ +#include "PyExceptionInfo.hpp" +#include "spdlog/common.h" +#include "spdlog/spdlog.h" +#include +#include +#include +#include + +auto PyExceptionInfo::build() -> std::optional { + // PyErr_Print обязатетен + // https://stackoverflow.com/a/57896281 + PyExceptionInfo info; + + try { + PyErr_Print(); + boost::python::object main_namespace = + boost::python::import("__main__").attr("__dict__"); + + exec("import traceback, sys", main_namespace); + boost::python::object py_last_type = + eval("str(sys.last_type)", main_namespace); + boost::python::object py_last_value = + eval("str(sys.last_value)", main_namespace); + boost::python::object py_last_traceback = + eval("str(sys.last_traceback)", main_namespace); + boost::python::object py_stack_trace = + eval("'\\n'.join(traceback.format_exception(sys.last_type, " + "sys.last_value, sys.last_traceback))", + main_namespace); + + info.last_type_ = boost::python::extract(py_last_type); + info.last_value_ = boost::python::extract(py_last_value); + info.last_traceback_ = + boost::python::extract(py_last_traceback); + info.stack_trace_ = boost::python::extract(py_stack_trace); + + PyErr_Clear(); + } catch (const boost::python::error_already_set &) { + SPDLOG_ERROR("Couldn't extract python exception info"); + return std::nullopt; + } + return info; +} diff --git a/server/lib/plugins/PyExceptionInfo/PyExceptionInfo.hpp b/server/lib/plugins/PyExceptionInfo/PyExceptionInfo.hpp new file mode 100644 index 0000000000000000000000000000000000000000..0b8e2015d12df4aff4fb91524afdb19b756b2f78 --- /dev/null +++ b/server/lib/plugins/PyExceptionInfo/PyExceptionInfo.hpp @@ -0,0 +1,38 @@ +#ifndef PY_EXCEPTION_INFO_H +#define PY_EXCEPTION_INFO_H + +#include +#include +#include +#include + +class PyExceptionInfo { + public: + static auto build() -> std::optional; + + [[nodiscard]] inline auto last_type() const -> const std::string & { + return last_type_; + } + + [[nodiscard]] inline auto last_value() const -> const std::string & { + return last_value_; + } + + [[nodiscard]] inline auto last_traceback() const -> const std::string & { + return last_traceback_; + } + + [[nodiscard]] inline auto stack_trace() const -> const std::string & { + return stack_trace_; + } + + private: + std::string last_type_; + std::string last_value_; + std::string last_traceback_; + std::string stack_trace_; + + PyExceptionInfo() = default; +}; + +#endif // !PY_EXCEPTION_INFO_H diff --git a/server/lib/plugins/wrappers/CMakeLists.txt b/server/lib/plugins/wrappers/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..c3b28592eaa3278226414a6e79b15babf52291b4 --- /dev/null +++ b/server/lib/plugins/wrappers/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(interfaces) +add_subdirectory(actual) diff --git a/server/lib/plugins/wrappers/actual/AudiosProviderWrapper/AudiosProviderWrapper.cpp b/server/lib/plugins/wrappers/actual/AudiosProviderWrapper/AudiosProviderWrapper.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e3a1d6dde0590d5f1ff4d3a76c61e7b0cc6554e8 --- /dev/null +++ b/server/lib/plugins/wrappers/actual/AudiosProviderWrapper/AudiosProviderWrapper.cpp @@ -0,0 +1,179 @@ +#include "AudiosProviderWrapper.hpp" +#include "BasePluginWrapper.hpp" +#include "IAudiosProviderWrapper.hpp" +#include "PyExceptionInfo.hpp" +#include "spdlog/common.h" +#include "spdlog/spdlog.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +auto AudiosProviderWrapper::AudiosProvidesFunctions::build( + const boost::python::object &module) + -> std::variant { + auto plugin_container = AudiosProvidesFunctions(); + try { + plugin_container.get = module.attr("get"); + } catch (const boost::python::error_already_set &) { + return PyExceptionInfo::build().value(); + } + return plugin_container; +} + +auto AudiosProviderWrapper::name() const -> const std::string & { + static auto base_name = '`' + BasePluginWrapper::name() + '`'; + static auto typed_provider = "[AudiosProviderWrapper] " + base_name; + return typed_provider; +} + +AudiosProviderWrapper::AudiosProviderWrapper(const AudiosProviderWrapper &other) + : BasePluginWrapper(other.name(), other.common_), + specifics_(other.specifics_) { +} + +AudiosProviderWrapper::AudiosProviderWrapper(BasePluginWrapper &&base) + : BasePluginWrapper(std::move(base)) { +} + +auto AudiosProviderWrapper::build(const std::string &name, + const boost::python::object &module) + -> std::variant { + auto base_or_error = BasePluginWrapper::build(name, module); + if (std::holds_alternative(base_or_error)) { + return std::get(base_or_error); + } + auto base = std::move(std::get(base_or_error)); + + auto specifics_or_error = AudiosProvidesFunctions::build(module); + if (std::holds_alternative(specifics_or_error)) { + return std::get(specifics_or_error); + } + auto specifics = std::get(specifics_or_error); + + auto wrapper = AudiosProviderWrapper(std::move(base)); + wrapper.specifics_ = specifics; + return wrapper; +} + +auto AudiosProviderWrapper::get(const std::string &word, + uint64_t batch_size, + bool restart) + -> std::variant { + SPDLOG_INFO( + "[{}] Handling request: word: `{}`, batch_size: `{}`, restart: `{}`", + name(), + word, + batch_size, + restart); + + if (restart) { + auto found_item = generators_.find(word); + if (found_item != generators_.end()) { + + SPDLOG_INFO( + "[{}] Restarting generator for word `{}`", name(), word); + + generators_.erase(found_item); + } else { + SPDLOG_WARN("[{}] restart request was given but no generator for " + "word `{}` was found", + name(), + word); + } + } + if (generators_.find(word) == generators_.end()) { + SPDLOG_INFO("[{}] Initializing generator for word `{}`", name(), word); + + boost::python::object generator; + try { + generator = specifics_.get(word); + generator.attr("__next__")(); + } catch (const boost::python::error_already_set &) { + return PyExceptionInfo::build().value(); + } + generators_[word] = generator; + } + + try { + SPDLOG_INFO("[{}] (Python side) Loading JSON module", name()); + + boost::python::object py_json = boost::python::import("json"); + boost::python::object py_json_dumps = py_json.attr("dumps"); + + SPDLOG_INFO( + "[{}] (Python side) Trying to obtain data batch for word `{}`", + name(), + word); + + boost::python::object py_res = + generators_[word].attr("send")(batch_size); + + SPDLOG_INFO("[{}] (Python side) successfully obtained data batch for " + "word `{}`. Trying to " + "serialize it to JSON", + name(), + word); + + boost::python::object py_json_res = py_json_dumps(py_res); + + SPDLOG_INFO("[{}] (Python side) successfully serialized data batch for " + "word `{}` to JSON", + name(), + word); + + std::string str_res = boost::python::extract(py_json_res); + nlohmann::json json_res = nlohmann::json::parse(str_res); + + try { + SPDLOG_INFO( + "[{}] (Server side) Trying to deserialize JSON data batch " + "for word `{}`", + name(), + word); + + auto audio_information = json_res[0].get(); + auto error_message = json_res[1].get(); + + SPDLOG_INFO( + "[{}] (Server side) successfully deserialized JSON data batch " + "for word `{}`", + name(), + word); + + return std::make_pair(audio_information, error_message); + } catch (const std::exception &error) { + return error.what(); + } + } catch (boost::python::error_already_set &) { + SPDLOG_INFO("[{}] caught Python exception during response construction " + "for word `{}`. Destroying corresponding Python generator", + name(), + word); + + generators_.erase(word); + auto py_exc_info = PyExceptionInfo::build().value(); + const auto &exception_type = py_exc_info.last_type(); + + if (exception_type == "") { + SPDLOG_INFO("[{}] caught Python StopIteration exception during " + "response construction " + "for word `{}`. Respoding with empty object", + name(), + word); + + return {}; + } + + return py_exc_info; + } + Media empty; + return std::make_pair(empty, ""); +} diff --git a/server/lib/plugins/wrappers/actual/AudiosProviderWrapper/AudiosProviderWrapper.hpp b/server/lib/plugins/wrappers/actual/AudiosProviderWrapper/AudiosProviderWrapper.hpp new file mode 100644 index 0000000000000000000000000000000000000000..a1f6b9ac6156fff435237cd607eaf228f0feed07 --- /dev/null +++ b/server/lib/plugins/wrappers/actual/AudiosProviderWrapper/AudiosProviderWrapper.hpp @@ -0,0 +1,56 @@ +#ifndef AUDIOS_PROVIDER_WRAPPER_H +#define AUDIOS_PROVIDER_WRAPPER_H + +#include +#include +#include +#include +#include +#include +#include + +#include "BasePluginWrapper.hpp" +#include "IAudiosProviderWrapper.hpp" +#include "PyExceptionInfo.hpp" + +class AudiosProviderWrapper : public BasePluginWrapper, + public IAudiosProviderWrapper { + public: + AudiosProviderWrapper(const AudiosProviderWrapper &); + AudiosProviderWrapper(AudiosProviderWrapper &&) = default; + auto operator=(const AudiosProviderWrapper &) + -> AudiosProviderWrapper & = delete; + auto operator=(AudiosProviderWrapper &&) + -> AudiosProviderWrapper & = default; + ~AudiosProviderWrapper() override = default; + + static auto build(const std::string &name, + const boost::python::object &module) + -> std::variant; + + auto get(const std::string &word, uint64_t batch_size, bool restart) + -> std::variant override; + + [[nodiscard]] auto name() const -> const std::string & override; + + protected: + struct AudiosProvidesFunctions { + static auto build(const boost::python::object &module) + -> std::variant; + + boost::python::object get; + }; + + AudiosProvidesFunctions specifics_; + + private: + explicit AudiosProviderWrapper(BasePluginWrapper &&base); + + std::unordered_map generators_; +}; + +static_assert(is_plugin_wrapper); + +#endif // !AUDIOS_PROVIDER_WRAPPER_H diff --git a/server/lib/plugins/wrappers/actual/AudiosProviderWrapper/CMakeLists.txt b/server/lib/plugins/wrappers/actual/AudiosProviderWrapper/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..4345c6aafb0cce6df1095861536af3bde2d1d481 --- /dev/null +++ b/server/lib/plugins/wrappers/actual/AudiosProviderWrapper/CMakeLists.txt @@ -0,0 +1,9 @@ +add_library(audios_provider_wrapper AudiosProviderWrapper.cpp) + +target_include_directories(audios_provider_wrapper PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} +) +target_link_libraries(audios_provider_wrapper + base_plugin_wrapper + audios_provider_interface +) diff --git a/server/lib/plugins/wrappers/actual/BasePluginWrapper/BasePluginWrapper.cpp b/server/lib/plugins/wrappers/actual/BasePluginWrapper/BasePluginWrapper.cpp new file mode 100644 index 0000000000000000000000000000000000000000..21a1d0632043a119da53c0964332d034f2f4e6c0 --- /dev/null +++ b/server/lib/plugins/wrappers/actual/BasePluginWrapper/BasePluginWrapper.cpp @@ -0,0 +1,131 @@ +#include "BasePluginWrapper.hpp" +#include "PyExceptionInfo.hpp" +#include "spdlog/spdlog.h" +#include +#include + +auto BasePluginWrapper::CommonFunctions::build( + const boost::python::object &module) + -> std::variant { + auto plugin_container = CommonFunctions(); + try { + plugin_container.load = module.attr("load"); + plugin_container.get_config_description = + module.attr("get_config_description"); + plugin_container.validate_config = module.attr("validate_config"); + plugin_container.get_default_config = module.attr("get_default_config"); + plugin_container.unload = module.attr("unload"); + } catch (const boost::python::error_already_set &) { + return PyExceptionInfo::build().value(); + } + return plugin_container; +} + +BasePluginWrapper::BasePluginWrapper(const std::string &name, + const CommonFunctions &common) + : name_(name), common_(common) { +} + +auto BasePluginWrapper::build(const std::string &name, + const boost::python::object &module) + -> std::variant { + auto common_or_error = CommonFunctions::build(module); + if (std::holds_alternative(common_or_error)) { + auto info = std::get(common_or_error); + return info; + } + auto common = std::get(common_or_error); + return BasePluginWrapper(name, common); +} + +[[nodiscard]] auto BasePluginWrapper::name() const -> const std::string & { + return name_; +} + +auto BasePluginWrapper::load() -> std::optional { + SPDLOG_INFO("{} is being loaded", name()); + try { + boost::python::object &plugin_load = common_.load; + plugin_load(); + } catch (const boost::python::error_already_set &) { + return PyExceptionInfo::build().value(); + } + return std::nullopt; +} + +auto BasePluginWrapper::unload() -> std::optional { + SPDLOG_INFO("{} is being unloaded", name()); + try { + boost::python::object &plugin_unload = common_.unload; + plugin_unload(); + } catch (const boost::python::error_already_set &) { + return PyExceptionInfo::build().value(); + } + return std::nullopt; +} + +auto BasePluginWrapper::get_config_description() + -> std::variant { + nlohmann::json config_description; + try { + boost::python::object py_json = boost::python::import("json"); + boost::python::object py_json_dumps = py_json.attr("dumps"); + + boost::python::object py_plugin_conf_description = + common_.get_config_description(); + boost::python::object py_str_json_conf_description = + py_json_dumps(py_plugin_conf_description); + + std::string cpp_plugin_conf_description = + boost::python::extract(py_str_json_conf_description); + config_description = nlohmann::json::parse(cpp_plugin_conf_description); + } catch (const boost::python::error_already_set &) { + return PyExceptionInfo::build().value(); + } + return config_description; +} + +auto BasePluginWrapper::get_default_config() + -> std::variant { + nlohmann::json default_config; + try { + boost::python::object py_json = boost::python::import("json"); + boost::python::object py_json_dumps = py_json.attr("dumps"); + + boost::python::object py_plugin_default_conf = + common_.get_default_config(); + boost::python::object py_str_json_default_conf = + py_json_dumps(py_plugin_default_conf); + + std::string cpp_plugin_default_conf = + boost::python::extract(py_str_json_default_conf); + default_config = nlohmann::json::parse(cpp_plugin_default_conf); + } catch (const boost::python::error_already_set &) { + return PyExceptionInfo::build().value(); + } + return default_config; +} + +auto BasePluginWrapper::validate_config(nlohmann::json &&new_config) + -> std::variant { + nlohmann::json diagnostics; + try { + boost::python::object py_json = boost::python::import("json"); + boost::python::object py_json_loads = py_json.attr("loads"); + boost::python::object py_json_dumps = py_json.attr("dumps"); + + std::string new_conf_str = new_config.dump(); + boost::python::object py_new_conf = py_json_loads(new_conf_str); + boost::python::object py_conf_diagnostics = + common_.validate_config(py_new_conf); + boost::python::object py_conf_diagnostics_str = + py_json_dumps(py_conf_diagnostics); + std::string cpp_conf_diagnostics_str = + boost::python::extract(py_conf_diagnostics_str); + diagnostics = nlohmann::json::parse(cpp_conf_diagnostics_str); + return diagnostics; + } catch (const boost::python::error_already_set &) { + return PyExceptionInfo::build().value(); + } + return diagnostics; +} diff --git a/server/lib/plugins/wrappers/actual/BasePluginWrapper/BasePluginWrapper.hpp b/server/lib/plugins/wrappers/actual/BasePluginWrapper/BasePluginWrapper.hpp new file mode 100644 index 0000000000000000000000000000000000000000..fe2c74fa8c34f38d77f83fa929bc516bcd14d5ea --- /dev/null +++ b/server/lib/plugins/wrappers/actual/BasePluginWrapper/BasePluginWrapper.hpp @@ -0,0 +1,60 @@ +#ifndef BASE_PLUGIN_WRAPPER_H +#define BASE_PLUGIN_WRAPPER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "IPluginWrapper.hpp" +#include "PyExceptionInfo.hpp" + +class BasePluginWrapper : public virtual IPluginWrapper { + public: + BasePluginWrapper(const BasePluginWrapper &) = default; + BasePluginWrapper(BasePluginWrapper &&) = default; + auto operator=(const BasePluginWrapper &) -> BasePluginWrapper & = delete; + auto operator=(BasePluginWrapper &&) -> BasePluginWrapper & = default; + ~BasePluginWrapper() override = default; + + static auto build(const std::string &name, + const boost::python::object &module) + -> std::variant; + + [[nodiscard]] auto name() const -> const std::string & override; + auto load() -> std::optional override; + auto unload() -> std::optional override; + auto get_config_description() + -> std::variant override; + auto get_default_config() + -> std::variant override; + auto validate_config(nlohmann::json &&new_config) + -> std::variant override; + + protected: + struct CommonFunctions { + static auto build(const boost::python::object &module) + -> std::variant; + + boost::python::object load; + boost::python::object get_config_description; + boost::python::object validate_config; + boost::python::object get_default_config; + boost::python::object unload; + }; + + BasePluginWrapper(const std::string &name, const CommonFunctions &common); + + std::string name_; + CommonFunctions common_; + nlohmann::json config_; +}; + +static_assert(is_plugin_wrapper); + +#endif // !BASE_PLUGIN_WRAPPER_H diff --git a/server/lib/plugins/wrappers/actual/BasePluginWrapper/CMakeLists.txt b/server/lib/plugins/wrappers/actual/BasePluginWrapper/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..d611d97b1b662ed69fa9269ac3e805a3488effa6 --- /dev/null +++ b/server/lib/plugins/wrappers/actual/BasePluginWrapper/CMakeLists.txt @@ -0,0 +1,5 @@ +add_library(base_plugin_wrapper BasePluginWrapper.cpp) +target_include_directories(base_plugin_wrapper PUBLIC ./) +target_link_libraries(base_plugin_wrapper PUBLIC + plugin_wrapper_interface +) diff --git a/server/lib/plugins/wrappers/actual/CMakeLists.txt b/server/lib/plugins/wrappers/actual/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..541c099f32b3d7acc2cd25f6a0924c69cee51e6d --- /dev/null +++ b/server/lib/plugins/wrappers/actual/CMakeLists.txt @@ -0,0 +1,6 @@ +add_subdirectory(BasePluginWrapper) +add_subdirectory(AudiosProviderWrapper) +add_subdirectory(DefinitionsProviderWrapper) +add_subdirectory(FormatProcessorWrapper) +add_subdirectory(ImagesProviderWrapper) +add_subdirectory(SentencesProviderWrapper) diff --git a/server/lib/plugins/wrappers/actual/DefinitionsProviderWrapper/CMakeLists.txt b/server/lib/plugins/wrappers/actual/DefinitionsProviderWrapper/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..91958d098abcdcdf8addf88df2f9c4583c3b816f --- /dev/null +++ b/server/lib/plugins/wrappers/actual/DefinitionsProviderWrapper/CMakeLists.txt @@ -0,0 +1,10 @@ +add_library(definitions_provider_wrapper DefinitionsProviderWrapper.cpp) +target_include_directories(definitions_provider_wrapper PUBLIC ./) +target_link_libraries(definitions_provider_wrapper + base_plugin_wrapper + definitions_provider_interface +) + +# set(serverside_plugin_wrappers +# "${serverside_plugin_wrappers} definitions_provider_wrapper" +# PARENT_SCOPE) diff --git a/server/lib/plugins/wrappers/actual/DefinitionsProviderWrapper/DefinitionsProviderWrapper.cpp b/server/lib/plugins/wrappers/actual/DefinitionsProviderWrapper/DefinitionsProviderWrapper.cpp new file mode 100644 index 0000000000000000000000000000000000000000..bf6bf43b8d8c48801229205d8852f0d10d544169 --- /dev/null +++ b/server/lib/plugins/wrappers/actual/DefinitionsProviderWrapper/DefinitionsProviderWrapper.cpp @@ -0,0 +1,186 @@ +#include "DefinitionsProviderWrapper.hpp" +#include "PyExceptionInfo.hpp" +#include "pyerrors.h" +#include "pythonrun.h" +#include "spdlog/spdlog.h" +#include +#include +#include +#include +#include +#include +#include + +auto DefinitionsProviderWrapper::DefinitionsProvidersFunctions::build( + const boost::python::object &module) + -> std::variant { + auto plugin_container = DefinitionsProvidersFunctions(); + try { + plugin_container.get = module.attr("get"); + } catch (const boost::python::error_already_set &) { + return PyExceptionInfo::build().value(); + } + return plugin_container; +} + +auto DefinitionsProviderWrapper::name() const -> const std::string & { + static auto base_name = '`' + BasePluginWrapper::name() + '`'; + static auto typed_provider = "[DefinitionsProviderWrapper] " + base_name; + return typed_provider; +} + +DefinitionsProviderWrapper::DefinitionsProviderWrapper(BasePluginWrapper &&base) + : BasePluginWrapper(std::move(base)) { +} + +DefinitionsProviderWrapper::DefinitionsProviderWrapper( + const DefinitionsProviderWrapper &other) + : BasePluginWrapper(other.name(), other.common_), + specifics_(other.specifics_) { +} + +auto DefinitionsProviderWrapper::build(const std::string &name, + const boost::python::object &module) + -> std::variant { + SPDLOG_INFO("Trying to build " + name + " definition provider"); + auto base_or_error = BasePluginWrapper::build(name, module); + if (std::holds_alternative(base_or_error)) { + return std::get(base_or_error); + } + SPDLOG_INFO("Trying to build " + name + " definition provider"); + auto base = std::move(std::get(base_or_error)); + + auto specifics_or_error = DefinitionsProvidersFunctions::build(module); + if (std::holds_alternative(specifics_or_error)) { + return std::get(specifics_or_error); + } + auto specifics = + std::get(specifics_or_error); + + auto wrapper = DefinitionsProviderWrapper(std::move(base)); + wrapper.specifics_ = specifics; + return wrapper; +} + +auto DefinitionsProviderWrapper::get(const std::string &word, + uint64_t batch_size, + bool restart) -> std:: + variant { + SPDLOG_INFO( + "[{}] Handling request: word: `{}`, batch_size: `{}`, restart: `{}`", + name(), + word, + batch_size, + restart); + + if (restart) { + auto found_item = generators_.find(word); + if (found_item != generators_.end()) { + SPDLOG_INFO( + "[{}] Restarting generator for word `{}`", name(), word); + + generators_.erase(found_item); + } else { + SPDLOG_WARN("[{}] restart request was given but no generator for " + "word `{}` was found", + name(), + word); + } + } + if (generators_.find(word) == generators_.end()) { + SPDLOG_INFO("[{}] Initializing generator for word `{}`", name(), word); + + boost::python::object generator; + try { + generator = specifics_.get(word); + generator.attr("__next__")(); + } catch (const boost::python::error_already_set &) { + return PyExceptionInfo::build().value(); + } + generators_[word] = generator; + } + + try { + SPDLOG_INFO("[{}] (Python side) Loading JSON module", name()); + + boost::python::object py_json = boost::python::import("json"); + boost::python::object py_json_dumps = py_json.attr("dumps"); + + SPDLOG_INFO( + "[{}] (Python side) Trying to obtain data batch for word `{}`", + name(), + word); + + boost::python::object py_res = + generators_[word].attr("send")(batch_size); + + SPDLOG_INFO("[{}] (Python side) successfully obtained data batch for " + "word `{}`. Trying to " + "serialize it to JSON", + name(), + word); + + boost::python::object py_json_res = py_json_dumps(py_res); + + SPDLOG_INFO("[{}] (Python side) successfully serialized data batch for " + "word `{}` to JSON", + name(), + word); + + std::string str_res = boost::python::extract(py_json_res); + nlohmann::json json_res = nlohmann::json::parse(str_res); + + try { + SPDLOG_INFO( + "[{}] (Server side) Trying to deserialize JSON data batch " + "for word `{}`", + name(), + word); + + auto error_message = json_res[1].get(); + auto cards = json_res[0].get>(); + + SPDLOG_INFO( + "[{}] (Server side) successfully deserialized JSON data batch " + "for word `{}`", + name(), + word); + + return std::make_pair(cards, error_message); + } catch (const std::exception &error) { + return error.what(); + } + } catch (boost::python::error_already_set &) { + SPDLOG_INFO("[{}] caught Python exception during response construction " + "for word `{}`. Destroying corresponding Python generator", + name(), + word); + + auto py_exc_info = PyExceptionInfo::build().value(); + const auto &exception_type = py_exc_info.last_type(); + + generators_.erase(word); + if (exception_type == "") { + SPDLOG_INFO("[{}] caught Python StopIteration exception during " + "response construction " + "for word `{}`. Respoding with empty object", + name(), + word); + return {}; + } + + // SPDLOG_ERROR("[{}] caught Python exception of type: {} during " + // "response construction " + // "for word `{}`. Forwarding exception info to the + // caller", name(), py_exc_info.last_type(), word); + + return py_exc_info; + } + std::vector empty(0); + return std::make_pair(empty, ""); +} + +auto DefinitionsProviderWrapper::get_dictionary_scheme() + -> std::variant { + return {}; +} diff --git a/server/lib/plugins/wrappers/actual/DefinitionsProviderWrapper/DefinitionsProviderWrapper.hpp b/server/lib/plugins/wrappers/actual/DefinitionsProviderWrapper/DefinitionsProviderWrapper.hpp new file mode 100644 index 0000000000000000000000000000000000000000..0d5ccb62a5b4dc515fa80db75d97fde98824c5f3 --- /dev/null +++ b/server/lib/plugins/wrappers/actual/DefinitionsProviderWrapper/DefinitionsProviderWrapper.hpp @@ -0,0 +1,60 @@ +#ifndef DEFINITIONS_PROVIDER_WRAPPER_H +#define DEFINITIONS_PROVIDER_WRAPPER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "BasePluginWrapper.hpp" +#include "IDefinitionsProviderWrapper.hpp" +#include "PyExceptionInfo.hpp" + +class DefinitionsProviderWrapper : public IDefinitionsProviderWrapper, + public BasePluginWrapper { + public: + DefinitionsProviderWrapper(const DefinitionsProviderWrapper &); + DefinitionsProviderWrapper(DefinitionsProviderWrapper &&) = default; + auto operator=(const DefinitionsProviderWrapper &) + -> DefinitionsProviderWrapper & = delete; + auto operator=(DefinitionsProviderWrapper &&) + -> DefinitionsProviderWrapper & = default; + + static auto build(const std::string &name, + const boost::python::object &module) + -> std::variant; + + auto get_dictionary_scheme() + -> std::variant override; + + auto get(const std::string &word, uint64_t batch_size, bool restart) + -> std::variant override; + + [[nodiscard]] auto name() const -> const std::string & override; + + protected: + struct DefinitionsProvidersFunctions { + static auto build(const boost::python::object &module) + -> std::variant; + + boost::python::object get; + }; + + DefinitionsProvidersFunctions specifics_; + + private: + explicit DefinitionsProviderWrapper(BasePluginWrapper &&base); + + std::unordered_map generators_; +}; + +static_assert(is_plugin_wrapper); + +#endif // !DEFINITIONS_PROVIDER_WRAPPER_H diff --git a/server/lib/plugins/wrappers/actual/FormatProcessorWrapper/CMakeLists.txt b/server/lib/plugins/wrappers/actual/FormatProcessorWrapper/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..b93dc44de11937a3f84ce96ccb5b6f39700c612b --- /dev/null +++ b/server/lib/plugins/wrappers/actual/FormatProcessorWrapper/CMakeLists.txt @@ -0,0 +1,11 @@ +add_library(format_processor_wrapper FormatProcessorWrapper.cpp) + +target_include_directories(format_processor_wrapper PUBLIC ./) +target_link_libraries(format_processor_wrapper + base_plugin_wrapper + format_processor_interface +) + +# set(serverside_plugin_wrappers +# "${serverside_plugin_wrappers} format_processor_wrapper" +# PARENT_SCOPE) diff --git a/server/lib/plugins/wrappers/actual/FormatProcessorWrapper/FormatProcessorWrapper.cpp b/server/lib/plugins/wrappers/actual/FormatProcessorWrapper/FormatProcessorWrapper.cpp new file mode 100644 index 0000000000000000000000000000000000000000..b06d077f4174d4e3cb4c46338626adf311c9c29c --- /dev/null +++ b/server/lib/plugins/wrappers/actual/FormatProcessorWrapper/FormatProcessorWrapper.cpp @@ -0,0 +1,66 @@ +#include "FormatProcessorWrapper.hpp" +#include "BasePluginWrapper.hpp" +#include "PyExceptionInfo.hpp" +#include +#include +#include + +auto FormatProcessorWrapper::FormatProcessorsFunctions::build( + const boost::python::object &module) + -> std::variant { + auto plugin_container = FormatProcessorsFunctions(); + try { + plugin_container.save = module.attr("save"); + } catch (const boost::python::error_already_set &) { + return PyExceptionInfo::build().value(); + } + return plugin_container; +} + +FormatProcessorWrapper::FormatProcessorWrapper(BasePluginWrapper &&base) + : BasePluginWrapper(std::move(base)) { +} + +auto FormatProcessorWrapper::name() const -> const std::string & { + static auto base_name = '`' + BasePluginWrapper::name() + '`'; + static auto typed_provider = "[FormatProcessorWrapper] " + base_name; + return typed_provider; +} + +FormatProcessorWrapper::FormatProcessorWrapper( + const FormatProcessorWrapper &other) + : BasePluginWrapper(other.name(), other.common_), + specifics_(other.specifics_) { +} + +auto FormatProcessorWrapper::build(const std::string &name, + const boost::python::object &module) + -> std::variant { + auto base_or_error = BasePluginWrapper::build(name, module); + if (std::holds_alternative(base_or_error)) { + return std::get(base_or_error); + } + auto base = std::move(std::get(base_or_error)); + + auto specifics_or_error = FormatProcessorsFunctions::build(module); + if (std::holds_alternative(specifics_or_error)) { + return std::get(specifics_or_error); + } + auto specifics = std::get(specifics_or_error); + + auto wrapper = FormatProcessorWrapper(std::move(base)); + wrapper.specifics_ = specifics; + return wrapper; +} + +auto FormatProcessorWrapper::save(const ResultFilesPaths &paths) + -> std::variant { + std::string res; + try { + boost::python::object py_res = specifics_.save(paths.cards.string()); + res = boost::python::extract(py_res); + } catch (const boost::python::error_already_set &) { + return PyExceptionInfo::build().value(); + } + return res; +} diff --git a/server/lib/plugins/wrappers/actual/FormatProcessorWrapper/FormatProcessorWrapper.hpp b/server/lib/plugins/wrappers/actual/FormatProcessorWrapper/FormatProcessorWrapper.hpp new file mode 100644 index 0000000000000000000000000000000000000000..22169ab97c01587aa073a2963095bb112112eeda --- /dev/null +++ b/server/lib/plugins/wrappers/actual/FormatProcessorWrapper/FormatProcessorWrapper.hpp @@ -0,0 +1,49 @@ +#ifndef FORMAT_PROCESSOR_WRAPPER_H +#define FORMAT_PROCESSOR_WRAPPER_H + +#include +#include +#include +#include +#include + +#include "BasePluginWrapper.hpp" +#include "IFormatProcessorWrapper.hpp" +#include "PyExceptionInfo.hpp" + +class FormatProcessorWrapper : public IFormatProcessorWrapper, + public BasePluginWrapper { + public: + FormatProcessorWrapper(const FormatProcessorWrapper &); + FormatProcessorWrapper(FormatProcessorWrapper &&) = default; + auto operator=(const FormatProcessorWrapper &) + -> FormatProcessorWrapper & = delete; + auto operator=(FormatProcessorWrapper &&) + -> FormatProcessorWrapper & = default; + + static auto build(const std::string &name, + const boost::python::object &module) + -> std::variant; + + auto save(const ResultFilesPaths &paths) + -> std::variant override; + + [[nodiscard]] auto name() const -> const std::string & override; + + protected: + struct FormatProcessorsFunctions { + static auto build(const boost::python::object &module) + -> std::variant; + + boost::python::object save; + }; + + FormatProcessorsFunctions specifics_; + + private: + explicit FormatProcessorWrapper(BasePluginWrapper &&base); +}; + +static_assert(is_plugin_wrapper); + +#endif diff --git a/server/lib/plugins/wrappers/actual/ImagesProviderWrapper/CMakeLists.txt b/server/lib/plugins/wrappers/actual/ImagesProviderWrapper/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..a6cfc444833460faa92f1eb715fea723e55a9003 --- /dev/null +++ b/server/lib/plugins/wrappers/actual/ImagesProviderWrapper/CMakeLists.txt @@ -0,0 +1,9 @@ +add_library(images_provider_wrapper ImagesProviderWrapper.cpp) +target_include_directories(images_provider_wrapper PUBLIC ./) +target_link_libraries(images_provider_wrapper + base_plugin_wrapper + images_provider_interface) +# +# set(serverside_plugin_wrappers +# "${serverside_plugin_wrappers} images_provider_wrapper" +# PARENT_SCOPE) diff --git a/server/lib/plugins/wrappers/actual/ImagesProviderWrapper/ImagesProviderWrapper.cpp b/server/lib/plugins/wrappers/actual/ImagesProviderWrapper/ImagesProviderWrapper.cpp new file mode 100644 index 0000000000000000000000000000000000000000..2be0f0cf4267280e5d4e234de4e2a79fb4c14aad --- /dev/null +++ b/server/lib/plugins/wrappers/actual/ImagesProviderWrapper/ImagesProviderWrapper.cpp @@ -0,0 +1,164 @@ +#include "ImagesProviderWrapper.hpp" +#include "Media.hpp" +#include "spdlog/spdlog.h" +#include +#include +#include + +auto ImagesProviderWrapper::ImagesProvidersFunctions::build( + const boost::python::object &module) + -> std::variant { + auto plugin_container = ImagesProvidersFunctions(); + try { + plugin_container.get = module.attr("get"); + } catch (const boost::python::error_already_set &) { + return PyExceptionInfo::build().value(); + } + return plugin_container; +} + +auto ImagesProviderWrapper::name() const -> const std::string & { + static auto base_name = '`' + BasePluginWrapper::name() + '`'; + static auto typed_provider = "[ImagesProviderWrapper] " + base_name; + return typed_provider; +} + +ImagesProviderWrapper::ImagesProviderWrapper(BasePluginWrapper &&base) + : BasePluginWrapper(std::move(base)) { +} + +ImagesProviderWrapper::ImagesProviderWrapper(const ImagesProviderWrapper &other) + : BasePluginWrapper(other.name(), other.common_), + specifics_(other.specifics_) { +} + +auto ImagesProviderWrapper::build(const std::string &name, + const boost::python::object &module) + -> std::variant { + auto base_or_error = BasePluginWrapper::build(name, module); + if (std::holds_alternative(base_or_error)) { + return std::get(base_or_error); + } + auto base = std::move(std::get(base_or_error)); + + auto specifics_or_error = ImagesProvidersFunctions::build(module); + if (std::holds_alternative(specifics_or_error)) { + return std::get(specifics_or_error); + } + auto specifics = std::get(specifics_or_error); + + auto wrapper = ImagesProviderWrapper(std::move(base)); + wrapper.specifics_ = specifics; + return wrapper; +} + +auto ImagesProviderWrapper::get(const std::string &word, + uint64_t batch_size, + bool restart) + -> std::variant { + SPDLOG_INFO( + "[{}] Handling request: word: `{}`, batch_size: `{}`, restart: `{}`", + name(), + word, + batch_size, + restart); + + if (restart) { + auto found_item = generators_.find(word); + if (found_item != generators_.end()) { + + SPDLOG_INFO( + "[{}] Restarting generator for word `{}`", name(), word); + + generators_.erase(found_item); + } else { + SPDLOG_WARN("[{}] restart request was given but no generator for " + "word `{}` was found", + name(), + word); + } + } + if (generators_.find(word) == generators_.end()) { + SPDLOG_INFO("[{}] Initializing generator for word `{}`", name(), word); + + boost::python::object generator; + try { + generator = specifics_.get(word); + generator.attr("__next__")(); + } catch (const boost::python::error_already_set &) { + return PyExceptionInfo::build().value(); + } + generators_[word] = generator; + } + try { + SPDLOG_INFO("[{}] (Python side) Loading JSON module", name()); + + boost::python::object py_json = boost::python::import("json"); + boost::python::object py_json_dumps = py_json.attr("dumps"); + + SPDLOG_INFO( + "[{}] (Python side) Trying to obtain data batch for word `{}`", + name(), + word); + + boost::python::object py_res = + generators_[word].attr("send")(batch_size); + + SPDLOG_INFO("[{}] (Python side) successfully obtained data batch for " + "word `{}`. Trying to " + "serialize it to JSON", + name(), + word); + + boost::python::object py_json_res = py_json_dumps(py_res); + + SPDLOG_INFO("[{}] (Python side) successfully serialized data batch for " + "word `{}` to JSON", + name(), + word); + + std::string str_res = boost::python::extract(py_json_res); + nlohmann::json json_res = nlohmann::json::parse(str_res); + + try { + SPDLOG_INFO( + "[{}] (Server side) Trying to deserialize JSON data batch " + "for word `{}`", + name(), + word); + + auto images_urls = json_res[0].get(); + auto error_message = json_res[1].get(); + + SPDLOG_INFO( + "[{}] (Server side) successfully deserialized JSON data batch " + "for word `{}`", + name(), + word); + return std::make_pair(images_urls, error_message); + } catch (const std::exception &error) { + return error.what(); + } + } catch (boost::python::error_already_set &) { + SPDLOG_INFO("[{}] caught Python exception during response construction " + "for word `{}`. Destroying corresponding Python generator", + name(), + word); + + auto py_exc_info = PyExceptionInfo::build().value(); + const auto &exception_type = py_exc_info.last_type(); + + generators_.erase(word); + if (exception_type == "") { + SPDLOG_INFO("[{}] caught Python StopIteration exception during " + "response construction " + "for word `{}`. Respoding with empty object", + name(), + word); + return {}; + } + return py_exc_info; + } + Media empty; + return std::make_pair(empty, ""); +} diff --git a/server/lib/plugins/wrappers/actual/ImagesProviderWrapper/ImagesProviderWrapper.hpp b/server/lib/plugins/wrappers/actual/ImagesProviderWrapper/ImagesProviderWrapper.hpp new file mode 100644 index 0000000000000000000000000000000000000000..3676f71afad4015e5a6ee5f6242a5d314d41f385 --- /dev/null +++ b/server/lib/plugins/wrappers/actual/ImagesProviderWrapper/ImagesProviderWrapper.hpp @@ -0,0 +1,53 @@ +#ifndef IMAGES_PROVIDER_WRAPPER_H +#define IMAGES_PROVIDER_WRAPPER_H + +#include +#include +#include +#include +#include + +#include "BasePluginWrapper.hpp" +#include "IImagesProviderWrapper.hpp" +#include "PyExceptionInfo.hpp" + +class ImagesProviderWrapper : public IImagesProviderWrapper, + public BasePluginWrapper { + public: + ImagesProviderWrapper(const ImagesProviderWrapper &); + ImagesProviderWrapper(ImagesProviderWrapper &&) = default; + auto operator=(const ImagesProviderWrapper &) + -> ImagesProviderWrapper & = delete; + auto operator=(ImagesProviderWrapper &&) + -> ImagesProviderWrapper & = default; + + static auto build(const std::string &name, + const boost::python::object &module) + -> std::variant; + + auto get(const std::string &word, uint64_t batch_size, bool restart) + -> std::variant override; + + [[nodiscard]] auto name() const -> const std::string & override; + + protected: + struct ImagesProvidersFunctions { + static auto build(const boost::python::object &module) + -> std::variant; + + boost::python::object get; + }; + + ImagesProvidersFunctions specifics_; + + private: + explicit ImagesProviderWrapper(BasePluginWrapper &&base); + + std::unordered_map generators_; +}; + +static_assert(is_plugin_wrapper); + +#endif diff --git a/server/lib/plugins/wrappers/actual/SentencesProviderWrapper/CMakeLists.txt b/server/lib/plugins/wrappers/actual/SentencesProviderWrapper/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..8cf6031e80e8b2a464df9765901859dc0a0adc0a --- /dev/null +++ b/server/lib/plugins/wrappers/actual/SentencesProviderWrapper/CMakeLists.txt @@ -0,0 +1,6 @@ +add_library(sentences_provider_wrapper SentencesProviderWrapper.cpp) +target_include_directories(sentences_provider_wrapper PUBLIC ./) +target_link_libraries(sentences_provider_wrapper + base_plugin_wrapper + sentences_provider_interface +) diff --git a/server/lib/plugins/wrappers/actual/SentencesProviderWrapper/SentencesProviderWrapper.cpp b/server/lib/plugins/wrappers/actual/SentencesProviderWrapper/SentencesProviderWrapper.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e7e9492c17f2fe673d76331c9145f3bd9302d9a8 --- /dev/null +++ b/server/lib/plugins/wrappers/actual/SentencesProviderWrapper/SentencesProviderWrapper.cpp @@ -0,0 +1,164 @@ +#include "SentencesProviderWrapper.hpp" +#include "BasePluginWrapper.hpp" +#include "spdlog/spdlog.h" +#include +#include + +auto SentencesProviderWrapper::SentencesProvidersFunctions::build( + const boost::python::object &module) + -> std::variant { + auto plugin_container = SentencesProvidersFunctions(); + try { + plugin_container.get = module.attr("get"); + } catch (const boost::python::error_already_set &) { + return PyExceptionInfo::build().value(); + } + return plugin_container; +} + +auto SentencesProviderWrapper::name() const -> const std::string & { + static auto base_name = '`' + BasePluginWrapper::name() + '`'; + static auto typed_provider = "[SentencesProviderWrapper] " + base_name; + return typed_provider; +} + +SentencesProviderWrapper::SentencesProviderWrapper(BasePluginWrapper &&base) + : BasePluginWrapper(std::move(base)) { +} + +SentencesProviderWrapper::SentencesProviderWrapper( + const SentencesProviderWrapper &other) + : BasePluginWrapper(other.name(), other.common_), + specifics_(other.specifics_) { +} + +auto SentencesProviderWrapper::get(const std::string &word, + uint64_t batch_size, + bool restart) -> std:: + variant { + SPDLOG_INFO( + "[{}] Handling request: word: `{}`, batch_size: `{}`, restart: `{}`", + name(), + word, + batch_size, + restart); + + if (restart) { + auto found_item = generators_.find(word); + if (found_item != generators_.end()) { + SPDLOG_INFO( + "[{}] Restarting generator for word `{}`", name(), word); + + generators_.erase(found_item); + } else { + SPDLOG_WARN("[{}] restart request was given but no generator for " + "word `{}` was found", + name(), + word); + } + } + if (generators_.find(word) == generators_.end()) { + SPDLOG_INFO("[{}] Initializing generator for word `{}`", name(), word); + + boost::python::object generator; + try { + generator = specifics_.get(word); + generator.attr("__next__")(); + } catch (const boost::python::error_already_set &) { + return PyExceptionInfo::build().value(); + } + generators_[word] = generator; + } + try { + SPDLOG_INFO("[{}] (Python side) Loading JSON module", name()); + + boost::python::object py_json = boost::python::import("json"); + boost::python::object py_json_dumps = py_json.attr("dumps"); + + SPDLOG_INFO( + "[{}] (Python side) Trying to obtain data batch for word `{}`", + name(), + word); + + boost::python::object py_res = + generators_[word].attr("send")(batch_size); + + SPDLOG_INFO("[{}] (Python side) successfully obtained data batch for " + "word `{}`. Trying to " + "serialize it to JSON", + name(), + word); + + boost::python::object py_json_res = py_json_dumps(py_res); + + SPDLOG_INFO("[{}] (Python side) successfully serialized data batch for " + "word `{}` to JSON", + name(), + word); + + std::string str_res = boost::python::extract(py_json_res); + nlohmann::json json_res = nlohmann::json::parse(str_res); + + try { + SPDLOG_INFO( + "[{}] (Server side) Trying to deserialize JSON data batch " + "for word `{}`", + name(), + word); + + auto sentences = json_res[0].get>(); + auto error_message = json_res[1].get(); + + SPDLOG_INFO( + "[{}] (Server side) successfully deserialized JSON data batch " + "for word `{}`", + name(), + word); + + return std::make_pair(sentences, error_message); + } catch (const std::exception &error) { + return error.what(); + } + } catch (boost::python::error_already_set &) { + SPDLOG_INFO("[{}] caught Python exception during response construction " + "for word `{}`. Destroying corresponding Python generator", + name(), + word); + + auto py_exc_info = PyExceptionInfo::build().value(); + const auto &exception_type = py_exc_info.last_type(); + + generators_.erase(word); + if (exception_type == "") { + SPDLOG_INFO("[{}] caught Python StopIteration exception during " + "response construction " + "for word `{}`. Respoding with empty object", + name(), + word); + return {}; + } + return py_exc_info; + } + std::vector empty(0); + return std::make_pair(empty, ""); +} + +auto SentencesProviderWrapper::build(const std::string &name, + const boost::python::object &module) + -> std::variant { + auto base_or_error = BasePluginWrapper::build(name, module); + if (std::holds_alternative(base_or_error)) { + return std::get(base_or_error); + } + auto base = std::move(std::get(base_or_error)); + + auto specifics_or_error = SentencesProvidersFunctions::build(module); + if (std::holds_alternative(specifics_or_error)) { + return std::get(specifics_or_error); + } + auto specifics = std::get(specifics_or_error); + + auto wrapper = SentencesProviderWrapper(std::move(base)); + wrapper.specifics_ = specifics; + return wrapper; +} diff --git a/server/lib/plugins/wrappers/actual/SentencesProviderWrapper/SentencesProviderWrapper.hpp b/server/lib/plugins/wrappers/actual/SentencesProviderWrapper/SentencesProviderWrapper.hpp new file mode 100644 index 0000000000000000000000000000000000000000..d7a089c83e45efda72245635f1005a8d0f53cb61 --- /dev/null +++ b/server/lib/plugins/wrappers/actual/SentencesProviderWrapper/SentencesProviderWrapper.hpp @@ -0,0 +1,56 @@ +#ifndef SENTENCES_PROVIDER_WRAPPER_H +#define SENTENCES_PROVIDER_WRAPPER_H + +#include +#include +#include +#include +#include +#include +#include + +#include "BasePluginWrapper.hpp" +#include "ISentencesProviderWrapper.hpp" +#include "PyExceptionInfo.hpp" + +class SentencesProviderWrapper : public BasePluginWrapper, + public ISentencesProviderWrapper { + public: + SentencesProviderWrapper(const SentencesProviderWrapper &other); + SentencesProviderWrapper(SentencesProviderWrapper &&) = default; + auto operator=(const SentencesProviderWrapper &) + -> SentencesProviderWrapper & = delete; + auto operator=(SentencesProviderWrapper &&) + -> SentencesProviderWrapper & = default; + + static auto build(const std::string &name, + const boost::python::object &module) + -> std::variant; + + auto get(const std::string &word, uint64_t batch_size, bool restart) + -> std::variant override; + + + [[nodiscard]] auto name() const -> const std::string & override; + + protected: + struct SentencesProvidersFunctions { + static auto build(const boost::python::object &module) + -> std::variant; + + boost::python::object get; + }; + + SentencesProvidersFunctions specifics_; + + private: + explicit SentencesProviderWrapper(BasePluginWrapper &&base); + + std::unordered_map generators_; +}; + +static_assert(is_plugin_wrapper); + +#endif diff --git a/server/lib/plugins/wrappers/interfaces/CMakeLists.txt b/server/lib/plugins/wrappers/interfaces/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..e126249280813f4e9f343f1a51825d6e36ac3272 --- /dev/null +++ b/server/lib/plugins/wrappers/interfaces/CMakeLists.txt @@ -0,0 +1,6 @@ +add_subdirectory(IPluginWrapper) +add_subdirectory(IAudiosProviderWrapper) +add_subdirectory(IDefinitionsProviderWrapper) +add_subdirectory(IFormatProcessorWrapper) +add_subdirectory(IImagesProviderWrapper) +add_subdirectory(ISentencesProviderWrapper) diff --git a/server/lib/plugins/wrappers/interfaces/IAudiosProviderWrapper/CMakeLists.txt b/server/lib/plugins/wrappers/interfaces/IAudiosProviderWrapper/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..c6c563918719193e18863e17a2009672a018603c --- /dev/null +++ b/server/lib/plugins/wrappers/interfaces/IAudiosProviderWrapper/CMakeLists.txt @@ -0,0 +1,7 @@ +add_library(audios_provider_interface INTERFACE) +target_include_directories(audios_provider_interface INTERFACE .) +target_link_libraries(audios_provider_interface + INTERFACE + plugin_wrapper_interface + media +) diff --git a/server/lib/plugins/wrappers/interfaces/IAudiosProviderWrapper/IAudiosProviderWrapper.hpp b/server/lib/plugins/wrappers/interfaces/IAudiosProviderWrapper/IAudiosProviderWrapper.hpp new file mode 100644 index 0000000000000000000000000000000000000000..c34566d128d701ad7969609e15f11da13884ded1 --- /dev/null +++ b/server/lib/plugins/wrappers/interfaces/IAudiosProviderWrapper/IAudiosProviderWrapper.hpp @@ -0,0 +1,21 @@ +#ifndef I_AUDIOS_PROVIDER_WRAPPER_H +#define I_AUDIOS_PROVIDER_WRAPPER_H + +#include +#include +#include +#include + +#include "IPluginWrapper.hpp" +#include "Media.hpp" + +// TODO(blackdeer): REWORK RETURN: ADD SUPPORT FOR LOCAL AND WEB MEDIA +class IAudiosProviderWrapper : public virtual IPluginWrapper { + public: + using type = std::pair; + + virtual auto get(const std::string &word, uint64_t batch_size, bool restart) + -> std::variant = 0; +}; + +#endif // !I_AUDIOS_PROVIDER_WRAPPER diff --git a/server/lib/plugins/wrappers/interfaces/IDefinitionsProviderWrapper/CMakeLists.txt b/server/lib/plugins/wrappers/interfaces/IDefinitionsProviderWrapper/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..9c8e0a74a88ed95881510a4a70f1b4668e077aa5 --- /dev/null +++ b/server/lib/plugins/wrappers/interfaces/IDefinitionsProviderWrapper/CMakeLists.txt @@ -0,0 +1,7 @@ +add_library(definitions_provider_interface INTERFACE) +target_include_directories(definitions_provider_interface INTERFACE .) +target_link_libraries(definitions_provider_interface + INTERFACE + plugin_wrapper_interface + media +) diff --git a/server/lib/plugins/wrappers/interfaces/IDefinitionsProviderWrapper/IDefinitionsProviderWrapper.hpp b/server/lib/plugins/wrappers/interfaces/IDefinitionsProviderWrapper/IDefinitionsProviderWrapper.hpp new file mode 100644 index 0000000000000000000000000000000000000000..a821603fa741722dd8d15d9d15117364c3852a32 --- /dev/null +++ b/server/lib/plugins/wrappers/interfaces/IDefinitionsProviderWrapper/IDefinitionsProviderWrapper.hpp @@ -0,0 +1,55 @@ +#ifndef I_DEFINITIONS_PROVIDER_WRAPPER_H +#define I_DEFINITIONS_PROVIDER_WRAPPER_H + +#include +#include +#include +#include +#include +#include + +#include "IPluginWrapper.hpp" +#include "Media.hpp" + +struct Card { + Card() = default; + Card(const Card &) = default; + Card(Card &&) = default; + auto operator=(const Card &) -> Card & = default; + auto operator=(Card &&) -> Card & = default; + + public: + std::string word; + std::vector special; + std::string definition; + std::vector examples; + Media audios; + Media images; + nlohmann::json tags; + nlohmann::json other; +}; + +// TODO(blackdeer): solve optional keys + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Card, + word, + special, + definition, + examples, + images, + audios, + tags, + other); + +class IDefinitionsProviderWrapper : public virtual IPluginWrapper { + public: + using type = std::pair, std::string>; + + virtual auto get_dictionary_scheme() + -> std::variant = 0; + + virtual auto get(const std::string &word, uint64_t batch_size, bool restart) + -> std::variant = 0; +}; + +#endif diff --git a/server/lib/plugins/wrappers/interfaces/IFormatProcessorWrapper/CMakeLists.txt b/server/lib/plugins/wrappers/interfaces/IFormatProcessorWrapper/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..716b08b0b81e393e0458169efb5678a61af5b251 --- /dev/null +++ b/server/lib/plugins/wrappers/interfaces/IFormatProcessorWrapper/CMakeLists.txt @@ -0,0 +1,6 @@ +add_library(format_processor_interface INTERFACE) +target_include_directories(format_processor_interface INTERFACE .) +target_link_libraries(format_processor_interface + INTERFACE + plugin_wrapper_interface +) diff --git a/server/lib/plugins/wrappers/interfaces/IFormatProcessorWrapper/IFormatProcessorWrapper.hpp b/server/lib/plugins/wrappers/interfaces/IFormatProcessorWrapper/IFormatProcessorWrapper.hpp new file mode 100644 index 0000000000000000000000000000000000000000..a06e114e645cce8d82b732cf3020371a5f20960f --- /dev/null +++ b/server/lib/plugins/wrappers/interfaces/IFormatProcessorWrapper/IFormatProcessorWrapper.hpp @@ -0,0 +1,34 @@ +#ifndef I_FORMAT_PROCESSOR_WRAPPER_H +#define I_FORMAT_PROCESSOR_WRAPPER_H + +#include "IPluginWrapper.hpp" + +#include +#include +#include + +struct ResultFilesPaths { + std::filesystem::path cards; + // std::filesystem::path audios; + // std::filesystem::path images; +}; + +inline void to_json(nlohmann ::json &nlohmann_json_j, + const ResultFilesPaths &nlohmann_json_t) { + nlohmann_json_j[0] = nlohmann_json_t.cards; +} + +inline void from_json(const nlohmann ::json &nlohmann_json_j, + ResultFilesPaths &nlohmann_json_t) { + nlohmann_json_j[0].get_to(nlohmann_json_t.cards); +} + +class IFormatProcessorWrapper : public virtual IPluginWrapper { + public: + using type = std::string; + + virtual auto save(const ResultFilesPaths &paths) + -> std::variant = 0; +}; + +#endif diff --git a/server/lib/plugins/wrappers/interfaces/IImagesProviderWrapper/CMakeLists.txt b/server/lib/plugins/wrappers/interfaces/IImagesProviderWrapper/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..256be1ff161bdd6fc772be1f5fa51a74dab715b6 --- /dev/null +++ b/server/lib/plugins/wrappers/interfaces/IImagesProviderWrapper/CMakeLists.txt @@ -0,0 +1,8 @@ +add_library(images_provider_interface INTERFACE) +target_include_directories(images_provider_interface INTERFACE .) +target_link_libraries(images_provider_interface + INTERFACE + plugin_wrapper_interface + media +) + diff --git a/server/lib/plugins/wrappers/interfaces/IImagesProviderWrapper/IImagesProviderWrapper.hpp b/server/lib/plugins/wrappers/interfaces/IImagesProviderWrapper/IImagesProviderWrapper.hpp new file mode 100644 index 0000000000000000000000000000000000000000..ed68c187a0847403e37add69c8820aaa6f0d3335 --- /dev/null +++ b/server/lib/plugins/wrappers/interfaces/IImagesProviderWrapper/IImagesProviderWrapper.hpp @@ -0,0 +1,20 @@ +#ifndef I_IMAGES_PROVIDER_WRAPPER_H +#define I_IMAGES_PROVIDER_WRAPPER_H + +#include +#include +#include + +#include "IPluginWrapper.hpp" +#include "Media.hpp" + +// TODO(blackdeer): REWORK RETURN: ADD SUPPORT FOR LOCAL AND WEB MEDIA +class IImagesProviderWrapper : public virtual IPluginWrapper { + public: + using type = std::pair; + + virtual auto get(const std::string &word, uint64_t batch_size, bool restart) + -> std::variant = 0; +}; + +#endif // !I_AUDIOS_PROVIDER_WRAPPER diff --git a/server/lib/plugins/wrappers/interfaces/IPluginWrapper/CMakeLists.txt b/server/lib/plugins/wrappers/interfaces/IPluginWrapper/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..0ea195728665ad44a63f65a42faf32c492a84f15 --- /dev/null +++ b/server/lib/plugins/wrappers/interfaces/IPluginWrapper/CMakeLists.txt @@ -0,0 +1,6 @@ +add_library(plugin_wrapper_interface INTERFACE) +target_include_directories(plugin_wrapper_interface INTERFACE .) +target_link_libraries(plugin_wrapper_interface + INTERFACE + py_exception_info +) diff --git a/server/lib/plugins/wrappers/interfaces/IPluginWrapper/IPluginWrapper.hpp b/server/lib/plugins/wrappers/interfaces/IPluginWrapper/IPluginWrapper.hpp new file mode 100644 index 0000000000000000000000000000000000000000..76b8ce2b340d97cf5b88069db1d16c7f741acf63 --- /dev/null +++ b/server/lib/plugins/wrappers/interfaces/IPluginWrapper/IPluginWrapper.hpp @@ -0,0 +1,41 @@ +#ifndef PLUGIN_WRAPPER_INTERFACE_H +#define PLUGIN_WRAPPER_INTERFACE_H + +#include "PyExceptionInfo.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class IPluginWrapper { + public: + virtual ~IPluginWrapper() = default; + + [[nodiscard]] virtual auto name() const -> const std::string & = 0; + virtual auto load() -> std::optional = 0; + virtual auto get_config_description() + -> std::variant = 0; + virtual auto get_default_config() + -> std::variant = 0; + virtual auto validate_config(nlohmann::json &&new_config) + -> std::variant = 0; + virtual auto unload() -> std::optional = 0; +}; + +template +concept is_plugin_wrapper = + std::derived_from && + requires(T instance, + const std::string &name, + const boost::python::object &module) { + { + T::build(name, module) + } -> std::same_as>; + }; + +#endif // !PLUGIN_WRAPPER_INTERFACE_H diff --git a/server/lib/plugins/wrappers/interfaces/ISentencesProviderWrapper/CMakeLists.txt b/server/lib/plugins/wrappers/interfaces/ISentencesProviderWrapper/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..8c2a841c51e6fca46b45fb15fc3cb5250e1833d3 --- /dev/null +++ b/server/lib/plugins/wrappers/interfaces/ISentencesProviderWrapper/CMakeLists.txt @@ -0,0 +1,6 @@ +add_library(sentences_provider_interface INTERFACE) +target_include_directories(sentences_provider_interface INTERFACE .) +target_link_libraries(sentences_provider_interface + INTERFACE + plugin_wrapper_interface +) diff --git a/server/lib/plugins/wrappers/interfaces/ISentencesProviderWrapper/ISentencesProviderWrapper.hpp b/server/lib/plugins/wrappers/interfaces/ISentencesProviderWrapper/ISentencesProviderWrapper.hpp new file mode 100644 index 0000000000000000000000000000000000000000..642e2a71c2f5dce70bae7a7b7287fdc3d0b7a590 --- /dev/null +++ b/server/lib/plugins/wrappers/interfaces/ISentencesProviderWrapper/ISentencesProviderWrapper.hpp @@ -0,0 +1,20 @@ +#ifndef I_SENTENCES_PROVIDER_WRAPPER_H +#define I_SENTENCES_PROVIDER_WRAPPER_H + +#include +#include +#include + +#include "IPluginWrapper.hpp" + +class ISentencesProviderWrapper : public virtual IPluginWrapper { + public: + using type = std::pair, std::string>; + + virtual auto get(const std::string &word, uint64_t batch_size, bool restart) + -> std::variant = 0; +}; + +#endif // !I_AUDIOS_PROVIDER_WRAPPER diff --git a/server/lib/query_language/CMakeLists.txt b/server/lib/query_language/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..21d9ec3f0e4987f8f965824a8fe0711179866984 --- /dev/null +++ b/server/lib/query_language/CMakeLists.txt @@ -0,0 +1,28 @@ +set(LANG_HEADERS + include/scaner.hpp + include/parser.hpp + include/interpreter.hpp + include/classes.hpp + include/exception.hpp +) + +set(LANG_SOURCE + src/scaner.cpp + src/parser.cpp + src/interpreter.cpp + src/classes.cpp + src/exception.cpp +) + +add_library(query_lang ${LANG_SOURCE} ${LANG_HEADERS}) +target_include_directories(query_lang PUBLIC ./include) + +add_library(querying src/querying.cpp) +target_include_directories(querying PUBLIC ./include) +target_link_libraries(querying query_lang) + + +option(BUILD_TESTS "Build tests" ON) +if(BUILD_TESTS) + add_subdirectory(query_lang_tests) +endif() \ No newline at end of file diff --git a/server/lib/query_language/include/classes.hpp b/server/lib/query_language/include/classes.hpp new file mode 100644 index 0000000000000000000000000000000000000000..b280759820aad106e61320fa71d5d3deb42c254e --- /dev/null +++ b/server/lib/query_language/include/classes.hpp @@ -0,0 +1,127 @@ +#pragma once + +#include "scaner.hpp" +#include + +class Binary; +class Grouping; +class Unary; +class LogicalExpr; +class FuncIn; +class Literal; + +// Value передается по вершинам аст дерева +using Value = + std::variant; + +// интерпретатор аст дерева обязан определить +// все функции для посещения его вершин +class ExprVisitor { + public: + virtual ~ExprVisitor() = default; + virtual Value visit(Binary *Expr) = 0; + virtual Value visit(Unary *Expr) = 0; + virtual Value visit(Literal *Expr) = 0; + virtual Value visit(LogicalExpr *Expr) = 0; + virtual Value visit(FuncIn *Expr) = 0; + virtual Value visit(Grouping *Expr) = 0; +}; + +// базовый класс аст дерева +class Expr { + public: + virtual ~Expr() = default; + virtual Value accept(ExprVisitor *visitor) = 0; +}; + +// класс для переменной, считанной из запроса +class Literal : public Expr { + public: + explicit Literal(std::string v); + explicit Literal(double d); + explicit Literal(bool b); + explicit Literal(std::vector json_namevec); + Literal(); + + Value &get_value(); + std::vector get_json_namevec(); + Value accept(ExprVisitor *visitor) override; + + private: + std::vector json_namevec; + Value val; +}; + +// класс бинарных операций +class Binary : public Expr { + public: + Binary(std::unique_ptr left_, + token tok, + std::unique_ptr right_); + + Expr *get_leftptr(); + Expr *get_rightptr(); + token get_opername(); + Value accept(ExprVisitor *visitor) override; + + private: + std::unique_ptr left; + std::unique_ptr right; + token oper; +}; + +class FuncIn : public Expr { + public: + FuncIn(std::unique_ptr left_, std::unique_ptr right_); + + Expr *get_leftptr(); + Expr *get_rightptr(); + Value accept(ExprVisitor *visitor) override; + + private: + std::unique_ptr left; + std::unique_ptr right; +}; + +// элементарный класс унарных операций +class Unary : public Expr { + public: + Unary(std::unique_ptr Expression_, token tok); + + Expr *get_expr(); + token get_opername(); + Value accept(ExprVisitor *visitor) override; + + private: + std::unique_ptr expression; + token oper; +}; + +class LogicalExpr : public Expr { + public: + LogicalExpr(std::unique_ptr left_, + token tok, + std::unique_ptr right_); + + Expr *get_leftptr(); + Expr *get_rightptr(); + token get_opername(); + Value accept(ExprVisitor *visitor) override; + + private: + std::unique_ptr left; + std::unique_ptr right; + token oper; +}; + +// класс для выражений в скобках +class Grouping : public Expr { + public: + Grouping(std::unique_ptr Expression_); + + Expr *get_expr(); + Value accept(ExprVisitor *visitor); + + private: + std::unique_ptr expression; +}; diff --git a/server/lib/query_language/include/exception.hpp b/server/lib/query_language/include/exception.hpp new file mode 100644 index 0000000000000000000000000000000000000000..b7ed51458118a54f5fc73a8ecda734b59ff524f0 --- /dev/null +++ b/server/lib/query_language/include/exception.hpp @@ -0,0 +1,13 @@ +#pragma once +#include + +// Базовый класс исключений для компонентов (сканер, парсер, интерпретатор) +class ComponentException : public std::runtime_error { + public: + explicit ComponentException(const char *message); + + const char *what() const throw() override; + + private: + std::string message_; +}; \ No newline at end of file diff --git a/server/lib/query_language/include/interpreter.hpp b/server/lib/query_language/include/interpreter.hpp new file mode 100644 index 0000000000000000000000000000000000000000..3e1b5d73870e0000e56c197f951960db628dace5 --- /dev/null +++ b/server/lib/query_language/include/interpreter.hpp @@ -0,0 +1,39 @@ +#pragma once +#include "parser.hpp" + +class interpreter : ExprVisitor { + public: + interpreter(); + Value interpret(Expr *expression, nlohmann::json card_); + + private: + nlohmann::json card; + Value result; + auto evaluate(Expr *expression) -> Value; + auto is_truthy(const Value &val) -> bool; + auto is_equal(const Value &left, const Value &right) -> bool; + Value visit(Binary *expr) override; + Value visit(Grouping *expr) override; + Value visit(Unary *expr) override; + Value visit(Literal *expr) override; + Value visit(FuncIn *expr) override; + Value visit(LogicalExpr *ex) override; + auto find_word_inJson(std::string word, nlohmann::json jsonValue) -> bool; + void check_json_operand(const Value &operand); + void check_json_is_number(const Value &operand); + void check_number_operand(const Value &operand); + auto find_json_value(const nlohmann::json &card, std::vector) + -> nlohmann::json; + auto json_length(const nlohmann::json &jsonValue) -> double; + auto split_string(const std::string &str) -> std::vector; + auto split_json(const nlohmann::json &jsonValue) -> nlohmann::json; + auto upper_json_string(const nlohmann::json &data) -> nlohmann::json; + auto lower_json_string(const nlohmann::json &data) -> nlohmann::json; + auto reduce_json(const nlohmann::json &jsonElem) -> nlohmann::json; + auto get_self_keys(const nlohmann::json &json_Value) -> nlohmann::json; + auto handle_any_key(const nlohmann::json &json_Value, + const std::vector &levels_vec, + size_t current_index) -> nlohmann::json; + auto mergeJson(const nlohmann::json &jsonLeft, + const nlohmann::json &jsonRight) -> nlohmann::json; +}; diff --git a/server/lib/query_language/include/parser.hpp b/server/lib/query_language/include/parser.hpp new file mode 100644 index 0000000000000000000000000000000000000000..13e37dd956900e6bf29c14c2aed61897fd6bd9d4 --- /dev/null +++ b/server/lib/query_language/include/parser.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include "classes.hpp" + +class parser { + public: + parser(std::vector &t); + std::unique_ptr parse(); + + private: + std::vector tokens; + std::vector read_json_elem(); + int current = 0; + bool is_at_end(); + bool check(token_type type); + bool match(std::vector types); + token peek(); + token previous(); + token advance(); + std::unique_ptr primary(); + std::unique_ptr func_in(); + std::unique_ptr unar(); + std::unique_ptr multiplication(); + std::unique_ptr addition(); + std::unique_ptr comparison(); + std::unique_ptr equality(); + std::unique_ptr and_expr(); + std::unique_ptr or_expr(); + std::unique_ptr expression(); +}; diff --git a/server/lib/query_language/include/querying.hpp b/server/lib/query_language/include/querying.hpp new file mode 100644 index 0000000000000000000000000000000000000000..0c7d40bbb04e06399201d1346fac0efa87ed4383 --- /dev/null +++ b/server/lib/query_language/include/querying.hpp @@ -0,0 +1,13 @@ +#ifndef QUERYING_H +#define QUERYING_H + +#include +#include +#include +#include +#include + +auto prepare_filter(const std::string &query) -> std::function< + std::variant(const nlohmann::json &json_card)>; + +#endif // !QUERYING_H diff --git a/server/lib/query_language/include/scaner.hpp b/server/lib/query_language/include/scaner.hpp new file mode 100644 index 0000000000000000000000000000000000000000..b5d2699b2d2756e89828e44c11adbace84e8b46c --- /dev/null +++ b/server/lib/query_language/include/scaner.hpp @@ -0,0 +1,103 @@ +#pragma once + +#include "exception.hpp" +#include +#include +#include +#include + +enum token_type { + LEFT_PAREN, + RIGHT_PAREN, + LEFT_BRACKET, + RIGHT_BRACKET, + COMMA, + DOT, + MINUS, + PLUS, + SEMICOLON, + SLASH, + STAR, + QMARK, + COLON, + + BANG, + BANG_EQUAL, + EQUAL, + EQUAL_EQUAL, + GREATER, + GREATER_EQUAL, + LESS, + LESS_EQUAL, + + // Literals + IDENTIFIER, + STRING, + NUMBER, + JSON, + BOOL, + EMPTY, + DOUBLE, + + // logic + NOT, + AND, + OR, + ANY, + ALL, + FALSE, + TRUE, + + // func + IN, + LEN, + SPLIT, + LOWER, + UPPER, + REDUCE, + EOTF, + NUM, + NUL +}; + +using tt = token_type; + +struct token { + token(); + token(token_type type); + token(token_type type, const std::string &lexeme); + token(token_type type, + const std::string &lexeme, + const std::string &literal); + token_type type; + std::string lexeme; + std::string literal; +}; + +class scanner { + public: + scanner(const std::string &s); + std::vector scan_tokens(); + + private: + std::string source; + std::vector tokens; + std::map keywords; + int start = 0; + int current = 0; + + void init_keywords(); + bool has_next(size_t i = 0); + bool is_digit(char ch); + void add_token(token_type type); + void add_token(token_type type, const std::string &literal); + char advance(); + bool match(const std::string &expected); + char peek(); + char peek_next(); + void number(); + void read_json_level(); + void read_json_keyword(); + void string(); + void scan_token(); +}; diff --git a/server/lib/query_language/query_lang_tests/CMakeLists.txt b/server/lib/query_language/query_lang_tests/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..f00a0f5337b2339a9ca9aa442cb4f9ddad7c76eb --- /dev/null +++ b/server/lib/query_language/query_lang_tests/CMakeLists.txt @@ -0,0 +1,8 @@ +add_executable(query_lang_test + src/test_query_lang.cpp +) +target_link_libraries(query_lang_test + query_lang + GTest::gtest_main) + +gtest_discover_tests(query_lang_test) diff --git a/server/lib/query_language/query_lang_tests/src/test_query_lang.cpp b/server/lib/query_language/query_lang_tests/src/test_query_lang.cpp new file mode 100644 index 0000000000000000000000000000000000000000..748b8d9f5d91dafbfa1f559f30fce431583bd9ad --- /dev/null +++ b/server/lib/query_language/query_lang_tests/src/test_query_lang.cpp @@ -0,0 +1,91 @@ +#include "interpreter.hpp" +#include + +TEST(QueryLangTest, ScannerTest1) { + scanner scan("1 + 2"); + std::vector tokens = scan.scan_tokens(); + + ASSERT_EQ(tokens.size(), 4); + ASSERT_EQ(tokens[0].type, tt::NUMBER); + ASSERT_EQ(tokens[1].type, tt::PLUS); + ASSERT_EQ(tokens[2].type, tt::NUMBER); +} + +TEST(QueryLangTest, ScannerTest2) { + scanner scan("\"A2\" in tags[level]"); + std::vector result = scan.scan_tokens(); + ASSERT_EQ(result.size(), 7); + ASSERT_EQ(result[0].type, tt::STRING); + ASSERT_EQ(result[1].type, tt::IN); + ASSERT_EQ(result[2].type, tt::IDENTIFIER); + ASSERT_EQ(result[3].type, tt::LEFT_BRACKET); + ASSERT_EQ(result[4].type, tt::IDENTIFIER); + ASSERT_EQ(result[5].type, tt::RIGHT_BRACKET); +} + +nlohmann::json jsonCard = { + {"word", "example" }, + {"special", {"special1", "special2"} }, + {"definition", "This is an example" }, + {"examples", {"Example 1", "Example 2"} }, + {"image_links", {"image1.jpg", "image2.jpg"} }, + {"audio_links", {"audio1.mp3", "audio2.mp3"} }, + {"tags", {{"tag1", {"value1", "moscow"}}, {"level", {"A1"}}} }, + {"other", + {{"key1", "value1 слово pnfvinv 345"}, {"key2", 5}, {"key3", "value3"}}} +}; + +TEST(QueryLangTest, InterpreterTestFuncInTrue) { + scanner scan("\"A1\" in tags[level]"); + std::vector tokens = scan.scan_tokens(); + parser par(tokens); + std::unique_ptr exp = par.parse(); + interpreter inter; + Value result = inter.interpret(exp.get(), jsonCard); + bool containsBool = std::holds_alternative(result); + ASSERT_TRUE(std::get(result)); +} + +TEST(QueryLangTest, InterpreterTestFuncInFalse) { + scanner scan("\"A2\" in tags[level]"); + std::vector tokens = scan.scan_tokens(); + parser par(tokens); + std::unique_ptr exp = par.parse(); + interpreter inter; + Value result = inter.interpret(exp.get(), jsonCard); + bool containsBool = std::holds_alternative(result); + ASSERT_FALSE(std::get(result)); +} + +TEST(QueryLangTest, InterpreterTestFuncInAnd) { + scanner scan("\"A1\" in tags[level] and \"value3\" in other[key3]"); + std::vector tokens = scan.scan_tokens(); + parser par(tokens); + std::unique_ptr exp = par.parse(); + interpreter inter; + Value result = inter.interpret(exp.get(), jsonCard); + bool containsBool = std::holds_alternative(result); + ASSERT_TRUE(std::get(result)); +} + +TEST(QueryLangTest, InterpreterTestNumber) { + scanner scan("1 + 3/(4-1)*(2+1)"); + std::vector tokens = scan.scan_tokens(); + parser par(tokens); + std::unique_ptr exp = par.parse(); + interpreter inter; + Value result = inter.interpret(exp.get(), jsonCard); + double containsDouble = std::holds_alternative(result); + ASSERT_EQ(std::get(result), 4); +} + +TEST(QueryLangTest, InterpreterTestFuncNum) { + scanner scan("num(other[key2])"); + std::vector tokens = scan.scan_tokens(); + parser par(tokens); + std::unique_ptr exp = par.parse(); + interpreter inter; + Value result = inter.interpret(exp.get(), jsonCard); + double containsDouble = std::holds_alternative(result); + ASSERT_EQ(std::get(result), 5); +} diff --git a/server/lib/query_language/src/classes.cpp b/server/lib/query_language/src/classes.cpp new file mode 100644 index 0000000000000000000000000000000000000000..6fb26d5af748256d18e7cebfa05a2da0af0e0d7e --- /dev/null +++ b/server/lib/query_language/src/classes.cpp @@ -0,0 +1,112 @@ +#include "classes.hpp" + +Literal::Literal(std::string v) : val(std::move(v)) { +} + +Literal::Literal(double d) : val(d) { +} + +Literal::Literal(bool b) : val(b) { +} + +Literal::Literal(std::vector json_namevec_) + : json_namevec(std::move(json_namevec_)) { +} + +Literal::Literal() : val(std::monostate{}) { +} + +Value &Literal::get_value() { + return val; +} + +std::vector Literal::get_json_namevec() { + return json_namevec; +} + +Value Literal::accept(ExprVisitor *visitor) { + return visitor->visit(this); +} + +Binary::Binary(std::unique_ptr left_, + token tok, + std::unique_ptr right_) + : left(std::move(left_)), oper(tok), right(std::move(right_)){}; + +Value Binary::accept(ExprVisitor *visitor) { + return visitor->visit(this); +} + +Expr *Binary::get_leftptr() { + return left.get(); +} + +Expr *Binary::get_rightptr() { + return right.get(); +} + +token Binary::get_opername() { + return oper; +} + +Unary::Unary(std::unique_ptr expr_, token tok) + : expression(std::move(expr_)), oper(tok){}; + +Expr *Unary::get_expr() { + return expression.get(); +} + +token Unary::get_opername() { + return oper; +} + +Value Unary::accept(ExprVisitor *visitor) { + return visitor->visit(this); +} + +LogicalExpr::LogicalExpr(std::unique_ptr left_, + token tok, + std::unique_ptr right_) + : left(std::move(left_)), oper(tok), right(std::move(right_)){}; + +Value LogicalExpr::accept(ExprVisitor *visitor) { + return visitor->visit(this); +} + +Expr *LogicalExpr::get_leftptr() { + return left.get(); +} + +Expr *LogicalExpr::get_rightptr() { + return right.get(); +} + +token LogicalExpr::get_opername() { + return oper; +} + +FuncIn::FuncIn(std::unique_ptr left_, std::unique_ptr right_) + : left(std::move(left_)), right(std::move(right_)){}; + +Value FuncIn::accept(ExprVisitor *visitor) { + return visitor->visit(this); +} + +Expr *FuncIn::get_leftptr() { + return left.get(); +} + +Expr *FuncIn::get_rightptr() { + return right.get(); +} + +Grouping::Grouping(std::unique_ptr Expr_) + : expression(std::move(Expr_)){}; + +Value Grouping::accept(ExprVisitor *visitor) { + return visitor->visit(this); +} + +Expr *Grouping::get_expr() { + return expression.get(); +} \ No newline at end of file diff --git a/server/lib/query_language/src/exception.cpp b/server/lib/query_language/src/exception.cpp new file mode 100644 index 0000000000000000000000000000000000000000..053fbedbb1a279318cdc0dc0ed922704996db9bb --- /dev/null +++ b/server/lib/query_language/src/exception.cpp @@ -0,0 +1,10 @@ +#include "exception.hpp" +#include + +ComponentException::ComponentException(const char *message) + : std::runtime_error(message), message_(message) { +} + +const char *ComponentException::what() const throw() { + return message_.c_str(); +} \ No newline at end of file diff --git a/server/lib/query_language/src/interpreter.cpp b/server/lib/query_language/src/interpreter.cpp new file mode 100644 index 0000000000000000000000000000000000000000..98ae3578c2bff9e724b9641540dae5a3578d22ea --- /dev/null +++ b/server/lib/query_language/src/interpreter.cpp @@ -0,0 +1,407 @@ +#include "interpreter.hpp" + +interpreter::interpreter(){}; + +Value interpreter::interpret(Expr *expression, nlohmann::json card_) { + card = card_; + return evaluate(expression); +} + +Value interpreter::evaluate(Expr *expression) { + return expression->accept(this); +} + +bool interpreter::is_truthy(const Value &val) { + if (std::holds_alternative(val)) { + return false; + } + if (std::holds_alternative(val)) { + return std::get(val) != 0; + } + if (std::holds_alternative(val)) { + return std::get(val); + } + + return true; +} + +bool interpreter::is_equal(const Value &left, const Value &right) { + if (std::holds_alternative(left) && + std::holds_alternative(right)) { + + return std::get(left) == std::get(right); + + } else if (std::holds_alternative(left) && + std::holds_alternative(right)) { + + return std::get(left) == std::get(right); + + } else if (std::holds_alternative(left) && + std::holds_alternative(right)) { + + return std::get(left) == + std::get(right); + + } else if (std::holds_alternative(left) && + std::holds_alternative(right)) { + + return true; // Если оба значения имеют тип std::monostate, считаем их + // равными + } + + return false; // Если типы не совпадают, считаем значения неравными +} + +void interpreter::check_number_operand(const Value &operand) { + if (!std::holds_alternative(operand)) { + throw ComponentException("Invalid operand type"); + } +} + +void interpreter::check_json_operand(const Value &operand) { + if (!std::holds_alternative(operand)) { + throw ComponentException("Expected JSON"); + } +} + +void interpreter::check_json_is_number(const Value &operand) { + if (!std::get(operand).is_number()) { + throw ComponentException("JSON is not a number"); + } +} + +Value interpreter::visit(Binary *expr) { + Value left = evaluate(expr->get_leftptr()); + Value right = evaluate(expr->get_rightptr()); + + if (std::holds_alternative(left) && + std::holds_alternative(right)) { + double left_ = std::get(left); + double right_ = std::get(right); + switch (expr->get_opername().type) { + case PLUS: + return Value(left_ + right_); + case MINUS: + return Value(left_ - right_); + + case STAR: + return Value(left_ * right_); + + case SLASH: + return Value(left_ / right_); + + case LESS: + return Value(left_ < right_); + + case LESS_EQUAL: + return Value(left_ <= right_); + + case GREATER: + return Value(left_ > right_); + + case GREATER_EQUAL: + return Value(left >= right); + + case BANG_EQUAL: + return Value(!is_equal(left_, right_)); + + case EQUAL_EQUAL: + return Value(is_equal(left_, right_)); + + default: + throw ComponentException("Invalid operand type"); + } + } else if (std::holds_alternative(left) && + std::holds_alternative(right)) { + nlohmann::json left_ = std::get(left); + nlohmann::json right_ = std::get(right); + if (expr->get_opername().type == PLUS) { + return Value(mergeJson(left_, right_)); + } else { + throw ComponentException("Invalid operand type"); + } + } else { + throw ComponentException("Invalid operand type"); + } +} + +Value interpreter::visit(Grouping *expr) { + return expr->get_expr()->accept(this); +} + +Value interpreter::visit(FuncIn *expr) { + Value left = evaluate(expr->get_leftptr()); + Value right = evaluate(expr->get_rightptr()); + + if (std::holds_alternative(left) && + std::holds_alternative(right)) { + + std::string left_ = std::get(left); + nlohmann::json right_ = std::get(right); + return Value(find_word_inJson(left_, right_)); + } else { + return Value(false); + } +} + +Value interpreter::visit(Unary *expr) { + Value right = evaluate(expr->get_expr()); + + switch (expr->get_opername().type) { + case tt::MINUS: + check_number_operand(right); + return Value(-std::get(right)); + case tt::NOT: + return Value(!is_truthy(right)); + case tt::LEN: + check_json_operand(right); + return Value(json_length(std::get(right))); + case tt::SPLIT: + check_json_operand(right); + return Value(split_json(std::get(right))); + case tt::UPPER: + check_json_operand(right); + return Value(upper_json_string(std::get(right))); + case tt::LOWER: + check_json_operand(right); + return Value(lower_json_string(std::get(right))); + case tt::REDUCE: + check_json_operand(right); + return Value(reduce_json(std::get(right))); + case tt::NUM: + check_json_operand(right); + check_json_is_number(right); + return Value(std::get(right).get()); + default: + throw ComponentException("Invalid operand type"); + } +} + +Value interpreter::visit(LogicalExpr *ex) { + Value left = evaluate(ex->get_leftptr()); + if (ex->get_opername().type == OR) { + if (is_truthy(left)) { + return left; + } + } else { + if (!is_truthy(left)) { + return left; + } + } + return evaluate(ex->get_rightptr()); +} + +Value interpreter::visit(Literal *expr) { + if (!expr->get_json_namevec().empty()) { + nlohmann::json json_val = + find_json_value(card, expr->get_json_namevec()); + if (!json_val.empty()) { + expr->get_value() = Value(json_val); + } else { + expr->get_value() = Value(); + } + } + return expr->get_value(); +} + +nlohmann::json interpreter::upper_json_string(const nlohmann::json &data) { + + if (data.is_string()) { + std::string str = data.get(); + std::transform(str.begin(), str.end(), str.begin(), ::toupper); + return str; + } + + if (data.is_array()) { + nlohmann::json result = nlohmann::json::array(); + for (const auto &item : data) { + result.push_back(upper_json_string(item)); + } + return result; + } + + return data; +} + +nlohmann::json interpreter::lower_json_string(const nlohmann::json &data) { + + if (data.is_string()) { + std::string str = data.get(); + std::transform(str.begin(), str.end(), str.begin(), ::tolower); + return str; + } + + if (data.is_array()) { + nlohmann::json result = nlohmann::json::array(); + for (const auto &item : data) { + result.push_back(lower_json_string(item)); + } + return result; + } + + return data; +} + +std::vector interpreter::split_string(const std::string &str) { + std::vector words; + std::string word; + std::istringstream iss(str); + while (iss >> word) { + words.push_back(word); + } + return words; +} + +// Рекурсивная функция для разделения JSON элемента +nlohmann::json interpreter::split_json(const nlohmann::json &jsonValue) { + if (jsonValue.is_null()) { + throw ComponentException("Element is null"); + } + + if (jsonValue.is_string()) { + std::string str = jsonValue.get(); + return split_string(str); + } + + if (jsonValue.is_array()) { + nlohmann::json result = nlohmann::json::array(); + for (const auto &item : jsonValue) { + if (item.is_string()) { + std::string str = item.get(); + result.push_back(split_string(str)); + } + } + return result; + } + + throw ComponentException("Invalid operation"); +} + +double interpreter::json_length(const nlohmann::json &jsonValue) { + if (jsonValue.is_null()) { + throw ComponentException("Element is null"); + } + + if (jsonValue.is_array() || jsonValue.is_object()) { + return jsonValue.size(); + } + + return 1; // неитерируемый объект, например число или строка +} + +bool interpreter::find_word_inJson(std::string word, nlohmann::json jsonValue) { + if (jsonValue.is_string()) { + return jsonValue.get() == word; + } + + if (!jsonValue.is_array()) { + return false; + } + + bool found = std::ranges::any_of(jsonValue, [&](const auto &element) { + return find_word_inJson(word, element); + }); + + if (found) { + return true; + } + + return false; +} + +nlohmann::json +interpreter::find_json_value(const nlohmann::json &card, + std::vector levels_vec) { + nlohmann::json current_json = card; + + for (size_t i = 0; i < levels_vec.size(); ++i) { + std::string key = levels_vec[i]; + + if (current_json.is_object()) { + if (current_json.contains(key)) { + current_json = current_json[key]; + } else if (key == "$ANY") { + return handle_any_key(current_json, levels_vec, i); + } else if (key == "$SELF") { + return get_self_keys(current_json); + } else { + throw ComponentException("Invalid operand type"); + } + } else { + throw ComponentException("Invalid operand type"); + } + } + + return current_json; +} + +nlohmann::json +interpreter::handle_any_key(const nlohmann::json &json_Value, + const std::vector &levels_vec, + size_t current_index) { + nlohmann::json any_values; + + for (auto it = json_Value.begin(); it != json_Value.end(); ++it) { + const auto &result = find_json_value( + it.value(), + std::vector(levels_vec.begin() + current_index + 1, + levels_vec.end())); + + if (!result.is_null()) { + any_values.push_back(result); + } + } + + if (any_values.size() == 1) { // возвращаем элемент, а не массив + return any_values[0]; + } else { + return any_values; + } +} + +nlohmann::json interpreter::get_self_keys(const nlohmann::json &json_Value) { + nlohmann::json self_keys; + + for (auto it = json_Value.begin(); it != json_Value.end(); ++it) { + self_keys.push_back(it.key()); + } + + return self_keys; +} + +nlohmann::json interpreter::reduce_json(const nlohmann::json &jsonElem) { + if (jsonElem.is_null()) { + throw ComponentException("Element is null"); + } + + nlohmann::json result; + + for (const auto &item : jsonElem) { + if (item.is_array()) { + for (const auto &nestedItem : item) { + result.push_back(nestedItem); + } + } else { + result.push_back(item); + } + } + + return result; +} + +nlohmann::json interpreter::mergeJson(const nlohmann::json &jsonLeft, + const nlohmann::json &jsonRight) { + nlohmann::json mergedJson = nlohmann::json::object(); + + for (nlohmann::json::const_iterator it = jsonLeft.begin(); + it != jsonLeft.end(); + ++it) { + mergedJson[it.key()] = it.value(); + } + for (nlohmann::json::const_iterator it = jsonRight.begin(); + it != jsonRight.end(); + ++it) { + mergedJson[it.key()] = it.value(); + } + return mergedJson; +} diff --git a/server/lib/query_language/src/parser.cpp b/server/lib/query_language/src/parser.cpp new file mode 100644 index 0000000000000000000000000000000000000000..ec9223b344039f70e52c376b4a678665796c6039 --- /dev/null +++ b/server/lib/query_language/src/parser.cpp @@ -0,0 +1,197 @@ +#include "parser.hpp" + +parser::parser(std::vector &t) : tokens(t){}; + +bool parser::is_at_end() { + return tokens[current].type == token_type::EOTF; +} + +token parser::peek() { + return tokens[current]; +} + +token parser::previous() { + return tokens[current - 1]; +} + +token parser::advance() { + if (!is_at_end()) + current++; + return previous(); +} + +bool parser::check(token_type type) { + return !is_at_end() && peek().type == type; +} + +bool parser::match(std::vector types) { + return std::ranges::any_of(types, [this](const token_type &type) { + if (check(type)) { + advance(); + return true; + } + return false; + }); +} + +std::unique_ptr parser::primary() { + token_type type = peek().type; + advance(); + + switch (type) { + case tt::FALSE: + return std::make_unique(false); + case tt::TRUE: + return std::make_unique(true); + case tt::NUMBER: + return std::make_unique(std::stod(previous().literal)); + case tt::STRING: + return std::make_unique(previous().literal); + case tt::LEFT_PAREN: { + std::unique_ptr Expr = expression(); + if (match({tt::RIGHT_PAREN})) { + return std::make_unique(std::move(Expr)); + } + throw ComponentException( + "Missing closing parenthesis for grouping."); + } + case tt::IDENTIFIER: { + std::vector json_fields = read_json_elem(); + if (!json_fields.empty()) { + return std::make_unique(json_fields); + } + throw ComponentException("Invalid JSON element."); + } + default: + throw ComponentException("Unexpected token encountered."); + } +} + +std::vector parser::read_json_elem() { + std::vector json_fields; + json_fields.push_back(previous().lexeme); + while (match({tt::LEFT_BRACKET})) { + if (match({tt::IDENTIFIER})) { + json_fields.push_back(previous().lexeme); + } else { + return std::vector(); + } + + if (!match({tt::RIGHT_BRACKET})) { + throw ComponentException("Missing closing parenthesis for JSON."); + } + } + return json_fields; +} + +std::unique_ptr parser::func_in() { + std::unique_ptr left = primary(); + if (match({tt::IN})) { + /*if(!match({tt::LEFT_PAREN})){ + throw ComponentException("Missing open parenthesis for function + call."); + }*/ + + std::unique_ptr right = primary(); + + /*if (!match({tt::RIGHT_PAREN})) { + throw ComponentException("Missing closing parenthesis for function + call."); + }*/ + + return std::make_unique(std::move(left), std::move(right)); + } + return left; +} + +std::unique_ptr parser::unar() { + if (match({tt::NOT, + tt::MINUS, + tt::LEN, + tt::SPLIT, + tt::UPPER, + tt::LOWER, + tt::REDUCE, + tt::NUM})) { + token oper = previous(); + std::unique_ptr right = unar(); + return std::make_unique(std::move(right), oper); + } + return func_in(); +} + +std::unique_ptr parser::multiplication() { + std::unique_ptr left = unar(); + while (match({tt::SLASH, tt::STAR})) { + token oper = previous(); + std::unique_ptr right = unar(); + left = + std::make_unique(std::move(left), oper, std::move(right)); + } + return left; +} + +std::unique_ptr parser::addition() { + std::unique_ptr left = multiplication(); + while (match({tt::MINUS, tt::PLUS})) { + token oper = previous(); + std::unique_ptr right = multiplication(); + left = + std::make_unique(std::move(left), oper, std::move(right)); + } + return left; +} + +std::unique_ptr parser::comparison() { + std::unique_ptr left = addition(); + while (match({tt::LESS, tt::LESS_EQUAL, tt::GREATER, tt::GREATER_EQUAL})) { + token oper = previous(); + std::unique_ptr right = addition(); + left = + std::make_unique(std::move(left), oper, std::move(right)); + } + return left; +} + +std::unique_ptr parser::equality() { + std::unique_ptr left = comparison(); + while (match({tt::BANG_EQUAL, tt::EQUAL_EQUAL})) { + token oper = previous(); + std::unique_ptr right = comparison(); + left = + std::make_unique(std::move(left), oper, std::move(right)); + } + return left; +} + +std::unique_ptr parser::and_expr() { + std::unique_ptr left = equality(); + + while (match({tt::AND})) { + token oper = previous(); + std::unique_ptr right = equality(); + left = std::make_unique( + std::move(left), oper, std::move(right)); + } + return left; +} + +std::unique_ptr parser::or_expr() { + std::unique_ptr left = and_expr(); + + while (match({tt::OR})) { + token oper = previous(); + std::unique_ptr right = and_expr(); + left = std::make_unique( + std::move(left), oper, std::move(right)); + } + return left; +} + +std::unique_ptr parser::expression() { + return or_expr(); +} + +std::unique_ptr parser::parse() { + return expression(); +} diff --git a/server/lib/query_language/src/querying.cpp b/server/lib/query_language/src/querying.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e8bf1c08a1ea89a7f6632bb1186079bba93cdac3 --- /dev/null +++ b/server/lib/query_language/src/querying.cpp @@ -0,0 +1,53 @@ +#include "querying.hpp" + +#include "exception.hpp" +#include "interpreter.hpp" +#include "parser.hpp" +#include "scaner.hpp" +#include "spdlog/fmt/bundled/core.h" +#include "spdlog/spdlog.h" +#include +#include +#include + +auto prepare_filter(const std::string &query) -> std::function< + std::variant(const nlohmann::json &json_card)> { + SPDLOG_INFO("Preparing query: `{}`", query); + if (query.empty()) { + SPDLOG_INFO("Query `{}` is considered empty, returning a constant true " + "function", + query); + return [](const nlohmann::json &) -> bool { return true; }; + } + + SPDLOG_INFO("Scanning query: `{}`", query); + scanner scan(query); + std::vector tokens = scan.scan_tokens(); + + SPDLOG_INFO("Parsing query: `{}`", query); + auto p = parser(tokens); + std::shared_ptr exp = p.parse(); + if (exp == nullptr) { + SPDLOG_WARN("Parsing query `{}` resulted in empty expression, " + "returning a constant false function", + query); + return [](const nlohmann::json &) -> bool { return false; }; + } + + SPDLOG_INFO("Setting up an interpreter for ther query: `{}`", query); + interpreter inter; + return [i = std::move(inter), + e = std::move(exp)](const nlohmann::json &json_card) mutable + -> std::variant { + Value val; + try { + val = i.interpret(e.get(), json_card); + } catch (const ComponentException &error) { + return error.what(); + } + if (std::holds_alternative(val)) { + return std::get(val); + } + return fmt::format("Resulting type has to be `bool`"); + }; +} diff --git a/server/lib/query_language/src/scaner.cpp b/server/lib/query_language/src/scaner.cpp new file mode 100644 index 0000000000000000000000000000000000000000..53677fb32af9d00edc62054bea90bb4b01446cfd --- /dev/null +++ b/server/lib/query_language/src/scaner.cpp @@ -0,0 +1,223 @@ +#include "scaner.hpp" + +token::token() : type(token_type::NUL) { +} + +token::token(token_type type) : type(type) { +} + +token::token(token_type type, const std::string &lexeme) + : type(type), lexeme(lexeme) { +} + +token::token(token_type type, + const std::string &lexeme, + const std::string &literal) + : type(type), lexeme(lexeme), literal(literal) { +} + +void scanner::init_keywords() { + keywords["in"] = tt::IN; + keywords["len"] = tt::LEN; + keywords["split"] = tt::SPLIT; + keywords["lower"] = tt::LOWER; + keywords["upper"] = tt::UPPER; + keywords["all"] = tt::ALL; + keywords["any"] = tt::ANY; + keywords["and"] = tt::AND; + keywords["or"] = tt::OR; + keywords["not"] = tt::NOT; + keywords["reduce"] = tt::REDUCE; + keywords["num"] = tt::NUM; +} + +bool scanner::has_next(size_t i) { + return current + i < source.size(); +} + +bool scanner::is_digit(char ch) { + return std::isdigit(ch); +} + +void scanner::add_token(token_type type) { + add_token(type, ""); +} + +void scanner::add_token(token_type type, const std::string &literal) { + std::string text = source.substr(start, current - start); + tokens.emplace_back(type, text, literal); +} + +char scanner::advance() { + current++; + return source[current - 1]; +} + +bool scanner::match(const std::string &expected) { + if (!has_next(expected.size())) + return false; + if (source.substr(current, expected.size()) != expected) + return false; + current += expected.size(); + return true; +} + +char scanner::peek() { + if (!has_next()) { + return '\0'; + } + return source[current]; +} + +char scanner::peek_next() { + if (current + 1 >= source.size()) { + return '\0'; + } + return source[current + 1]; +} + +void scanner::number() { + + while (is_digit(peek())) { + advance(); + } + + if (peek() == '.' && is_digit(peek_next())) { + advance(); + while (is_digit(peek())) { + advance(); + } + } + + add_token(tt::NUMBER, source.substr(start, current - start)); +} + +void scanner::read_json_keyword() { + advance(); // считали $ + while (isalnum(peek()) || peek() == '_') { + advance(); + } + std::string text = source.substr(start, current - start); + if (text == "$ANY" || text == "$SELF") { + add_token(tt::IDENTIFIER); + } + // +} + +void scanner::read_json_level() { + while (isalnum(peek()) || peek() == '_') { + advance(); + } + std::string text = source.substr(start, current - start); + auto type = keywords.find(text); + type == keywords.end() ? add_token(tt::IDENTIFIER) + : add_token(type->second); +} + +void scanner::string() { + + while (peek() != '"' && has_next()) { + advance(); + } + + // нет вторых кавычек + if (!has_next()) { + + throw ComponentException("Missing closing quotation mark for string."); + } + + advance(); // пропуск для кавычки + // добавляем слово без кавычек + std::string value = source.substr(start + 1, current - start - 2); + add_token(tt::STRING, value); +} + +void scanner::scan_token() { + const char token = advance(); + switch (token) { + case '(': + add_token(tt::LEFT_PAREN); + break; + case ')': + add_token(tt::RIGHT_PAREN); + break; + case '[': + add_token(tt::LEFT_BRACKET); + break; + case ']': + add_token(tt::RIGHT_BRACKET); + break; + case ',': + add_token(tt::COMMA); + break; + case '.': + add_token(tt::DOT); + break; + case '-': + add_token(tt::MINUS); + break; + case '+': + add_token(tt::PLUS); + break; + case ';': + add_token(tt::SEMICOLON); + break; + case '*': + add_token(tt::STAR); + break; + case '/': + add_token(SLASH); + break; + case '?': + add_token(tt::QMARK); + break; + case ':': + add_token(tt::COLON); + break; + case '!': + add_token(match(std::string(1, '=')) ? tt::BANG_EQUAL : tt::BANG); + break; + case '=': + add_token(match(std::string("=")) ? tt::EQUAL_EQUAL : tt::EQUAL); + break; + case '<': + add_token(match(std::string("=")) ? tt::LESS_EQUAL : tt::LESS); + break; + case '>': + add_token(match(std::string("=")) ? tt::GREATER_EQUAL + : tt::GREATER); + break; + case ' ': + case '\r': + case '\n': + case '\t': + break; + case '"': + string(); + break; + + default: + if (is_digit(token)) { + number(); + } else if (token == '$') { + read_json_keyword(); + } else if (isalpha(token)) { + read_json_level(); + } else + throw ComponentException("Unexpected character encountered."); + break; + } +} + +scanner::scanner(const std::string &source_) : source(source_) { + init_keywords(); +} + +std::vector scanner::scan_tokens() { + while (has_next()) { + start = current; + scan_token(); + } + tokens.emplace_back(EOTF, "", ""); + return tokens; +} diff --git a/server/main.cpp b/server/main.cpp new file mode 100644 index 0000000000000000000000000000000000000000..f15fa6f4e5bda02855cbf2e20d10b6eafd7f3c74 --- /dev/null +++ b/server/main.cpp @@ -0,0 +1,109 @@ +// https://github.com/gabime/spdlog/issues/1515 +#include "spdlog/common.h" +#define SPDLOG_ACTIVE_LEVEL \ + SPDLOG_LEVEL_TRACE // Must: define SPDLOG_ACTIVE_LEVEL before `#include + // "spdlog/spdlog.h"` +#include "spdlog/sinks/stdout_sinks.h" +#include "spdlog/spdlog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "argparse.hpp" + +#include "PluginsProvider.hpp" +#include "Server.hpp" +#include "sysmodule.h" + +auto main(int argc, char *argv[]) -> int { + // https://github.com/gabime/spdlog/wiki/3.-Custom-formatting + spdlog::set_pattern("[%H:%M:%S] [%s] line %# %v"); + + argparse::ArgumentParser program{}; + program.add_argument("--plugins-path") + .help("A path to a directory with plugins") + .default_value((std::filesystem::current_path() / "plugins").string()); + + program.add_argument("--python-path") + .help("A path to a Python Interpreter") + .default_value(""); + + constexpr uint16_t default_port = 8888; + program.add_argument("-p", "--port") + .help("A port to deploy server on") + .default_value(default_port) + .scan<'u', uint16_t>(); + + try { + program.parse_args(argc, argv); + } catch (const std::runtime_error &err) { + SPDLOG_ERROR("Couldn't parse arguments. Reason: {}", err.what()); + std::exit(1); + } + + std::filesystem::path plugins_dir = program.get("--plugins-path"); + std::filesystem::path python_path = program.get("--python-path"); + auto port = program.get("--port"); + + std::filesystem::path absolute_path_to_plugins_dir; + try { + if (!std::filesystem::exists(plugins_dir)) { + SPDLOG_ERROR( + "Couldn't parse a path to plugins. Reason: path is not valid"); + std::exit(1); + } + + absolute_path_to_plugins_dir = std::filesystem::absolute(plugins_dir); + } catch (const std::filesystem::filesystem_error &err) { + SPDLOG_ERROR("Couldn't parse a path to plugins. Reason: {}", + err.what()); + std::exit(1); + } + + if (!python_path.empty()) { + if (!std::filesystem::exists(python_path)) { + SPDLOG_ERROR("Couldn't parse a path to Python interpreter. Reason " + "path is not valid"); + std::exit(1); + } + + std::filesystem::path absolute_path_to_python_interpreter; + try { + absolute_path_to_python_interpreter = + std::filesystem::absolute(python_path); + } catch (const std::filesystem::filesystem_error &err) { + SPDLOG_ERROR( + "Couldn't parse a path to Python interpreter. Reason: {}", + err.what()); + std::exit(1); + } + Py_SetProgramName(absolute_path_to_python_interpreter.wstring().data()); + } + Py_Initialize(); + auto plugins_dirs = PluginTypesLocationsConfig{ + .definitions_providers_dir = + absolute_path_to_plugins_dir / "definitions/", + .sentences_providers_dir = absolute_path_to_plugins_dir / "sentences/", + .images_providers_dir = absolute_path_to_plugins_dir / "images/", + .audios_providers_dir = absolute_path_to_plugins_dir / "audios/", + .format_processors_dir = + absolute_path_to_plugins_dir / "format_processors/"}; + + boost::asio::io_context io_context; + boost::asio::signal_set signals(io_context, SIGINT, SIGTERM); + signals.async_wait( + boost::bind(&boost::asio::io_service::stop, &io_context)); + + auto plugins_provider = + std::make_shared(std::move(plugins_dirs)); + auto server = PluginServer(std::move(plugins_provider), io_context, port); + io_context.run(); +} diff --git a/server/test_client/CMakeLists.txt b/server/test_client/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..2b35220094d21150e62234b5cd0339070f8129eb --- /dev/null +++ b/server/test_client/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(Sender) +add_subdirectory(tests) diff --git a/server/test_client/Sender/CMakeLists.txt b/server/test_client/Sender/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..971c458ecf82d69677a95ca9653a2ea47636ea70 --- /dev/null +++ b/server/test_client/Sender/CMakeLists.txt @@ -0,0 +1,2 @@ +add_library(test_sender Sender.cpp) +target_include_directories(test_sender PUBLIC ./) diff --git a/server/test_client/Sender/Sender.cpp b/server/test_client/Sender/Sender.cpp new file mode 100644 index 0000000000000000000000000000000000000000..32f037da7b405f1d6fa737c5ecce2e14c51ec502 --- /dev/null +++ b/server/test_client/Sender/Sender.cpp @@ -0,0 +1,34 @@ +#include "Sender.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Sender::Sender(const std::string &hostname, uint16_t port) : socket_(ios_) { + boost::asio::ip::tcp::endpoint endpoint( + boost::asio::ip::address::from_string(hostname), port); + socket_.connect(endpoint); +} + +auto Sender::request(const std::string &str_request) -> nlohmann::json { + bufffer_ = {}; + + strncpy(bufffer_.data(), str_request.c_str(), str_request.size()); + + boost::system::error_code error; + + size_t len = socket_.write_some( + boost::asio::buffer(bufffer_, strlen(bufffer_.data())), error); + assert(len == str_request.size()); + size_t len2 = socket_.read_some(boost::asio::buffer(bufffer_), error); + auto parsed_response = + nlohmann::json::parse(bufffer_.data(), bufffer_.data() + len2); + + return parsed_response; +} diff --git a/server/test_client/Sender/Sender.hpp b/server/test_client/Sender/Sender.hpp new file mode 100644 index 0000000000000000000000000000000000000000..11fa19461623d6a94b396059d202fbb656cfc7b5 --- /dev/null +++ b/server/test_client/Sender/Sender.hpp @@ -0,0 +1,22 @@ +#ifndef TEST_SENDER_H +#define TEST_SENDER_H + +#include +#include +#include +#include +#include + +class Sender { + public: + explicit Sender(const std::string &hostname, uint16_t port); + + auto request(const std::string &str_request) -> nlohmann::json; + + private: + std::array bufffer_; + boost::asio::io_service ios_; + boost::asio::ip::tcp::socket socket_; +}; + +#endif diff --git a/server/test_client/deck.apkg b/server/test_client/deck.apkg new file mode 100644 index 0000000000000000000000000000000000000000..95772e1be6b177a44c3249d4e30118d77ee56004 Binary files /dev/null and b/server/test_client/deck.apkg differ diff --git a/server/test_client/deck.json b/server/test_client/deck.json new file mode 100644 index 0000000000000000000000000000000000000000..ee3da6a1a93ed0262bcc1b18e097ba7e2f58d8eb --- /dev/null +++ b/server/test_client/deck.json @@ -0,0 +1,197 @@ + + [ + { + "audios": { + "local": [], + "web": [ + [ + "https://dictionary.cambridge.org//media/english/uk_pron/u/ukg/ukglu/ukglutt024.mp3", + "" + ] + ] + }, + "definition": "to travel or move to another place", + "examples": [ + "We went into the house.", + "I went to Paris last summer. Have you ever been there?", + "We don't go to the cinema very often these days.", + "Wouldn't it be quicker to go by train?", + "Does this train go to Newcastle?", + "Where do you think you're going? Shouldn't you be at school?" + ], + "images": { + "local": [], + "web": [] + }, + "other": [], + "special": [ + "present participle going", + "past tense went", + "past participle gone" + ], + "tags": { + "domain": [], + "level": "A1", + "pos": [ + "verb" + ], + "region": [], + "usage": [] + }, + "word": "go" + }, + { + "audios": { + "local": [], + "web": [ + [ + "https://dictionary.cambridge.org//media/english/uk_pron/u/ukg/ukglu/ukglutt024.mp3", + "" + ] + ] + }, + "definition": "to be in the process of moving", + "examples": [ + "Can't we go any faster?", + "We were going along at about 50 miles an hour.", + "to go down the road", + "to go up/down stairs", + "to go over the bridge", + "to go through a tunnel", + "I've got a tune going around/round in my head (= I am continually hearing it) and I just can't remember the name of it." + ], + "images": { + "local": [], + "web": [] + }, + "other": [], + "special": [ + "present participle going", + "past tense went", + "past participle gone" + ], + "tags": { + "domain": [], + "level": "A1", + "pos": [ + "verb" + ], + "region": [], + "usage": [] + }, + "word": "go" + }, + { + "audios": { + "local": [], + "web": [ + [ + "https://dictionary.cambridge.org//media/english/uk_pron/u/ukg/ukglu/ukglutt024.mp3", + "" + ] + ] + }, + "definition": "to move or travel somewhere in order to do something", + "examples": [ + "We go shopping every Friday night.", + "I've never gone skiing.", + "They've gone for a walk, but they should be back soon.", + "She went to meet Blake at the station.", + "There's a good film on at the Odeon. Shall we go?" + ], + "images": { + "local": [], + "web": [] + }, + "other": [], + "special": [ + "present participle going", + "past tense went", + "past participle gone" + ], + "tags": { + "domain": [], + "level": "A1", + "pos": [ + "verb" + ], + "region": [], + "usage": [] + }, + "word": "go" + }, + { + "audios": { + "local": [], + "web": [ + [ + "https://dictionary.cambridge.org//media/english/uk_pron/u/ukg/ukglu/ukglutt024.mp3", + "" + ] + ] + }, + "definition": "to leave a place, especially in order to travel to somewhere else", + "examples": [ + "Is it midnight already? I really must go/must be going.", + "She wasn't feeling well, so she went home early.", + "What time does the last train to Bath go?", + "I'm afraid he'll have to go (= be dismissed from his job) - he's far too inefficient to continue working for us.", + "This carpet's terribly old and worn out - it really will have to go (= be got rid of)." + ], + "images": { + "local": [], + "web": [] + }, + "other": [], + "special": [ + "present participle going", + "past tense went", + "past participle gone" + ], + "tags": { + "domain": [], + "level": "B1", + "pos": [ + "verb" + ], + "region": [], + "usage": [] + }, + "word": "go" + }, + { + "audios": { + "local": [], + "web": [ + [ + "https://dictionary.cambridge.org//media/english/uk_pron/u/ukg/ukglu/ukglutt024.mp3", + "" + ] + ] + }, + "definition": "polite word for to die", + "examples": [ + "She went peacefully in her sleep." + ], + "images": { + "local": [], + "web": [] + }, + "other": [], + "special": [ + "present participle going", + "past tense went", + "past participle gone" + ], + "tags": { + "domain": [], + "pos": [ + "verb" + ], + "region": [], + "usage": [] + }, + "word": "go" + } + ] + diff --git a/server/test_client/tests/CMakeLists.txt b/server/test_client/tests/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..a7a2274dad41db6a485fcd068e1acd5e7a1013b6 --- /dev/null +++ b/server/test_client/tests/CMakeLists.txt @@ -0,0 +1,74 @@ +add_library(server_test_fixture INTERFACE) +target_include_directories(server_test_fixture INTERFACE ./) +target_link_libraries(server_test_fixture INTERFACE test_sender +) + +# AUDIO +add_executable( + audios_provider_test + audios_provider_test.cpp +) + +target_link_libraries( + audios_provider_test + test_sender + media + GTest::gtest_main +) + + +# DEFINITIONS +add_executable( + definitions_provider_test + definitions_provider_test.cpp +) + +target_link_libraries( + definitions_provider_test + test_sender + GTest::gtest_main +) + + +# Images +add_executable( + images_provider_test + images_provider_test.cpp +) + +target_link_libraries( + images_provider_test + test_sender + media + GTest::gtest_main +) + +# Sentences +add_executable( + sentences_provider_test + sentences_provider_test.cpp +) + +target_link_libraries( + sentences_provider_test + test_sender + GTest::gtest_main +) + +# FORMAT PROCESSOR +add_executable( + format_processor_test + format_processor_test.cpp +) + +target_link_libraries( + format_processor_test + test_sender + GTest::gtest_main +) + +gtest_discover_tests(audios_provider_test) +gtest_discover_tests(definitions_provider_test) +gtest_discover_tests(images_provider_test) +gtest_discover_tests(sentences_provider_test) +gtest_discover_tests(format_processor_test) diff --git a/server/test_client/tests/audios_provider_test.cpp b/server/test_client/tests/audios_provider_test.cpp new file mode 100644 index 0000000000000000000000000000000000000000..2739783e3d27cefa35c7cacd0d812da6252913e2 --- /dev/null +++ b/server/test_client/tests/audios_provider_test.cpp @@ -0,0 +1,117 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "Media.hpp" +#include "Sender.hpp" + +class AudiosProvider : public ::testing::Test { + protected: + static constexpr auto HOST = "127.0.0.1"; + static constexpr auto PORT = 8888; + + auto SetUp() -> void override { + boost::asio::io_service ios; + boost::asio::ip::tcp::socket socket(ios); + std::construct_at(&sender_, HOST, PORT); + } + + Sender sender_; + + public: + AudiosProvider() : sender_(HOST, PORT) { + } +}; + +// Макро потому что возможно буду тестить код без него +#define Init(sender) \ + do { \ + const auto *request = \ + R"({"query_type": "init",)" \ + R"("plugin_name": "audios", "plugin_type": "audios" })" \ + "\r\n"; \ + auto expected = R"*({"status": 0, "message": ""})*"_json; \ + auto actual = (sender).request(request); \ + ASSERT_EQ(expected, actual); \ + } while (0) + +TEST_F(AudiosProvider, Init) { + Init(sender_); +} + +TEST_F(AudiosProvider, ValidateConfig) { + Init(sender_); + + const auto *request = R"( + { + "query_type": "validate_config", + "plugin_type": "audios", + "config": {"audio region": "uk", "timeout": 1} + })" + "\r\n"; + + auto expected = R"*({"status": 0, "result": {}})*"_json; + auto actual = sender_.request(request); + ASSERT_EQ(expected, actual); +} + +TEST_F(AudiosProvider, blankGet) { + Init(sender_); + + const auto *request = R"*( + { + "query_type": "get", + "plugin_type": "audios", + "word": "sunshade", + "filter": "", + "batch_size": 0, + "restart": false + })*" + "\r\n"; + + auto expected = + R"*({"result": [{"local": [], "web": []}, ""], "status": 0})*"_json; + auto actual = sender_.request(request); + ASSERT_EQ(expected, actual); +} + +TEST_F(AudiosProvider, Get) { + Init(sender_); + + const auto *request = R"*( + { + "query_type": "get", + "plugin_type": "audios", + "word": "sunshade", + "filter": "", + "batch_size": 5, + "restart": false + })*" + "\r\n"; + + auto actual = sender_.request(request); + ASSERT_EQ(actual["status"], 0); + ASSERT_FALSE(actual["result"][1].empty()); + Media links = actual["result"][0]; + ASSERT_LE(links.web.size(), 5); +} + +TEST_F(AudiosProvider, ListPlugins) { + Init(sender_); + + const auto *request = R"( + { + "query_type": "list_plugins", + "plugin_type": "audios" + })" + "\r\n"; + + auto expected = + R"*({"result": {"success": ["audios"], "failed": []}, "status": 0})*"_json; + auto actual = sender_.request(request); + ASSERT_EQ(expected, actual); +} diff --git a/server/test_client/tests/definitions_provider_test.cpp b/server/test_client/tests/definitions_provider_test.cpp new file mode 100644 index 0000000000000000000000000000000000000000..d3b3ac1a83302dfa35438fdd68a97eb7200fb6fb --- /dev/null +++ b/server/test_client/tests/definitions_provider_test.cpp @@ -0,0 +1,188 @@ +#include +#include +#include +#include +#include +#include + +#include "Sender.hpp" + +class DefinitionsProvider : public ::testing::Test { + protected: + static constexpr auto HOST = "127.0.0.1"; + static constexpr auto PORT = 8888; + + auto SetUp() -> void override { + boost::asio::io_service ios; + boost::asio::ip::tcp::socket socket(ios); + std::construct_at(&sender_, HOST, PORT); + } + + Sender sender_; + + public: + DefinitionsProvider() : sender_(HOST, PORT) { + } +}; + +// Макро потому что возможно буду тестить код без него +#define Init(sender) \ + do { \ + const auto *request = \ + R"({"query_type": "init",)" \ + R"("plugin_name": "definitions", "plugin_type": "word" })" \ + "\r\n"; \ + auto expected = R"*({"status": 0, "message": ""})*"_json; \ + auto actual = (sender).request(request); \ + ASSERT_EQ(expected, actual); \ + } while (0) + +TEST_F(DefinitionsProvider, Init) { + Init(sender_); +} + +TEST_F(DefinitionsProvider, ValidateConfig) { + Init(sender_); + + const auto *request = R"( + { + "query_type": "validate_config", + "plugin_type": "word", + "config": {"audio region": "uk", "timeout": 1} + })" + "\r\n"; + + auto expected = R"*({"status": 0, "result": {}})*"_json; + auto actual = sender_.request(request); + ASSERT_EQ(expected, actual); +} + +TEST_F(DefinitionsProvider, get) { + Init(sender_); + + const auto *request = R"*( + { + "query_type": "get", + "plugin_type": "word", + "word": "sunshade", + "filter": "", + "batch_size": 2, + "restart": false + })*" + "\r\n"; + + auto expected = R"*({ + "result": [ + [ + { + "audios": { + "local": [], + "web": [ + [ + "https://dictionary.cambridge.org//media/english/uk_pron/u/uks/uksun/uksunbu025.mp3", + "" + ] + ] + }, + "definition": "an object similar to an umbrella that you carry to protect yourself from light from the sun", + "examples": [], + "images": { + "local": [], + "web": [ + [ + "https://dictionary.cambridge.org/images/thumb/paraso_noun_002_26494_2.jpg?version=5.0.318", + "" + ] + ] + }, + "other": [], + "special": [], + "tags": { + "domain": [], + "pos": [ + "noun" + ], + "region": [], + "usage": [] + }, + "word": "sunshade" + }, + { + "audios": { + "local": [], + "web": [ + [ + "https://dictionary.cambridge.org//media/english/uk_pron/u/uks/uksun/uksunbu025.mp3", + "" + ] + ] + }, + "definition": "a larger folding frame of this type that you put into the ground to form an area that is sheltered from the light of the sun", + "examples": [], + "images": { + "local": [], + "web": [ + [ + "https://dictionary.cambridge.org/images/thumb/sunsha_noun_002_36661.jpg?version=5.0.318", + "" + ] + ] + }, + "other": [], + "special": [ + "(US also umbrella)" + ], + "tags": { + "domain": [], + "pos": [ + "noun" + ], + "region": [], + "usage": [] + }, + "word": "sunshade" + } + ], + "" + ], + "status": 0 +})*"_json; + + auto actual = sender_.request(request); + ASSERT_EQ(expected, actual); +} + +TEST_F(DefinitionsProvider, blankGet) { + Init(sender_); + + const auto *request = R"*( + { + "query_type": "get", + "plugin_type": "word", + "word": "sunshade", + "filter": "", + "batch_size": 0, + "restart": false + })*" + "\r\n"; + + auto expected = R"*({"result": [[], ""], "status": 0})*"_json; + auto actual = sender_.request(request); + ASSERT_EQ(expected, actual); +} + +TEST_F(DefinitionsProvider, ListPlugins) { + Init(sender_); + + const auto *request = R"( + { + "query_type": "list_plugins", + "plugin_type": "word" + })" + "\r\n"; + + auto expected = + R"*({"result": {"success": ["definitions"], "failed": []}, "status": 0})*"_json; + auto actual = sender_.request(request); + ASSERT_EQ(expected, actual); +} diff --git a/server/test_client/tests/format_processor_test.cpp b/server/test_client/tests/format_processor_test.cpp new file mode 100644 index 0000000000000000000000000000000000000000..b94b13e6184f3fa239a569bc0c3a2b73f08bab05 --- /dev/null +++ b/server/test_client/tests/format_processor_test.cpp @@ -0,0 +1,89 @@ +#include +#include +#include +#include +#include + +#include "Sender.hpp" + +class FormatProcessor : public ::testing::Test { + protected: + static constexpr auto HOST = "127.0.0.1"; + static constexpr auto PORT = 8888; + + auto SetUp() -> void override { + boost::asio::io_service ios; + boost::asio::ip::tcp::socket socket(ios); + std::construct_at(&sender_, HOST, PORT); + } + + Sender sender_; + + public: + FormatProcessor() : sender_(HOST, PORT) { + } +}; + +// Макро потому что возможно буду тестить код без него +#define Init(sender) \ + do { \ + const auto *request = \ + R"({"query_type": "init",)" \ + R"("plugin_name": "processor", "plugin_type": "format" })" \ + "\r\n"; \ + auto expected = R"*({"status": 0, "message": ""})*"_json; \ + auto actual = sender.request(request); \ + ASSERT_EQ(expected, actual); \ + } while (0) + +TEST_F(FormatProcessor, Init) { + Init(sender_); +} + +TEST_F(FormatProcessor, ValidateConfig) { + Init(sender_); + + const auto *request = R"( + { + "query_type": "validate_config", + "plugin_type": "format", + "config": {} + })" + "\r\n"; + + auto expected = R"*({"status": 0, "result": {}})*"_json; + auto actual = sender_.request(request); + ASSERT_EQ(expected, actual); +} + +TEST_F(FormatProcessor, Save) { + Init(sender_); + + const auto *request = R"( + { + "query_type": "save", + "cards_path": + "/home/blackdeer/projects/cpp/technopark/server/test_client/deck.json" + })" + "\r\n"; + + auto expected = R"*({"message": "", "status": 0})*"_json; + auto actual = sender_.request(request); + ASSERT_EQ(expected, actual); +} + +TEST_F(FormatProcessor, ListPlugins) { + Init(sender_); + + const auto *request = R"( + { + "query_type": "list_plugins", + "plugin_type": "format" + })" + "\r\n"; + + auto expected = + R"*({"result": {"success": ["processor"], "failed": []}, "status": 0})*"_json; + auto actual = sender_.request(request); + ASSERT_EQ(expected, actual); +} diff --git a/server/test_client/tests/images_provider_test.cpp b/server/test_client/tests/images_provider_test.cpp new file mode 100644 index 0000000000000000000000000000000000000000..945427c61b280ca68148541212bc1344f084e582 --- /dev/null +++ b/server/test_client/tests/images_provider_test.cpp @@ -0,0 +1,114 @@ +#include +#include +#include +#include +#include + +#include "Media.hpp" +#include "Sender.hpp" + +class ImagesProvider : public ::testing::Test { + protected: + static constexpr auto HOST = "127.0.0.1"; + static constexpr auto PORT = 8888; + + auto SetUp() -> void override { + boost::asio::io_service ios; + boost::asio::ip::tcp::socket socket(ios); + std::construct_at(&sender_, HOST, PORT); + } + + Sender sender_; + + public: + ImagesProvider() : sender_(HOST, PORT) { + } +}; + +// Макро потому что возможно буду тестить код без него +#define Init(sender) \ + do { \ + const auto *request = \ + R"({"query_type": "init",)" \ + R"("plugin_name": "images", "plugin_type": "images" })" \ + "\r\n"; \ + auto expected = R"*({"status": 0, "message": ""})*"_json; \ + auto actual = (sender).request(request); \ + ASSERT_EQ(expected, actual); \ + } while (0) + +TEST_F(ImagesProvider, Init) { + Init(sender_); +} + +TEST_F(ImagesProvider, ValidateConfig) { + Init(sender_); + + const auto *request = R"( + { + "query_type": "validate_config", + "plugin_type": "images", + "config": {"timeout": 1} + })" + "\r\n"; + + auto expected = R"*({"status": 0, "result": {}})*"_json; + auto actual = sender_.request(request); + ASSERT_EQ(expected, actual); +} + +TEST_F(ImagesProvider, blankGet) { + Init(sender_); + + const auto *request = R"*( + { + "query_type": "get", + "plugin_type": "images", + "word": "sunshade", + "filter": "", + "batch_size": 0, + "restart": false + })*" + "\r\n"; + + auto expected = + R"*({"result": [{"local": [], "web": []}, ""], "status": 0})*"_json; + auto actual = sender_.request(request); + ASSERT_EQ(expected, actual); +} + +TEST_F(ImagesProvider, Get) { + Init(sender_); + + const auto *request = R"*( + { + "query_type": "get", + "plugin_type": "images", + "word": "sunshade", + "batch_size": 5, + "restart": false + })*" + "\r\n"; + + auto actual = sender_.request(request); + ASSERT_EQ(actual["status"], 0); + ASSERT_FALSE(actual["result"][1].empty()); + Media links = actual["result"][0]; + ASSERT_LE(links.web.size(), 5); +} + +TEST_F(ImagesProvider, ListPlugins) { + Init(sender_); + + const auto *request = R"( + { + "query_type": "list_plugins", + "plugin_type": "images" + })" + "\r\n"; + + auto expected = + R"*({"result": {"success": ["images"], "failed": []}, "status": 0})*"_json; + auto actual = sender_.request(request); + ASSERT_EQ(expected, actual); +} diff --git a/server/test_client/tests/sentences_provider_test.cpp b/server/test_client/tests/sentences_provider_test.cpp new file mode 100644 index 0000000000000000000000000000000000000000..b6f61e2a42db44eb5a1a25ee094248b846f43fbb --- /dev/null +++ b/server/test_client/tests/sentences_provider_test.cpp @@ -0,0 +1,95 @@ +#include +#include +#include +#include +#include + +#include "Sender.hpp" + +class SentencesProvider : public ::testing::Test { + protected: + static constexpr auto HOST = "127.0.0.1"; + static constexpr auto PORT = 8888; + + auto SetUp() -> void override { + boost::asio::io_service ios; + boost::asio::ip::tcp::socket socket(ios); + std::construct_at(&sender_, HOST, PORT); + } + + Sender sender_; + + public: + SentencesProvider() : sender_(HOST, PORT) { + } +}; + +// Макро потому что возможно буду тестить код без него +#define Init(sender) \ + do { \ + const auto *request = \ + R"({"query_type": "init",)" \ + R"("plugin_name": "sentences", "plugin_type": "sentences" })" \ + "\r\n"; \ + auto expected = R"*({"status": 0, "message": ""})*"_json; \ + auto actual = sender.request(request); \ + ASSERT_EQ(expected, actual); \ + } while (0) + +TEST_F(SentencesProvider, Init) { + SetUp(); + Init(sender_); +} + +TEST_F(SentencesProvider, ValidateConfig) { + SetUp(); + Init(sender_); + + const auto *request = R"( + { + "query_type": "validate_config", + "plugin_type": "sentences", + "config": {"timeout": 1} + })" + "\r\n"; + + auto expected = R"*({"status": 0, "result": {}})*"_json; + auto actual = sender_.request(request); + ASSERT_EQ(expected, actual); +} + +TEST_F(SentencesProvider, blankGet) { + SetUp(); + Init(sender_); + + const auto *request = R"*( + { + "query_type": "get", + "plugin_type": "sentences", + "word": "go", + "batch_size": 0, + "restart": false + })*" + "\r\n"; + + auto expected = R"*({"result": [[], ""], "status": 0})*"_json; + auto actual = sender_.request(request); + ASSERT_EQ(expected, actual); +} + +TEST_F(SentencesProvider, ListPlugins) { + SetUp(); + Init(sender_); + + const auto *request = R"( + { + "query_type": "list_plugins", + "plugin_type": "sentences" + })" + "\r\n"; + + auto expected = + R"*({"result": {"success": ["sentences"], "failed": []}, "status": 0})*"_json; + auto actual = sender_.request(request); + ASSERT_EQ(expected, actual); +} diff --git a/spdlog b/spdlog new file mode 160000 index 0000000000000000000000000000000000000000..57a9fd0841f00e92b478a07fef62636d7be612a8 --- /dev/null +++ b/spdlog @@ -0,0 +1 @@ +Subproject commit 57a9fd0841f00e92b478a07fef62636d7be612a8