#pragma once // CollectionService: high-level CRUD over the per-game collection. // Header-only template that operates on the ICollectionRepository port and // delegates image cleanup to an IImageStore on remove. // // This is the C++ equivalent of the Rust generic helpers in // `templates/card_service_templates.rs` (add_entry_to_collection, // update_entry_in_collection, get_entry_by_id, delete_entry_by_id, ...). // // Each TCard must expose `std::uint32_t id` and `std::vector images`. #include "ccm/domain/Enums.hpp" #include "ccm/ports/ICollectionRepository.hpp" #include "ccm/ports/IImageStore.hpp" #include "ccm/util/Result.hpp" #include #include #include #include #include #include #include namespace ccm { template class CollectionService { public: using Map = std::map; CollectionService(ICollectionRepository& repo, IImageStore& imageStore) : repo_(repo), imageStore_(imageStore) {} Result> list(Game game) { auto loaded = repo_.load(game); if (!loaded) return Result>::err(loaded.error()); Map map = std::move(loaded).value(); std::vector out; out.reserve(map.size()); for (auto& [id, card] : map) { (void)id; out.push_back(std::move(card)); } return Result>::ok(std::move(out)); } [[nodiscard]] static std::uint32_t nextId(const Map& map) noexcept { if (map.empty()) return 0; return map.rbegin()->first + 1; } // Add a card. The `id` field on the input is overwritten with the next // free id (matching the Rust HashMap-keyed behavior). Result add(Game game, TCard card) { auto loaded = repo_.load(game); if (!loaded) return Result::err(loaded.error()); Map map = std::move(loaded).value(); const std::uint32_t newId = nextId(map); card.id = newId; map.emplace(newId, std::move(card)); auto saved = repo_.save(game, map); if (!saved) return Result::err(saved.error()); return Result::ok(newId); } // Update an existing entry, identified by `card.id`. If the id is not // present, an error is returned. Result update(Game game, TCard card) { auto loaded = repo_.load(game); if (!loaded) return Result::err(loaded.error()); Map map = std::move(loaded).value(); auto it = map.find(card.id); if (it == map.end()) { return Result::err("Card with id " + std::to_string(card.id) + " not found in collection."); } it->second = std::move(card); return repo_.save(game, map); } // Replace the entire collection map in one save (e.g. after bulk set-id sync). Result saveAll(Game game, std::vector cards) { Map map; for (auto& card : cards) { map.insert_or_assign(card.id, std::move(card)); } return repo_.save(game, map); } // Remove the card with the given id. Also deletes any associated images // via the IImageStore (best-effort - image removal failures are logged in // the error string but the card itself is still purged from the JSON). Result remove(Game game, std::uint32_t id) { auto loaded = repo_.load(game); if (!loaded) return Result::err(loaded.error()); Map map = std::move(loaded).value(); auto it = map.find(id); if (it == map.end()) { return Result::err("Card with id " + std::to_string(id) + " not found."); } std::string imgErr; for (const auto& image : it->second.images) { auto rm = imageStore_.remove(game, image); if (!rm) { if (!imgErr.empty()) imgErr += "; "; imgErr += rm.error(); } } map.erase(it); auto saved = repo_.save(game, map); if (!saved) return saved; if (!imgErr.empty()) { return Result::err("Card removed but image cleanup had issues: " + imgErr); } return Result::ok(); } // Look up a card by id without mutating storage. Result> findById(Game game, std::uint32_t id) { auto loaded = repo_.load(game); if (!loaded) return Result>::err(loaded.error()); const auto& m = loaded.value(); auto it = m.find(id); if (it == m.end()) return Result>::ok(std::nullopt); return Result>::ok(it->second); } private: ICollectionRepository& repo_; IImageStore& imageStore_; }; } // namespace ccm