diff --git a/CMakeLists.txt b/CMakeLists.txt index bd2815d..582849b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.25) project(creature-server - VERSION "2.1.8" + VERSION "2.2.0" DESCRIPTION "Server for April's Creatures" HOMEPAGE_URL https://github.com/opsnlops/creature-server LANGUAGES C CXX) @@ -186,6 +186,10 @@ add_executable(creature-server src/model/LogItem.cpp src/model/Notice.h src/model/Notice.cpp + src/model/Playlist.h + src/model/Playlist.cpp + src/model/PlaylistItem.h + src/model/PlaylistItem.cpp src/model/StreamFrame.cpp src/model/StreamFrame.h src/model/Sound.h @@ -244,22 +248,25 @@ add_executable(creature-server src/server/ws/messaging/StreamFrameHandler.h src/server/ws/messaging/StreamFrameHandler.cpp - src/server/ws/service/AnimationService.cpp src/server/ws/service/AnimationService.h - src/server/ws/service/CreatureService.cpp + src/server/ws/service/AnimationService.cpp src/server/ws/service/CreatureService.h + src/server/ws/service/CreatureService.cpp + src/server/ws/service/MetricsService.h src/server/ws/service/MetricsService.cpp - src/server/ws/service/MetricsService.cpp - src/server/ws/service/SoundService.cpp + src/server/ws/service/PlaylistService.h + src/server/ws/service/PlaylistService.cpp src/server/ws/service/SoundService.h + src/server/ws/service/SoundService.cpp + src/server/ws/service/VoiceService.h + src/server/ws/service/VoiceService.cpp src/server/ws/websocket/ClientCafe.h src/server/ws/websocket/ClientConnection.h src/server/ws/websocket/ClientCafe.cpp src/server/ws/websocket/ClientConnection.cpp src/server/ws/messaging/NoticeMessageCommandDTO.h - src/server/ws/service/VoiceService.h - src/server/ws/service/VoiceService.cpp + src/server/ws/messaging/SensorReportHandler.h src/server/ws/messaging/SensorReportHandler.cpp diff --git a/src/model/Animation.cpp b/src/model/Animation.cpp index ad42398..9dad2d1 100644 --- a/src/model/Animation.cpp +++ b/src/model/Animation.cpp @@ -1,7 +1,6 @@ - -#include #include +#include #include "model/Animation.h" @@ -32,7 +31,6 @@ namespace creatures { animationDto->metadata = convertToDto(animation.metadata); animationDto->tracks = oatpp::Vector>::createShared(); - for (const auto &frame : animation.tracks) { animationDto->tracks->emplace_back(convertToDto(frame)); } diff --git a/src/model/Playlist.cpp b/src/model/Playlist.cpp new file mode 100644 index 0000000..a8e8e1b --- /dev/null +++ b/src/model/Playlist.cpp @@ -0,0 +1,52 @@ + +#include +#include + +#include + +#include "model/Playlist.h" +#include "model/PlaylistItem.h" + +namespace creatures { + + + std::vector playlist_required_fields = { + "id", "number_of_items", "items" + }; + + std::vector playlistitems_required_fields = { + "animation_id", "weight" + }; + + + oatpp::Object convertToDto(const Playlist &playlist) { + auto playlistDto = PlaylistDto::createShared(); + playlistDto->id = playlist.id; + playlistDto->name = playlist.name; + playlistDto->number_of_items = playlist.number_of_items; + playlistDto->items = oatpp::List>::createShared(); + + // Now do the items + for (const auto &item: playlist.items) { + playlistDto->items->emplace_back(convertToDto(item)); + } + + return playlistDto; + } + + Playlist convertFromDto(const std::shared_ptr &playlistDto) { + Playlist playlist; + playlist.id = playlistDto->id; + playlist.name = playlistDto->name; + playlist.number_of_items = playlistDto->number_of_items; + + // Ensure the list is initialized before iterating + playlist.items = std::vector(); + for (const auto &listItem: *playlistDto->items.getPtr()) { + playlist.items.push_back(convertFromDto(listItem.getPtr())); + } + + return playlist; + } + +} \ No newline at end of file diff --git a/src/model/Playlist.h b/src/model/Playlist.h new file mode 100644 index 0000000..b4d6a4e --- /dev/null +++ b/src/model/Playlist.h @@ -0,0 +1,64 @@ + +#pragma once + +#include +#include + +#include +#include + +#include "server/namespace-stuffs.h" + +#include "model/PlaylistItem.h" + + +namespace creatures { + + struct Playlist { + playlistId_t id; + std::string name; + std::vector items; + uint32_t number_of_items; + }; + + +#include OATPP_CODEGEN_BEGIN(DTO) + +class PlaylistDto : public oatpp::DTO { + + DTO_INIT(PlaylistDto, DTO /* extends */) + + DTO_FIELD_INFO(id) { + info->description = "The ID of this playlist in the form of an UUID"; + } + + DTO_FIELD(String, id); + + DTO_FIELD_INFO(name) { + info->description = "The name of this playlist in the UI"; + } + + DTO_FIELD(String, name); + + DTO_FIELD_INFO(items) { + info->description = "The items in the playlist"; + } + + DTO_FIELD(List < Object < PlaylistItemDto >>, items); + + DTO_FIELD_INFO(number_of_items) { + info->description = "The number of items in this playlist"; + } + + DTO_FIELD(UInt32, number_of_items); + + +}; + +#include OATPP_CODEGEN_END(DTO) + + oatpp::Object convertToDto(const Playlist &playlist); + + Playlist convertFromDto(const std::shared_ptr &playlistDto); + +} \ No newline at end of file diff --git a/src/model/PlaylistItem.cpp b/src/model/PlaylistItem.cpp new file mode 100644 index 0000000..d4960c5 --- /dev/null +++ b/src/model/PlaylistItem.cpp @@ -0,0 +1,37 @@ + + +#include + +#include +#include + +#include "model/PlaylistItem.h" + +namespace creatures { + + std::vector playlistitem_required_fields = { + "animation_id", "weight" + }; + + + // Convert a DTO to the real thing + PlaylistItem convertFromDto(const std::shared_ptr &playlistItemDto) { + + trace("Converting PlaylistItemDto to PlaylistItem"); + + PlaylistItem playlistItem; + playlistItem.animation_id = playlistItemDto->animation_id; + playlistItem.weight = playlistItemDto->weight; + trace("animation_id: {}, weight: {}", playlistItem.animation_id, playlistItem.weight); + + return playlistItem; + } + + oatpp::Object convertToDto(const PlaylistItem &playlistItem) { + auto playlistItemDto = PlaylistItemDto::createShared(); + playlistItemDto->animation_id = playlistItem.animation_id; + playlistItemDto->weight = playlistItem.weight; + return playlistItemDto; + } + +} \ No newline at end of file diff --git a/src/model/PlaylistItem.h b/src/model/PlaylistItem.h new file mode 100644 index 0000000..72878c3 --- /dev/null +++ b/src/model/PlaylistItem.h @@ -0,0 +1,58 @@ + +#pragma once + +#include +#include + +#include +#include + +#include "server/namespace-stuffs.h" + + +namespace creatures { + + /* + * This is one item in a playlist + */ + + + struct PlaylistItem { + + /** + * The ID of the playlist + */ + animationId_t animation_id; + + /** + * The weight of the item + */ + uint32_t weight; + + }; + + +#include OATPP_CODEGEN_BEGIN(DTO) + +class PlaylistItemDto : public oatpp::DTO { + + DTO_INIT(PlaylistItemDto, DTO /* extends */) + + DTO_FIELD_INFO(animation_id) { + info->description = "The ID of the animation for this entry"; + } + DTO_FIELD(String, animation_id); + + DTO_FIELD_INFO(weight) { + info->description = "This item's weight"; + } + DTO_FIELD(UInt32 , weight); + +}; + +#include OATPP_CODEGEN_END(DTO) + + oatpp::Object convertToDto(const PlaylistItem &playlistItem); + creatures::PlaylistItem convertFromDto(const std::shared_ptr &playlistItemDto); + +} \ No newline at end of file diff --git a/src/server/creature/helpers.cpp b/src/server/creature/helpers.cpp index f4e951b..523db08 100644 --- a/src/server/creature/helpers.cpp +++ b/src/server/creature/helpers.cpp @@ -35,6 +35,10 @@ namespace creatures { extern std::vector creature_required_top_level_fields; extern std::vector creature_required_input_fields; + extern std::vector playlist_required_fields; + extern std::vector playlistitems_required_fields; + + Result Database::creatureFromJson(json creatureJson) { debug("attempting to create a creature from JSON via creatureFromJson()"); @@ -189,4 +193,22 @@ namespace creatures { return Result{true}; } + + Result Database::validatePlaylistJson(const nlohmann::json &json) { + + auto topLevelOkay = has_required_fields(json, creatures::playlist_required_fields); + if(!topLevelOkay.isSuccess()) { + return topLevelOkay; + } + + // Confirm that the items are valid + for( const auto& item : json["items"]) { + auto itemOkay = has_required_fields(item, playlistitems_required_fields); + if(!itemOkay.isSuccess()) { + return itemOkay; + } + } + + return Result{true}; + } } \ No newline at end of file diff --git a/src/server/database.h b/src/server/database.h index ee8eee6..9f6ab20 100644 --- a/src/server/database.h +++ b/src/server/database.h @@ -29,6 +29,7 @@ using json = nlohmann::json; #include "model/Animation.h" #include "model/AnimationMetadata.h" #include "model/Creature.h" +#include "model/Playlist.h" #include "model/Track.h" #include "model/SortBy.h" #include "server/namespace-stuffs.h" @@ -80,6 +81,14 @@ namespace creatures { */ static Result validateAnimationJson(const nlohmann::json& json); + /** + * Validates that the JSON for an Playlist contains the fields we expect. + * + * @param json the JSON to validate + * @return true if good, or ServerError if not + */ + static Result validatePlaylistJson(const nlohmann::json& json); + /** * Helper function that checks if a JSON object has all of the required fields. Used @@ -108,11 +117,10 @@ namespace creatures { // Playlist stuff -// grpc::Status createPlaylist(const Playlist *playlist, DatabaseInfo *reply); -// grpc::Status listPlaylists(const PlaylistFilter *filter, ListPlaylistsResponse *animationList); -// void getPlaylist(const PlaylistIdentifier *playlistIdentifier, Playlist *playlist); -// void updatePlaylist(const Playlist *playlist); - + Result getPlaylistJson(playlistId_t playlistId); + Result> getAllPlaylists(); + Result getPlaylist(const playlistId_t& playlistId); + Result upsertPlaylist(const std::string& playlistJson); /** * Request that the database perform a health check @@ -155,15 +163,12 @@ namespace creatures { /* * Playlists */ -// static bsoncxx::document::value playlistToBson(const Playlist *playlist, bsoncxx::oid playlistId); -// static int32_t playlistItemsToBson(bsoncxx::builder::stream::document &doc, const server::Playlist *playlist); -// static void bsonToPlaylist(const bsoncxx::document::view &doc, Playlist *playlist); -// static void bsonToPlaylistItems(const bsoncxx::document::view &doc, server::Playlist *playlist); + static Result playlistFromJson(json playlistJson); + static Result playlistItemFromJson(json playlistItemJson); // Start out thinking that the server is pingable std::atomic serverPingable{true}; - }; diff --git a/src/server/namespace-stuffs.h b/src/server/namespace-stuffs.h index a696f2b..bfb9b76 100644 --- a/src/server/namespace-stuffs.h +++ b/src/server/namespace-stuffs.h @@ -15,4 +15,5 @@ using universe_t = uint32_t; using framenum_t = uint64_t; using creatureId_t = std::string; -using animationId_t = std::string; \ No newline at end of file +using animationId_t = std::string; +using playlistId_t = std::string; diff --git a/src/server/playlist/create.cpp b/src/server/playlist/create.cpp deleted file mode 100644 index 5f7563c..0000000 --- a/src/server/playlist/create.cpp +++ /dev/null @@ -1,107 +0,0 @@ -#include "server/config.h" - -#include -#include "spdlog/spdlog.h" - -#include "server/database.h" -#include "server/creature-server.h" -#include "exception/exception.h" - -#include - - -#include -#include - - -#include -#include - -#include "server/namespace-stuffs.h" - - -namespace creatures { - - extern std::shared_ptr db; - - /** - * Save a new playlist in the database - * - * @param context the `ServerContext` of this request - * @param playlist a `Playlist` to save - * @param reply a `DatabaseInfo` that will be filled out for the reply - * @return state of the request - */ -// Status CreatureServerImpl::CreatePlaylist(grpc::ServerContext *context, const server::Playlist *playlist, -// server::DatabaseInfo *reply) { -// -// info("Creating a new playlist in the database"); -// return db->createPlaylist(playlist, reply); -// } - - /** - * Create a new playlist in the database - * - * @param playlist the `Playlist` to save - * @param reply Information about the save - * @return a gRPC Status on how things went - */ -// grpc::Status Database::createPlaylist(const Playlist *playlist, DatabaseInfo *reply) { -// -// debug("creating a new playlist in the database"); -// -// grpc::Status status; -// -// auto collection = getCollection(PLAYLISTS_COLLECTION); -// trace("collection obtained"); -// -// // Create a BSON doc with this playlist -// try { -// -// // Create the new playlistId -// bsoncxx::oid playlistId; -// -// auto doc_view = playlistToBson(playlist, playlistId); -// trace("doc_value made: {}", bsoncxx::to_json(doc_view)); -// -// collection.insert_one(doc_view.view()); -// trace("run_command done"); -// -// info("saved new playlist in the database 🎶"); -// -// status = grpc::Status(grpc::StatusCode::OK, "✅ Saved new playlist in the database"); -// reply->set_message("✅ Saved new playlist in the database"); -// } -// catch (const mongocxx::exception &e) { -// -// // Code 11000 means id collision -// if (e.code().value() == 11000) { -// error("attempted to insert a duplicate Playlist in the database"); -// status = grpc::Status(grpc::StatusCode::ALREADY_EXISTS, e.what()); -// reply->set_message("Unable to create new playlist"); -// reply->set_help("ID already exists"); -// } else { -// critical("Error updating database: {}", e.what()); -// status = grpc::Status(grpc::StatusCode::UNKNOWN, e.what(), fmt::to_string(e.code().value())); -// reply->set_message( -// fmt::format("Unable to create Animation in database: {} ({})", -// e.what(), e.code().value())); -// reply->set_help(e.code().message()); -// } -// } -// catch (creatures::DataFormatException &e) { -// error("server refused to save playlist: {}", e.what()); -// status = grpc::Status(grpc::StatusCode::FAILED_PRECONDITION, e.what()); -// reply->set_message(fmt::format("Unable to create new playlist: {}", e.what())); -// reply->set_help("Sorry! 💜"); -// } -// catch (const bsoncxx::exception &e) { -// error("unable to convert the incoming playlist to BSON: {}", e.what()); -// status = grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, e.what()); -// reply->set_message(fmt::format("Unable to create new playlist: {}", e.what())); -// reply->set_help(fmt::format("Check to make sure the message is well-formed")); -// } -// -// return status; -// } -} \ No newline at end of file diff --git a/src/server/playlist/get.cpp b/src/server/playlist/get.cpp index a11b55f..a464afc 100644 --- a/src/server/playlist/get.cpp +++ b/src/server/playlist/get.cpp @@ -22,101 +22,79 @@ namespace creatures { extern std::shared_ptr db; -// Status CreatureServerImpl::GetPlaylist(grpc::ServerContext *context, const server::PlaylistIdentifier *id, -// server::Playlist *playlist) { -// -// grpc::Status status; -// -// info("Loading one animation from the database"); -// -// try { -// db->getPlaylist(id, playlist); -// status = grpc::Status(grpc::StatusCode::OK, -// "Loaded a playlist from the database", -// fmt::format("Name: {}, Number of Items: {}", -// playlist->name(), -// playlist->items_size())); -// } -// catch(const InvalidArgumentException &e) { -// status = grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, -// "PlaylistIdentifier was empty on getPlaylist()", -// fmt::format("⛔️️ A PlaylistIdentifier must be supplied")); -// } -// catch(const NotFoundException &e) { -// status = grpc::Status(grpc::StatusCode::NOT_FOUND, -// fmt::format("⚠️ No playlist with ID '{}' found", bsoncxx::oid(id->_id()).to_string()), -// "Try another ID! 😅"); -// } -// catch(const DataFormatException &e) { -// status = grpc::Status(grpc::StatusCode::INTERNAL, -// "Unable to encode request into BSON", -// e.what()); -// } -// catch(const InternalError &e) { -// status = grpc::Status(grpc::StatusCode::INTERNAL, -// fmt::format("MongoDB error while loading a playlist: {}", e.what()), -// e.what()); -// } -// catch( ... ) { -// status = grpc::Status(grpc::StatusCode::INTERNAL, -// "An unknown error happened while getting a playlist", -// "Default catch hit?"); -// } -// -// return status; -// } -// -// -// void Database::getPlaylist(const PlaylistIdentifier *playlistIdentifier, Playlist *playlist) { -// -// if (playlistIdentifier->_id().empty()) { -// error("an empty playlistIdentifier was passed into getPlaylist()"); -// throw InvalidArgumentException("an empty playlistIdentifier was passed into getPlaylist()"); -// } -// -// auto collection = getCollection(PLAYLISTS_COLLECTION); -// trace("collection gotten"); -// -// -// try { -// -// // Convert the ID into an OID -// trace("attempting to convert the ID"); -// bsoncxx::oid id = bsoncxx::oid(playlistIdentifier->_id().data(), bsoncxx::oid::k_oid_length); -// debug("found playlistIdentifier ID: {}", id.to_string()); -// -// // Create a filter BSON document to match the target document -// auto filter = bsoncxx::builder::stream::document{} << "_id" << id << bsoncxx::builder::stream::finalize; -// trace("filter doc: {}", bsoncxx::to_json(filter)); -// -// // Go try to load it -// bsoncxx::stdx::optional result = collection.find_one(filter.view()); -// -// if (!result) { -// info("🚫 No playlist with ID '{}' found", id.to_string()); -// throw NotFoundException(fmt::format("🚫 No playlist with ID '{}' found", id.to_string())); -// } -// -// // Get an owning reference to this doc since it's ours now -// bsoncxx::document::value doc = *result; -// -// Database::bsonToPlaylist(doc, playlist); -// debug("loaded the playlist"); -// -// -// } -// catch (const bsoncxx::exception &e) { -// error("BSON exception while loading a playlist: {}", e.what()); -// throw DataFormatException(fmt::format("BSON exception while loading a playlist: {}", e.what())); -// } -// catch (const mongocxx::exception &e) { -// error("MongoDB exception while loading a playlist: {}", e.what()); -// -// } -// -// // Hooray, we loaded it all! -// info("done loading a playlist"); -// -// } + Result Database::getPlaylistJson(playlistId_t playlistId) { + debug("attempting to get the JSON for a playlist by ID: {}", playlistId); + + if(playlistId.empty()) { + info("an empty playlistId was passed into getPlaylistJson()"); + return Result{ServerError(ServerError::InvalidData, "unable to get a playlist because the id was empty")}; + } + + try { + bsoncxx::builder::stream::document filter_builder; + filter_builder << "id" << playlistId; + + // Search for the document + auto collection = getCollection(PLAYLISTS_COLLECTION); + auto maybe_result = collection.find_one(filter_builder.view()); + + // Check if the document was found + if (maybe_result) { + // Convert BSON document to JSON using nlohmann::json + bsoncxx::document::view view = maybe_result->view(); + nlohmann::json json_result = nlohmann::json::parse(bsoncxx::to_json(view)); + return Result{json_result}; + } else { + std::string errorMessage = fmt::format("no playlist id '{}' found", playlistId); + warn(errorMessage); + return Result{ServerError(ServerError::NotFound, errorMessage)}; + } + } catch (const mongocxx::exception &e) { + std::string errorMessage = fmt::format("a MongoDB error happened while loading a playlist by ID: {}", e.what()); + critical(errorMessage); + return Result{ServerError(ServerError::InternalError, errorMessage)}; + } catch ( ... ) { + std::string errorMessage = fmt::format("An unknown error happened while loading a playlist by ID"); + critical(errorMessage); + return Result{ServerError(ServerError::InternalError, errorMessage)}; + } + + } + + + Result Database::getPlaylist(const playlistId_t& playlistId) { + + if (playlistId.empty()) { + std::string errorMessage = "unable to get a playlist because the id was empty"; + warn(errorMessage); + return Result{ServerError(ServerError::InvalidData, errorMessage)}; + } + + + // Go to the database and get the playlist's raw JSON + auto playlistJson = getPlaylistJson(playlistId); + if (!playlistJson.isSuccess()) { + auto error = playlistJson.getError().value(); + std::string errorMessage = fmt::format("unable to get a playlist by ID: {}", + playlistJson.getError()->getMessage()); + warn(errorMessage); + return Result{error}; + } + + // Covert it to an Playlist object (if possible) + auto result = playlistFromJson(playlistJson.getValue().value()); + if (!result.isSuccess()) { + auto error = result.getError().value(); + std::string errorMessage = fmt::format("unable to get a playlist by ID: {}", + result.getError()->getMessage()); + warn(errorMessage); + return Result{error}; + } + + // Create the playlist + auto playlist = result.getValue().value(); + return Result{playlist}; + + } } \ No newline at end of file diff --git a/src/server/playlist/getall.cpp b/src/server/playlist/getall.cpp new file mode 100644 index 0000000..0a9ae15 --- /dev/null +++ b/src/server/playlist/getall.cpp @@ -0,0 +1,98 @@ + +#include "server/config.h" + +#include "spdlog/spdlog.h" + +#include +#include +#include +#include + + +#include "server/database.h" +#include "exception/exception.h" +#include "server/creature-server.h" + +#include "server/namespace-stuffs.h" + +using bsoncxx::builder::stream::document; + +namespace creatures { + + extern std::shared_ptr db; + + Result> Database::getAllPlaylists() { + + debug("attempting to get all of the playlists"); + + std::vector playlists; + + try { + auto collection = getCollection(PLAYLISTS_COLLECTION); + trace("collection obtained"); + + document query_doc{}; + document projection_doc{}; + document sort_doc{}; + + // Only sort by name + sort_doc << "name" << 1; + + mongocxx::options::find findOptions{}; + findOptions.projection(projection_doc.view()); + findOptions.sort(sort_doc.view()); + + mongocxx::cursor cursor = collection.find(query_doc.view(), findOptions); + + // Go Mongo, go! 🎉 + for (auto &&doc: cursor) { + + std::string json_str = bsoncxx::to_json(doc); + debug("Document JSON out of Mongo: {}", json_str); + + // Parse JSON string to nlohmann::json + nlohmann::json json_doc = nlohmann::json::parse(json_str); + + // Create the playlist from JSON + auto playlistResult = playlistFromJson(json_doc); + if (!playlistResult.isSuccess()) { + std::string errorMessage = fmt::format("Unable to parse the JSON in the database to Playlist: {}", + playlistResult.getError()->getMessage()); + warn(errorMessage); + return Result>{ServerError(ServerError::InvalidData, errorMessage)}; + } + + auto playlist = playlistResult.getValue().value(); + playlists.push_back(playlist); + debug("found {}", playlist.name); + + } + } + catch(const DataFormatException& e) { + std::string errorMessage = fmt::format("Failed to get all playlists: {}", e.what()); + warn(errorMessage); + return Result>{ServerError(ServerError::InvalidData, errorMessage)}; + } + catch(const mongocxx::exception &e) { + std::string errorMessage = fmt::format("MongoDB Exception while loading the playlists: {}", e.what()); + critical(errorMessage); + return Result>{ServerError(ServerError::InternalError, errorMessage)}; + } + catch(const bsoncxx::exception &e) { + std::string errorMessage = fmt::format("BSON error while attempting to load all the playlists: {}", e.what()); + critical(errorMessage); + return Result>{ServerError(ServerError::InternalError, errorMessage)}; + } + + // Return a 404 if nothing as found + if(playlists.empty()) { + std::string errorMessage = fmt::format("No playlists found"); + warn(errorMessage); + return Result>{ServerError(ServerError::NotFound, errorMessage)}; + } + + info("done loading all the playlists"); + return Result>{playlists}; + } + +} \ No newline at end of file diff --git a/src/server/playlist/helpers.cpp b/src/server/playlist/helpers.cpp index f8d7e47..b0b81af 100644 --- a/src/server/playlist/helpers.cpp +++ b/src/server/playlist/helpers.cpp @@ -1,176 +1,95 @@ -#include "spdlog/spdlog.h" +#include +#include -#include +#include -#include -#include -#include -#include +#include +using json = nlohmann::json; -#include "exception/exception.h" -#include "server/creature-server.h" #include "server/database.h" - #include "server/namespace-stuffs.h" +#include "model/Playlist.h" +#include "model/PlaylistItem.h" +#include "util/Result.h" + namespace creatures { - /** - * Convert a playlist into BSON - * - * @param playlist the playlist in question - * @param playlistId it's id - * @return A BSON document - */ -// bsoncxx::document::value Database::playlistToBson(const server::Playlist *playlist, bsoncxx::oid playlistId) { -// -// trace("converting a playlist to BSON"); -// -// using bsoncxx::builder::stream::document; -// using std::chrono::system_clock; -// -// document doc{}; -// try { -// -// system_clock::time_point last_updated = protobufTimestampToTimePoint(playlist->last_updated()); -// -// // The `_id` is the top level, as Mongo expects -// doc << "_id" << playlistId; -// trace("_id set"); -// -// doc << "name" << playlist->name() -// << "last_updated" << bsoncxx::types::b_date{last_updated}; -// -// // Add the items -// int32_t itemCount = playlistItemsToBson(doc, playlist); -// doc << "item_count" << bsoncxx::types::b_int32{itemCount}; -// -// return doc.extract(); -// } -// catch (const bsoncxx::exception &e) { -// error("Error encoding the playlist to BSON: {}", e.what()); -// throw DataFormatException(fmt::format("Error encoding the playlist to BSON: {} ({})", -// e.what(), -// e.code().message())); -// } -// } -// -// -// int32_t Database::playlistItemsToBson(bsoncxx::builder::stream::document &doc, const server::Playlist *playlist) { -// -// using bsoncxx::builder::stream::document; -// using bsoncxx::builder::stream::finalize; -// -// try { -// auto items = doc << "items" << bsoncxx::builder::stream::open_array; -// -// trace("adding items to the playlist BSON"); -// int32_t itemCount = 0; -// -// for (const auto &i : playlist->items()) { -// -// items << bsoncxx::builder::stream::open_document -// << "animation_id" << bsoncxx::oid(i.animationid()._id().data(), bsoncxx::oid::k_oid_length) -// << "weight" << bsoncxx::types::b_int32{static_cast(i.weight())} -// << bsoncxx::builder::stream::close_document; -// -// itemCount++; -// } -// -// items << bsoncxx::builder::stream::close_array; -// -// debug("added {} items to the playlist BSON", itemCount); -// -// return itemCount; -// } -// catch (const bsoncxx::exception &e) { -// error("Error encoding the playlist items to BSON: {}", e.what()); -// throw e; -// } -// } -// -// void Database::bsonToPlaylist(const bsoncxx::document::view &doc, server::Playlist *playlist) { -// using bsoncxx::types::b_oid; -// using bsoncxx::types::b_date; -// -// trace("converting a BSON document to a playlist"); -// -// // Extract the ID -// auto element = doc["_id"]; -// if (element && element.type() != bsoncxx::type::k_oid) { -// error("Field `_id` was not an OID in the database while loading an playlist"); -// throw DataFormatException("Field '_id' was not a bsoncxx::oid in the database"); -// } -// const bsoncxx::oid &oid = element.get_oid().value; -// const char *oid_data = oid.bytes(); -// playlist->mutable__id()->set__id(oid_data, bsoncxx::oid::k_oid_length); -// trace("set the _id to {}", oid.to_string()); -// -// // Getting name -// element = doc["name"]; -// if (!element) { -// error("Playlist value 'name' is not found"); -// throw DataFormatException("Playlist value 'name' is not found"); -// } -// -// if (element.type() != bsoncxx::type::k_utf8) { -// error("Playlist value 'name' is not a string"); -// throw creatures::DataFormatException("Playlist value 'name' is not a string"); -// } -// bsoncxx::stdx::string_view string_value = element.get_string().value; -// playlist->set_name(std::string{string_value}); -// trace("set the name to {}", playlist->name()); -// -// -// // Last updated -// element = doc["last_updated"]; -// *playlist->mutable_last_updated() = convertMongoDateToProtobufTimestamp(element); -// -// -// -// // Now go get the playlist items -// bsonToPlaylistItems(doc, playlist); -// } -// -// -// -// void Database::bsonToPlaylistItems(const bsoncxx::document::view &doc, server::Playlist *playlist) { -// -// trace("loading the items in a playlist from BSON"); -// -// // Get the array of items -// auto itemsArray = doc["items"].get_array().value; -// -// for (const auto &itemDoc : itemsArray) { -// -// auto item = playlist->add_items(); // Create new item in playlist -// -// // Get the animation id -// auto element = itemDoc["animation_id"]; -// if (element && element.type() != bsoncxx::type::k_oid) { -// error("Field `animation_id` was not an OID in the database while loading a playlist item"); -// throw DataFormatException("Field 'animation_id' was not a bsoncxx::oid in the database"); -// } -// const bsoncxx::oid &oid = element.get_oid().value; -// const char *oid_data = oid.bytes(); -// item->mutable_animationid()->set__id(oid_data, bsoncxx::oid::k_oid_length); -// trace("set the animation_id to {}", oid.to_string()); -// -// -// // Getting weight -// element = itemDoc["weight"]; -// if (element && element.type() == bsoncxx::type::k_int32) { -// int32_t int32_value = element.get_int32().value; -// item->set_weight(int32_value); -// trace("set the animation weight to {}", item->weight()); -// } else { -// error("playlist item field 'weight' was not an int32 in the database"); -// throw creatures::DataFormatException("playlist item field 'weight' was not an int32 in the database"); -// } -// -// } -// -// trace("done loading playlist items"); -// } + + Result Database::playlistFromJson(json playlistJson) { + + debug("attempting to create a playlist from JSON via playlistFromJson(): {}", + playlistJson.dump(4)); + + // Keep track of what we're working on so we can make a good error message + std::string working_on; + + try { + + auto playlist = Playlist(); + working_on = "id"; + playlist.id = playlistJson[working_on]; + debug("id: {}", playlist.id); + + working_on = "name"; + playlist.name = playlistJson[working_on]; + debug("name: {}", playlist.name); + + working_on = "number_of_items"; + playlist.number_of_items = playlistJson[working_on]; + debug("number_of_items: {}", playlist.number_of_items); + + // Add all the items + std::vector itemsJson = playlistJson["items"]; + for(const auto& itemJson : itemsJson) { + auto itemResult = playlistItemFromJson(itemJson); + if (!itemResult.isSuccess()) { + auto error = itemResult.getError(); + warn("Error while creating a playlistItem from JSON while making a playlist: {}", error->getMessage()); + return Result{ServerError(ServerError::InvalidData, error->getMessage())}; + } + playlist.items.push_back(itemResult.getValue().value()); + } + + return Result{playlist}; + } + catch (const nlohmann::json::exception &e) { + std::string errorMessage = fmt::format("Error while creating a playlist from JSON (field '{}'): {}", + working_on, e.what()); + warn(errorMessage); + return Result{ServerError(ServerError::InvalidData, errorMessage)}; + } + } + + Result Database::playlistItemFromJson(json playlistItemJson) { + + debug("attempting to create a playlistItem from JSON via playlistItemFromJson()"); + + // Keep track of the element we're working on so we can have good error messages + std::string working_on; + + try { + + auto playlistItem = PlaylistItem(); + working_on = "animation_id"; + playlistItem.animation_id = playlistItemJson[working_on]; + debug("animation_id: {}", playlistItem.animation_id); + + working_on = "weight"; + playlistItem.weight = playlistItemJson[working_on]; + debug("weight: {}", playlistItem.weight); + + debug("done with playlistItemFromJson"); + return Result{playlistItem}; + } + catch (const nlohmann::json::exception &e) { + std::string errorMessage = fmt::format("Error while creating a playlistItem from JSON (field '{}'): {}", + working_on, e.what()); + warn(errorMessage); + return Result{ServerError(ServerError::InvalidData, errorMessage)}; + } + } + + } \ No newline at end of file diff --git a/src/server/playlist/list.cpp b/src/server/playlist/list.cpp deleted file mode 100644 index 157cc12..0000000 --- a/src/server/playlist/list.cpp +++ /dev/null @@ -1,103 +0,0 @@ - -#include "server/config.h" - -#include "spdlog/spdlog.h" - -#include -#include -#include -#include - - -#include "server/database.h" -#include "exception/exception.h" -#include "server/creature-server.h" - -#include "server/namespace-stuffs.h" - -using bsoncxx::builder::stream::document; - -namespace creatures { - - extern std::shared_ptr db; - -// Status CreatureServerImpl::ListPlaylists(grpc::ServerContext *context, const server::PlaylistFilter *request, -// server::ListPlaylistsResponse *response) { -// -// info("Listing the animations in the database"); -// return db->listPlaylists(request, response); -// } - - /** - * List animations in the database for a given creature type - * - * @param filter what CreatureType to look for - * @param playlistsResponse the list to fill out - * @return the status of this request - */ -// grpc::Status Database::listPlaylists(const PlaylistFilter *filter, ListPlaylistsResponse *playlistsResponse) { -// -// trace("attempting to list all of the playlists"); -// -// grpc::Status status; -// -// uint32_t numberOfPlaylistsFound = 0; -// -// try { -// auto collection = getCollection(PLAYLISTS_COLLECTION); -// trace("collection obtained"); -// -// document query_doc{}; -// document sort_doc{}; -// -// // First pass, sort by name -// sort_doc << "name" << 1; -// -// mongocxx::options::find findOptions{}; -// findOptions.sort(sort_doc.view()); -// -// mongocxx::cursor cursor = collection.find(query_doc.view(), findOptions); -// -// // Go Mongo, go! 🎉 -// for (auto &&doc: cursor) { -// -// auto playlist = playlistsResponse->add_playlists(); -// -// Database::bsonToPlaylist(doc, playlist); -// -// numberOfPlaylistsFound++; -// } -// } -// catch(const DataFormatException &e) { -// warn("DataFormatException while getting playlists: {}", e.what()); -// status = grpc::Status(grpc::StatusCode::INTERNAL, -// fmt::format("🚨 Server-side error while getting playlists: {}", e.what())); -// return status; -// } -// catch(const mongocxx::exception &e) { -// critical("MongoDB error while attempting to load playlists: {}", e.what()); -// status = grpc::Status(grpc::StatusCode::INTERNAL, -// fmt::format("🚨 MongoDB error while attempting to load playlists: {}", e.what())); -// return status; -// } -// catch(const bsoncxx::exception &e) { -// critical("BSON error while attempting to load playlists: {}", e.what()); -// status = grpc::Status(grpc::StatusCode::INTERNAL, -// fmt::format("🚨 BSON error while attempting to load playlists: {}", e.what())); -// return status; -// } -// -// // Return a 404 if nothing as found -// if(numberOfPlaylistsFound == 0) { -// status = grpc::Status(grpc::StatusCode::NOT_FOUND, -// "🚫 No playlists for that creature type found"); -// return status; -// } -// -// // If we made this far, we're good! 😍 -// status = grpc::Status(grpc::StatusCode::OK, -// fmt::format("✅ Found {} playlists", numberOfPlaylistsFound)); -// return status; -// } - -} \ No newline at end of file diff --git a/src/server/playlist/update.cpp b/src/server/playlist/update.cpp deleted file mode 100644 index 499270b..0000000 --- a/src/server/playlist/update.cpp +++ /dev/null @@ -1,148 +0,0 @@ - -#include "server/config.h" - -#include "spdlog/spdlog.h" - -#include -#include -#include - - -#include -#include - - -#include "server/database.h" -#include "exception/exception.h" -#include "server/creature-server.h" - -#include "server/namespace-stuffs.h" - - -namespace creatures { - - extern std::shared_ptr db; - - -// Status CreatureServerImpl::UpdatePlaylist(grpc::ServerContext *context, const server::Playlist *playlist, -// server::DatabaseInfo *reply) { -// -// grpc::Status status; -// -// debug("trying to update a playlist"); -// -// try { -// db->updatePlaylist(playlist); -// status = grpc::Status(grpc::StatusCode::OK, -// "🎉 Playlist updated in database!", -// fmt::format("Name: {}, Number of Items: {}", -// playlist->name(), -// playlist->items_size())); -// reply->set_message(fmt::format("🎉 Playlist updated in database!")); -// } -// catch(const InvalidArgumentException &e) { -// status = grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, -// "A playlist with an empty id was sent to updateAnimation()", -// fmt::format("⛔️️ A playlist ID must be supplied")); -// reply->set_message(fmt::format("⛔️ A playlist ID must be supplied")); -// reply->set_help(fmt::format("playlist's ID cannot be empty")); -// } -// catch(const NotFoundException &e) { -// status = grpc::Status(grpc::StatusCode::NOT_FOUND, -// fmt::format("⚠️ No playlist with ID '{}' found", bsoncxx::oid(playlist->_id()._id()).to_string()), -// "Try another ID! 😅"); -// reply->set_message(fmt::format("⚠️ No playlist with ID '{}' found", bsoncxx::oid(playlist->_id()._id()).to_string())); -// reply->set_help("Try another ID! 😅"); -// } -// catch(const DataFormatException &e) { -// status = grpc::Status(grpc::StatusCode::INTERNAL, -// "Unable to encode request into BSON", -// e.what()); -// reply->set_message("Unable to encode playlist into BSON"); -// reply->set_help(e.what()); -// } -// catch(const InternalError &e) { -// status = grpc::Status(grpc::StatusCode::INTERNAL, -// fmt::format("MongoDB error while updating a playlist: {}", e.what()), -// e.what()); -// reply->set_message("MongoDB error while updating a playlist"); -// reply->set_help(e.what()); -// } -// catch( ... ) { -// status = grpc::Status(grpc::StatusCode::INTERNAL, -// "🚨 An unknown error happened while updating a playlist 🚨", -// "Default catch hit?"); -// reply->set_message("Unknown error while updating a playlist"); -// reply->set_help("Default catch hit. How did this happen? 🤔"); -// } -// -// return status; -// } -// -// -// void Database::updatePlaylist(const server::Playlist *playlist) { -// -// debug("attempting to update a playlist in the database"); -// -// // Error checking -// if (playlist->_id()._id().empty()) { -// error("a playlist with an empty id was passed into updatePlaylist()"); -// throw InvalidArgumentException("a playlist with an empty id was passed into updatePlaylist()"); -// } -// -// auto collection = getCollection(PLAYLISTS_COLLECTION); -// trace("collection obtained"); -// -// try { -// -// // Convert the animationId to a proper oid -// bsoncxx::oid playlistId = bsoncxx::oid(playlist->_id()._id().data(), bsoncxx::oid::k_oid_length); -// -// // Create a filter for just this one animation -// bsoncxx::builder::stream::document filter_builder{}; -// filter_builder << "_id" << playlistId; -// -// auto doc_view = playlistToBson(playlist, playlistId); -// auto result = collection.replace_one(filter_builder.view(), doc_view.view()); -// -// if (result) { -// if (result->matched_count() > 1) { -// -// // Whoa, this should never happen -// std::string errorMessage = fmt::format( -// "more than one document updated at once in updatePlaylist()!! Count: {}", -// result->matched_count()); -// critical(errorMessage); -// throw InternalError(errorMessage); -// -// } else if (result->matched_count() == 0) { -// -// // We didn't update anything -// std::string errorMessage = fmt::format("Update to update playlist. Reason: playlist {} not found", -// playlistId.to_string()); -// info(errorMessage); -// throw NotFoundException(errorMessage); -// } -// -// // Hooray, we only did one. :) -// info("✅ playlist {} updated", playlistId.to_string()); -// return; -// -// } -// -// // Something went wrong -// std::string errorMessage = fmt::format("Unknown errors while updating playlist {} (result wasn't)", -// playlistId.to_string()); -// throw InternalError(errorMessage); -// } -// catch (const bsoncxx::exception &e) { -// error("BSON exception while updating an animation: {}", e.what()); -// throw DataFormatException(fmt::format("BSON exception while updating an animation: {}", e.what())); -// } -// catch (const mongocxx::exception &e) { -// error("MongoDB exception while updating an animation: {}", e.what()); -// throw InternalError(fmt::format("MongoDB exception while updating an animation: {}", e.what())); -// } -// } - -} \ No newline at end of file diff --git a/src/server/playlist/upsert.cpp b/src/server/playlist/upsert.cpp new file mode 100644 index 0000000..56b33f3 --- /dev/null +++ b/src/server/playlist/upsert.cpp @@ -0,0 +1,79 @@ + +#include "server/config.h" + +#include "spdlog/spdlog.h" + +#include +#include +#include + + +#include +#include + + +#include "server/database.h" +#include "exception/exception.h" +#include "server/creature-server.h" + +#include "server/namespace-stuffs.h" + + +namespace creatures { + + extern std::shared_ptr db; + + Result Database::upsertPlaylist(const std::string& playlistJson) { + + debug("upserting a playlist in the database"); + + try { + auto jsonObject = nlohmann::json::parse(playlistJson); + + auto playlistResult = playlistFromJson(jsonObject); + if (!playlistResult.isSuccess()) { + auto error = playlistResult.getError(); + warn("Error while creating a playlist from JSON: {}", error->getMessage()); + return Result{ServerError(ServerError::InvalidData, error->getMessage())}; + } + auto playlist = playlistResult.getValue().value(); + + // Now go save it in Mongo + auto collection = getCollection(PLAYLISTS_COLLECTION); + trace("collection obtained"); + + // Convert the JSON string into BSON + auto bsonDoc = bsoncxx::from_json(playlistJson); + + bsoncxx::builder::stream::document filter_builder; + filter_builder << "id" << playlist.id; + + // Upsert options + mongocxx::options::update update_options; + update_options.upsert(true); + + // Upsert operation + collection.update_one( + filter_builder.view(), + bsoncxx::builder::stream::document{} << "$set" << bsonDoc.view() + << bsoncxx::builder::stream::finalize, + update_options + ); + + info("Playlist upserted in the database: {}", playlist.id); + return Result{playlist}; + + } catch (const mongocxx::exception &e) { + error("Error (mongocxx::exception) while upserting a playlist in database: {}", e.what()); + return Result{ServerError(ServerError::InternalError, e.what())}; + } catch (const bsoncxx::exception &e) { + error("Error (bsoncxx::exception) while upserting a playlist in database: {}", e.what()); + return Result{ServerError(ServerError::InvalidData, e.what())}; + } catch (...) { + std::string error_message = "Unknown error while upserting a playlist in the database"; + critical(error_message); + return Result{ServerError(ServerError::InternalError, error_message)}; + } + } + +} \ No newline at end of file diff --git a/src/server/ws/App.cpp b/src/server/ws/App.cpp index 8115bc4..59ae3d4 100644 --- a/src/server/ws/App.cpp +++ b/src/server/ws/App.cpp @@ -17,6 +17,7 @@ #include "controller/AnimationController.h" #include "controller/CreatureController.h" #include "controller/MetricsController.h" +#include "controller/PlaylistController.h" #include "controller/SoundController.h" #include "controller/StaticController.h" #include "controller/VoiceController.h" @@ -70,6 +71,7 @@ namespace creatures ::ws { router->addController(AnimationController::createShared()); router->addController(CreatureController::createShared()); router->addController(MetricsController::createShared()); + router->addController(PlaylistController::createShared()); router->addController(SoundController::createShared()); router->addController(StaticController::createShared()); router->addController(VoiceController::createShared()); diff --git a/src/server/ws/controller/PlaylistController.h b/src/server/ws/controller/PlaylistController.h new file mode 100644 index 0000000..658e879 --- /dev/null +++ b/src/server/ws/controller/PlaylistController.h @@ -0,0 +1,118 @@ + +#pragma once + +#include +#include +#include +#include + + +#include "model/Playlist.h" + +#include "server/database.h" + +#include "server/ws/service/PlaylistService.h" +#include "server/metrics/counters.h" + +namespace creatures { + extern std::shared_ptr metrics; +} + +#include OATPP_CODEGEN_BEGIN(ApiController) //<- Begin Codegen + +namespace creatures :: ws { + + class PlaylistController : public oatpp::web::server::api::ApiController { + public: + PlaylistController(OATPP_COMPONENT(std::shared_ptr, objectMapper)): + oatpp::web::server::api::ApiController(objectMapper) {} + private: + PlaylistService m_playlistService; // Create the animation service + public: + + static std::shared_ptr createShared( + OATPP_COMPONENT(std::shared_ptr, + objectMapper) // Inject objectMapper component here as default parameter + ) { + return std::make_shared(objectMapper); + } + + ENDPOINT_INFO(getAllPlaylists) { + info->summary = "Get all of the playlists"; + + info->addResponse>(Status::CODE_200, "application/json; charset=utf-8"); + info->addResponse>(Status::CODE_400, "application/json; charset=utf-8"); + info->addResponse>(Status::CODE_404, "application/json; charset=utf-8"); + info->addResponse>(Status::CODE_500, "application/json; charset=utf-8"); + } + ENDPOINT("GET", "api/v1/playlist", getAllPlaylists) + { + creatures::metrics->incrementRestRequestsProcessed(); + return createDtoResponse(Status::CODE_200, m_playlistService.getAllPlaylists()); + } + + ENDPOINT_INFO(getPlaylist) { + info->summary = "Get a playlist by id"; + + info->addResponse>(Status::CODE_200, "application/json; charset=utf-8"); + info->addResponse>(Status::CODE_400, "application/json; charset=utf-8"); + info->addResponse>(Status::CODE_404, "application/json; charset=utf-8"); + info->addResponse>(Status::CODE_500, "application/json; charset=utf-8"); + + info->pathParams["playlistId"].description = "Playlist ID in the form of a UUID"; + } + ENDPOINT("GET", "api/v1/playlist/{playlistId}", getPlaylist, + PATH(String, playlistId)) + { + debug("get playlist by ID via REST API: {}", std::string(playlistId)); + creatures::metrics->incrementRestRequestsProcessed(); + return createDtoResponse(Status::CODE_200, m_playlistService.getPlaylist(playlistId)); + } + + /** + * This one is like the Creature upsert. It allows any JSON to come in. It validates that the + * JSON is correct, but stores whatever comes in in the DB. + * + * @return + */ + ENDPOINT_INFO(upsertPlaylist) { + info->summary = "Create or update a playlist in the database"; + + info->addResponse>(Status::CODE_200, "application/json; charset=utf-8"); + info->addResponse>(Status::CODE_400, "application/json; charset=utf-8"); + info->addResponse>(Status::CODE_500, "application/json; charset=utf-8"); + } + ENDPOINT("POST", "api/v1/playlist", upsertPlaylist, + REQUEST(std::shared_ptr, request)) + { + debug("new playlist uploaded via REST API"); + creatures::metrics->incrementRestRequestsProcessed(); + auto requestAsString = std::string(request->readBodyToString()); + trace("request was: {}", requestAsString); + + return createDtoResponse(Status::CODE_200, + m_playlistService.upsertPlaylist(requestAsString)); + } +// +// +// ENDPOINT_INFO(playStoredAnimation) { +// info->summary = "Play one animation out of the database on a given universe"; +// +// info->addResponse>(Status::CODE_200, "application/json; charset=utf-8"); +// info->addResponse>(Status::CODE_404, "application/json; charset=utf-8"); +// info->addResponse>(Status::CODE_500, "application/json; charset=utf-8"); +// } +// ENDPOINT("POST", "api/v1/animation/play", playStoredAnimation, +// BODY_DTO(Object, requestBody)) +// { +// creatures::metrics->incrementRestRequestsProcessed(); +// return createDtoResponse(Status::CODE_200, +// m_animationService.playStoredAnimation(std::string(requestBody->animation_id), +// requestBody->universe)); +// } + + }; + +} + +#include OATPP_CODEGEN_END(ApiController) //<- End Codegen \ No newline at end of file diff --git a/src/server/ws/service/PlaylistService.cpp b/src/server/ws/service/PlaylistService.cpp new file mode 100644 index 0000000..1ca2a30 --- /dev/null +++ b/src/server/ws/service/PlaylistService.cpp @@ -0,0 +1,218 @@ + + +#include +#include + +#include "model/Playlist.h" +#include "server/database.h" +#include "server/ws/dto/ListDto.h" +#include "server/ws/dto/StatusDto.h" + +#include "PlaylistService.h" + + +namespace creatures { + extern std::shared_ptr db; +} + +namespace creatures :: ws { + + using oatpp::web::protocol::http::Status; + + oatpp::Object>> PlaylistService::getAllPlaylists() { + + OATPP_COMPONENT(std::shared_ptr, appLogger); + + appLogger->debug("PlaylistService::getAllPlaylists()"); + + bool error = false; + oatpp::String errorMessage; + Status status = Status::CODE_200; + + auto result = db->getAllPlaylists(); + if(!result.isSuccess()) { + + debug("getAllPlaylists() was not a success 😫"); + + // If we get an error, let's set it up right + auto errorCode = result.getError().value().getCode(); + switch(errorCode) { + case ServerError::NotFound: + status = Status::CODE_404; + break; + case ServerError::InvalidData: + status = Status::CODE_400; + break; + default: + status = Status::CODE_500; + break; + } + errorMessage = result.getError()->getMessage(); + appLogger->warn(std::string(result.getError()->getMessage())); + error = true; + } + OATPP_ASSERT_HTTP(!error, status, errorMessage) + appLogger->trace("done fetching items"); + + + auto items = oatpp::Vector>::createShared(); + + auto playlists = result.getValue().value(); + for (const auto &playlist : playlists) { + appLogger->debug("Adding playlist: {}", playlist.id); + items->emplace_back(creatures::convertToDto(playlist)); + appLogger->trace("added"); + } + + auto page = ListDto>::createShared(); + page->count = items->size(); + page->items = items; + + return page; + + } + + + oatpp::Object PlaylistService::getPlaylist(const oatpp::String &inPlaylistId) { + OATPP_COMPONENT(std::shared_ptr, appLogger); + + // Convert the oatpp string to a std::string + std::string playlistId = std::string(inPlaylistId); + + appLogger->debug("PlaylistService::getPlaylist({})", playlistId); + + bool error = false; + oatpp::String errorMessage; + Status status = Status::CODE_200; + + auto result = db->getPlaylist(playlistId); + if(!result.isSuccess()) { + + auto errorCode = result.getError().value().getCode(); + switch(errorCode) { + case ServerError::NotFound: + status = Status::CODE_404; + break; + case ServerError::InvalidData: + status = Status::CODE_400; + break; + default: + status = Status::CODE_500; + break; + } + errorMessage = result.getError().value().getMessage(); + appLogger->warn(std::string(errorMessage)); + error = true; + } + OATPP_ASSERT_HTTP(!error, status, errorMessage) + + auto playlist = result.getValue().value(); + return creatures::convertToDto(playlist); + + } + + oatpp::Object PlaylistService::upsertPlaylist(const std::string &playlistJson) { + OATPP_COMPONENT(std::shared_ptr, appLogger); + + + appLogger->info("attempting to upsert a playlist"); + + appLogger->debug("JSON: {}", playlistJson); + + + bool error = false; + oatpp::String errorMessage; + Status status = Status::CODE_200; + + try { + + /* + * There's the same weirdness here that's in the Creature version of this Service (which is what + * this one is based on). I want to be able to store the raw JSON in the database, but I also want + * to validate it to make sure it has what data the front end needs. + */ + auto jsonObject = nlohmann::json::parse(playlistJson); + auto result = db->validatePlaylistJson(jsonObject); + if(!result.isSuccess()) { + errorMessage = result.getError()->getMessage(); + appLogger->warn(std::string(result.getError()->getMessage())); + status = Status::CODE_400; + error = true; + } + } + catch ( const nlohmann::json::parse_error& e) { + errorMessage = e.what(); + appLogger->warn(std::string(e.what())); + status = Status::CODE_400; + error = true; + } + OATPP_ASSERT_HTTP(!error, status, errorMessage) + + + appLogger->debug("passing the upsert request off to the database"); + auto result = db->upsertPlaylist(playlistJson); + + // If there's an error, let the client know + if(!result.isSuccess()) { + + errorMessage = result.getError()->getMessage(); + appLogger->warn(std::string(result.getError()->getMessage())); + status = Status::CODE_500; + error = true; + } + OATPP_ASSERT_HTTP(!error, status, errorMessage) + + // This should never happen and is a bad bug if it does 😱 + if(!result.getValue().has_value()) { + errorMessage = "DB didn't return a value after upserting a playlist. This is a bug. Please report it."; + appLogger->error(std::string(errorMessage)); + OATPP_ASSERT_HTTP(true, Status::CODE_500, errorMessage); + } + + // Yay! All good! Send it along + auto playlist = result.getValue().value(); + info("Updated playlist '{}' in the database (id: {})", + playlist.name, playlist.id); + return convertToDto(playlist); + } + +// +// oatpp::Object AnimationService::playStoredAnimation(const oatpp::String& animationId, universe_t universe) { +// OATPP_COMPONENT(std::shared_ptr, appLogger); +// +// appLogger->debug("AnimationService::playStoredAnimation({}, {})", std::string(animationId), universe); +// +// bool error = false; +// oatpp::String errorMessage; +// Status status = Status::CODE_200; +// +// auto result = db->playStoredAnimation(std::string(animationId), universe); +// if(!result.isSuccess()) { +// +// auto errorCode = result.getError().value().getCode(); +// switch(errorCode) { +// case ServerError::NotFound: +// status = Status::CODE_404; +// break; +// case ServerError::InvalidData: +// status = Status::CODE_400; +// break; +// default: +// status = Status::CODE_500; +// break; +// } +// errorMessage = result.getError().value().getMessage(); +// appLogger->warn(std::string(errorMessage)); +// error = true; +// } +// OATPP_ASSERT_HTTP(!error, status, errorMessage) +// +// auto playResult = StatusDto::createShared(); +// playResult->status = "OK"; +// playResult->message = result.getValue().value(); +// playResult->code = 200; +// +// return playResult; +// } + +} // creatures :: ws \ No newline at end of file diff --git a/src/server/ws/service/PlaylistService.h b/src/server/ws/service/PlaylistService.h new file mode 100644 index 0000000..c42442d --- /dev/null +++ b/src/server/ws/service/PlaylistService.h @@ -0,0 +1,38 @@ + +#pragma once + +#include "spdlog/spdlog.h" + +#include +#include + +#include "model/Playlist.h" +#include "server/ws/dto/ListDto.h" +#include "server/ws/dto/StatusDto.h" + +namespace creatures :: ws { + + class PlaylistService { + + private: + typedef oatpp::web::protocol::http::Status Status; + + public: + + static oatpp::Object>> getAllPlaylists(); + oatpp::Object getPlaylist(const oatpp::String& playlistId); + oatpp::Object upsertPlaylist(const std::string& playlistJson); + + + /** + * Play a single animation on one universe out of the database + * + * @param animationId the animation to play + * @param universe which universe to play the animation in + * @return The status of what happened + */ + // oatpp::Object playStoredAnimation(const oatpp::String& animationId, universe_t universe); + }; + + +} // creatures :: ws