# HG changeset patch # User David Demelier # Date 1538985072 -7200 # Node ID ffe8ac5c35c0d3ce70a82d6119eacc5df544c1e2 # Parent c5274f2d4658bad30f3b7c3ffb48870a7a358313 Database: implement as a separate library, closes #906 @1h diff -r c5274f2d4658 -r ffe8ac5c35c0 CMakeLists.txt --- a/CMakeLists.txt Wed Aug 29 17:04:57 2018 +0200 +++ b/CMakeLists.txt Mon Oct 08 09:51:12 2018 +0200 @@ -44,6 +44,7 @@ add_subdirectory(tools) add_subdirectory(extern) +add_subdirectory(libdb) add_subdirectory(libcommon) add_subdirectory(libserver) add_subdirectory(server) diff -r c5274f2d4658 -r ffe8ac5c35c0 libdb/CMakeLists.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libdb/CMakeLists.txt Mon Oct 08 09:51:12 2018 +0200 @@ -0,0 +1,50 @@ +# +# CMakeLists.txt -- CMake build system for malikania +# +# Copyright (c) 2013-2018 David Demelier +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# + +project(libmlk-db) + +find_package(PostgreSQL REQUIRED) + +set( + HEADERS + ${libmlk-db_SOURCE_DIR}/malikania/db/account.hpp + ${libmlk-db_SOURCE_DIR}/malikania/db/character.hpp + ${libmlk-db_SOURCE_DIR}/malikania/db/database.hpp + ${libmlk-db_SOURCE_DIR}/malikania/db/model.hpp + ${libmlk-db_SOURCE_DIR}/malikania/db/spell.hpp +) + +set( + SOURCES + ${libmlk-db_SOURCE_DIR}/malikania/db/account.cpp + ${libmlk-db_SOURCE_DIR}/malikania/db/character.cpp + ${libmlk-db_SOURCE_DIR}/malikania/db/database.cpp + ${libmlk-db_SOURCE_DIR}/malikania/db/spell.cpp +) + +malikania_define_library( + TARGET libmlk-db + SOURCES ${HEADERS} ${SOURCES} + LIBRARIES + ${PostgreSQL_LIBRARIES} + PUBLIC_INCLUDES + ${Boost_INCLUDE_DIRS} + ${libmlk-db_SOURCE_DIR}/malikania + PRIVATE_INCLUDES + ${PostgreSQL_INCLUDE_DIRS} +) diff -r c5274f2d4658 -r ffe8ac5c35c0 libdb/malikania/db/account.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libdb/malikania/db/account.cpp Mon Oct 08 09:51:12 2018 +0200 @@ -0,0 +1,242 @@ +/* + * account.cpp -- database account object + * + * Copyright (c) 2013-2018 David Demelier + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include "account.hpp" + +namespace mlk::db { + +auto account::load(const result& res) -> account +{ + account a(PQgetvalue(res.get(), 0, 1), PQgetvalue(res.get(), 0, 2)); + + a.id_ = std::stoi(PQgetvalue(res.get(), 0, 0)); + a.firstname_ = PQgetvalue(res.get(), 0, 3); + a.lastname_ = PQgetvalue(res.get(), 0, 4); + a.email_ = PQgetvalue(res.get(), 0, 5); + a.characters_ = character::load(a); + + return a; +} + +void account::clear() +{ + id_ = 0U; +} + +account::account(std::string login, std::string password) noexcept + : login_(std::move(login)) + , password_(std::move(password)) +{ + assert(!login_.empty()); + assert(!password_.empty()); +} + +auto account::get_login() const noexcept -> const std::string& +{ + return login_; +} + +void account::set_password(std::string password) +{ + static const std::string sql( + "UPDATE account" + " SET password = $1" + " WHERE id = $2" + ); + + assert(!password.empty()); + + if (is_published() && password_ != password) + db::exec(sql, { password, id_ }); + + password_ = std::move(password); +} + +auto account::get_email() const noexcept -> const std::string& +{ + return email_; +} + +void account::set_email(std::string email) +{ + static const std::string sql( + "UPDATE account" + " SET email = $1" + " WHERE id = $2" + ); + + assert(!email.empty()); + + if (is_published() && email_ != email) + db::exec(sql, { email, id_ }); + + email_ = std::move(email); +} + +auto account::get_firstname() const noexcept -> const std::string& +{ + return firstname_; +} + +void account::set_firstname(std::string name) +{ + static const std::string sql( + "UPDATE account" + " SET firstname = $1" + " WHERE id = $2" + ); + + if (is_published() && firstname_ != name) + db::exec(sql, { name, id_ }); + + firstname_ = std::move(name); +} + +auto account::get_lastname() const noexcept -> const std::string& +{ + return lastname_; +} + +void account::set_lastname(std::string name) +{ + static const std::string sql( + "UPDATE account" + " SET lastname = $1" + " WHERE id = $2" + ); + + if (is_published() && lastname_ != name) + db::exec(sql, { name, id_ }); + + lastname_ = std::move(name); +} + +auto account::get_characters() const noexcept -> const std::vector& +{ + return characters_; +} + +void account::add(character ch) +{ + assert(ch.is_draft()); + + characters_.reserve(characters_.size() + 1); + + if (is_published()) + ch.publish(*this); + + characters_.push_back(std::move(ch)); +} + +void account::remove(std::vector::iterator it) +{ + // TODO: assert 'it' is in vector. + it->unpublish(); + characters_.erase(it); +} + +void account::publish() +{ + static const std::string sql( + "INSERT INTO account(" + " login," + " password," + " firstname," + " lastname," + " email" + ") " + "VALUES ($1, $2, $3, $4, $5) " + "RETURNING id" + ); + + assert(is_draft()); + + /* + * Recursively save the account, its characters and spells of those + * characters. + */ + transaction txn([this] { + clear(); + + for (auto& c : characters_) + c.clear(); + }); + + const auto r = select(sql, { login_, password_, firstname_, lastname_, email_ }); + + id_ = std::stoi(PQgetvalue(r.get(), 0, 0)); + + for (auto& c : characters_) + c.publish(*this); + + txn.commit(); + + assert(is_published()); +} + +void account::unpublish() +{ + static const std::string sql( + "DELETE" + " FROM account" + " WHERE id = $1" + ); + + assert(is_published()); + + id_ = 0; + + /* + * Recursively make all characters and their spells draft. + */ + for (auto& c : characters_) + c.clear(); + + assert(is_draft()); +} + +auto account::find_by_login(const std::string& login) -> std::optional +{ + static const std::string sql( + "SELECT *" + " FROM account" + " WHERE login = $1" + ); + + const auto r = select(sql, { login }); + + if (PQntuples(r.get()) == 0) + return std::nullopt; + + return load(r); +} + +auto account::authenticate(const std::string& login, + const std::string& password) -> std::optional +{ + auto ac = find_by_login(login); + + if (!ac || ac->password_ != password) + return std::nullopt; + + return ac; +} + +} // !mlk::db diff -r c5274f2d4658 -r ffe8ac5c35c0 libdb/malikania/db/account.hpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libdb/malikania/db/account.hpp Mon Oct 08 09:51:12 2018 +0200 @@ -0,0 +1,189 @@ +/* + * account.hpp -- database account object + * + * Copyright (c) 2013-2018 David Demelier + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef MALIKANIA_DB_ACCOUNT_HPP +#define MALIKANIA_DB_ACCOUNT_HPP + +/** + * \file account.hpp + * \brief Database account object. + */ + +#include + +#include "character.hpp" +#include "database.hpp" +#include "model.hpp" + +namespace mlk::db { + +/** + * \brief Database account object. + */ +class account : public model { +private: + std::string login_; + std::string password_; + std::string email_; + std::string firstname_; + std::string lastname_; + std::vector characters_; + + static auto load(const result&) -> account; + + void clear(); + +public: + /** + * Create a draft account. + * + * \pre !login.empty() + * \pre !password.empty() + * \param login the login name + * \param password the password + * \warning the password is saved as-is and **must** be hashed by the caller. + */ + account(std::string login, std::string password) noexcept; + + /** + * Get the account login. + * + * \return the login + */ + auto get_login() const noexcept -> const std::string&; + + /** + * Set the password. + * + * \pre !password.empty() + * \warning the password is saved as-is and **must** be hashed by the caller. + */ + void set_password(std::string password); + + /** + * Get the account email. + * + * \return the email + */ + auto get_email() const noexcept -> const std::string&; + + /** + * Set the account email. + * + * \pre !email.empty() + * \param email the new email + */ + void set_email(std::string email); + + /** + * Get the account first name. + * + * \return the name + */ + auto get_firstname() const noexcept -> const std::string&; + + /** + * Set the account firstname. + * + * \param name the new name + */ + void set_firstname(std::string name); + + /** + * Get the account last name. + * + * \return the name + */ + auto get_lastname() const noexcept -> const std::string&; + + /** + * Set the account last name. + * + * \param name the new name + */ + void set_lastname(std::string name); + + /** + * Get the caracter list. + * + * \return the associated characters. + */ + auto get_characters() const noexcept -> const std::vector&; + + /** + * Add the character to the account. + * + * Account takes ownership of character. + * + * \pre ch.is_draft() + * \param ch the character + * \post ch.is_published() + */ + void add(character ch); + + /** + * Remove the character from the account. + * + * \pre it is in the the account character list + * \param ch the character + * \post ch == nullptr + */ + void remove(std::vector::iterator it); + + /** + * Save the account, does nothing if is_published(). + * + * \throw std::exception on errors + * \post is_published() + */ + void publish(); + + /** + * Destroy the account. + * + * The account will contains no characters anymore. + * + * \throw std::exception on errors + * \post is_draft() + */ + void unpublish(); + + /** + * Find an account by login. + * + * \param login the login + * \return the account or none if not found + * \throw std::runtime_error on database errors + */ + static auto find_by_login(const std::string& login) -> std::optional; + + /** + * Find and authenticate a user. + * + * \param login the login + * \param password the password + * \return the account or none if not found + * \throw std::runtime_error on database errors + */ + static auto authenticate(const std::string& login, + const std::string& password) -> std::optional; +}; + +} // !mlk::db + +#endif // !MALIKANIA_DB_ACCOUNT_HPP diff -r c5274f2d4658 -r ffe8ac5c35c0 libdb/malikania/db/character.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libdb/malikania/db/character.cpp Mon Oct 08 09:51:12 2018 +0200 @@ -0,0 +1,266 @@ +/* + * character.cpp -- database character object + * + * Copyright (c) 2013-2018 David Demelier + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include + +#include "account.hpp" +#include "character.hpp" +#include "database.hpp" + +namespace mlk::db { + +auto character::load(const result& result, int row) -> character +{ + character ch(PQgetvalue(result.get(), row, 2), PQgetvalue(result.get(), row, 3)); + + ch.levels_[stat::hp] = std::stoi(PQgetvalue(result.get(), row, 4)); + ch.factors_[stat::hp] = std::stoi(PQgetvalue(result.get(), row, 5)); + ch.exp_[stat::hp] = std::stoi(PQgetvalue(result.get(), row, 6)); + + ch.levels_[stat::force] = std::stoi(PQgetvalue(result.get(), row, 7)); + ch.factors_[stat::force] = std::stoi(PQgetvalue(result.get(), row, 8)); + ch.exp_[stat::force] = std::stoi(PQgetvalue(result.get(), row, 9)); + + ch.levels_[stat::defense] = std::stoi(PQgetvalue(result.get(), row, 10)); + ch.factors_[stat::defense] = std::stoi(PQgetvalue(result.get(), row, 11)); + ch.exp_[stat::defense] = std::stoi(PQgetvalue(result.get(), row, 12)); + + ch.levels_[stat::agility] = std::stoi(PQgetvalue(result.get(), row, 13)); + ch.factors_[stat::agility] = std::stoi(PQgetvalue(result.get(), row, 14)); + ch.exp_[stat::agility] = std::stoi(PQgetvalue(result.get(), row, 15)); + + ch.levels_[stat::luck] = std::stoi(PQgetvalue(result.get(), row, 16)); + ch.factors_[stat::luck] = std::stoi(PQgetvalue(result.get(), row, 17)); + ch.exp_[stat::luck] = std::stoi(PQgetvalue(result.get(), row, 18)); + + ch.id_ = std::stoi(PQgetvalue(result.get(), row, 0)); + ch.spells_ = spell::load(ch); + + return ch; +} + +auto character::load(const account& parent) -> std::vector +{ + static const std::string sql( + "SELECT *" + " FROM character" + " WHERE account_id = $1" + ); + + const auto r = select(sql, { parent.get_id() }); + + if (PQntuples(r.get()) == 0) + throw std::runtime_error("failed to load characters"); + + std::vector characters; + + for (int i = 0; i < PQntuples(r.get()); ++i) + characters.push_back(load(r, i)); + + return characters; +} + +void character::clear() +{ + id_ = 0U; + + for (auto& s : spells_) + s.clear(); +} + +void character::publish(account& parent) +{ + static const std::string sql( + "INSERT INTO character (" + " account_id," + " nickname," + " type," + " hp_level," + " hp_factor," + " hp_exp," + " force_level," + " force_factor," + " force_exp," + " defense_level," + " defense_factor," + " defense_exp," + " agility_level," + " agility_factor," + " agility_exp," + " luck_level," + " luck_factor," + " luck_exp" + ") " + "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) " + "RETURNING id" + ); + + const auto r = select(sql, { + parent.get_id(), + nickname_, + type_, + levels_[stat::hp], + factors_[stat::hp], + exp_[stat::hp], + levels_[stat::force], + factors_[stat::force], + exp_[stat::force], + levels_[stat::defense], + factors_[stat::defense], + exp_[stat::defense], + levels_[stat::agility], + factors_[stat::agility], + exp_[stat::agility], + levels_[stat::luck], + factors_[stat::luck], + exp_[stat::luck] + }); + + if (PQntuples(r.get()) == 0) + throw std::runtime_error("failed to save character"); + + id_ = std::stoi(PQgetvalue(r.get(), 0, 0)); + + for (auto& s : spells_) + s.publish(*this); +} + +void character::unpublish() +{ + static const std::string sql( + "DELETE" + " FROM character" + " WHERE id = $1" + ); + + exec(sql, { id_ }); + clear(); +} + +character::character(std::string nickname, std::string type) noexcept + : nickname_(std::move(nickname)) + , type_(std::move(type)) +{ + assert(!nickname_.empty()); + assert(!type_.empty()); +} + +auto character::get_nickname() const noexcept -> const std::string& +{ + return nickname_; +} + +auto character::get_type() const noexcept -> const std::string& +{ + return type_; +} + +auto character::get_spells() const noexcept -> const std::vector& +{ + return spells_; +} + +auto character::get_levels() const noexcept -> const std::array& +{ + return levels_; +} + +void character::set_levels(std::array levels) +{ + static const std::string sql( + "UPDATE character" + " SET hp_level = $1" + " , force_level = $2" + " , defense_level = $3" + " , agility_level = $4" + " , luck_level = $5" + ); + + if (is_published()) + exec(sql, { id_, levels[0], levels[1], levels[2], levels[3], levels[4] }); + + levels_ = std::move(levels); +} + +auto character::get_factors() const noexcept -> const std::array& +{ + return factors_; +} + +void character::set_factors(std::array factors) +{ + static const std::string sql( + "UPDATE character" + " SET hp_factor = $1" + " , force_factor = $2" + " , defense_factor = $3" + " , agility_factor = $4" + " , luck_factor = $5" + ); + + assert(std::accumulate(factors.begin(), factors.end(), 0U) == 100U); + + if (is_published()) + exec(sql, { id_, factors[0], factors[1], factors[2], factors[3], factors[4] }); + + factors_ = std::move(factors); +} + +auto character::get_experience() const noexcept -> const std::array& +{ + return exp_; +} + +void character::set_experience(std::array experience) +{ + static const std::string sql( + "UPDATE character" + " SET hp_experience = $1" + " , force_experience = $2" + " , defense_experience = $3" + " , agility_experience = $4" + " , luck_experience = $5" + ); + + if (is_published()) + exec(sql, { id_, experience[0], experience[1], experience[2], experience[3], experience[4] }); + + exp_ = std::move(experience); +} + +void character::add(spell sp) +{ + assert(sp.is_draft()); + + spells_.reserve(spells_.size() + 1); + + if (is_published()) + sp.publish(*this); + + spells_.push_back(std::move(sp)); +} + +void character::remove(std::vector::iterator it) +{ + // TODO: assert 'it' is in vector. + it->unpublish(); + spells_.erase(it); +} + +} // !mlk::db diff -r c5274f2d4658 -r ffe8ac5c35c0 libdb/malikania/db/character.hpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libdb/malikania/db/character.hpp Mon Oct 08 09:51:12 2018 +0200 @@ -0,0 +1,171 @@ +/* + * character.hpp -- database character object + * + * Copyright (c) 2013-2018 David Demelier + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef MALIKANIA_DB_CHARACTER_HPP +#define MALIKANIA_DB_CHARACTER_HPP + +/* + * \file character.hpp + * \brief Database character object. + */ + +#include +#include +#include + +#include "database.hpp" +#include "model.hpp" +#include "spell.hpp" + +namespace mlk::db { + +class account; + +/* + * \brief Database character object. + */ +class character : public model { +public: + friend class account; + + /** + * \brief Stats index in arrays + * \see set_levels + * \see set_factors + * \see set_experience + */ + enum stat { + hp = 0, //!< hp index + force, //!< force index + defense, //!< defense index + agility, //!< agility index + luck //!< luck index + }; + +private: + std::string nickname_; + std::string type_; + std::vector spells_; + std::array levels_{1U, 1U, 1U, 1U, 1U}; + std::array factors_{20U, 20U, 20U, 20U, 20U}; + std::array exp_{0U, 0U, 0U, 0U, 0U}; + + static auto load(const result&, int) -> character; + static auto load(const account&) -> std::vector; + + void clear(); + void publish(account&); + void unpublish(); + +public: + /** + * Construct a character, no database is modified yet. + * + * \pre !nickname.empty() + * \pre !type.empty() + * \param nickname the nickname + * \param type the type + */ + character(std::string nickname, std::string type) noexcept; + + /** + * Get the character nickname. + * + * \return the name + */ + auto get_nickname() const noexcept -> const std::string&; + + /** + * Get the character type name. + * + * \return the type name + */ + auto get_type() const noexcept -> const std::string&; + + /** + * Get the list of spells. + * + * \return the spells + */ + auto get_spells() const noexcept -> const std::vector&; + + /** + * Get the list of levels. + * + * \return the levels + */ + auto get_levels() const noexcept -> const std::array&; + + /** + * Set the new levels. + * + * \param levels the levels + */ + void set_levels(std::array levels); + + /** + * Get the list of factors. + * + * \return the factors + */ + auto get_factors() const noexcept -> const std::array&; + + /** + * Set progression factors. + * + * \pre factors sum must be 100 + * \param factors the factors + */ + void set_factors(std::array factors); + + /** + * Get the stats experience. + * + * \return the experience + */ + auto get_experience() const noexcept -> const std::array&; + + /** + * Set stats experience. + * + * \param experience the experience + */ + void set_experience(std::array experience); + + /** + * Add a spell. + * + * Character takes ownership of spell. + * + * \pre sp.is_draft() + * \param sp the spell (moved) + */ + void add(spell sp); + + /** + * Remove the spell from the character. + * + * \pre it must be valid + * \param it the spell iterator + */ + void remove(std::vector::iterator it); +}; + +} // !mlk::db + +#endif // !MALIKANIA_DB_CHARACTER_HPP diff -r c5274f2d4658 -r ffe8ac5c35c0 libdb/malikania/db/database.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libdb/malikania/db/database.cpp Mon Oct 08 09:51:12 2018 +0200 @@ -0,0 +1,210 @@ +/* + * database.cpp -- connection to postgresql database + * + * Copyright (c) 2013-2018 David Demelier + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include + +#include "database.hpp" + +namespace mlk::db { + +namespace { + +struct strify { + template >> + auto operator()(T i) const -> std::string + { + return std::to_string(i); + } + + auto operator()(bool v) const -> std::string + { + return v ? "t" : "f"; + } + + auto operator()(std::string s) const -> std::string + { + return s; + } +}; + +std::unique_ptr connection{nullptr, nullptr}; + +auto run(const std::string& sql, std::vector args = {}) -> result +{ + std::vector list; + + /* + * PQexecParams requires an array of C strings, convert all arguments in the + * args vector to string and then keep the C string value into a temporary + * array. + */ + for (auto& arg : args) { + arg = std::visit(strify(), arg); + list.push_back(std::get(arg).c_str()); + } + + return { + PQexecParams(connection.get(), sql.c_str(), list.size(), nullptr, + list.data(), nullptr, nullptr, 0 + ), + PQclear + }; +} + +} // !namespace + +void open(const std::string& host, + const std::string& port, + const std::string& user, + const std::string& database, + const std::string& password) +{ + assert(!connection); + + std::ostringstream oss; + + if (!host.empty()) + oss << "host=" << host << " "; + if (!port.empty()) + oss << "port=" << port << " "; + if (!user.empty()) + oss << "user=" << user << " "; + if (!database.empty()) + oss << "dbname=" << database << " "; + if (!password.empty()) + oss << "password=" << password; + + connection = { PQconnectdb(oss.str().c_str()), PQfinish }; + + if (PQstatus(connection.get()) != CONNECTION_OK) + throw std::runtime_error(PQerrorMessage(connection.get())); +} + +const std::string init_account( + "CREATE TABLE IF NOT EXISTS account(" + " id SERIAL," + " login TEXT NOT NULL," + " password TEXT NOT NULL," + " firstname TEXT," + " lastname TEXT," + " email TEXT," + " PRIMARY KEY(id)" + ")" +); + +const std::string init_character( + "CREATE TABLE IF NOT EXISTS character(" + " id SERIAL," + " account_id INTEGER NOT NULL," + " nickname TEXT NOT NULL," + " type TEXT NOT NULL," + " hp_level SMALLINT NOT NULL DEFAULT 1," + " hp_factor SMALLINT NOT NULL," + " hp_exp INT NOT NULL," + " force_level SMALLINT NOT NULL DEFAULT 1," + " force_factor SMALLINT NOT NULL," + " force_exp INT NOT NULL," + " defense_level SMALLINT NOT NULL DEFAULT 1," + " defense_factor SMALLINT NOT NULL," + " defense_exp INT NOT NULL," + " agility_level SMALLINT NOT NULL DEFAULT 1," + " agility_factor SMALLINT NOT NULL," + " agility_exp INT NOT NULL," + " luck_level SMALLINT NOT NULL DEFAULT 1," + " luck_factor SMALLINT NOT NULL," + " luck_exp INT NOT NULL," + " PRIMARY KEY(id)," + " FOREIGN KEY (account_id) REFERENCES account(id) ON DELETE CASCADE" + ")" +); + +const std::string init_spell( + "CREATE TABLE IF NOT EXISTS spell(" + " id SERIAL," + " character_id INTEGER NOT NULL," + " type TEXT NOT NULL," + " level SMALLINT NOT NULL," + " PRIMARY KEY(id)," + " FOREIGN KEY (character_id) REFERENCES character(id) ON DELETE CASCADE" + ")" +); + +void init() +{ + assert(connection); + + exec(init_account); + exec(init_character); + exec(init_spell); +} + +auto select(const std::string& sql, const std::vector& args) -> result +{ + assert(connection); + + auto result = run(sql, args); + + switch (PQresultStatus(result.get())) { + case PGRES_COMMAND_OK: + case PGRES_TUPLES_OK: + break; + default: + throw std::runtime_error(PQerrorMessage(connection.get())); + } + + return result; +} + +void exec(const std::string& sql, const std::vector& args) +{ + assert(connection); + + switch (const auto result = run(sql, args); PQresultStatus(result.get())) { + case PGRES_COMMAND_OK: + break; + default: + throw std::runtime_error(PQerrorMessage(connection.get())); + } +} + +transaction::transaction(rollback fn) + : rollback_(std::move(fn)) +{ + assert(rollback_); + + exec("BEGIN"); +} + +transaction::~transaction() +{ + if (!commit_) { + rollback_(); + exec("ROLLBACK"); + } +} + +void transaction::commit() +{ + if (!commit_) { + exec("COMMIT"); + commit_ = true; + } +} + +} // !db::mlk diff -r c5274f2d4658 -r ffe8ac5c35c0 libdb/malikania/db/database.hpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libdb/malikania/db/database.hpp Mon Oct 08 09:51:12 2018 +0200 @@ -0,0 +1,151 @@ +/* + * database.hpp -- connection to postgresql database + * + * Copyright (c) 2013-2018 David Demelier + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef MALIKANIA_DB_DATABASE_HPP +#define MALIKANIA_DB_DATABASE_HPP + +/** + * \file database.hpp + * \brief Abstract database interface. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace mlk::db { + +/** + * \brief Statement argument. + * + * When using prepared statement, you may pass any data you like using this + * variant. + */ +using arg = std::variant< + bool, + double, + std::int8_t, + std::uint8_t, + std::int16_t, + std::uint16_t, + std::int32_t, + std::uint32_t, + std::int64_t, + std::uint64_t, + std::string +>; + +/** + * \brief Convenient result type. + * + * This unique pointer holds the PostgreSQL result and free it automatically on + * out of scope. + */ +using result = std::unique_ptr; + +/** + * Open the connection to the database. + * + * All arguments are optional. + * + * \pre connection must be closed + * \param host the hostname + * \param port the port number + * \param user the user name + * \param database the database name + * \param password the user password + * \throw std::runtime_error on errors + */ +void open(const std::string& host, + const std::string& port, + const std::string& user, + const std::string& database, + const std::string& password); + +/** + * Unconditionally init the database. + * + * \throw std::runtime_error on errors + */ +void init(); + +/** + * Execute a SELECT statement. + * + * \param sql the SQL statement + * \param args the optional arguments to bind + * \return the result value + * \throw std::runtime_error on errors + */ +auto select(const std::string& sql, const std::vector& args = {}) -> result; + +/** + * Execute an UPDATE statement. + * + * \param sql the SQL statement + * \param args the optional arguments to bind + * \throw std::runtime_error on errors + */ +void exec(const std::string& sql, const std::vector& args = {}); + +class transaction { +public: + /** + * \brief Rollback function. + */ + using rollback = std::function; + +private: + rollback rollback_; + bool commit_{false}; + +public: + /** + * Start a transaction. + * + * If there is already a transaction, the returned object is no-op. + * + * \param rollback the rollback function + * \return the transaction object + */ + transaction(rollback fn); + + /** + * Rollback the transaction if commit() has not been called. + */ + ~transaction(); + + /** + * Commit the transaction. + * + * Does nothing if already committed. + * + * \throw std::exception on errors + */ + void commit(); +}; + +} // !mlk::db + +#endif // !MALIKANIA_DB_DATABASE_HPP diff -r c5274f2d4658 -r ffe8ac5c35c0 libdb/malikania/db/model.hpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libdb/malikania/db/model.hpp Mon Oct 08 09:51:12 2018 +0200 @@ -0,0 +1,120 @@ +/* + * model.hpp -- abstract database object + * + * Copyright (c) 2013-2018 David Demelier + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef MALIKANIA_DB_MODEL_HPP +#define MALIKANIA_DB_MODEL_HPP + +/** + * \file model.hpp + * \brief Abstract database object. + */ + +#include + +namespace mlk::db { + +/** + * \brief Abstract database object. + */ +class model { +public: + /** + * Id type. + */ + using id_type = std::uint64_t; + +private: + model(const model&) = delete; + model& operator=(const model&) = delete; + +protected: + /** + * Object id. + */ + id_type id_{0U}; + +public: + /** + * Default constructor. + */ + model() = default; + + /** + * Move constructor. + * + * \param other the original value (id resets to 0) + */ + inline model(model&& other) noexcept + : id_(other.id_) + { + other.id_ = 0U; + } + + /** + * Virtual destructor defaulted. + */ + virtual ~model() noexcept = default; + + /** + * Get the id. + * + * \return the id + */ + auto get_id() const noexcept -> id_type + { + return id_; + } + + /** + * Tells if the object is not persistent. + * + * \return true if object was never saved + */ + auto is_draft() const noexcept -> bool + { + return id_ == 0U; + } + + /** + * Tells if the object is present in database + * + * \return true if object is saved + */ + auto is_published() const noexcept -> bool + { + return id_ > 0U; + } + + /** + * Move operator. + * + * \param other the original value (id resets to 0) + * \return *this + */ + auto operator=(model&& other) noexcept -> model& + { + id_ = other.id_; + other.id_ = 0U; + + return *this; + } +}; + +} // !mlk::db + +#endif // !MALIKANIA_DB_MODEL_HPP diff -r c5274f2d4658 -r ffe8ac5c35c0 libdb/malikania/db/spell.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libdb/malikania/db/spell.cpp Mon Oct 08 09:51:12 2018 +0200 @@ -0,0 +1,124 @@ +/* + * spell.cpp -- database spell object + * + * Copyright (c) 2013-2018 David Demelier + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include "character.hpp" +#include "spell.hpp" + +namespace mlk::db { + +auto spell::load(const result& result, int row) -> spell +{ + spell s(PQgetvalue(result.get(), row, 2)); + + s.id_ = std::stoi(PQgetvalue(result.get(), row, 0)); + s.level_ = std::stoi(PQgetvalue(result.get(), row, 3)); + + return s; +} + +auto spell::load(const character& parent) -> std::vector +{ + static const std::string sql( + "SELECT *" + " FROM spell" + " WHERE id = $1" + ); + + const auto r = select(sql, { parent.get_id() }); + + if (PQntuples(r.get()) == 0) + throw std::runtime_error("failed to load spells"); + + std::vector spells; + + for (int i = 0; i < PQntuples(r.get()); ++i) + spells.push_back(load(r, i)); + + return spells; +} + +void spell::clear() +{ + id_ = 0U; +} + +void spell::unpublish() +{ + static const std::string sql( + "DELETE" + " FROM spell" + " WHERE id = $1" + ); + + exec(sql, { id_ }); + clear(); +} + +void spell::publish(character& parent) +{ + static const std::string sql( + "INSERT INTO spell(" + " character_id," + " type," + " level" + ") " + "VALUES ($1, $2, $3) " + "RETURNING id" + ); + + const auto r = select(sql, { parent.get_id(), type_, level_ }); + + if (PQntuples(r.get()) == 0) + throw std::runtime_error("failed to save spell"); + + id_ = std::stoi(PQgetvalue(r.get(), 0, 0)); +} + +spell::spell(std::string type) noexcept + : type_(std::move(type)) +{ + assert(!type_.empty()); +} + +auto spell::get_type() const noexcept -> const std::string& +{ + return type_; +} + +auto spell::get_level() const noexcept -> std::uint8_t +{ + return level_; +} + +void spell::set_level(std::uint8_t level) +{ + static const std::string sql( + "UPDATE spell" + " SET level = $1" + " WHERE id = $2" + ); + + if (is_published() && level_ != level) + db::exec(sql, { level_, id_ }); + + level_ = level; +} + +} // !mlk::db diff -r c5274f2d4658 -r ffe8ac5c35c0 libdb/malikania/db/spell.hpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libdb/malikania/db/spell.hpp Mon Oct 08 09:51:12 2018 +0200 @@ -0,0 +1,88 @@ +/* + * spell.hpp -- database spell object + * + * Copyright (c) 2013-2018 David Demelier + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef MALIKANIA_DB_SPELL_HPP +#define MALIKANIA_DB_SPELL_HPP + +/** + * \file spell.hpp + * \brief Database spell object. + */ + +#include +#include +#include + +#include "database.hpp" +#include "model.hpp" + +namespace mlk::db { + +class character; + +/** + * \brief Describe a spell. + */ +class spell : public model { +private: + friend class character; + + std::string type_; + std::uint8_t level_{1U}; + + static auto load(const result&, int) -> spell; + static auto load(const character&) -> std::vector; + + void clear(); + void unpublish(); + void publish(character&); + +public: + /** + * Constructor. + * + * \pre !type.empty() + * \param type the type name + */ + spell(std::string type) noexcept; + + /** + * Get the type. + * + * \return the type + */ + auto get_type() const noexcept -> const std::string&; + + /** + * Get the level. + * + * \return the level + */ + auto get_level() const noexcept -> std::uint8_t; + + /** + * Set the spell level. + * + * \param level the level + */ + void set_level(std::uint8_t level); +}; + +} // !mlk::db + +#endif // !MALIKANIA_DB_SPELL_HPP diff -r c5274f2d4658 -r ffe8ac5c35c0 libserver/CMakeLists.txt --- a/libserver/CMakeLists.txt Wed Aug 29 17:04:57 2018 +0200 +++ b/libserver/CMakeLists.txt Mon Oct 08 09:51:12 2018 +0200 @@ -18,17 +18,11 @@ project(libmlk-server) -find_package(PostgreSQL REQUIRED) find_package(Threads REQUIRED) set( HEADERS ${libmlk-server_SOURCE_DIR}/malikania/server/client.hpp - ${libmlk-server_SOURCE_DIR}/malikania/server/db/account.hpp - ${libmlk-server_SOURCE_DIR}/malikania/server/db/character.hpp - ${libmlk-server_SOURCE_DIR}/malikania/server/db/database.hpp - ${libmlk-server_SOURCE_DIR}/malikania/server/db/model.hpp - ${libmlk-server_SOURCE_DIR}/malikania/server/db/spell.hpp ${libmlk-server_SOURCE_DIR}/malikania/server/net/auth_handler.hpp ${libmlk-server_SOURCE_DIR}/malikania/server/server.hpp ) @@ -36,10 +30,6 @@ set( SOURCES ${libmlk-server_SOURCE_DIR}/malikania/server/client.cpp - ${libmlk-server_SOURCE_DIR}/malikania/server/db/account.cpp - ${libmlk-server_SOURCE_DIR}/malikania/server/db/character.cpp - ${libmlk-server_SOURCE_DIR}/malikania/server/db/database.cpp - ${libmlk-server_SOURCE_DIR}/malikania/server/db/spell.cpp ${libmlk-server_SOURCE_DIR}/malikania/server/net/auth_handler.cpp ${libmlk-server_SOURCE_DIR}/malikania/server/server.cpp ) @@ -49,15 +39,14 @@ SOURCES ${HEADERS} ${SOURCES} LIBRARIES ${Boost_LIBRARIES} - ${PostgreSQL_LIBRARIES} Threads::Threads OpenSSL::Crypto OpenSSL::SSL libmlk-common + libmlk-db $<$:mswsock> PUBLIC_INCLUDES ${Boost_INCLUDE_DIRS} - ${PostgreSQL_INCLUDE_DIRS} ${libmlk-server_SOURCE_DIR} PRIVATE_INCLUDES ${libmlk-server_SOURCE_DIR}/malikania/server diff -r c5274f2d4658 -r ffe8ac5c35c0 libserver/malikania/server/db/account.cpp --- a/libserver/malikania/server/db/account.cpp Wed Aug 29 17:04:57 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,242 +0,0 @@ -/* - * account.cpp -- database account object - * - * Copyright (c) 2013-2018 David Demelier - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -#include - -#include "account.hpp" - -namespace mlk::server::db { - -auto account::load(const result& res) -> account -{ - account a(PQgetvalue(res.get(), 0, 1), PQgetvalue(res.get(), 0, 2)); - - a.id_ = std::stoi(PQgetvalue(res.get(), 0, 0)); - a.firstname_ = PQgetvalue(res.get(), 0, 3); - a.lastname_ = PQgetvalue(res.get(), 0, 4); - a.email_ = PQgetvalue(res.get(), 0, 5); - a.characters_ = character::load(a); - - return a; -} - -void account::clear() -{ - id_ = 0U; -} - -account::account(std::string login, std::string password) noexcept - : login_(std::move(login)) - , password_(std::move(password)) -{ - assert(!login_.empty()); - assert(!password_.empty()); -} - -auto account::get_login() const noexcept -> const std::string& -{ - return login_; -} - -void account::set_password(std::string password) -{ - static const std::string sql( - "UPDATE account" - " SET password = $1" - " WHERE id = $2" - ); - - assert(!password.empty()); - - if (is_published() && password_ != password) - db::exec(sql, { password, id_ }); - - password_ = std::move(password); -} - -auto account::get_email() const noexcept -> const std::string& -{ - return email_; -} - -void account::set_email(std::string email) -{ - static const std::string sql( - "UPDATE account" - " SET email = $1" - " WHERE id = $2" - ); - - assert(!email.empty()); - - if (is_published() && email_ != email) - db::exec(sql, { email, id_ }); - - email_ = std::move(email); -} - -auto account::get_firstname() const noexcept -> const std::string& -{ - return firstname_; -} - -void account::set_firstname(std::string name) -{ - static const std::string sql( - "UPDATE account" - " SET firstname = $1" - " WHERE id = $2" - ); - - if (is_published() && firstname_ != name) - db::exec(sql, { name, id_ }); - - firstname_ = std::move(name); -} - -auto account::get_lastname() const noexcept -> const std::string& -{ - return lastname_; -} - -void account::set_lastname(std::string name) -{ - static const std::string sql( - "UPDATE account" - " SET lastname = $1" - " WHERE id = $2" - ); - - if (is_published() && lastname_ != name) - db::exec(sql, { name, id_ }); - - lastname_ = std::move(name); -} - -auto account::get_characters() const noexcept -> const std::vector& -{ - return characters_; -} - -void account::add(character ch) -{ - assert(ch.is_draft()); - - characters_.reserve(characters_.size() + 1); - - if (is_published()) - ch.publish(*this); - - characters_.push_back(std::move(ch)); -} - -void account::remove(std::vector::iterator it) -{ - // TODO: assert 'it' is in vector. - it->unpublish(); - characters_.erase(it); -} - -void account::publish() -{ - static const std::string sql( - "INSERT INTO account(" - " login," - " password," - " firstname," - " lastname," - " email" - ") " - "VALUES ($1, $2, $3, $4, $5) " - "RETURNING id" - ); - - assert(is_draft()); - - /* - * Recursively save the account, its characters and spells of those - * characters. - */ - transaction txn([this] { - clear(); - - for (auto& c : characters_) - c.clear(); - }); - - const auto r = select(sql, { login_, password_, firstname_, lastname_, email_ }); - - id_ = std::stoi(PQgetvalue(r.get(), 0, 0)); - - for (auto& c : characters_) - c.publish(*this); - - txn.commit(); - - assert(is_published()); -} - -void account::unpublish() -{ - static const std::string sql( - "DELETE" - " FROM account" - " WHERE id = $1" - ); - - assert(is_published()); - - id_ = 0; - - /* - * Recursively make all characters and their spells draft. - */ - for (auto& c : characters_) - c.clear(); - - assert(is_draft()); -} - -auto account::find_by_login(const std::string& login) -> std::optional -{ - static const std::string sql( - "SELECT *" - " FROM account" - " WHERE login = $1" - ); - - const auto r = select(sql, { login }); - - if (PQntuples(r.get()) == 0) - return std::nullopt; - - return load(r); -} - -auto account::authenticate(const std::string& login, - const std::string& password) -> std::optional -{ - auto ac = find_by_login(login); - - if (!ac || ac->password_ != password) - return std::nullopt; - - return ac; -} - -} // !mlk::server::db diff -r c5274f2d4658 -r ffe8ac5c35c0 libserver/malikania/server/db/account.hpp --- a/libserver/malikania/server/db/account.hpp Wed Aug 29 17:04:57 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,189 +0,0 @@ -/* - * account.hpp -- database account object - * - * Copyright (c) 2013-2018 David Demelier - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -#ifndef MALIKANIA_SERVER_DB_ACCOUNT_HPP -#define MALIKANIA_SERVER_DB_ACCOUNT_HPP - -/** - * \file account.hpp - * \brief Database account object. - */ - -#include - -#include "character.hpp" -#include "database.hpp" -#include "model.hpp" - -namespace mlk::server::db { - -/** - * \brief Database account object. - */ -class account : public model { -private: - std::string login_; - std::string password_; - std::string email_; - std::string firstname_; - std::string lastname_; - std::vector characters_; - - static auto load(const result&) -> account; - - void clear(); - -public: - /** - * Create a draft account. - * - * \pre !login.empty() - * \pre !password.empty() - * \param login the login name - * \param password the password - * \warning the password is saved as-is and **must** be hashed by the caller. - */ - account(std::string login, std::string password) noexcept; - - /** - * Get the account login. - * - * \return the login - */ - auto get_login() const noexcept -> const std::string&; - - /** - * Set the password. - * - * \pre !password.empty() - * \warning the password is saved as-is and **must** be hashed by the caller. - */ - void set_password(std::string password); - - /** - * Get the account email. - * - * \return the email - */ - auto get_email() const noexcept -> const std::string&; - - /** - * Set the account email. - * - * \pre !email.empty() - * \param email the new email - */ - void set_email(std::string email); - - /** - * Get the account first name. - * - * \return the name - */ - auto get_firstname() const noexcept -> const std::string&; - - /** - * Set the account firstname. - * - * \param name the new name - */ - void set_firstname(std::string name); - - /** - * Get the account last name. - * - * \return the name - */ - auto get_lastname() const noexcept -> const std::string&; - - /** - * Set the account last name. - * - * \param name the new name - */ - void set_lastname(std::string name); - - /** - * Get the caracter list. - * - * \return the associated characters. - */ - auto get_characters() const noexcept -> const std::vector&; - - /** - * Add the character to the account. - * - * Account takes ownership of character. - * - * \pre ch.is_draft() - * \param ch the character - * \post ch.is_published() - */ - void add(character ch); - - /** - * Remove the character from the account. - * - * \pre it is in the the account character list - * \param ch the character - * \post ch == nullptr - */ - void remove(std::vector::iterator it); - - /** - * Save the account, does nothing if is_published(). - * - * \throw std::exception on errors - * \post is_published() - */ - void publish(); - - /** - * Destroy the account. - * - * The account will contains no characters anymore. - * - * \throw std::exception on errors - * \post is_draft() - */ - void unpublish(); - - /** - * Find an account by login. - * - * \param login the login - * \return the account or none if not found - * \throw std::runtime_error on database errors - */ - static auto find_by_login(const std::string& login) -> std::optional; - - /** - * Find and authenticate a user. - * - * \param login the login - * \param password the password - * \return the account or none if not found - * \throw std::runtime_error on database errors - */ - static auto authenticate(const std::string& login, - const std::string& password) -> std::optional; -}; - -} // !mlk::server::db - -#endif // !MALIKANIA_SERVER_DB_ACCOUNT_HPP diff -r c5274f2d4658 -r ffe8ac5c35c0 libserver/malikania/server/db/character.cpp --- a/libserver/malikania/server/db/character.cpp Wed Aug 29 17:04:57 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,266 +0,0 @@ -/* - * character.cpp -- database character object - * - * Copyright (c) 2013-2018 David Demelier - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -#include -#include - -#include "account.hpp" -#include "character.hpp" -#include "database.hpp" - -namespace mlk::server::db { - -auto character::load(const result& result, int row) -> character -{ - character ch(PQgetvalue(result.get(), row, 2), PQgetvalue(result.get(), row, 3)); - - ch.levels_[stat::hp] = std::stoi(PQgetvalue(result.get(), row, 4)); - ch.factors_[stat::hp] = std::stoi(PQgetvalue(result.get(), row, 5)); - ch.exp_[stat::hp] = std::stoi(PQgetvalue(result.get(), row, 6)); - - ch.levels_[stat::force] = std::stoi(PQgetvalue(result.get(), row, 7)); - ch.factors_[stat::force] = std::stoi(PQgetvalue(result.get(), row, 8)); - ch.exp_[stat::force] = std::stoi(PQgetvalue(result.get(), row, 9)); - - ch.levels_[stat::defense] = std::stoi(PQgetvalue(result.get(), row, 10)); - ch.factors_[stat::defense] = std::stoi(PQgetvalue(result.get(), row, 11)); - ch.exp_[stat::defense] = std::stoi(PQgetvalue(result.get(), row, 12)); - - ch.levels_[stat::agility] = std::stoi(PQgetvalue(result.get(), row, 13)); - ch.factors_[stat::agility] = std::stoi(PQgetvalue(result.get(), row, 14)); - ch.exp_[stat::agility] = std::stoi(PQgetvalue(result.get(), row, 15)); - - ch.levels_[stat::luck] = std::stoi(PQgetvalue(result.get(), row, 16)); - ch.factors_[stat::luck] = std::stoi(PQgetvalue(result.get(), row, 17)); - ch.exp_[stat::luck] = std::stoi(PQgetvalue(result.get(), row, 18)); - - ch.id_ = std::stoi(PQgetvalue(result.get(), row, 0)); - ch.spells_ = spell::load(ch); - - return ch; -} - -auto character::load(const account& parent) -> std::vector -{ - static const std::string sql( - "SELECT *" - " FROM character" - " WHERE account_id = $1" - ); - - const auto r = select(sql, { parent.get_id() }); - - if (PQntuples(r.get()) == 0) - throw std::runtime_error("failed to load characters"); - - std::vector characters; - - for (int i = 0; i < PQntuples(r.get()); ++i) - characters.push_back(load(r, i)); - - return characters; -} - -void character::clear() -{ - id_ = 0U; - - for (auto& s : spells_) - s.clear(); -} - -void character::publish(account& parent) -{ - static const std::string sql( - "INSERT INTO character (" - " account_id," - " nickname," - " type," - " hp_level," - " hp_factor," - " hp_exp," - " force_level," - " force_factor," - " force_exp," - " defense_level," - " defense_factor," - " defense_exp," - " agility_level," - " agility_factor," - " agility_exp," - " luck_level," - " luck_factor," - " luck_exp" - ") " - "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) " - "RETURNING id" - ); - - const auto r = select(sql, { - parent.get_id(), - nickname_, - type_, - levels_[stat::hp], - factors_[stat::hp], - exp_[stat::hp], - levels_[stat::force], - factors_[stat::force], - exp_[stat::force], - levels_[stat::defense], - factors_[stat::defense], - exp_[stat::defense], - levels_[stat::agility], - factors_[stat::agility], - exp_[stat::agility], - levels_[stat::luck], - factors_[stat::luck], - exp_[stat::luck] - }); - - if (PQntuples(r.get()) == 0) - throw std::runtime_error("failed to save character"); - - id_ = std::stoi(PQgetvalue(r.get(), 0, 0)); - - for (auto& s : spells_) - s.publish(*this); -} - -void character::unpublish() -{ - static const std::string sql( - "DELETE" - " FROM character" - " WHERE id = $1" - ); - - exec(sql, { id_ }); - clear(); -} - -character::character(std::string nickname, std::string type) noexcept - : nickname_(std::move(nickname)) - , type_(std::move(type)) -{ - assert(!nickname_.empty()); - assert(!type_.empty()); -} - -auto character::get_nickname() const noexcept -> const std::string& -{ - return nickname_; -} - -auto character::get_type() const noexcept -> const std::string& -{ - return type_; -} - -auto character::get_spells() const noexcept -> const std::vector& -{ - return spells_; -} - -auto character::get_levels() const noexcept -> const std::array& -{ - return levels_; -} - -void character::set_levels(std::array levels) -{ - static const std::string sql( - "UPDATE character" - " SET hp_level = $1" - " , force_level = $2" - " , defense_level = $3" - " , agility_level = $4" - " , luck_level = $5" - ); - - if (is_published()) - exec(sql, { id_, levels[0], levels[1], levels[2], levels[3], levels[4] }); - - levels_ = std::move(levels); -} - -auto character::get_factors() const noexcept -> const std::array& -{ - return factors_; -} - -void character::set_factors(std::array factors) -{ - static const std::string sql( - "UPDATE character" - " SET hp_factor = $1" - " , force_factor = $2" - " , defense_factor = $3" - " , agility_factor = $4" - " , luck_factor = $5" - ); - - assert(std::accumulate(factors.begin(), factors.end(), 0U) == 100U); - - if (is_published()) - exec(sql, { id_, factors[0], factors[1], factors[2], factors[3], factors[4] }); - - factors_ = std::move(factors); -} - -auto character::get_experience() const noexcept -> const std::array& -{ - return exp_; -} - -void character::set_experience(std::array experience) -{ - static const std::string sql( - "UPDATE character" - " SET hp_experience = $1" - " , force_experience = $2" - " , defense_experience = $3" - " , agility_experience = $4" - " , luck_experience = $5" - ); - - if (is_published()) - exec(sql, { id_, experience[0], experience[1], experience[2], experience[3], experience[4] }); - - exp_ = std::move(experience); -} - -void character::add(spell sp) -{ - assert(sp.is_draft()); - - spells_.reserve(spells_.size() + 1); - - if (is_published()) - sp.publish(*this); - - spells_.push_back(std::move(sp)); -} - -void character::remove(std::vector::iterator it) -{ - // TODO: assert 'it' is in vector. - it->unpublish(); - spells_.erase(it); -} - -} // !mlk::server::db diff -r c5274f2d4658 -r ffe8ac5c35c0 libserver/malikania/server/db/character.hpp --- a/libserver/malikania/server/db/character.hpp Wed Aug 29 17:04:57 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,171 +0,0 @@ -/* - * character.hpp -- database character object - * - * Copyright (c) 2013-2018 David Demelier - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -#ifndef MALIKANIA_SERVER_DB_CHARACTER_HPP -#define MALIKANIA_SERVER_DB_CHARACTER_HPP - -/* - * \file character.hpp - * \brief Database character object. - */ - -#include -#include -#include - -#include "database.hpp" -#include "model.hpp" -#include "spell.hpp" - -namespace mlk::server::db { - -class account; - -/* - * \brief Database character object. - */ -class character : public model { -public: - friend class account; - - /** - * \brief Stats index in arrays - * \see set_levels - * \see set_factors - * \see set_experience - */ - enum stat { - hp = 0, //!< hp index - force, //!< force index - defense, //!< defense index - agility, //!< agility index - luck //!< luck index - }; - -private: - std::string nickname_; - std::string type_; - std::vector spells_; - std::array levels_{1U, 1U, 1U, 1U, 1U}; - std::array factors_{20U, 20U, 20U, 20U, 20U}; - std::array exp_{0U, 0U, 0U, 0U, 0U}; - - static auto load(const result&, int) -> character; - static auto load(const account&) -> std::vector; - - void clear(); - void publish(account&); - void unpublish(); - -public: - /** - * Construct a character, no database is modified yet. - * - * \pre !nickname.empty() - * \pre !type.empty() - * \param nickname the nickname - * \param type the type - */ - character(std::string nickname, std::string type) noexcept; - - /** - * Get the character nickname. - * - * \return the name - */ - auto get_nickname() const noexcept -> const std::string&; - - /** - * Get the character type name. - * - * \return the type name - */ - auto get_type() const noexcept -> const std::string&; - - /** - * Get the list of spells. - * - * \return the spells - */ - auto get_spells() const noexcept -> const std::vector&; - - /** - * Get the list of levels. - * - * \return the levels - */ - auto get_levels() const noexcept -> const std::array&; - - /** - * Set the new levels. - * - * \param levels the levels - */ - void set_levels(std::array levels); - - /** - * Get the list of factors. - * - * \return the factors - */ - auto get_factors() const noexcept -> const std::array&; - - /** - * Set progression factors. - * - * \pre factors sum must be 100 - * \param factors the factors - */ - void set_factors(std::array factors); - - /** - * Get the stats experience. - * - * \return the experience - */ - auto get_experience() const noexcept -> const std::array&; - - /** - * Set stats experience. - * - * \param experience the experience - */ - void set_experience(std::array experience); - - /** - * Add a spell. - * - * Character takes ownership of spell. - * - * \pre sp.is_draft() - * \param sp the spell (moved) - */ - void add(spell sp); - - /** - * Remove the spell from the character. - * - * \pre it must be valid - * \param it the spell iterator - */ - void remove(std::vector::iterator it); -}; - -} // !mlk::server::db - -#endif // !MALIKANIA_SERVER_DB_CHARACTER_HPP diff -r c5274f2d4658 -r ffe8ac5c35c0 libserver/malikania/server/db/database.cpp --- a/libserver/malikania/server/db/database.cpp Wed Aug 29 17:04:57 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,210 +0,0 @@ -/* - * database.cpp -- connection to postgresql database - * - * Copyright (c) 2013-2018 David Demelier - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -#include -#include - -#include "database.hpp" - -namespace mlk::server::db { - -namespace { - -struct strify { - template >> - auto operator()(T i) const -> std::string - { - return std::to_string(i); - } - - auto operator()(bool v) const -> std::string - { - return v ? "t" : "f"; - } - - auto operator()(std::string s) const -> std::string - { - return s; - } -}; - -std::unique_ptr connection{nullptr, nullptr}; - -auto run(const std::string& sql, std::vector args = {}) -> result -{ - std::vector list; - - /* - * PQexecParams requires an array of C strings, convert all arguments in the - * args vector to string and then keep the C string value into a temporary - * array. - */ - for (auto& arg : args) { - arg = std::visit(strify(), arg); - list.push_back(std::get(arg).c_str()); - } - - return { - PQexecParams(connection.get(), sql.c_str(), list.size(), nullptr, - list.data(), nullptr, nullptr, 0 - ), - PQclear - }; -} - -} // !namespace - -void open(const std::string& host, - const std::string& port, - const std::string& user, - const std::string& database, - const std::string& password) -{ - assert(!connection); - - std::ostringstream oss; - - if (!host.empty()) - oss << "host=" << host << " "; - if (!port.empty()) - oss << "port=" << port << " "; - if (!user.empty()) - oss << "user=" << user << " "; - if (!database.empty()) - oss << "dbname=" << database << " "; - if (!password.empty()) - oss << "password=" << password; - - connection = { PQconnectdb(oss.str().c_str()), PQfinish }; - - if (PQstatus(connection.get()) != CONNECTION_OK) - throw std::runtime_error(PQerrorMessage(connection.get())); -} - -const std::string init_account( - "CREATE TABLE IF NOT EXISTS account(" - " id SERIAL," - " login TEXT NOT NULL," - " password TEXT NOT NULL," - " firstname TEXT," - " lastname TEXT," - " email TEXT," - " PRIMARY KEY(id)" - ")" -); - -const std::string init_character( - "CREATE TABLE IF NOT EXISTS character(" - " id SERIAL," - " account_id INTEGER NOT NULL," - " nickname TEXT NOT NULL," - " type TEXT NOT NULL," - " hp_level SMALLINT NOT NULL DEFAULT 1," - " hp_factor SMALLINT NOT NULL," - " hp_exp INT NOT NULL," - " force_level SMALLINT NOT NULL DEFAULT 1," - " force_factor SMALLINT NOT NULL," - " force_exp INT NOT NULL," - " defense_level SMALLINT NOT NULL DEFAULT 1," - " defense_factor SMALLINT NOT NULL," - " defense_exp INT NOT NULL," - " agility_level SMALLINT NOT NULL DEFAULT 1," - " agility_factor SMALLINT NOT NULL," - " agility_exp INT NOT NULL," - " luck_level SMALLINT NOT NULL DEFAULT 1," - " luck_factor SMALLINT NOT NULL," - " luck_exp INT NOT NULL," - " PRIMARY KEY(id)," - " FOREIGN KEY (account_id) REFERENCES account(id) ON DELETE CASCADE" - ")" -); - -const std::string init_spell( - "CREATE TABLE IF NOT EXISTS spell(" - " id SERIAL," - " character_id INTEGER NOT NULL," - " type TEXT NOT NULL," - " level SMALLINT NOT NULL," - " PRIMARY KEY(id)," - " FOREIGN KEY (character_id) REFERENCES character(id) ON DELETE CASCADE" - ")" -); - -void init() -{ - assert(connection); - - exec(init_account); - exec(init_character); - exec(init_spell); -} - -auto select(const std::string& sql, const std::vector& args) -> result -{ - assert(connection); - - auto result = run(sql, args); - - switch (PQresultStatus(result.get())) { - case PGRES_COMMAND_OK: - case PGRES_TUPLES_OK: - break; - default: - throw std::runtime_error(PQerrorMessage(connection.get())); - } - - return result; -} - -void exec(const std::string& sql, const std::vector& args) -{ - assert(connection); - - switch (const auto result = run(sql, args); PQresultStatus(result.get())) { - case PGRES_COMMAND_OK: - break; - default: - throw std::runtime_error(PQerrorMessage(connection.get())); - } -} - -transaction::transaction(rollback fn) - : rollback_(std::move(fn)) -{ - assert(rollback_); - - exec("BEGIN"); -} - -transaction::~transaction() -{ - if (!commit_) { - rollback_(); - exec("ROLLBACK"); - } -} - -void transaction::commit() -{ - if (!commit_) { - exec("COMMIT"); - commit_ = true; - } -} - -} // !db::server::mlk diff -r c5274f2d4658 -r ffe8ac5c35c0 libserver/malikania/server/db/database.hpp --- a/libserver/malikania/server/db/database.hpp Wed Aug 29 17:04:57 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,151 +0,0 @@ -/* - * database.hpp -- connection to postgresql database - * - * Copyright (c) 2013-2018 David Demelier - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -#ifndef MALIKANIA_SERVER_DB_DATABASE_HPP -#define MALIKANIA_SERVER_DB_DATABASE_HPP - -/** - * \file database.hpp - * \brief Abstract database interface. - */ - -#include -#include -#include -#include -#include -#include -#include - -#include - -namespace mlk::server::db { - -/** - * \brief Statement argument. - * - * When using prepared statement, you may pass any data you like using this - * variant. - */ -using arg = std::variant< - bool, - double, - std::int8_t, - std::uint8_t, - std::int16_t, - std::uint16_t, - std::int32_t, - std::uint32_t, - std::int64_t, - std::uint64_t, - std::string ->; - -/** - * \brief Convenient result type. - * - * This unique pointer holds the PostgreSQL result and free it automatically on - * out of scope. - */ -using result = std::unique_ptr; - -/** - * Open the connection to the database. - * - * All arguments are optional. - * - * \pre connection must be closed - * \param host the hostname - * \param port the port number - * \param user the user name - * \param database the database name - * \param password the user password - * \throw std::runtime_error on errors - */ -void open(const std::string& host, - const std::string& port, - const std::string& user, - const std::string& database, - const std::string& password); - -/** - * Unconditionally init the database. - * - * \throw std::runtime_error on errors - */ -void init(); - -/** - * Execute a SELECT statement. - * - * \param sql the SQL statement - * \param args the optional arguments to bind - * \return the result value - * \throw std::runtime_error on errors - */ -auto select(const std::string& sql, const std::vector& args = {}) -> result; - -/** - * Execute an UPDATE statement. - * - * \param sql the SQL statement - * \param args the optional arguments to bind - * \throw std::runtime_error on errors - */ -void exec(const std::string& sql, const std::vector& args = {}); - -class transaction { -public: - /** - * \brief Rollback function. - */ - using rollback = std::function; - -private: - rollback rollback_; - bool commit_{false}; - -public: - /** - * Start a transaction. - * - * If there is already a transaction, the returned object is no-op. - * - * \param rollback the rollback function - * \return the transaction object - */ - transaction(rollback fn); - - /** - * Rollback the transaction if commit() has not been called. - */ - ~transaction(); - - /** - * Commit the transaction. - * - * Does nothing if already committed. - * - * \throw std::exception on errors - */ - void commit(); -}; - -} // !mlk::server::db - -#endif // !MALIKANIA_SERVER_DB_DATABASE_HPP diff -r c5274f2d4658 -r ffe8ac5c35c0 libserver/malikania/server/db/errors.hpp diff -r c5274f2d4658 -r ffe8ac5c35c0 libserver/malikania/server/db/model.hpp --- a/libserver/malikania/server/db/model.hpp Wed Aug 29 17:04:57 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,136 +0,0 @@ -/* - * model.hpp -- abstract database object - * - * Copyright (c) 2013-2018 David Demelier - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -#ifndef MALIKANIA_SERVER_DB_MODEL_HPP -#define MALIKANIA_SERVER_DB_MODEL_HPP - -/** - * \file model.hpp - * \brief Abstract database object. - */ - -#include - -namespace mlk::server::db { - -/** - * \brief Abstract database object. - */ -class model { -public: - /** - * Id type. - */ - using id_type = std::uint64_t; - -private: - model(const model&) = delete; - model& operator=(const model&) = delete; - -protected: - id_type id_{0U}; //!< object id - -public: - /** - * Default constructor. - */ - model() = default; - - /** - * Move constructor. - * - * \param other the original value (id resets to 0) - */ - inline model(model&& other) noexcept - : id_(other.id_) - { - other.id_ = 0U; - } - - /** - * Virtual destructor defaulted. - */ - virtual ~model() noexcept = default; - - /** - * Get the id. - * - * \return the id - */ - auto get_id() const noexcept -> id_type - { - return id_; - } - -#if 0 -#if defined(MALIKANIA_PRIVATE) - /** - * Set the internal id. - * - * This function is only available if MALIKANIA_PRIVATE is defined as it is - * not a end user function but is required to implement new database - * backends. - * - * \warning only use this function in database backends - * \param id the new id - */ - inline void set_id(id_type id) noexcept - { - id_ = id; - } -#endif -#endif - - /** - * Tells if the object is not persistent. - * - * \return true if object was never saved - */ - auto is_draft() const noexcept -> bool - { - return id_ == 0U; - } - - /** - * Tells if the object is present in database - * - * \return true if object is saved - */ - auto is_published() const noexcept -> bool - { - return id_ > 0U; - } - - /** - * Move operator. - * - * \param other the original value (id resets to 0) - * \return *this - */ - auto operator=(model&& other) noexcept -> model& - { - id_ = other.id_; - other.id_ = 0U; - - return *this; - } -}; - -} // !mlk::server::db - -#endif // !MODEL_HPP diff -r c5274f2d4658 -r ffe8ac5c35c0 libserver/malikania/server/db/spell.cpp --- a/libserver/malikania/server/db/spell.cpp Wed Aug 29 17:04:57 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,124 +0,0 @@ -/* - * spell.cpp -- database spell object - * - * Copyright (c) 2013-2018 David Demelier - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -#include - -#include "character.hpp" -#include "spell.hpp" - -namespace mlk::server::db { - -auto spell::load(const result& result, int row) -> spell -{ - spell s(PQgetvalue(result.get(), row, 2)); - - s.id_ = std::stoi(PQgetvalue(result.get(), row, 0)); - s.level_ = std::stoi(PQgetvalue(result.get(), row, 3)); - - return s; -} - -auto spell::load(const character& parent) -> std::vector -{ - static const std::string sql( - "SELECT *" - " FROM spell" - " WHERE id = $1" - ); - - const auto r = select(sql, { parent.get_id() }); - - if (PQntuples(r.get()) == 0) - throw std::runtime_error("failed to load spells"); - - std::vector spells; - - for (int i = 0; i < PQntuples(r.get()); ++i) - spells.push_back(load(r, i)); - - return spells; -} - -void spell::clear() -{ - id_ = 0U; -} - -void spell::unpublish() -{ - static const std::string sql( - "DELETE" - " FROM spell" - " WHERE id = $1" - ); - - exec(sql, { id_ }); - clear(); -} - -void spell::publish(character& parent) -{ - static const std::string sql( - "INSERT INTO spell(" - " character_id," - " type," - " level" - ") " - "VALUES ($1, $2, $3) " - "RETURNING id" - ); - - const auto r = select(sql, { parent.get_id(), type_, level_ }); - - if (PQntuples(r.get()) == 0) - throw std::runtime_error("failed to save spell"); - - id_ = std::stoi(PQgetvalue(r.get(), 0, 0)); -} - -spell::spell(std::string type) noexcept - : type_(std::move(type)) -{ - assert(!type_.empty()); -} - -auto spell::get_type() const noexcept -> const std::string& -{ - return type_; -} - -auto spell::get_level() const noexcept -> std::uint8_t -{ - return level_; -} - -void spell::set_level(std::uint8_t level) -{ - static const std::string sql( - "UPDATE spell" - " SET level = $1" - " WHERE id = $2" - ); - - if (is_published() && level_ != level) - db::exec(sql, { level_, id_ }); - - level_ = level; -} - -} // !mlk::server::db diff -r c5274f2d4658 -r ffe8ac5c35c0 libserver/malikania/server/db/spell.hpp --- a/libserver/malikania/server/db/spell.hpp Wed Aug 29 17:04:57 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,88 +0,0 @@ -/* - * spell.hpp -- database spell object - * - * Copyright (c) 2013-2018 David Demelier - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -#ifndef MALIKANIA_SERVER_DB_SPELL_HPP -#define MALIKANIA_SERVER_DB_SPELL_HPP - -/** - * \file spell.hpp - * \brief Database spell object. - */ - -#include -#include -#include - -#include "database.hpp" -#include "model.hpp" - -namespace mlk::server::db { - -class character; - -/** - * \brief Describe a spell. - */ -class spell : public model { -private: - friend class character; - - std::string type_; - std::uint8_t level_{1U}; - - static auto load(const result&, int) -> spell; - static auto load(const character&) -> std::vector; - - void clear(); - void unpublish(); - void publish(character&); - -public: - /** - * Constructor. - * - * \pre !type.empty() - * \param type the type name - */ - spell(std::string type) noexcept; - - /** - * Get the type. - * - * \return the type - */ - auto get_type() const noexcept -> const std::string&; - - /** - * Get the level. - * - * \return the level - */ - auto get_level() const noexcept -> std::uint8_t; - - /** - * Set the spell level. - * - * \param level the level - */ - void set_level(std::uint8_t level); -}; - -} // !mlk::server::db - -#endif // !MALIKANIA_SERVER_DB_SPELL_HPP diff -r c5274f2d4658 -r ffe8ac5c35c0 server/main.cpp --- a/server/main.cpp Wed Aug 29 17:04:57 2018 +0200 +++ b/server/main.cpp Mon Oct 08 09:51:12 2018 +0200 @@ -18,8 +18,8 @@ #include -#include -#include +#include +#include #include @@ -27,22 +27,22 @@ int main() { - boost::asio::io_context ctx; + boost::asio::io_context ctx; - try { - server::settings settings; + try { + server::settings settings; - settings.key = "/home/markand/server.key"; - settings.certificate = "/home/markand/server.crt"; - settings.port = 3320; - - server::server sv(ctx, settings); + settings.key = "/home/markand/server.key"; + settings.certificate = "/home/markand/server.crt"; + settings.port = 3320; + + server::server sv(ctx, settings); - server::db::open("", "", "markand", "malikaniadb", ""); - server::db::init(); - - ctx.run(); - } catch (const std::exception& ex) { - std::cerr << "abort: " << ex.what() << std::endl; - } + db::open("", "", "markand", "malikaniadb", ""); + db::init(); + + ctx.run(); + } catch (const std::exception& ex) { + std::cerr << "abort: " << ex.what() << std::endl; + } }