diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..3d8a4d6 --- /dev/null +++ b/.clang-format @@ -0,0 +1,56 @@ +BasedOnStyle: LLVM +AccessModifierOffset: '-2' +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: 'true' +AlignConsecutiveDeclarations: 'true' +AlignEscapedNewlines: Left +AlignOperands: 'true' +AlignTrailingComments: 'true' +AllowAllConstructorInitializersOnNextLine: 'false' +AllowAllParametersOfDeclarationOnNextLine: 'true' +AllowShortBlocksOnASingleLine: 'false' +AllowShortCaseLabelsOnASingleLine: 'true' +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Inline +AllowShortLoopsOnASingleLine: 'false' +AllowShortCompoundRequirementOnASingleLine: 'true' +AlwaysBreakTemplateDeclarations: 'Yes' +BinPackArguments: 'true' +BinPackParameters: 'true' +BreakBeforeBraces: Attach +BreakBeforeTernaryOperators: 'true' +BreakConstructorInitializers: BeforeComma +BreakInheritanceList: BeforeComma +BreakBeforeConceptDeclarations: Always +ColumnLimit: '100' +CompactNamespaces: 'false' +ConstructorInitializerAllOnOneLineOrOnePerLine: 'false' +Cpp11BracedListStyle: 'false' +FixNamespaceComments: 'true' +IncludeBlocks: Regroup +IndentCaseLabels: 'true' +IndentPPDirectives: AfterHash +IndentWidth: '2' +IndentWrappedFunctionNames: 'false' +KeepEmptyLinesAtTheStartOfBlocks: 'false' +Language: Cpp +MaxEmptyLinesToKeep: '1' +RequiresExpressionIndentation: OuterScope +SortIncludes: 'true' +SortUsingDeclarations: 'true' +SpaceAfterCStyleCast: 'false' +SpaceAfterLogicalNot: 'false' +SpaceAfterTemplateKeyword: 'true' +SpaceBeforeAssignmentOperators: 'true' +SpaceBeforeCpp11BracedList: 'false' +SpaceBeforeCtorInitializerColon: 'false' +SpaceBeforeInheritanceColon: 'false' +SpaceBeforeParens: Never +SpaceInEmptyParentheses: 'false' +SpacesInAngles: 'false' +SpacesInContainerLiterals: 'false' +SpacesInParentheses: 'false' +Standard: Cpp11 +TabWidth: '2' +UseTab: Never diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e7cb428 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "submodules/drogon"] + path = submodules/drogon + url = https://github.com/drogonframework/drogon.git +[submodule "submodules/json"] + path = submodules/json + url = https://github.com/nlohmann/json.git +[submodule "submodules/Catch2"] + path = submodules/Catch2 + url = https://github.com/catchorg/Catch2.git diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..280e1c4 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run auth_service", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/build/auth_service", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "MIMode": "gdb", + "miDebuggerPath": "/usr/bin/gdb", + "preLaunchTask": "build" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..0bc3915 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,43 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "type": "shell", + "command": "cmake", + "args": [ + "--build", + "build", + "--target", + "auth_service" + ], + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [ + "$gcc" + ] + }, + { + "label": "run", + "type": "shell", + "command": "./build/auth_service", + "dependsOn": "build", + "problemMatcher": [ + "$gcc" + ] + }, + { + "label": "test", + "type": "shell", + "command": "ctest", + "args": [ + "--test-dir", + "build", + "--output-on-failure" + ], + "dependsOn": "build" + } + ] +} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..3bcab0a --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,67 @@ +cmake_minimum_required(VERSION 3.22) + +project(auth_service VERSION 0.1.0 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +include(CTest) +enable_testing() + +find_package(SQLite3 REQUIRED) + +add_subdirectory(submodules/drogon) +add_subdirectory(submodules/json) +add_subdirectory(submodules/Catch2) + +add_executable(auth_service + src/main.cpp + src/application/application.cpp + src/http/controllers/health_controller.cpp + src/db/sqllite_db.cpp + src/db/statement.cpp +) + +target_include_directories(auth_service + PRIVATE + ${PROJECT_SOURCE_DIR}/include + ${PROJECT_SOURCE_DIR}/src +) + +target_compile_options(auth_service PRIVATE + -Wall + -Wextra + -Wpedantic +) + +target_link_libraries(auth_service PRIVATE + drogon + SQLite::SQLite3 + nlohmann_json::nlohmann_json +) + +add_executable(auth_service_tests + tests/smoke_test.cpp +) + +target_include_directories(auth_service_tests + PRIVATE + ${PROJECT_SOURCE_DIR}/include + ${PROJECT_SOURCE_DIR}/src +) + +target_compile_options(auth_service_tests PRIVATE + -Wall + -Wextra + -Wpedantic +) + +target_link_libraries(auth_service_tests PRIVATE + Catch2::Catch2WithMain + nlohmann_json::nlohmann_json +) + +include(${PROJECT_SOURCE_DIR}/submodules/Catch2/extras/Catch.cmake) +catch_discover_tests(auth_service_tests) \ No newline at end of file diff --git a/src/application/application.cpp b/src/application/application.cpp new file mode 100644 index 0000000..c6ba632 --- /dev/null +++ b/src/application/application.cpp @@ -0,0 +1,66 @@ +#include "application/application.hpp" +// #include +#include "db/sqllite_db.hpp" +#include "http/controllers/health_controller.hpp" + +#include +#include +#include +#include +namespace { +void register_routes() { + static HealthController health_controller; + drogon::app().registerHandler( + "/health", + [](const drogon::HttpRequestPtr &req, + std::function &&callback) { + health_controller.handle(req, std::move(callback)); + }, + { drogon::Get }); +} +} // namespace + +Application::Application(Settings settings) + : settings_(std::move(settings)) {} + +int Application::run() const { + SqlliteDb db(settings_.db_path); + db.initialize(); + + // { + // db.execute("CREATE TABLE IF NOT EXISTS healthcheck_tmp (" + // "id INTEGER PRIMARY KEY AUTOINCREMENT, " + // "name TEXT NOT NULL" + // ");"); + + // { + // auto insert_stmt = db.prepare("INSERT INTO healthcheck_tmp(name) VALUES(?1);"); + // insert_stmt.bind_text(1, "boot"); + // insert_stmt.execute(); + // } + + // { + // auto select_stmt = + // db.prepare("SELECT id, name FROM healthcheck_tmp ORDER BY id DESC LIMIT 1;"); + + // if(select_stmt.step()) { + // const auto id = select_stmt.column_int64(0); + // const auto name = select_stmt.column_text(1); + + // std::cout << "id=" << id << ", value=" << name << '\n'; + // } + // } + // } + + register_routes(); + drogon::app().addListener(settings_.host, settings_.port); + printInfo(); + drogon::app().run(); + return 0; +}; + +void Application::printInfo() const noexcept { + std::cout << std::format("Server starting at http://{}:{}", settings_.host, settings_.port) + << std::endl; + std::cout << std::format("SQLite initialize at:{}", settings_.db_path) << std::endl; +} diff --git a/src/application/application.hpp b/src/application/application.hpp new file mode 100644 index 0000000..1d39c51 --- /dev/null +++ b/src/application/application.hpp @@ -0,0 +1,11 @@ +#pragma once +#include "config/settings/settings.hpp" +class Application final { +public: + explicit Application(Settings settings); + int run() const; + void printInfo() const noexcept; + +private: + Settings settings_; +}; \ No newline at end of file diff --git a/src/config/settings/settings.hpp b/src/config/settings/settings.hpp new file mode 100644 index 0000000..527b801 --- /dev/null +++ b/src/config/settings/settings.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include +#include + +struct Settings { + std::string host{ "127.0.0.1" }; + std::uint16_t port{ 8080 }; + std::string db_path{ "auth.db" }; +}; \ No newline at end of file diff --git a/src/db/sqllite_db.cpp b/src/db/sqllite_db.cpp new file mode 100644 index 0000000..d9e5d17 --- /dev/null +++ b/src/db/sqllite_db.cpp @@ -0,0 +1,72 @@ +#include "db/sqllite_db.hpp" + +#include +#include + +SqlliteDb::SqlliteDb(const std::string &path) { + const int rc = sqlite3_open(path.c_str(), &db_); + if(rc != SQLITE_OK) { + std::string message = "Failed to open SQLite database"; + if(db_ != nullptr) { + message += ": "; + message += sqlite3_errmsg(db_); + closeDb(); + } + throw std::runtime_error(message); + } +} + +SqlliteDb::~SqlliteDb() { + closeDb(); +} + +SqlliteDb::SqlliteDb(SqlliteDb &&other) noexcept + : db_(std::exchange(other.db_, nullptr)) {} + +SqlliteDb &SqlliteDb::operator=(SqlliteDb &&other) noexcept { + if(this != &other) { + if(db_ != nullptr) { + sqlite3_close(db_); + } + db_ = std::exchange(other.db_, nullptr); + } + return *this; +} + +void SqlliteDb::execute(std::string_view sql) { + char *error_message = nullptr; + const int rc = sqlite3_exec(db_, sql.data(), nullptr, nullptr, &error_message); + + if(rc != SQLITE_OK) { + std::string message = "SQLite execute failed"; + if(error_message != nullptr) { + message += ": "; + message += error_message; + sqlite3_free(error_message); + } else { + message += "unknown error"; + } + throw std::runtime_error(message); + } +} + +void SqlliteDb::initialize() { + execute("PRAGMA foreign_keys = ON;"); + execute("PRAGMA journal_mode = WAL;"); + execute("PRAGMA busy_timeout = 5000;"); +} + +sqlite3 *SqlliteDb::native_handle() const noexcept { + return db_; +} + +Statement SqlliteDb::prepare(std::string_view sql) const { + return Statement{ db_, sql }; +} + +void SqlliteDb::closeDb() { + if(db_ != nullptr) { + sqlite3_close(db_); + db_ = nullptr; + } +} \ No newline at end of file diff --git a/src/db/sqllite_db.hpp b/src/db/sqllite_db.hpp new file mode 100644 index 0000000..dae2d41 --- /dev/null +++ b/src/db/sqllite_db.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include "db/statement.hpp" + +#include +#include +#include + +class SqlliteDb { +public: + explicit SqlliteDb(const std::string &path); + ~SqlliteDb(); + + SqlliteDb(const SqlliteDb &) = delete; + SqlliteDb &operator=(const SqlliteDb &) = delete; + + SqlliteDb(SqlliteDb &&other) noexcept; + SqlliteDb &operator=(SqlliteDb &&other) noexcept; + + void execute(std::string_view sql); + void initialize(); + + Statement prepare(std::string_view sql) const; + + sqlite3 *native_handle() const noexcept; + +private: + void closeDb(); + +private: + sqlite3 *db_{ nullptr }; +}; \ No newline at end of file diff --git a/src/db/statement.cpp b/src/db/statement.cpp new file mode 100644 index 0000000..d2742cb --- /dev/null +++ b/src/db/statement.cpp @@ -0,0 +1,96 @@ +#include "db/statement.hpp" + +#include +#include + +Statement::Statement(sqlite3 *db, std::string_view sql) { + const int rc = sqlite3_prepare_v2(db, sql.data(), static_cast(sql.size()), &stmt_, nullptr); + if(rc != SQLITE_OK) { + throw std::runtime_error("Failed to prepare SQLite statement"); + } +} + +Statement::~Statement() { + delete_stament(); +} + +Statement::Statement(Statement &&other) noexcept + : stmt_(std::exchange(other.stmt_, nullptr)) {} + +Statement &Statement::operator=(Statement &&other) noexcept { + if(this != &other) { + delete_stament(); + stmt_ = std::exchange(other.stmt_, nullptr); + } + return *this; +} + +void Statement::delete_stament() { + if(stmt_ != nullptr) { + sqlite3_finalize(stmt_); + stmt_ = nullptr; + } +} + +void Statement::bind_int64(int index, std::int64_t value) { + const int rc = sqlite3_bind_int64(stmt_, index, value); + if(rc != SQLITE_OK) { + throw std::runtime_error("Failed bind int64"); + } +} + +void Statement::bind_text(int index, std::string_view value) { + const int rc = sqlite3_bind_text(stmt_, index, value.data(), static_cast(value.size()), + SQLITE_TRANSIENT); + if(rc != SQLITE_OK) { + throw std::runtime_error("Failed bind text"); + } +} + +void Statement::bind_null(int index) { + const int rc = sqlite3_bind_null(stmt_, index); + if(rc != SQLITE_OK) { + throw std::runtime_error("Failed bind null"); + } +} + +bool Statement::step() { + const int rc = sqlite3_step(stmt_); + if(rc == SQLITE_ROW) { + return true; + } + if(rc == SQLITE_DONE) { + return false; + } + throw std::runtime_error("Failed to step SQLite statement"); +} + +void Statement::execute() { + const int rc = sqlite3_step(stmt_); + if(rc != SQLITE_DONE) { + throw std::runtime_error("Failed to execute statement"); + } +} + +std::int64_t Statement::column_int64(int index) const { + return sqlite3_column_int64(stmt_, index); +} + +std::string Statement::column_text(int index) const { + const auto *txt = reinterpret_cast(sqlite3_column_text(stmt_, index)); + if(txt == nullptr) { + return {}; + } + return std::string{ txt }; +} + +bool Statement::column_is_null(int index) const { + return sqlite3_column_type(stmt_, index) == SQLITE_NULL; +} + +void Statement::reset() { + const int rc = sqlite3_reset(stmt_); + if(rc != SQLITE_OK) { + throw std::runtime_error("Failed reset statement"); + } +} \ No newline at end of file diff --git a/src/db/statement.hpp b/src/db/statement.hpp new file mode 100644 index 0000000..281a782 --- /dev/null +++ b/src/db/statement.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include +#include + +class Statement { +public: + Statement(sqlite3 *db, std::string_view sql); + ~Statement(); + + Statement(const Statement &) = delete; + Statement &operator=(const Statement &) = delete; + + Statement(Statement &&other) noexcept; + Statement &operator=(Statement &&other) noexcept; + + void bind_int64(int index, std::int64_t value); + void bind_text(int inxex, std::string_view value); + void bind_null(int index); + + bool step(); + void execute(); + + std::int64_t column_int64(int index) const; + std::string column_text(int index) const; + bool column_is_null(int index) const; + + void reset(); + +private: + void delete_stament(); + +private: + sqlite3_stmt *stmt_{ nullptr }; +}; \ No newline at end of file diff --git a/src/http/controllers/health_controller.cpp b/src/http/controllers/health_controller.cpp new file mode 100644 index 0000000..b4c2217 --- /dev/null +++ b/src/http/controllers/health_controller.cpp @@ -0,0 +1,14 @@ +#include "http/controllers/health_controller.hpp" + +#include +#include +#include +void HealthController::handle( + const drogon::HttpRequestPtr &, + std::function &&callback) const { + Json::Value body; + body["status"] = "ok"; + auto response = drogon::HttpResponse::newHttpJsonResponse(body); + response->setStatusCode(drogon::k200OK); + callback(response); +} \ No newline at end of file diff --git a/src/http/controllers/health_controller.hpp b/src/http/controllers/health_controller.hpp new file mode 100644 index 0000000..3a559e9 --- /dev/null +++ b/src/http/controllers/health_controller.hpp @@ -0,0 +1,9 @@ +#pragma once +#include + +class HealthController { +public: + void handle(const drogon::HttpRequestPtr &req, + std::function &&callback) const; + void testFor() {} +}; \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..ad39cf2 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,10 @@ +#include +#include "application/application.hpp" +#include "config/settings/settings.hpp" + +int main() +{ + Settings s; + Application app(s); + return app.run(); +} \ No newline at end of file diff --git a/submodules/Catch2 b/submodules/Catch2 new file mode 160000 index 0000000..1df10d2 --- /dev/null +++ b/submodules/Catch2 @@ -0,0 +1 @@ +Subproject commit 1df10d28aec9967120c1faf10dae87adbd875b8b diff --git a/submodules/drogon b/submodules/drogon new file mode 160000 index 0000000..acad9c8 --- /dev/null +++ b/submodules/drogon @@ -0,0 +1 @@ +Subproject commit acad9c8ee7b843a7849e21a58cf7e8ec85b6cc4f diff --git a/submodules/json b/submodules/json new file mode 160000 index 0000000..9a73748 --- /dev/null +++ b/submodules/json @@ -0,0 +1 @@ +Subproject commit 9a737481aed085fd289f82dff1fa8c3c66627a7e diff --git a/tests/smoke_test.cpp b/tests/smoke_test.cpp new file mode 100644 index 0000000..7dc91d9 --- /dev/null +++ b/tests/smoke_test.cpp @@ -0,0 +1,6 @@ +#include + +TEST_CASE("smoke test") +{ + REQUIRE(1 + 1 == 2); +} \ No newline at end of file