#include "game_service.h"
#include <algorithm>
#include <cstdint>
#include <string>
#include <unordered_set>
#include <utility>
#include <vector>
#include <mw/utils.hpp>
#include "game_field.h"
namespace
{
mw::E<void> authorizeAdministrator(
DataSourceTransactionInterface& transaction,
const AuthorizationService& authorization,
std::int64_t actor_user_id)
{
auto actor = transaction.getUserForUpdate(actor_user_id);
if(!actor)
{
return std::unexpected(std::move(actor.error()));
}
if(!*actor || !(*actor)->username ||
!authorization.canAdminister(**actor))
{
return std::unexpected(mw::httpError(403, "Administrator required"));
}
return {};
}
bool validShortName(const std::string& value)
{
return !value.empty() && std::ranges::all_of(
value,
[](unsigned char character)
{
return (character >= 'a' && character <= 'z') ||
(character >= '0' && character <= '9');
});
}
bool validFieldKey(const std::string& value)
{
if(value.empty() || value.front() < 'a' || value.front() > 'z')
{
return false;
}
return std::ranges::all_of(
value,
[](unsigned char character)
{
return (character >= 'a' && character <= 'z') ||
(character >= '0' && character <= '9') ||
character == '_';
});
}
mw::E<void> validateDisplayText(
std::string& display_name,
const std::string& description,
MarkdownRenderer& renderer)
{
display_name = std::string(mw::strip(display_name));
if(display_name.empty())
{
return std::unexpected(mw::Error(GameDefinitionValidationError{
"display_name", "Game name is required"}));
}
if(!validGameText(display_name))
{
return std::unexpected(mw::Error(GameDefinitionValidationError{
"display_name", "Game name must be UTF-8 text"}));
}
if(!validGameText(description))
{
return std::unexpected(mw::Error(GameDefinitionValidationError{
"description", "Game description must be UTF-8 text"}));
}
auto rendered = renderer.render(description);
if(!rendered)
{
return std::unexpected(mw::Error(GameDefinitionValidationError{
"description", rendered.error().msg()}));
}
return {};
}
mw::E<std::vector<GameChoice>> validateChoices(
GameFieldType type, const std::vector<std::string>& values)
{
if(type != GameFieldType::CHOICE && !values.empty())
{
return std::unexpected(mw::Error(GameDefinitionValidationError{
"choice", "Only choice fields can define choices"}));
}
if(type == GameFieldType::CHOICE && values.empty())
{
return std::unexpected(mw::Error(GameDefinitionValidationError{
"choice", "A choice field requires at least one choice"}));
}
std::unordered_set<std::string> unique;
std::vector<GameChoice> choices;
choices.reserve(values.size());
for(std::size_t position = 0; position < values.size(); ++position)
{
const std::string& value = values[position];
if(value.empty() || !validGameText(value))
{
return std::unexpected(mw::Error(GameDefinitionValidationError{
"choice", "Choices must be nonempty UTF-8 text"}));
}
if(!unique.insert(value).second)
{
return std::unexpected(mw::httpError(
409, "Choice values must be unique"));
}
choices.push_back({value, static_cast<std::int64_t>(position)});
}
return choices;
}
const GameField* findField(
const GameDefinitionSnapshot& definition, std::int64_t field_id)
{
const auto field = std::ranges::find_if(
definition.fields,
[field_id](const GameField& candidate)
{
return candidate.id == field_id;
});
return field == definition.fields.end() ? nullptr : &*field;
}
mw::E<GameDefinitionSnapshot> readDefinition(
DataSourceTransactionInterface& transaction,
const std::string& short_name,
std::int64_t expected_revision)
{
auto definition = transaction.getGameDefinitionForUpdate(short_name);
if(!definition)
{
return std::unexpected(std::move(definition.error()));
}
if(!*definition)
{
return std::unexpected(mw::httpError(404, "Game not found"));
}
if((**definition).game.revision != expected_revision)
{
return std::unexpected(mw::httpError(
409, "The game definition changed; reload and try again"));
}
return std::move(**definition);
}
mw::E<void> advanceRevision(
DataSourceTransactionInterface& transaction,
const std::string& short_name,
std::int64_t expected_revision)
{
auto advanced = transaction.incrementGameRevision(
short_name, expected_revision);
if(!advanced)
{
return std::unexpected(std::move(advanced.error()));
}
if(!*advanced)
{
return std::unexpected(mw::httpError(
409, "The game definition changed; reload and try again"));
}
return {};
}
} // namespace
GameService::GameService(DataSourceInterface& data_source)
: data_source_(data_source)
{}
mw::E<std::string> GameService::createGame(
std::int64_t actor_user_id,
std::string short_name,
std::string display_name,
std::string description,
GameVisibility visibility)
{
if(!validShortName(short_name))
{
return std::unexpected(mw::Error(GameDefinitionValidationError{
"short_name",
"Short name must contain lowercase letters and digits"}));
}
auto valid = validateDisplayText(
display_name, description, markdown_renderer_);
if(!valid)
{
return std::unexpected(std::move(valid.error()));
}
auto transaction = data_source_.beginTransaction();
if(!transaction)
{
return std::unexpected(std::move(transaction.error()));
}
auto authorized = authorizeAdministrator(
**transaction, authorization_, actor_user_id);
if(!authorized)
{
return std::unexpected(std::move(authorized.error()));
}
auto existing = (*transaction)->getGameDefinitionForUpdate(short_name);
if(!existing)
{
return std::unexpected(std::move(existing.error()));
}
if(*existing)
{
return std::unexpected(mw::httpError(
409, "A game with this short name already exists"));
}
auto inserted = (*transaction)->insertGame(
{short_name, std::move(display_name), std::move(description),
visibility, 1});
if(!inserted)
{
return std::unexpected(std::move(inserted.error()));
}
auto committed = (*transaction)->commit();
if(!committed)
{
return std::unexpected(std::move(committed.error()));
}
return short_name;
}
mw::E<void> GameService::updateGame(
std::int64_t actor_user_id,
const std::string& short_name,
std::int64_t expected_revision,
std::string display_name,
std::string description,
GameVisibility visibility)
{
auto valid = validateDisplayText(
display_name, description, markdown_renderer_);
if(!valid)
{
return std::unexpected(std::move(valid.error()));
}
auto transaction = data_source_.beginTransaction();
if(!transaction)
{
return std::unexpected(std::move(transaction.error()));
}
auto authorized = authorizeAdministrator(
**transaction, authorization_, actor_user_id);
if(!authorized)
{
return std::unexpected(std::move(authorized.error()));
}
auto definition = readDefinition(
**transaction, short_name, expected_revision);
if(!definition)
{
return std::unexpected(std::move(definition.error()));
}
auto updated = (*transaction)->updateGame(
short_name, display_name, description, visibility,
expected_revision);
if(!updated)
{
return std::unexpected(std::move(updated.error()));
}
if(!*updated)
{
return std::unexpected(mw::httpError(409, "Game update conflict"));
}
return (*transaction)->commit();
}
mw::E<void> GameService::removeGame(
std::int64_t actor_user_id,
const std::string& short_name,
std::int64_t expected_revision)
{
auto transaction = data_source_.beginTransaction();
if(!transaction)
{
return std::unexpected(std::move(transaction.error()));
}
auto authorized = authorizeAdministrator(
**transaction, authorization_, actor_user_id);
if(!authorized)
{
return std::unexpected(std::move(authorized.error()));
}
auto definition = readDefinition(
**transaction, short_name, expected_revision);
if(!definition)
{
return std::unexpected(std::move(definition.error()));
}
auto usage = (*transaction)->getGameUsage(short_name);
if(!usage)
{
return std::unexpected(std::move(usage.error()));
}
if(usage->card_count != 0 || usage->series_count != 0 ||
usage->last_number != 0)
{
return std::unexpected(mw::httpError(
409,
"This game cannot be deleted because it has " +
std::to_string(usage->card_count) + " card(s), " +
std::to_string(usage->series_count) + " series, and has " +
"issued card number " + std::to_string(usage->last_number)));
}
auto deleted = (*transaction)->deleteGame(short_name);
if(!deleted)
{
return std::unexpected(std::move(deleted.error()));
}
return (*transaction)->commit();
}
mw::E<std::int64_t> GameService::createField(
std::int64_t actor_user_id,
const std::string& short_name,
std::int64_t expected_revision,
std::string key,
std::string label,
GameFieldType type,
std::vector<std::string> choice_values)
{
label = std::string(mw::strip(label));
if(!validFieldKey(key))
{
return std::unexpected(mw::Error(GameDefinitionValidationError{
"key", "Field key is invalid"}));
}
if(label.empty() || !validGameText(label))
{
return std::unexpected(mw::Error(GameDefinitionValidationError{
"label", "Field label is invalid"}));
}
auto choices = validateChoices(type, choice_values);
if(!choices)
{
return std::unexpected(std::move(choices.error()));
}
auto transaction = data_source_.beginTransaction();
if(!transaction)
{
return std::unexpected(std::move(transaction.error()));
}
auto authorized = authorizeAdministrator(
**transaction, authorization_, actor_user_id);
if(!authorized)
{
return std::unexpected(std::move(authorized.error()));
}
auto definition = readDefinition(
**transaction, short_name, expected_revision);
if(!definition)
{
return std::unexpected(std::move(definition.error()));
}
if(std::ranges::any_of(
definition->fields,
[&key](const GameField& field) { return field.key == key; }))
{
return std::unexpected(mw::httpError(
409, "A field with this key already exists"));
}
GameField field{
0, short_name, std::move(key), std::move(label), type,
static_cast<std::int64_t>(definition->fields.size()), {}};
auto inserted = (*transaction)->insertGameField(field, *choices);
if(!inserted)
{
return std::unexpected(std::move(inserted.error()));
}
auto advanced = advanceRevision(
**transaction, short_name, expected_revision);
if(!advanced)
{
return std::unexpected(std::move(advanced.error()));
}
auto committed = (*transaction)->commit();
if(!committed)
{
return std::unexpected(std::move(committed.error()));
}
return *inserted;
}
mw::E<void> GameService::updateField(
std::int64_t actor_user_id,
const std::string& short_name,
std::int64_t field_id,
std::int64_t expected_revision,
std::string label,
std::vector<std::string> choice_values)
{
label = std::string(mw::strip(label));
if(label.empty() || !validGameText(label))
{
return std::unexpected(mw::Error(GameDefinitionValidationError{
"label", "Field label is invalid"}));
}
auto transaction = data_source_.beginTransaction();
if(!transaction)
{
return std::unexpected(std::move(transaction.error()));
}
auto authorized = authorizeAdministrator(
**transaction, authorization_, actor_user_id);
if(!authorized)
{
return std::unexpected(std::move(authorized.error()));
}
auto definition = readDefinition(
**transaction, short_name, expected_revision);
if(!definition)
{
return std::unexpected(std::move(definition.error()));
}
const GameField* field = findField(*definition, field_id);
if(field == nullptr)
{
return std::unexpected(mw::httpError(404, "Game field not found"));
}
auto choices = validateChoices(field->type, choice_values);
if(!choices)
{
return std::unexpected(std::move(choices.error()));
}
for(const GameChoice& existing : field->choices)
{
const bool retained = std::ranges::any_of(
*choices,
[&existing](const GameChoice& choice)
{
return choice.value == existing.value;
});
if(!retained)
{
auto count = (*transaction)->countChoiceUsage(
field_id, existing.value);
if(!count)
{
return std::unexpected(std::move(count.error()));
}
if(*count != 0)
{
return std::unexpected(mw::httpError(
409,
"This choice cannot be deleted because " +
std::to_string(*count) + " card(s) use it"));
}
}
}
auto updated = (*transaction)->updateGameField(
field_id, label, *choices);
if(!updated)
{
return std::unexpected(std::move(updated.error()));
}
auto advanced = advanceRevision(
**transaction, short_name, expected_revision);
if(!advanced)
{
return std::unexpected(std::move(advanced.error()));
}
return (*transaction)->commit();
}
mw::E<void> GameService::removeField(
std::int64_t actor_user_id,
const std::string& short_name,
std::int64_t field_id,
std::int64_t expected_revision)
{
auto transaction = data_source_.beginTransaction();
if(!transaction)
{
return std::unexpected(std::move(transaction.error()));
}
auto authorized = authorizeAdministrator(
**transaction, authorization_, actor_user_id);
if(!authorized)
{
return std::unexpected(std::move(authorized.error()));
}
auto definition = readDefinition(
**transaction, short_name, expected_revision);
if(!definition)
{
return std::unexpected(std::move(definition.error()));
}
if(findField(*definition, field_id) == nullptr)
{
return std::unexpected(mw::httpError(404, "Game field not found"));
}
auto count = (*transaction)->countFieldUsage(field_id);
if(!count)
{
return std::unexpected(std::move(count.error()));
}
if(*count != 0)
{
return std::unexpected(mw::httpError(
409,
"This field cannot be deleted because " +
std::to_string(*count) + " card(s) use it"));
}
auto deleted = (*transaction)->deleteGameField(field_id);
if(!deleted)
{
return std::unexpected(std::move(deleted.error()));
}
auto advanced = advanceRevision(
**transaction, short_name, expected_revision);
if(!advanced)
{
return std::unexpected(std::move(advanced.error()));
}
return (*transaction)->commit();
}
mw::E<void> GameService::reorderFields(
std::int64_t actor_user_id,
const std::string& short_name,
std::int64_t expected_revision,
const std::vector<std::int64_t>& field_ids)
{
auto transaction = data_source_.beginTransaction();
if(!transaction)
{
return std::unexpected(std::move(transaction.error()));
}
auto authorized = authorizeAdministrator(
**transaction, authorization_, actor_user_id);
if(!authorized)
{
return std::unexpected(std::move(authorized.error()));
}
auto definition = readDefinition(
**transaction, short_name, expected_revision);
if(!definition)
{
return std::unexpected(std::move(definition.error()));
}
std::unordered_set<std::int64_t> requested(
field_ids.begin(), field_ids.end());
if(requested.size() != field_ids.size() ||
field_ids.size() != definition->fields.size() ||
std::ranges::any_of(
definition->fields,
[&requested](const GameField& field)
{
return !requested.contains(field.id);
}))
{
return std::unexpected(mw::httpError(422, "Invalid field order"));
}
auto reordered = (*transaction)->updateGameFieldOrder(
short_name, field_ids);
if(!reordered)
{
return std::unexpected(std::move(reordered.error()));
}
auto advanced = advanceRevision(
**transaction, short_name, expected_revision);
if(!advanced)
{
return std::unexpected(std::move(advanced.error()));
}
return (*transaction)->commit();
}