From 7300de3fe40748d538d06e9c8095c3de41cbae76 Mon Sep 17 00:00:00 2001 From: Kieran Kihn <114803508+kierankihn@users.noreply.github.com> Date: Sun, 23 Nov 2025 15:02:14 +0800 Subject: [PATCH] feat(network): refactor `MessageSerializer` for modular serialization and validation - Added reusable `serializeCards` function for card list serialization. - Enhanced `DrawCardPayload` with a `cards` field. - Introduced `serializeMessageStatus` and `deserializeMessageStatus` for message status handling. - Updated deserialization logic to validate and include `status_code` field. - Improved error messaging and validation for payload deserialization methods. --- src/network/Message.h | 1 + src/network/MessageSerializer.cpp | 269 +++++++++++++++++++++++------- src/network/MessageSerializer.h | 11 +- 3 files changed, 215 insertions(+), 66 deletions(-) diff --git a/src/network/Message.h b/src/network/Message.h index 47e7f8c..9517b86 100644 --- a/src/network/Message.h +++ b/src/network/Message.h @@ -26,6 +26,7 @@ namespace UNO::NETWORK { struct DrawCardPayload { int drawCount; + std::vector cards; }; struct PlayCardPayload { diff --git a/src/network/MessageSerializer.cpp b/src/network/MessageSerializer.cpp index 96b6dcb..8fbdfc1 100644 --- a/src/network/MessageSerializer.cpp +++ b/src/network/MessageSerializer.cpp @@ -1,5 +1,5 @@ /** - * @file + * @file MessageSerializer.cpp * * @author Yuzhe Guo * @date 2025.11.20 @@ -12,22 +12,26 @@ namespace UNO::NETWORK { return {{"card_color", card.colorToString()}, {"card_type", card.typeToString()}}; } - nlohmann::json MessageSerializer::serializeDiscardPile(const GAME::DiscardPile &discardPile) + template + nlohmann::json MessageSerializer::serializeCards(Iterator begin, Iterator end) { nlohmann::json res = nlohmann::json::array(); - for (const auto &i : discardPile.getCards()) { - res.push_back(serializeCard(i)); + for (auto it = begin; it != end; ++it) { + res.push_back(serializeCard(*it)); } return res; } + nlohmann::json MessageSerializer::serializeDiscardPile(const GAME::DiscardPile &discardPile) + { + const auto &cards = discardPile.getCards(); + return serializeCards(cards.begin(), cards.end()); + } + nlohmann::json MessageSerializer::serializeHandCard(const GAME::HandCard &handCard) { - nlohmann::json res = nlohmann::json::array(); - for (const auto &i : handCard.getCards()) { - res.push_back(serializeCard(i)); - } - return res; + const auto &cards = handCard.getCards(); + return serializeCards(cards.begin(), cards.end()); } nlohmann::json MessageSerializer::serializePayload(const std::monostate &payload) @@ -47,7 +51,7 @@ namespace UNO::NETWORK { nlohmann::json MessageSerializer::serializePayload(const DrawCardPayload &payload) { - return {{"draw_count", payload.drawCount}}; + return {{"draw_count", payload.drawCount}, {"cards", serializeCards(payload.cards.begin(), payload.cards.end())}}; } nlohmann::json MessageSerializer::serializePayload(const PlayCardPayload &payload) @@ -70,6 +74,7 @@ namespace UNO::NETWORK { std::string MessageSerializer::serializeMessagePayloadType(const MessagePayloadType &messagePayloadType) { switch (messagePayloadType) { + case MessagePayloadType::EMPTY: return "EMPTY"; case MessagePayloadType::JOIN_GAME: return "JOIN_GAME"; case MessagePayloadType::START_GAME: return "START_GAME"; case MessagePayloadType::DRAW_CARD: return "DRAW_CARD"; @@ -80,9 +85,19 @@ namespace UNO::NETWORK { throw std::invalid_argument("invalid message payload type"); } + std::string MessageSerializer::serializeMessageStatus(const MessageStatus &messageStatus) + { + switch (messageStatus) { + case MessageStatus::OK: return "OK"; + case MessageStatus::INVALID: return "INVALID"; + } + std::unreachable(); + } + nlohmann::json MessageSerializer::serializeMessage(const Message &message) { - return {{"payload_type", serializeMessagePayloadType(message.getMessagePayloadType())}, + return {{"status_code", serializeMessageStatus(message.getMessageStatus())}, + {"payload_type", serializeMessagePayloadType(message.getMessagePayloadType())}, {"payload", std::visit([](auto &&value) { return serializePayload(value); }, message.getMessagePayload())}}; } @@ -93,39 +108,83 @@ namespace UNO::NETWORK { GAME::CardColor MessageSerializer::deserializeCardColor(const std::string &cardColor) { - if (cardColor == "Red") return GAME::CardColor::RED; - if (cardColor == "Blue") return GAME::CardColor::BLUE; - if (cardColor == "Green") return GAME::CardColor::GREEN; - if (cardColor == "Yellow") return GAME::CardColor::YELLOW; + if (cardColor == "Red") { + return GAME::CardColor::RED; + } + if (cardColor == "Blue") { + return GAME::CardColor::BLUE; + } + if (cardColor == "Green") { + return GAME::CardColor::GREEN; + } + if (cardColor == "Yellow") { + return GAME::CardColor::YELLOW; + } throw std::invalid_argument("Invalid card color: '" + cardColor + "'. Expected: Red, Blue, Green, or Yellow"); } GAME::CardType MessageSerializer::deserializeCardType(const std::string &cardType) { - if (cardType == "0") return GAME::CardType::NUM0; - if (cardType == "1") return GAME::CardType::NUM1; - if (cardType == "2") return GAME::CardType::NUM2; - if (cardType == "3") return GAME::CardType::NUM3; - if (cardType == "4") return GAME::CardType::NUM4; - if (cardType == "5") return GAME::CardType::NUM5; - if (cardType == "6") return GAME::CardType::NUM6; - if (cardType == "7") return GAME::CardType::NUM7; - if (cardType == "8") return GAME::CardType::NUM8; - if (cardType == "9") return GAME::CardType::NUM9; - if (cardType == "Skip") return GAME::CardType::SKIP; - if (cardType == "Reverse") return GAME::CardType::REVERSE; - if (cardType == "Draw 2") return GAME::CardType::DRAW2; - if (cardType == "Wild") return GAME::CardType::WILD; - if (cardType == "Wild Draw 4") return GAME::CardType::WILDDRAWFOUR; + if (cardType == "0") { + return GAME::CardType::NUM0; + } + if (cardType == "1") { + return GAME::CardType::NUM1; + } + if (cardType == "2") { + return GAME::CardType::NUM2; + } + if (cardType == "3") { + return GAME::CardType::NUM3; + } + if (cardType == "4") { + return GAME::CardType::NUM4; + } + if (cardType == "5") { + return GAME::CardType::NUM5; + } + if (cardType == "6") { + return GAME::CardType::NUM6; + } + if (cardType == "7") { + return GAME::CardType::NUM7; + } + if (cardType == "8") { + return GAME::CardType::NUM8; + } + if (cardType == "9") { + return GAME::CardType::NUM9; + } + if (cardType == "Skip") { + return GAME::CardType::SKIP; + } + if (cardType == "Reverse") { + return GAME::CardType::REVERSE; + } + if (cardType == "Draw 2") { + return GAME::CardType::DRAW2; + } + if (cardType == "Wild") { + return GAME::CardType::WILD; + } + if (cardType == "Wild Draw 4") { + return GAME::CardType::WILDDRAWFOUR; + } throw std::invalid_argument("Invalid card type: '" + cardType + "'. Expected: 0-9, Skip, Reverse, Draw 2, Wild, or Wild Draw 4"); } GAME::Card MessageSerializer::deserializeCard(const nlohmann::json &card) { - if (card.is_object() == false) throw std::invalid_argument("Invalid card format: expected JSON object"); + if (card.is_object() == false) { + throw std::invalid_argument("Invalid card format: expected JSON object"); + } try { - if (card.at("card_color").is_string() == false) throw std::invalid_argument("Invalid card_color field: expected string"); - if (card.at("card_type").is_string() == false) throw std::invalid_argument("Invalid card_type field: expected string"); + if (card.at("card_color").is_string() == false) { + throw std::invalid_argument("Invalid card_color field: expected string"); + } + if (card.at("card_type").is_string() == false) { + throw std::invalid_argument("Invalid card_type field: expected string"); + } return {deserializeCardColor(card.at("card_color")), deserializeCardType(card.at("card_type"))}; } catch (const nlohmann::json::out_of_range &) { @@ -135,7 +194,9 @@ namespace UNO::NETWORK { GAME::DiscardPile MessageSerializer::deserializeDiscardPile(const nlohmann::json &discardPile) { - if (discardPile.is_array() == false) throw std::invalid_argument("Invalid discard_pile format: expected JSON array"); + if (discardPile.is_array() == false) { + throw std::invalid_argument("Invalid discard_pile format: expected JSON array"); + } GAME::DiscardPile res; for (const auto &i : std::views::reverse(discardPile)) { @@ -147,7 +208,9 @@ namespace UNO::NETWORK { GAME::HandCard MessageSerializer::deserializeHandCard(const nlohmann::json &handCard) { - if (handCard.is_array() == false) throw std::invalid_argument("Invalid hand_card format: expected JSON array"); + if (handCard.is_array() == false) { + throw std::invalid_argument("Invalid hand_card format: expected JSON array"); + } GAME::HandCard res; for (const auto &i : std::views::reverse(handCard)) { @@ -157,9 +220,11 @@ namespace UNO::NETWORK { return res; } - std::monostate MessageSerializer::deserializeMonostatePayload(const nlohmann::json &payload) + std::monostate MessageSerializer::deserializeEmptyPayload(const nlohmann::json &payload) { - if (payload.is_null() == false) throw std::invalid_argument("Invalid payload: expected null for empty payload"); + if (payload.is_null() == false) { + throw std::invalid_argument("Invalid payload: expected null for empty payload"); + } return {}; } @@ -167,9 +232,12 @@ namespace UNO::NETWORK { JoinGamePayload MessageSerializer::deserializeJoinGamePayload(const nlohmann::json &payload) { try { - if (payload.is_object() == false) throw std::invalid_argument("Invalid JOIN_GAME payload: expected JSON object"); - if (payload.at("name").is_string() == false) + if (payload.is_object() == false) { + throw std::invalid_argument("Invalid JOIN_GAME payload: expected JSON object"); + } + if (payload.at("name").is_string() == false) { throw std::invalid_argument("Invalid 'name' field in JOIN_GAME payload: expected string"); + } return {payload.at("name")}; } catch (const nlohmann::json::out_of_range &) { @@ -179,27 +247,43 @@ namespace UNO::NETWORK { StartGamePayload MessageSerializer::deserializeStartGamePayload(const nlohmann::json &payload) { - if (payload.is_null() == false) throw std::invalid_argument("Invalid START_GAME payload: expected null"); + if (payload.is_null() == false) { + throw std::invalid_argument("Invalid START_GAME payload: expected null"); + } return {}; } DrawCardPayload MessageSerializer::deserializeDrawCardPayload(const nlohmann::json &payload) { try { - if (payload.is_object() == false) throw std::invalid_argument("Invalid DRAW_CARD payload: expected JSON object"); - if (payload.at("draw_count").is_number_unsigned() == false) + if (payload.is_object() == false) { + throw std::invalid_argument("Invalid DRAW_CARD payload: expected JSON object"); + } + if (payload.at("draw_count").is_number_unsigned() == false) { throw std::invalid_argument("Invalid 'draw_count' field in DRAW_CARD payload: expected unsigned integer"); - return {payload.at("draw_count")}; + } + if (payload.at("cards").is_array() == false) { + throw std::invalid_argument("Invalid 'cards' field in DRAW_CARD payload: expected JSON array"); + } + + std::vector cards; + for (const auto &i : payload.at("cards")) { + cards.push_back(deserializeCard(i)); + } + + return {payload.at("draw_count"), cards}; } catch (const nlohmann::json::out_of_range &) { - throw std::invalid_argument("Missing required field 'draw_count' in DRAW_CARD payload"); + throw std::invalid_argument("Missing required field 'draw_count' and 'cards' in DRAW_CARD payload"); } } PlayCardPayload MessageSerializer::deserializePlayCardPayload(const nlohmann::json &payload) { try { - if (payload.is_object() == false) throw std::invalid_argument("Invalid PLAY_CARD payload: expected JSON object"); + if (payload.is_object() == false) { + throw std::invalid_argument("Invalid PLAY_CARD payload: expected JSON object"); + } return {deserializeCard(payload.at("card"))}; } catch (const nlohmann::json::out_of_range &) { @@ -210,9 +294,12 @@ namespace UNO::NETWORK { InitGamePayload MessageSerializer::deserializeInitGamePayload(const nlohmann::json &payload) { try { - if (payload.is_object() == false) throw std::invalid_argument("Invalid INIT_GAME payload: expected JSON object"); - if (payload.at("current_player").is_number_unsigned() == false) + if (payload.is_object() == false) { + throw std::invalid_argument("Invalid INIT_GAME payload: expected JSON object"); + } + if (payload.at("current_player").is_number_unsigned() == false) { throw std::invalid_argument("Invalid 'current_player' field in INIT_GAME payload: expected unsigned integer"); + } return {deserializeDiscardPile(payload.at("discard_pile")), deserializeHandCard(payload.at("hand_card")), payload.at("current_player")}; @@ -225,43 +312,97 @@ namespace UNO::NETWORK { EndGamePayload MessageSerializer::deserializeEndGamePayload(const nlohmann::json &payload) { - if (payload.is_null() == false) throw std::invalid_argument("Invalid END_GAME payload: expected null"); + if (payload.is_null() == false) { + throw std::invalid_argument("Invalid END_GAME payload: expected null"); + } return {}; } MessagePayloadType MessageSerializer::deserializeMessagePayloadType(const std::string &messagePayloadType) { - if (messagePayloadType == "JOIN_GAME") return MessagePayloadType::JOIN_GAME; - if (messagePayloadType == "START_GAME") return MessagePayloadType::START_GAME; - if (messagePayloadType == "DRAW_CARD") return MessagePayloadType::DRAW_CARD; - if (messagePayloadType == "PLAY_CARD") return MessagePayloadType::PLAY_CARD; - if (messagePayloadType == "INIT_GAME") return MessagePayloadType::INIT_GAME; - if (messagePayloadType == "END_GAME") return MessagePayloadType::END_GAME; + if (messagePayloadType == "EMPTY") { + return MessagePayloadType::EMPTY; + } + if (messagePayloadType == "JOIN_GAME") { + return MessagePayloadType::JOIN_GAME; + } + if (messagePayloadType == "START_GAME") { + return MessagePayloadType::START_GAME; + } + if (messagePayloadType == "DRAW_CARD") { + return MessagePayloadType::DRAW_CARD; + } + if (messagePayloadType == "PLAY_CARD") { + return MessagePayloadType::PLAY_CARD; + } + if (messagePayloadType == "INIT_GAME") { + return MessagePayloadType::INIT_GAME; + } + if (messagePayloadType == "END_GAME") { + return MessagePayloadType::END_GAME; + } throw std::invalid_argument("Invalid message payload type: '" + messagePayloadType - + "'. Expected: JOIN_GAME, START_GAME, DRAW_CARD, PLAY_CARD, INIT_GAME, or END_GAME"); + + "'. Expected: EMPTY, JOIN_GAME, START_GAME, DRAW_CARD, PLAY_CARD, INIT_GAME, or END_GAME"); + } + + MessageStatus MessageSerializer::deserializeMessageStatus(const std::string &messageStatus) + { + if (messageStatus == "OK") { + return MessageStatus::OK; + } + if (messageStatus == "INVALID") { + return MessageStatus::INVALID; + } + throw std::invalid_argument("Invalid message status: " + messageStatus + ". Expected: OK, INVALID"); } Message MessageSerializer::deserializeMessage(const nlohmann::json &message) { try { - if (message.is_object() == false) throw std::invalid_argument("Invalid message format: expected JSON object"); - if (message.at("payload_type").is_string() == false) + if (message.is_object() == false) { + throw std::invalid_argument("Invalid message format: expected JSON object"); + } + if (message.at("payload_type").is_string() == false) { throw std::invalid_argument("Invalid 'payload_type' field: expected string"); + } + if (message.at("status_code").is_string() == false) { + throw std::invalid_argument("Invalid message: expected string in 'status_code'"); + } auto payloadType = deserializeMessagePayloadType(message.at("payload_type")); switch (payloadType) { - case MessagePayloadType::JOIN_GAME: return {payloadType, deserializeJoinGamePayload(message.at("payload"))}; - case MessagePayloadType::START_GAME: return {payloadType, deserializeStartGamePayload(message.at("payload"))}; - case MessagePayloadType::DRAW_CARD: return {payloadType, deserializeDrawCardPayload(message.at("payload"))}; - case MessagePayloadType::PLAY_CARD: return {payloadType, deserializePlayCardPayload(message.at("payload"))}; - case MessagePayloadType::INIT_GAME: return {payloadType, deserializeInitGamePayload(message.at("payload"))}; - case MessagePayloadType::END_GAME: return {payloadType, deserializeEndGamePayload(message.at("payload"))}; + case MessagePayloadType::EMPTY: + return { + deserializeMessageStatus(message.at("status_code")), payloadType, deserializeEmptyPayload(message.at("payload"))}; + case MessagePayloadType::JOIN_GAME: + return {deserializeMessageStatus(message.at("status_code")), + payloadType, + deserializeJoinGamePayload(message.at("payload"))}; + case MessagePayloadType::START_GAME: + return {deserializeMessageStatus(message.at("status_code")), + payloadType, + deserializeStartGamePayload(message.at("payload"))}; + case MessagePayloadType::DRAW_CARD: + return {deserializeMessageStatus(message.at("status_code")), + payloadType, + deserializeDrawCardPayload(message.at("payload"))}; + case MessagePayloadType::PLAY_CARD: + return {deserializeMessageStatus(message.at("status_code")), + payloadType, + deserializePlayCardPayload(message.at("payload"))}; + case MessagePayloadType::INIT_GAME: + return {deserializeMessageStatus(message.at("status_code")), + payloadType, + deserializeInitGamePayload(message.at("payload"))}; + case MessagePayloadType::END_GAME: + return { + deserializeMessageStatus(message.at("status_code")), payloadType, deserializeEndGamePayload(message.at("payload"))}; } std::unreachable(); } catch (const nlohmann::json::out_of_range &) { - throw std::invalid_argument("Missing required field in message: expected 'payload_type' and 'payload'"); + throw std::invalid_argument("Missing required field in message: expected 'status_code', 'payload_type' and 'payload'"); } } diff --git a/src/network/MessageSerializer.h b/src/network/MessageSerializer.h index 12df2b2..a5f81d9 100644 --- a/src/network/MessageSerializer.h +++ b/src/network/MessageSerializer.h @@ -1,5 +1,5 @@ /** - * @file + * @file MessageSerializer.h * * @author Yuzhe Guo * @date 2025.11.20 @@ -19,6 +19,10 @@ namespace UNO::NETWORK { private: static nlohmann::json serializeCard(const GAME::Card &card); + + template + static nlohmann::json serializeCards(Iterator begin, Iterator end); + static nlohmann::json serializeDiscardPile(const GAME::DiscardPile &discardPile); static nlohmann::json serializeHandCard(const GAME::HandCard &handCard); @@ -31,6 +35,7 @@ namespace UNO::NETWORK { static nlohmann::json serializePayload(const EndGamePayload &payload); static std::string serializeMessagePayloadType(const MessagePayloadType &messagePayloadType); + static std::string serializeMessageStatus(const MessageStatus &messageStatus); static nlohmann::json serializeMessage(const Message &message); @@ -40,7 +45,7 @@ namespace UNO::NETWORK { static GAME::DiscardPile deserializeDiscardPile(const nlohmann::json &discardPile); static GAME::HandCard deserializeHandCard(const nlohmann::json &handCard); - static std::monostate deserializeMonostatePayload(const nlohmann::json &payload); + static std::monostate deserializeEmptyPayload(const nlohmann::json &payload); static JoinGamePayload deserializeJoinGamePayload(const nlohmann::json &payload); static StartGamePayload deserializeStartGamePayload(const nlohmann::json &payload); static PlayCardPayload deserializePlayCardPayload(const nlohmann::json &payload); @@ -49,6 +54,8 @@ namespace UNO::NETWORK { static EndGamePayload deserializeEndGamePayload(const nlohmann::json &payload); static MessagePayloadType deserializeMessagePayloadType(const std::string &messagePayloadType); + static MessageStatus deserializeMessageStatus(const std::string &messageStatus); + static Message deserializeMessage(const nlohmann::json &message); };