diff --git a/include/ikarus/errors.h b/include/ikarus/errors.h new file mode 100644 index 0000000..f501c22 --- /dev/null +++ b/include/ikarus/errors.h @@ -0,0 +1,71 @@ +#pragma once + +/// \file global.h +/// \author Folling + +#include + +/// \addtogroup errors Errors +/// \brief Error handling within libikarus +/// \details Errors are stored for each project, akin to the errno handling in C. +/// We store multiple pieces of information about the error occurring. For more information see +/// #ikarus_project_get_error_message. +/// @{ + +IKARUS_BEGIN_HEADER + +/// \brief Delineates what caused an error. +/// \details First 2 bytes delineate the major type, next 2 bytes delineate the minor type, next 4 bytes delineate the detail +/// type. +/// \remark Note that this doesn't show responsibility. An error with source "SubSystem" could still be the fault of +/// libikarus. +enum IkarusErrorInfo { + /// \brief No error occurred. + IkarusErrorInfo_Source_None = 0x0001000000000000, + /// \brief The error was caused by the client. + IkarusErrorInfo_Source_Client = 0x0001000000000001, + /// \brief The error was caused by a sub-system of libikarus. + IkarusErrorInfo_Source_SubSystem = 0x0001000000000002, + /// \brief The error was caused by libikarus itself. + IkarusErrorInfo_Source_LibIkarus = 0x0001000000000003, + /// \brief The error was caused by an unknown source. + IkarusErrorInfo_Source_Unknown = 0x00010000FFFFFFFF, + /// \brief No error occurred. + IkarusErrorInfo_Type_None = 0x0002000000000000, + /// \brief The user misused the API. + /// \example Accessing a resource that does not exist. + IkarusErrorInfo_Type_Client_Misuse = 0x0002000100000001, + /// \brief The user provided invalid input. + /// \example Passing null for a pointer that must not be null. + IkarusErrorInfo_Type_Client_Input = 0x0002000100000002, + /// \brief An error occurred while interacting with a dependency from ikarus. + /// \example An error occurred in the underlying OS library. + IkarusErrorInfo_Type_SubSystem_Dependency = 0x0002000200000001, + /// \brief An error occurred while interacting with the database. + /// \example An error occurred while executing a query. + IkarusErrorInfo_Type_SubSystem_Database = 0x0002000200000002, + /// \brief An error occurred while interacting with the filesystem. + /// \example An error occurred while reading a file. + IkarusErrorInfo_Type_SubSystem_Filesystem = 0x0002000200000003, + /// \brief A datapoint within ikarus is invalid for the current state of the system. + /// \example The name of an object is found to be invalid UTF8. + IkarusErrorInfo_Type_LibIkarus_InvalidState = 0x0002000300000001, + /// \brief LibIkarus is unable to perform a certain operation that should succeed. + /// \example Migrating a project fails + IkarusErrorInfo_Type_LibIkarus_CannotPerformOperation = 0x0002000300000002, + /// \brief LibIkarus is unable to perform a certain operation within a given timeframe. + /// \example A query takes longer than the timeout. + IkarusErrorInfo_Type_LibIkarus_Timeout = 0x0002000300000003, + /// \brief The type of error is unknown. + IkarusErrorInfo_Type_Unknown = 0xFFFFFFFF, +}; + +/// \brief Gets the name of an error info. +/// \param info The error info to get the name of. +/// \return The name of the error info. +/// \remark The returned pointer is valid for the lifetime of the program and must not be freed. +IKA_API char const * get_error_info_name(IkarusErrorInfo info); + +IKARUS_END_HEADER + +/// @} diff --git a/include/ikarus/global.h b/include/ikarus/global.h index 6d91227..1572b24 100644 --- a/include/ikarus/global.h +++ b/include/ikarus/global.h @@ -1,6 +1,6 @@ #pragma once -/// \file memory.h +/// \file global.h /// \author Folling #include @@ -9,8 +9,12 @@ /// \brief Information relevant to the entire library. /// @{ +IKARUS_BEGIN_HEADER + /// \brief Frees a pointer allocated by ikarus. Every pointer returned by a function must be freed using this function unless /// explicitly stated otherwise. IKA_API void ikarus_free(void * ptr); +IKARUS_END_HEADER + /// @} diff --git a/src/errors.cpp b/src/errors.cpp new file mode 100644 index 0000000..19c3bf9 --- /dev/null +++ b/src/errors.cpp @@ -0,0 +1,22 @@ +#include "ikarus/errors.h" + +char const * get_error_info_name(IkarusErrorInfo info) { + switch (info) { + case IkarusErrorInfo_Source_None: return "IkarusErrorSource_None"; + case IkarusErrorInfo_Source_Client: return "IkarusErrorSource_Client"; + case IkarusErrorInfo_Source_SubSystem: return "IkarusErrorSource_SubSystem"; + case IkarusErrorInfo_Source_LibIkarus: return "IkarusErrorSource_LibIkarus"; + case IkarusErrorInfo_Source_Unknown: return "IkarusErrorSource_Unknown"; + case IkarusErrorInfo_Type_None: return "IkarusErrorType_None"; + case IkarusErrorInfo_Type_Client_Misuse: return "IkarusErrorType_Client_Misuse"; + case IkarusErrorInfo_Type_Client_Input: return "IkarusErrorType_Client_Input"; + case IkarusErrorInfo_Type_SubSystem_Dependency: return "IkarusErrorType_SubSystem_Dependency"; + case IkarusErrorInfo_Type_SubSystem_Database: return "IkarusErrorType_SubSystem_Database"; + case IkarusErrorInfo_Type_SubSystem_Filesystem: return "IkarusErrorType_SubSystem_Filesystem"; + case IkarusErrorInfo_Type_LibIkarus_InvalidState: return "IkarusErrorType_LibIkarus_InvalidState"; + case IkarusErrorInfo_Type_LibIkarus_CannotPerformOperation: return "IkarusErrorType_LibIkarus_CannotPerformOperation"; + case IkarusErrorInfo_Type_LibIkarus_Timeout: return "IkarusErrorType_LibIkarus_Timeout"; + case IkarusErrorInfo_Type_Unknown: return "IkarusErrorType_Unknown"; + default: return "Unknown"; + } +} diff --git a/src/objects/blueprint.cpp b/src/objects/blueprint.cpp index 3d1c997..63ce726 100644 --- a/src/objects/blueprint.cpp +++ b/src/objects/blueprint.cpp @@ -21,39 +21,56 @@ IkarusBlueprint * ikarus_blueprint_create(struct IkarusProject * project, char c return nullptr; } - LOG_VERBOSE("project={}; name={}", project->path.c_str(), name); + auto ctx = project->function_context(); if (name == nullptr) { - LOG_ERROR("name is nullptr"); + ctx->set_error("name is nullptr", true, IkarusErrorInfo_Source_Client, IkarusErrorInfo_Type_Client_Input); return nullptr; } if (cppbase::is_empty_or_blank(name)) { - LOG_ERROR("name is empty or blank"); + ctx->set_error("name is empty or blank", true, IkarusErrorInfo_Source_Client, IkarusErrorInfo_Type_Client_Input); return nullptr; } - VTRYRV(auto id, nullptr, project->db->transact([name](auto * db) -> cppbase::Result { - LOG_VERBOSE("creating blueprint in objects table"); + LOG_VERBOSE("project={}; name={}", project->path().c_str(), name); - TRY(db->execute( - "INSERT INTO `objects` (`object_type`, `name`) VALUES(?, ?);", static_cast(IkarusObjectType_Blueprint), name - )); + VTRYRV( + auto id, + nullptr, + project->db() + ->transact([name](auto * db) -> cppbase::Result { + LOG_VERBOSE("creating blueprint in objects table"); - auto id = db->last_insert_rowid(); + TRY(db->execute( + "INSERT INTO `objects` (`object_type`, `name`) VALUES(?, ?);", + static_cast(IkarusObjectType_Blueprint), + name + )); - LOG_VERBOSE("blueprint is {}", id); + auto id = db->last_insert_rowid(); - LOG_VERBOSE("inserting blueprint into blueprints table"); + LOG_VERBOSE("id is {}", id); - TRY(db->execute("INSERT INTO `blueprints`(`id`) VALUES(?);", id)); + LOG_VERBOSE("inserting blueprint into blueprints table"); - return cppbase::ok(id); - })); + TRY(db->execute("INSERT INTO `blueprints`(`id`) VALUES(?);", id)); + + return cppbase::ok(id); + }) + .on_error([ctx](auto const& err) { + ctx->set_error( + fmt::format("unable to insert blueprint into database: {}", err), + true, + IkarusErrorInfo_Source_SubSystem, + IkarusErrorInfo_Type_SubSystem_Database + ); + }) + ); LOG_VERBOSE("successfully created blueprint"); - return new IkarusBlueprint{project, id}; + return project->get_blueprint(id); } void ikarus_blueprint_delete(IkarusBlueprint * blueprint) { @@ -64,16 +81,27 @@ void ikarus_blueprint_delete(IkarusBlueprint * blueprint) { return; } + auto * ctx = blueprint->project->function_context(); + LOG_VERBOSE("blueprint={}", blueprint->id); - if (auto res = blueprint->project->db->execute("DELETE FROM `objects` WHERE `id` = ?", blueprint->id); res.is_err()) { - LOG_ERROR("failed to delete blueprint {} from objects table: {}", blueprint->id, res.unwrap_error()); - return; - } + TRYRV( + , + blueprint->project->db() + ->execute("DELETE FROM `objects` WHERE `id` = ?", blueprint->id) + .on_error([ctx](auto const& err) { + ctx->set_error( + fmt::format("failed to delete blueprint from objects table: {}", err), + true, + IkarusErrorInfo_Source_SubSystem, + IkarusErrorInfo_Type_SubSystem_Database + ); + }) + ); - LOG_VERBOSE("blueprint was successfully deleted from database, freeing pointer"); + LOG_VERBOSE("blueprint was successfully deleted from database, freeing blueprint"); - delete blueprint; + blueprint->project->remove_blueprint(blueprint); LOG_VERBOSE("successfully deleted blueprint"); } @@ -86,14 +114,23 @@ size_t ikarus_blueprint_get_property_count(IkarusBlueprint const * blueprint) { return 0; } + auto * ctx = blueprint->project->function_context(); + LOG_VERBOSE("blueprint={}", blueprint->id); VTRYRV( auto count, 0, - blueprint->project->db->query_one( - "SELECT COUNT(*) FROM `blueprint_properties` WHERE `blueprint_id` = ?;", blueprint->id - ) + blueprint->project->db() + ->query_one("SELECT COUNT(*) FROM `blueprint_properties` WHERE `blueprint_id` = ?;", blueprint->id) + .on_error([ctx](auto const& err) { + ctx->set_error( + fmt::format("failed to fetch blueprint property count: {}", err), + true, + IkarusErrorInfo_Source_SubSystem, + IkarusErrorInfo_Type_SubSystem_Database + ); + }) ); return static_cast(count); @@ -109,6 +146,8 @@ void ikarus_blueprint_get_properties( return; } + auto * ctx = blueprint->project->function_context(); + if (properties_out == nullptr) { LOG_ERROR("properties_out is nullptr"); return; @@ -118,17 +157,28 @@ void ikarus_blueprint_get_properties( IkarusId ids[properties_out_size]; - if (auto res = blueprint->project->db->query_many_buffered( - "SELECT `id` FROM `properties` WHERE `source` = ?", static_cast(ids), properties_out_size, blueprint->id - ); - res.is_err()) { - LOG_ERROR("failed to fetch blueprint property ids: {}", res.unwrap_error()); - return; - } + TRYRV( + , + blueprint->project->db() + ->query_many_buffered( + "SELECT `id` FROM `properties` WHERE `source` = ?", + static_cast(ids), + properties_out_size, + blueprint->id + ) + .on_error([ctx](auto const& err) { + ctx->set_error( + fmt::format("failed to fetch blueprint property ids: {}", err), + true, + IkarusErrorInfo_Source_SubSystem, + IkarusErrorInfo_Type_SubSystem_Database + ); + }) + ); for (size_t i = 0; i < properties_out_size; ++i) { /// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) - properties_out[i] = new IkarusProperty{blueprint->project, ids[i]}; + properties_out[i] = blueprint->project->get_property(ids[i]); } LOG_VERBOSE("successfully fetched blueprint properties"); diff --git a/src/objects/entity.hpp b/src/objects/entity.hpp index 91b3d3e..28fb068 100644 --- a/src/objects/entity.hpp +++ b/src/objects/entity.hpp @@ -3,4 +3,7 @@ #include /// \private -struct IkarusEntity : public IkarusObject {}; +struct IkarusEntity : public IkarusObject { + inline IkarusEntity(struct IkarusProject * project, IkarusId id): + IkarusObject{project, id} {} +}; diff --git a/src/persistence/project.hpp b/src/persistence/project.hpp index db52508..5cbea00 100644 --- a/src/persistence/project.hpp +++ b/src/persistence/project.hpp @@ -1,11 +1,148 @@ +#pragma once + +#include #include +#include +#include #include #include +#include + +constexpr inline size_t MAXIMUM_ERROR_INFOS = 8; +constexpr inline size_t MAXIMUM_ERROR_MESSAGE_LENGTH = 256; + +class FunctionContext { +public: + explicit FunctionContext(struct IkarusProject * project); + FunctionContext(FunctionContext const&) noexcept = default; + FunctionContext(FunctionContext&&) noexcept = default; + + auto operator=(FunctionContext const&) noexcept -> FunctionContext& = default; + auto operator=(FunctionContext&&) noexcept -> FunctionContext& = default; + + ~FunctionContext(); + + template + requires(std::is_same_v && ...) && (sizeof...(Infos) <= MAXIMUM_ERROR_INFOS) + auto set_error(std::string_view error_message, bool log_error, Infos... infos) -> void; + +private: + struct IkarusProject * _project; +}; + /// \private struct IkarusProject { - std::string name; - std::filesystem::path path; - std::unique_ptr db; +public: + [[nodiscard]] inline auto name() const -> std::string_view { + return _name; + } + + [[nodiscard]] inline auto path() const -> std::filesystem::path const& { + return _path; + } + + [[nodiscard]] inline auto db() -> sqlitecpp::Connection * { + return _db.get(); + } + + inline auto function_context() -> FunctionContext * { + return &_function_contexts.emplace(this); + } + + [[nodiscard]] IkarusBlueprint * get_blueprint(IkarusId id) { + return get_cached_object(id, this->_blueprints); + } + + auto remove_blueprint(IkarusBlueprint * blueprint) -> void { + remove_cached_object(blueprint, _blueprints); + } + + [[nodiscard]] auto get_entity(IkarusId id) -> IkarusEntity * { + return get_cached_object(id, this->_entities); + } + + auto remove_entity(IkarusEntity * entity) -> void { + remove_cached_object(entity, _entities); + } + + [[nodiscard]] auto get_property(IkarusId id) -> IkarusProperty * { + return get_cached_object(id, this->_properties); + } + + auto remove_property(IkarusProperty * property) -> void { + remove_cached_object(property, _properties); + } + +private: + template + [[nodiscard]] T * get_cached_object(IkarusId id, std::unordered_map>& cache) { + if (auto iter = cache.find(id); iter == cache.cend()) { + auto [ret_iter, _] = cache.emplace(id, std::make_unique(this, id)); + + return ret_iter->second.get(); + } else { + return iter->second.get(); + } + } + + template + void remove_cached_object(T * object, std::unordered_map>& cache) { + cache.erase(object->id); + } + +private: + friend class FunctionContext; + + std::string _name; + std::filesystem::path _path; + std::unique_ptr _db; + + std::array error_infos; + std::string error_message_buffer; + + std::unordered_map> _blueprints; + std::unordered_map> _properties; + std::unordered_map> _entities; + + std::stack _function_contexts; }; + +FunctionContext::FunctionContext(struct IkarusProject * project): + _project{project} {} + +FunctionContext::~FunctionContext() { + if (_project->_function_contexts.size() == 1) { + if (_project->error_message_buffer.empty()) { + _project->error_message_buffer.push_back('\0'); + } else { + _project->error_message_buffer[0] = '\0'; + } + + _project->error_infos = {}; + } + + _project->_function_contexts.pop(); +} + +template + requires(std::is_same_v && ...) && (sizeof...(Infos) <= MAXIMUM_ERROR_INFOS) +auto FunctionContext::set_error(std::string_view error_message, bool log_error, Infos... infos) { + if (error_message.size() > _project->error_message_buffer.size()) { + _project->error_message_buffer.resize(error_message.size() + 1); + } + + for (int i = 0; i < error_message.size(); ++i) { + _project->error_message_buffer[i] = error_message[i]; + } + + _project->error_message_buffer[error_message.size()] = '\0'; + _project->error_infos = {infos...}; + + if (log_error) { + LOG_ERROR( + "Error({}): {}", fmt::join(_project->error_infos | std::views::transform(get_error_info_name), ", "), error_message + ); + } +}